mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 19:25:18 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bcbcbc6a1 | |||
| 5c8f494ac6 |
@@ -1 +0,0 @@
|
||||
../.claude/skills/
|
||||
@@ -0,0 +1,168 @@
|
||||
# Claude Code Skills and Reference Files
|
||||
|
||||
This directory contains Claude Skills and reference documentation for working with Home Assistant integrations.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.claude/
|
||||
├── skills/ # Claude Skills (auto-loaded)
|
||||
│ ├── testing/
|
||||
│ │ └── SKILL.md # Testing specialist skill
|
||||
│ ├── code-review/
|
||||
│ │ └── SKILL.md # Code review specialist skill
|
||||
│ └── quality-scale-architect/
|
||||
│ └── SKILL.md # Architecture guidance skill
|
||||
├── agents/ # Legacy agent definitions
|
||||
│ └── quality-scale-rule-verifier.md
|
||||
└── references/ # Deep-dive reference docs
|
||||
├── diagnostics.md # Diagnostics implementation
|
||||
├── sensor.md # Sensor platform
|
||||
├── binary_sensor.md # Binary sensor platform
|
||||
├── switch.md # Switch platform
|
||||
├── button.md # Button platform
|
||||
├── number.md # Number platform
|
||||
└── select.md # Select platform
|
||||
```
|
||||
|
||||
## Claude Skills
|
||||
|
||||
Claude Skills are modular capabilities that extend Claude's functionality. Each Skill packages instructions and metadata that Claude uses automatically when relevant.
|
||||
|
||||
### How Skills Work
|
||||
|
||||
Skills use **progressive disclosure** - they load content in stages:
|
||||
|
||||
1. **Level 1 - Metadata (always loaded)**: Skill name and description
|
||||
2. **Level 2 - Instructions (loaded when triggered)**: Main SKILL.md content
|
||||
3. **Level 3+ - Resources (loaded as needed)**: Reference files and additional docs
|
||||
|
||||
This means you can have many Skills installed with minimal context penalty. Claude only knows each Skill exists and when to use it until triggered.
|
||||
|
||||
### Available Skills
|
||||
|
||||
#### Testing (`testing`)
|
||||
**Use when**: Writing, running, or fixing tests for Home Assistant integrations
|
||||
|
||||
Specializes in:
|
||||
- Writing comprehensive test coverage (>95%)
|
||||
- Running pytest with appropriate flags
|
||||
- Fixing failing tests and updating snapshots
|
||||
- Following Home Assistant testing patterns
|
||||
- Modern fixture patterns and snapshot testing
|
||||
|
||||
**Triggers on**: Requests about writing tests, running tests, fixing test failures, test coverage, pytest, snapshots
|
||||
|
||||
#### Code Review (`code-review`)
|
||||
**Use when**: Reviewing code for quality, best practices, and standards compliance
|
||||
|
||||
Specializes in:
|
||||
- Reviewing pull requests and code changes
|
||||
- Identifying anti-patterns and security vulnerabilities
|
||||
- Verifying async patterns and error handling
|
||||
- Ensuring quality scale compliance
|
||||
- Performance optimization
|
||||
|
||||
**Triggers on**: Requests to review code, check for issues, analyze code quality, security review
|
||||
|
||||
#### Quality Scale Architect (`quality-scale-architect`)
|
||||
**Use when**: Needing architectural guidance and quality scale planning
|
||||
|
||||
Specializes in:
|
||||
- High-level architecture guidance
|
||||
- Quality scale tier selection (Bronze/Silver/Gold/Platinum)
|
||||
- Integration structure planning
|
||||
- Pattern recommendations (coordinator, push, hub)
|
||||
- Progression strategies between quality tiers
|
||||
|
||||
**Triggers on**: Requests about architecture, integration design, quality tiers, structural planning, choosing patterns
|
||||
|
||||
## Reference Files
|
||||
|
||||
Reference files provide deep-dive documentation for specific implementation areas. Skills can reference these for detailed guidance, and they're loaded on-demand to avoid consuming context.
|
||||
|
||||
### Available References
|
||||
|
||||
- **diagnostics.md**: Complete guide to implementing integration and device diagnostics, data redaction, testing
|
||||
- **sensor.md**: Sensor platform implementation, device classes, state classes, entity descriptions
|
||||
- **binary_sensor.md**: Binary sensor implementation, device classes, push-updated patterns
|
||||
- **switch.md**: Switch control implementation, state updates, configuration switches
|
||||
- **button.md**: Button action implementation, device classes, one-time actions
|
||||
- **number.md**: Numeric value control, ranges, display modes, units
|
||||
- **select.md**: Option selection implementation, enums, translations, dynamic options
|
||||
|
||||
## How to Use
|
||||
|
||||
### As a Developer
|
||||
|
||||
Skills work automatically - just ask Claude to help with tasks:
|
||||
|
||||
- **Testing**: "Write tests for my sensor platform" or "Fix the failing config flow tests"
|
||||
- **Review**: "Review this integration for security issues" or "Check my async patterns"
|
||||
- **Architecture**: "Help me design a hub integration" or "What quality tier should I target?"
|
||||
|
||||
### As Claude
|
||||
|
||||
Skills are triggered automatically when requests match the skill descriptions. Skills can reference the documentation files in `.claude/references/` for detailed implementation guidance.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# When a testing request comes in, Claude triggers the testing skill
|
||||
# The skill can then reference .claude/references/sensor.md for sensor-specific patterns
|
||||
```
|
||||
|
||||
## Quality Scale Overview
|
||||
|
||||
Home Assistant uses a Quality Scale system:
|
||||
|
||||
- **Bronze**: Basic requirements (mandatory baseline) - Config flow, unique IDs, auth flows
|
||||
- **Silver**: Enhanced functionality - Unavailability tracking, runtime data, parallel updates
|
||||
- **Gold**: Advanced features - Diagnostics, translations, device registry
|
||||
- **Platinum**: Highest quality - Strict typing, async-only dependencies, WebSession injection
|
||||
|
||||
All Bronze rules are mandatory. Higher tiers are additive.
|
||||
|
||||
## Skill Structure
|
||||
|
||||
Each Skill is a directory containing a `SKILL.md` file with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Brief description of what this Skill does and when to use it (max 1024 chars)
|
||||
---
|
||||
|
||||
# Skill Content in Markdown
|
||||
|
||||
Instructions, examples, and guidance...
|
||||
```
|
||||
|
||||
**Progressive Loading**: Only the name/description are loaded initially. The full content loads when the Skill is triggered.
|
||||
|
||||
## Creating Custom Skills
|
||||
|
||||
To add a new Skill:
|
||||
|
||||
1. Create a directory: `.claude/skills/my-skill/`
|
||||
2. Add a `SKILL.md` file with proper frontmatter
|
||||
3. Include clear instructions and examples
|
||||
4. Reference existing documentation when appropriate
|
||||
|
||||
See [Claude Skills Documentation](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) for complete guidance.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Main instructions: `/home/user/core/CLAUDE.md`
|
||||
- Home Assistant Docs: https://developers.home-assistant.io
|
||||
- Integration Quality Scale: https://developers.home-assistant.io/docs/core/integration-quality-scale/
|
||||
- Claude Skills Cookbook: https://platform.claude.com/cookbook/skills-notebooks-01-skills-introduction
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new Skills or references:
|
||||
1. Follow the proper Skill structure (SKILL.md with frontmatter)
|
||||
2. Keep descriptions concise and trigger-focused (max 1024 chars)
|
||||
3. Include practical examples in Skill content
|
||||
4. Link to reference documentation for deep dives
|
||||
5. Consider quality scale implications
|
||||
6. Test that Skills trigger appropriately
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
name: raise-pull-request
|
||||
description: |
|
||||
Use this agent when creating a pull request for the Home Assistant core repository after completing implementation work. This agent automates the PR creation process including running tests, formatting checks, and proper checkbox handling.
|
||||
model: inherit
|
||||
color: green
|
||||
tools: Read, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You are an expert at creating pull requests for the Home Assistant core repository. You will automate the PR creation process with proper verification, formatting, testing, and checkbox handling.
|
||||
|
||||
**Execute each step in order. Do not skip steps.**
|
||||
|
||||
## Step 1: Gather Information
|
||||
|
||||
Run these commands in parallel to analyze the changes:
|
||||
|
||||
```bash
|
||||
# Get current branch and remote
|
||||
git branch --show-current
|
||||
git remote -v | grep push
|
||||
|
||||
# Determine the best available dev reference
|
||||
if git rev-parse --verify --quiet upstream/dev >/dev/null; then
|
||||
BASE_REF="upstream/dev"
|
||||
elif git rev-parse --verify --quiet origin/dev >/dev/null; then
|
||||
BASE_REF="origin/dev"
|
||||
elif git rev-parse --verify --quiet dev >/dev/null; then
|
||||
BASE_REF="dev"
|
||||
else
|
||||
echo "Could not find upstream/dev, origin/dev, or local dev"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_SHA="$(git merge-base "$BASE_REF" HEAD)"
|
||||
echo "BASE_REF=$BASE_REF"
|
||||
echo "BASE_SHA=$BASE_SHA"
|
||||
|
||||
# Get commit info for this branch vs dev
|
||||
git log "${BASE_SHA}..HEAD" --oneline
|
||||
|
||||
# Check what files changed
|
||||
git diff "${BASE_SHA}..HEAD" --name-only
|
||||
|
||||
# Check if test files were added/modified
|
||||
git diff "${BASE_SHA}..HEAD" --name-only | grep -E "^tests/.*\.py$" || echo "NO_TESTS_CHANGED"
|
||||
|
||||
# Check if manifest.json changed
|
||||
git diff "${BASE_SHA}..HEAD" --name-only | grep "manifest.json" || echo "NO_MANIFEST_CHANGED"
|
||||
```
|
||||
|
||||
From the file paths, extract the **integration domain** from `homeassistant/components/{integration}/` or `tests/components/{integration}/`.
|
||||
|
||||
**Track results:**
|
||||
- `BASE_REF`: the dev reference used for comparison
|
||||
- `BASE_SHA`: the merge-base commit used for diff-based checks
|
||||
- `TESTS_CHANGED`: true if test files were added or modified
|
||||
- `MANIFEST_CHANGED`: true if manifest.json was modified
|
||||
|
||||
**If no suitable dev reference is available, STOP and tell the user to fetch `upstream/dev`, `origin/dev`, or a local `dev` branch before continuing.**
|
||||
|
||||
## Step 2: Run Code Quality Checks
|
||||
|
||||
Run `prek` to perform code quality checks (formatting, linting, hassfest, etc.) on the files changed since `BASE_SHA`:
|
||||
|
||||
```bash
|
||||
prek run --from-ref "$BASE_SHA" --to-ref HEAD
|
||||
```
|
||||
|
||||
**Track results:**
|
||||
- `PREK_PASSED`: true if `prek run` exits with code 0
|
||||
|
||||
**If `prek` fails or is not available, STOP and report the failure to the user. Do not proceed with PR creation. If the failure appears to be an environment setup issue (e.g., missing tools, command not found, venv not activated), also point the user to https://developers.home-assistant.io/docs/development_environment.**
|
||||
|
||||
## Step 3: Stage Any Changes from Checks
|
||||
|
||||
If `prek` made any formatting or generated file changes, stage and commit them as a separate commit:
|
||||
|
||||
```bash
|
||||
git status --porcelain
|
||||
# If changes exist:
|
||||
git add -A
|
||||
git commit -m "Apply prek formatting and generated file updates"
|
||||
```
|
||||
|
||||
## Step 4: Run Tests
|
||||
|
||||
Run pytest for the specific integration:
|
||||
|
||||
```bash
|
||||
pytest tests/components/{integration} \
|
||||
--timeout=60 \
|
||||
--durations-min=1 \
|
||||
--durations=0 \
|
||||
-q
|
||||
```
|
||||
|
||||
**Track results:**
|
||||
- `TESTS_PASSED`: true if pytest exits with code 0
|
||||
|
||||
**If tests fail, STOP and report the failures to the user. Do not proceed with PR creation.**
|
||||
|
||||
## Step 5: Identify PR Metadata
|
||||
|
||||
Write a release-note-style PR title summarizing the change. The title becomes the release notes entry, so it should be a complete sentence fragment describing what changed in imperative mood.
|
||||
|
||||
**PR Title Examples by Type:**
|
||||
| Type | Example titles |
|
||||
|------|----------------|
|
||||
| Bugfix | `Fix Hikvision NVR binary sensors not being detected` |
|
||||
| | `Fix JSON serialization of time objects in anthropic tool results` |
|
||||
| | `Fix config flow bug in Tesla Fleet` |
|
||||
| Dependency | `Bump eheimdigital to 1.5.0` |
|
||||
| | `Bump python-otbr-api to 2.7.1` |
|
||||
| New feature | `Add asyncio-level timeout to Backblaze B2 uploads` |
|
||||
| | `Add Nettleie optimization option` |
|
||||
| Code quality | `Add exception translations to Teslemetry` |
|
||||
| | `Improve test coverage of Tesla Fleet` |
|
||||
| | `Refactor adguard tests to use proper fixtures for mocking` |
|
||||
| | `Simplify entity init in Proxmox` |
|
||||
|
||||
## Step 6: Verify Development Checklist
|
||||
|
||||
Check each item from the [development checklist](https://developers.home-assistant.io/docs/development_checklist/):
|
||||
|
||||
| Item | How to verify |
|
||||
|------|---------------|
|
||||
| External libraries on PyPI | Check manifest.json requirements - all should be PyPI packages |
|
||||
| Dependencies in requirements_all.txt | Only if dependency declarations changed (the `requirements` field in `manifest.json` or `requirements_all.txt`), run `python -m script.gen_requirements_all` |
|
||||
| Codeowners updated | If this is a new integration, ensure its `manifest.json` includes a `codeowners` field with one or more GitHub usernames |
|
||||
| No commented out code | Visually scan the diff for blocks of commented-out code |
|
||||
|
||||
**Track results:**
|
||||
- `NO_COMMENTED_CODE`: true if no blocks of commented-out code found in the diff
|
||||
- `DEPENDENCIES_CHANGED`: true if the diff changes the `requirements` field in `manifest.json` or changes `requirements_all.txt`
|
||||
- `REQUIREMENTS_UPDATED`: true if `DEPENDENCIES_CHANGED` is true and requirements_all.txt was regenerated successfully; not applicable if `DEPENDENCIES_CHANGED` is false
|
||||
- `CHECKLIST_PASSED`: true if all items above pass
|
||||
|
||||
## Step 7: Determine Type of Change
|
||||
|
||||
Select exactly ONE based on the changes. Mark the selected type with `[x]` and all others with `[ ]` (space):
|
||||
|
||||
| Type | Condition |
|
||||
|------|-----------|
|
||||
| Dependency upgrade | Only manifest.json/requirements changes |
|
||||
| Bugfix | Fixes broken behavior, no new features |
|
||||
| New integration | New folder in components/ |
|
||||
| New feature | Adds capability to existing integration |
|
||||
| Deprecation | Adds deprecation warnings for future breaking change |
|
||||
| Breaking change | Removes or changes existing functionality |
|
||||
| Code quality | Only refactoring or test additions, no functional change |
|
||||
|
||||
**Track results:**
|
||||
- `CHANGE_TYPE`: the selected type (e.g., "Bugfix", "New feature", "Code quality", etc.)
|
||||
|
||||
**Important:** All seven type options must remain in the PR body. Only the selected type gets `[x]`, all others get `[ ]`.
|
||||
|
||||
## Step 8: Determine Checkbox States
|
||||
|
||||
Based on the verification steps above, determine checkbox states:
|
||||
|
||||
| Checkbox | Condition to tick |
|
||||
|----------|-------------------|
|
||||
| The code change is tested and works locally | Leave unchecked for the contributor to verify manually (this refers to manual testing, not unit tests) |
|
||||
| Local tests pass | Tick only if `TESTS_PASSED` is true |
|
||||
| I understand the code I am submitting and can explain how it works | Leave unchecked for the contributor to review and set manually |
|
||||
| There is no commented out code | Tick only if `NO_COMMENTED_CODE` is true |
|
||||
| Development checklist | Tick only if `CHECKLIST_PASSED` is true |
|
||||
| Perfect PR recommendations | Tick only if the PR affects a single integration or closely related modules, represents one primary type of change, and has a clear, self-contained scope |
|
||||
| Formatted using Ruff | Tick only if `PREK_PASSED` is true |
|
||||
| Tests have been added | Tick only if `TESTS_CHANGED` is true AND the changes exercise new or changed functionality (not only cosmetic test changes) |
|
||||
| Documentation added/updated | Tick if documentation PR created (or not applicable) |
|
||||
| Manifest file fields filled out | Tick if `PREK_PASSED` is true (or not applicable) |
|
||||
| Dependencies in requirements_all.txt | Tick only if `DEPENDENCIES_CHANGED` is false, or if `DEPENDENCIES_CHANGED` is true and `REQUIREMENTS_UPDATED` is true |
|
||||
| Dependency changelog linked | Tick if dependency changelog linked in PR description (or not applicable) |
|
||||
| Any generated code has been carefully reviewed | Leave unchecked for the contributor to review and set manually |
|
||||
|
||||
## Step 9: Breaking Change Section
|
||||
|
||||
**If `CHANGE_TYPE` is NOT "Breaking change" or "Deprecation": REMOVE the entire "## Breaking change" section from the PR body (including the heading).**
|
||||
|
||||
If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking change` section and describe:
|
||||
- What breaks
|
||||
- How users can fix it
|
||||
- Why it was necessary
|
||||
|
||||
## Step 10: Push Branch and Create PR
|
||||
|
||||
Push the branch with upstream tracking, and create a PR against `home-assistant/core` with the generated title and body:
|
||||
|
||||
```bash
|
||||
# Create PR (gh pr create pushes the branch automatically)
|
||||
gh pr create --repo home-assistant/core --base dev \
|
||||
--draft \
|
||||
--title "TITLE_HERE" \
|
||||
--body "$(cat <<'EOF'
|
||||
BODY_HERE
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### PR Body Template
|
||||
|
||||
Read the PR template from `.github/PULL_REQUEST_TEMPLATE.md` and use it as the basis for the PR body. **Do not hardcode the template — always read it from the file to stay in sync with upstream changes.**
|
||||
|
||||
Use any HTML comments (`<!-- ... -->`) in the template as guidance to understand what to fill in. For the final PR body sent to GitHub, keep the template text intact — do not delete any text from the template unless it explicitly instructs removal (e.g., the breaking change section when not applicable). Then fill in the sections:
|
||||
|
||||
1. **Breaking change section**: If the type is NOT "Breaking change" or "Deprecation", remove the entire `## Breaking change` section (heading and body). Otherwise, describe what breaks, how users can fix it, and why.
|
||||
2. **Proposed change section**: Fill in a description of the change extracted from commit messages.
|
||||
3. **Type of change**: Check exactly ONE checkbox matching the determined type from Step 7. Leave all others unchecked.
|
||||
4. **Additional information**: Fill in any related issue numbers if known.
|
||||
5. **Checklist**: Check boxes based on the conditions in Step 8. Leave manual-verification boxes unchecked for the contributor.
|
||||
|
||||
**Important:** Preserve all template structure, options, and link references exactly as they appear in the file — only modify checkbox states and fill in content sections.
|
||||
|
||||
## Step 11: Report Result
|
||||
|
||||
Provide the user with:
|
||||
1. **PR URL** - The created pull request link
|
||||
2. **Verification Summary** - Which checks passed/failed
|
||||
3. **Unchecked Items** - List any checkboxes left unchecked and why
|
||||
4. **User Action Required** - Remind user to:
|
||||
- Review and set manual-verification checkboxes ("I understand the code..." and "Any generated code...") as applicable
|
||||
- Consider reviewing two other open PRs
|
||||
- Add any related issue numbers if applicable
|
||||
@@ -0,0 +1,470 @@
|
||||
# Binary Sensor Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Binary sensors are read-only entities that represent an on/off, true/false, or open/closed state. They are simpler than regular sensors and don't have units or numeric values.
|
||||
|
||||
## Basic Binary Sensor Implementation
|
||||
|
||||
```python
|
||||
"""Binary sensor platform for my_integration."""
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MyBinarySensor(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MyBinarySensor(MyEntity, BinarySensorEntity):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "motion"
|
||||
_attr_device_class = BinarySensorDeviceClass.MOTION
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_motion"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if motion is detected."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.motion_detected
|
||||
return None
|
||||
```
|
||||
|
||||
## Binary Sensor State
|
||||
|
||||
The core property for binary sensors is `is_on`:
|
||||
|
||||
```python
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.device.is_active
|
||||
|
||||
# Alternatively, use attribute
|
||||
_attr_is_on = True # or False, or None
|
||||
```
|
||||
|
||||
**State Meaning**:
|
||||
- `True` / `"on"` - Active/detected/open
|
||||
- `False` / `"off"` - Inactive/not detected/closed
|
||||
- `None` - Unknown (displays as "unavailable")
|
||||
|
||||
## Device Classes
|
||||
|
||||
Binary sensors should use device classes for proper representation:
|
||||
|
||||
```python
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
|
||||
# Common device classes
|
||||
_attr_device_class = BinarySensorDeviceClass.MOTION
|
||||
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
|
||||
_attr_device_class = BinarySensorDeviceClass.DOOR
|
||||
_attr_device_class = BinarySensorDeviceClass.WINDOW
|
||||
_attr_device_class = BinarySensorDeviceClass.OPENING
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
_attr_device_class = BinarySensorDeviceClass.RUNNING
|
||||
_attr_device_class = BinarySensorDeviceClass.SMOKE
|
||||
_attr_device_class = BinarySensorDeviceClass.MOISTURE
|
||||
_attr_device_class = BinarySensorDeviceClass.LOCK
|
||||
_attr_device_class = BinarySensorDeviceClass.TAMPER
|
||||
_attr_device_class = BinarySensorDeviceClass.PLUG
|
||||
_attr_device_class = BinarySensorDeviceClass.POWER
|
||||
```
|
||||
|
||||
### Device Class Selection Guide
|
||||
|
||||
**Detection Sensors**:
|
||||
- Motion detector → `MOTION`
|
||||
- Presence detector → `OCCUPANCY`
|
||||
- Smoke detector → `SMOKE`
|
||||
- Water leak detector → `MOISTURE`
|
||||
|
||||
**Contact Sensors**:
|
||||
- Door sensor → `DOOR`
|
||||
- Window sensor → `WINDOW`
|
||||
- Generic contact → `OPENING`
|
||||
|
||||
**Status Sensors**:
|
||||
- Network connection → `CONNECTIVITY`
|
||||
- Device running → `RUNNING`
|
||||
- Low battery → `BATTERY`
|
||||
- Charging state → `BATTERY_CHARGING`
|
||||
- Problem/fault → `PROBLEM`
|
||||
- Tamper detection → `TAMPER`
|
||||
|
||||
**Power Sensors**:
|
||||
- Outlet state → `PLUG`
|
||||
- Power state → `POWER`
|
||||
- Lock state → `LOCK`
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple similar binary sensors:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MyBinarySensorDescription(BinarySensorEntityDescription):
|
||||
"""Describes a binary sensor."""
|
||||
is_on_fn: Callable[[MyData], bool | None]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[MyBinarySensorDescription, ...] = (
|
||||
MyBinarySensorDescription(
|
||||
key="motion",
|
||||
translation_key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
is_on_fn=lambda data: data.motion_detected,
|
||||
),
|
||||
MyBinarySensorDescription(
|
||||
key="door",
|
||||
translation_key="door",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
is_on_fn=lambda data: data.door_open,
|
||||
),
|
||||
MyBinarySensorDescription(
|
||||
key="battery_low",
|
||||
translation_key="battery_low",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda data: data.battery_level < 20,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MyBinarySensor(coordinator, device_id, description)
|
||||
for device_id in coordinator.data.devices
|
||||
for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class MyBinarySensor(MyEntity, BinarySensorEntity):
|
||||
"""Binary sensor using entity description."""
|
||||
|
||||
entity_description: MyBinarySensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
device_id: str,
|
||||
description: MyBinarySensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return self.entity_description.is_on_fn(device)
|
||||
return None
|
||||
```
|
||||
|
||||
## Entity Category
|
||||
|
||||
Mark diagnostic or configuration binary sensors:
|
||||
|
||||
```python
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
# Diagnostic sensors
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
# Examples: connectivity, update available, battery low
|
||||
|
||||
# Config sensors
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
# Examples: configuration status
|
||||
```
|
||||
|
||||
## State Inversion
|
||||
|
||||
For some sensors, you may need to invert the logic:
|
||||
|
||||
```python
|
||||
class MyBinarySensor(BinarySensorEntity):
|
||||
"""Binary sensor with inverted state."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if sensor is on."""
|
||||
if self.device.is_closed:
|
||||
return False # Closed = off for door sensor
|
||||
if self.device.is_open:
|
||||
return True # Open = on for door sensor
|
||||
return None
|
||||
```
|
||||
|
||||
## Push-Updated Binary Sensor
|
||||
|
||||
For event-driven sensors:
|
||||
|
||||
```python
|
||||
class MyPushBinarySensor(BinarySensorEntity):
|
||||
"""Push-updated binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to updates when added."""
|
||||
self.async_on_remove(
|
||||
self.device.subscribe_state(self._handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_state_update(self, state: bool) -> None:
|
||||
"""Handle state update from device."""
|
||||
self._attr_is_on = state
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Testing Binary Sensors
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```python
|
||||
"""Test binary sensors."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test binary sensor entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### State Testing
|
||||
|
||||
```python
|
||||
async def test_binary_sensor_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test binary sensor states."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Test on state
|
||||
state = hass.states.get("binary_sensor.my_device_motion")
|
||||
assert state
|
||||
assert state.state == "on"
|
||||
assert state.attributes["device_class"] == "motion"
|
||||
|
||||
# Test off state
|
||||
state = hass.states.get("binary_sensor.my_device_door")
|
||||
assert state
|
||||
assert state.state == "off"
|
||||
assert state.attributes["device_class"] == "door"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Coordinator-Based
|
||||
|
||||
```python
|
||||
class MyBinarySensor(CoordinatorEntity[MyCoordinator], BinarySensorEntity):
|
||||
"""Coordinator-based binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Get state from coordinator data."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.is_active
|
||||
return None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.device_id in self.coordinator.data
|
||||
```
|
||||
|
||||
### Pattern 2: Event-Driven
|
||||
|
||||
```python
|
||||
class MyEventBinarySensor(BinarySensorEntity):
|
||||
"""Event-driven binary sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to events."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_event",
|
||||
self._handle_event,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_type: str, active: bool) -> None:
|
||||
"""Handle incoming event."""
|
||||
if event_type == self.event_type:
|
||||
self._attr_is_on = active
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
### Pattern 3: Calculated/Derived
|
||||
|
||||
```python
|
||||
class MyCalculatedBinarySensor(BinarySensorEntity):
|
||||
"""Binary sensor calculated from other sensors."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to source sensors."""
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
["sensor.temperature", "sensor.humidity"],
|
||||
self._handle_source_update,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_source_update(self, event: Event) -> None:
|
||||
"""Recalculate when sources change."""
|
||||
temp = self.hass.states.get("sensor.temperature")
|
||||
humidity = self.hass.states.get("sensor.humidity")
|
||||
|
||||
if temp and humidity:
|
||||
# Example: high comfort if temp 20-25 and humidity 30-60
|
||||
temp_ok = 20 <= float(temp.state) <= 25
|
||||
humidity_ok = 30 <= float(humidity.state) <= 60
|
||||
self._attr_is_on = temp_ok and humidity_ok
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Use appropriate device classes
|
||||
- Return `None` for unknown state
|
||||
- Use `is_on` property (not state)
|
||||
- Implement unique IDs
|
||||
- Use entity descriptions for similar sensors
|
||||
- Mark diagnostic sensors with entity_category
|
||||
- Use translation keys for entity names
|
||||
- Handle availability properly
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Return strings like "on"/"off" from is_on
|
||||
- Use regular Sensor for binary states
|
||||
- Hardcode entity names
|
||||
- Create binary sensors without device classes (when available)
|
||||
- Use unavailable/unknown as state values
|
||||
- Block the event loop
|
||||
- Poll unnecessarily (use coordinator or events)
|
||||
|
||||
## Disabled by Default
|
||||
|
||||
For less important binary sensors:
|
||||
|
||||
```python
|
||||
class MyConnectivitySensor(BinarySensorEntity):
|
||||
"""Connectivity sensor - diagnostic."""
|
||||
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Binary Sensor Not Appearing
|
||||
|
||||
Check:
|
||||
- [ ] Unique ID is set
|
||||
- [ ] Platform is in PLATFORMS list
|
||||
- [ ] Entity is added with async_add_entities
|
||||
- [ ] is_on returns bool or None (not string)
|
||||
|
||||
### State Not Updating
|
||||
|
||||
Check:
|
||||
- [ ] Coordinator is updating (if used)
|
||||
- [ ] Event subscriptions are working
|
||||
- [ ] is_on returns correct value
|
||||
- [ ] async_write_ha_state() is called (push updates)
|
||||
|
||||
### Wrong Icon
|
||||
|
||||
Check:
|
||||
- [ ] Device class is set correctly
|
||||
- [ ] Device class matches sensor purpose
|
||||
- [ ] Icon translations if using Gold tier
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, device class, entity category
|
||||
- **Platinum**: Full type hints
|
||||
|
||||
## References
|
||||
|
||||
- [Binary Sensor Documentation](https://developers.home-assistant.io/docs/core/entity/binary-sensor)
|
||||
- [Device Classes](https://www.home-assistant.io/integrations/binary_sensor/#device-class)
|
||||
@@ -0,0 +1,459 @@
|
||||
# Button Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Buttons are entities that trigger an action when pressed. They don't have a state (on/off) and are used for one-time actions like rebooting a device, triggering an update, or running a routine.
|
||||
|
||||
## Basic Button Implementation
|
||||
|
||||
```python
|
||||
"""Button platform for my_integration."""
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up buttons."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MyButton(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MyButton(MyEntity, ButtonEntity):
|
||||
"""Representation of a button."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "reboot"
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_reboot"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.coordinator.client.reboot(self.device_id)
|
||||
```
|
||||
|
||||
## Button Method
|
||||
|
||||
The only required method for buttons:
|
||||
|
||||
```python
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.device.trigger_action()
|
||||
```
|
||||
|
||||
**Note**: Buttons don't have state. They only perform an action when pressed.
|
||||
|
||||
## Device Class
|
||||
|
||||
Buttons can have device classes to indicate their purpose:
|
||||
|
||||
```python
|
||||
from homeassistant.components.button import ButtonDeviceClass
|
||||
|
||||
_attr_device_class = ButtonDeviceClass.RESTART
|
||||
_attr_device_class = ButtonDeviceClass.UPDATE
|
||||
_attr_device_class = ButtonDeviceClass.IDENTIFY
|
||||
```
|
||||
|
||||
Device classes:
|
||||
- `RESTART` - Reboot/restart device
|
||||
- `UPDATE` - Trigger update check or installation
|
||||
- `IDENTIFY` - Make device identify itself (blink LED, beep, etc.)
|
||||
|
||||
## Entity Category
|
||||
|
||||
Most buttons are configuration actions:
|
||||
|
||||
```python
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
# Config buttons (device settings/actions)
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
# Examples: reboot, reset, identify
|
||||
|
||||
# Diagnostic buttons (troubleshooting)
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
# Examples: test connection, refresh diagnostics
|
||||
```
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple buttons:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.components.button import ButtonEntityDescription, ButtonDeviceClass
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MyButtonDescription(ButtonEntityDescription):
|
||||
"""Describes a button."""
|
||||
press_fn: Callable[[MyClient, str], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTONS: tuple[MyButtonDescription, ...] = (
|
||||
MyButtonDescription(
|
||||
key="reboot",
|
||||
translation_key="reboot",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda client, device_id: client.reboot(device_id),
|
||||
),
|
||||
MyButtonDescription(
|
||||
key="identify",
|
||||
translation_key="identify",
|
||||
device_class=ButtonDeviceClass.IDENTIFY,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda client, device_id: client.identify(device_id),
|
||||
),
|
||||
MyButtonDescription(
|
||||
key="check_update",
|
||||
translation_key="check_update",
|
||||
device_class=ButtonDeviceClass.UPDATE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda client, device_id: client.check_updates(device_id),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up buttons."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MyButton(coordinator, device_id, description)
|
||||
for device_id in coordinator.data.devices
|
||||
for description in BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class MyButton(MyEntity, ButtonEntity):
|
||||
"""Button using entity description."""
|
||||
|
||||
entity_description: MyButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
device_id: str,
|
||||
description: MyButtonDescription,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.entity_description.press_fn(
|
||||
self.coordinator.client,
|
||||
self.device_id,
|
||||
)
|
||||
```
|
||||
|
||||
## Common Button Types
|
||||
|
||||
### Restart Button
|
||||
|
||||
```python
|
||||
class RestartButton(ButtonEntity):
|
||||
"""Restart device button."""
|
||||
|
||||
_attr_device_class = ButtonDeviceClass.RESTART
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "restart"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Restart the device."""
|
||||
await self.device.restart()
|
||||
```
|
||||
|
||||
### Update Button
|
||||
|
||||
```python
|
||||
class UpdateButton(ButtonEntity):
|
||||
"""Trigger update check button."""
|
||||
|
||||
_attr_device_class = ButtonDeviceClass.UPDATE
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "check_update"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Check for updates."""
|
||||
await self.device.check_for_updates()
|
||||
```
|
||||
|
||||
### Identify Button
|
||||
|
||||
```python
|
||||
class IdentifyButton(ButtonEntity):
|
||||
"""Make device identify itself."""
|
||||
|
||||
_attr_device_class = ButtonDeviceClass.IDENTIFY
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "identify"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger device identification."""
|
||||
await self.device.identify()
|
||||
```
|
||||
|
||||
### Custom Action Button
|
||||
|
||||
```python
|
||||
class CustomButton(ButtonEntity):
|
||||
"""Custom action button."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "run_cycle"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Run cleaning cycle."""
|
||||
await self.device.start_cleaning_cycle()
|
||||
```
|
||||
|
||||
## State Updates After Press
|
||||
|
||||
Buttons trigger coordinator refresh if needed:
|
||||
|
||||
```python
|
||||
async def async_press(self) -> None:
|
||||
"""Handle press with refresh."""
|
||||
await self.coordinator.client.reboot(self.device_id)
|
||||
# Refresh coordinator to update related entities
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Handle errors appropriately:
|
||||
|
||||
```python
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle press with error handling."""
|
||||
try:
|
||||
await self.device.reboot()
|
||||
except DeviceOfflineError as err:
|
||||
raise HomeAssistantError(f"Device is offline: {err}") from err
|
||||
except DeviceError as err:
|
||||
raise HomeAssistantError(f"Failed to reboot: {err}") from err
|
||||
```
|
||||
|
||||
## Testing Buttons
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```python
|
||||
"""Test buttons."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_buttons(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test button entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### Press Testing
|
||||
|
||||
```python
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
|
||||
|
||||
async def test_button_press(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device,
|
||||
) -> None:
|
||||
"""Test button press."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Press button
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: "button.my_device_reboot"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Verify action was called
|
||||
mock_device.reboot.assert_called_once()
|
||||
```
|
||||
|
||||
### Error Testing
|
||||
|
||||
```python
|
||||
async def test_button_press_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device,
|
||||
) -> None:
|
||||
"""Test button press with error."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
mock_device.reboot.side_effect = DeviceError("Connection failed")
|
||||
|
||||
# Press button should raise error
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: "button.my_device_reboot"},
|
||||
blocking=True,
|
||||
)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Simple Action Button
|
||||
|
||||
```python
|
||||
class SimpleButton(ButtonEntity):
|
||||
"""Simple button that triggers action."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger action."""
|
||||
await self.device.do_something()
|
||||
```
|
||||
|
||||
### Pattern 2: Button with Coordinator Refresh
|
||||
|
||||
```python
|
||||
class RefreshingButton(CoordinatorEntity[MyCoordinator], ButtonEntity):
|
||||
"""Button that refreshes coordinator."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger action and refresh."""
|
||||
await self.coordinator.client.action(self.device_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
### Pattern 3: Button with Validation
|
||||
|
||||
```python
|
||||
class ValidatingButton(ButtonEntity):
|
||||
"""Button with pre-action validation."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Validate then trigger action."""
|
||||
if not self.device.is_ready:
|
||||
raise HomeAssistantError("Device not ready")
|
||||
|
||||
await self.device.trigger_action()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Use appropriate device class
|
||||
- Set entity category (usually CONFIG)
|
||||
- Handle errors with HomeAssistantError
|
||||
- Implement unique IDs
|
||||
- Use translation keys
|
||||
- Refresh coordinator if state changes
|
||||
- Provide clear button names/translations
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Create buttons that track state (use switch instead)
|
||||
- Poll buttons (they have no state)
|
||||
- Block the event loop
|
||||
- Ignore errors silently
|
||||
- Create buttons without entity category
|
||||
- Hardcode entity names
|
||||
- Use buttons for binary controls (use switch)
|
||||
|
||||
## Button vs. Switch vs. Service
|
||||
|
||||
**Use Button when**:
|
||||
- One-time action with no state
|
||||
- Trigger command (reboot, identify)
|
||||
- User initiates action
|
||||
|
||||
**Use Switch when**:
|
||||
- Binary control (on/off)
|
||||
- State matters
|
||||
- Can be turned on and off
|
||||
|
||||
**Use Service when**:
|
||||
- Complex parameters needed
|
||||
- Multiple related actions
|
||||
- Integration-wide operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Button Not Appearing
|
||||
|
||||
Check:
|
||||
- [ ] Unique ID is set
|
||||
- [ ] Platform is in PLATFORMS list
|
||||
- [ ] Entity is added with async_add_entities
|
||||
- [ ] async_press is implemented
|
||||
|
||||
### Button Press Not Working
|
||||
|
||||
Check:
|
||||
- [ ] async_press is async def
|
||||
- [ ] Not blocking the event loop
|
||||
- [ ] API client is working
|
||||
- [ ] Errors are being raised properly
|
||||
|
||||
### Button Not in Expected Category
|
||||
|
||||
Check:
|
||||
- [ ] entity_category is set
|
||||
- [ ] Using correct EntityCategory value
|
||||
- [ ] Device class is appropriate
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, device class, entity category
|
||||
- **Platinum**: Full type hints
|
||||
|
||||
## References
|
||||
|
||||
- [Button Documentation](https://developers.home-assistant.io/docs/core/entity/button)
|
||||
- [Button Integration](https://www.home-assistant.io/integrations/button/)
|
||||
@@ -0,0 +1,420 @@
|
||||
# Diagnostics Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Diagnostics provide a way to collect and export integration data for troubleshooting purposes. This is a **Gold tier** quality scale requirement that helps users and developers debug issues.
|
||||
|
||||
## When to Implement Diagnostics
|
||||
|
||||
Diagnostics are required for:
|
||||
- ✅ Gold tier and above integrations
|
||||
- ✅ Any integration where users might need support
|
||||
- ✅ Integrations with complex configuration or state
|
||||
|
||||
## Diagnostics Types
|
||||
|
||||
Home Assistant supports two types of diagnostics:
|
||||
|
||||
### 1. Config Entry Diagnostics
|
||||
Provides data about a specific configuration entry.
|
||||
|
||||
**File**: `diagnostics.py` in your integration folder
|
||||
|
||||
```python
|
||||
"""Diagnostics support for My Integration."""
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
"api_key",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"username",
|
||||
"email",
|
||||
"latitude",
|
||||
"longitude",
|
||||
}
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"entry": {
|
||||
"title": entry.title,
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": async_redact_data(entry.options, TO_REDACT),
|
||||
},
|
||||
"coordinator_data": coordinator.data.to_dict(),
|
||||
"last_update_success": coordinator.last_update_success,
|
||||
"last_update": coordinator.last_update_success_time.isoformat()
|
||||
if coordinator.last_update_success_time
|
||||
else None,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Device Diagnostics
|
||||
Provides data about a specific device.
|
||||
|
||||
```python
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
device: dr.DeviceEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Find device identifier
|
||||
device_id = None
|
||||
for identifier in device.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id is None:
|
||||
return {}
|
||||
|
||||
device_data = coordinator.data.devices.get(device_id)
|
||||
if device_data is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"device_info": {
|
||||
"id": device_id,
|
||||
"name": device_data.name,
|
||||
"model": device_data.model,
|
||||
"firmware": device_data.firmware_version,
|
||||
},
|
||||
"device_data": device_data.to_dict(),
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": entity.entity_id,
|
||||
"name": entity.name,
|
||||
"state": hass.states.get(entity.entity_id).state
|
||||
if (state := hass.states.get(entity.entity_id))
|
||||
else None,
|
||||
}
|
||||
for entity in er.async_entries_for_device(
|
||||
er.async_get(hass), device.id, include_disabled_entities=True
|
||||
)
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Data Redaction
|
||||
|
||||
**CRITICAL**: Always redact sensitive information!
|
||||
|
||||
### What to Redact
|
||||
|
||||
Always redact:
|
||||
- API keys, tokens, secrets
|
||||
- Passwords, credentials
|
||||
- Email addresses, usernames
|
||||
- Precise GPS coordinates (latitude, longitude)
|
||||
- MAC addresses (sometimes)
|
||||
- Serial numbers (if sensitive)
|
||||
- Personal information
|
||||
|
||||
### Using async_redact_data
|
||||
|
||||
```python
|
||||
from homeassistant.helpers import async_redact_data
|
||||
|
||||
# Basic redaction
|
||||
data = async_redact_data(entry.data, TO_REDACT)
|
||||
|
||||
# With nested redaction
|
||||
TO_REDACT = {
|
||||
"api_key",
|
||||
"auth.password", # Nested key
|
||||
"user.email", # Nested key
|
||||
}
|
||||
|
||||
# Redacting from multiple sources
|
||||
diagnostics = {
|
||||
"config": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": async_redact_data(entry.options, TO_REDACT),
|
||||
"coordinator": async_redact_data(coordinator.data, TO_REDACT),
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Redaction
|
||||
|
||||
For complex data structures:
|
||||
|
||||
```python
|
||||
def redact_device_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Redact sensitive device data."""
|
||||
redacted = data.copy()
|
||||
|
||||
# Redact specific fields
|
||||
if "serial_number" in redacted:
|
||||
redacted["serial_number"] = "**REDACTED**"
|
||||
|
||||
# Redact nested structures
|
||||
if "location" in redacted:
|
||||
redacted["location"] = {
|
||||
"city": redacted["location"].get("city"),
|
||||
# Don't include exact coordinates
|
||||
}
|
||||
|
||||
return redacted
|
||||
```
|
||||
|
||||
## What to Include
|
||||
|
||||
### Good Diagnostic Data
|
||||
|
||||
Include information helpful for troubleshooting:
|
||||
- ✅ Integration version/state
|
||||
- ✅ Configuration (redacted)
|
||||
- ✅ Coordinator/connection status
|
||||
- ✅ Device information (model, firmware)
|
||||
- ✅ API response examples (redacted)
|
||||
- ✅ Error states
|
||||
- ✅ Entity states
|
||||
- ✅ Feature flags/capabilities
|
||||
|
||||
### Example Comprehensive Diagnostics
|
||||
|
||||
```python
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
# Integration state
|
||||
"integration": {
|
||||
"version": coordinator.version,
|
||||
"entry_id": entry.entry_id,
|
||||
"title": entry.title,
|
||||
"state": entry.state,
|
||||
},
|
||||
# Configuration (redacted)
|
||||
"configuration": {
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": async_redact_data(entry.options, TO_REDACT),
|
||||
},
|
||||
# Connection/Coordinator status
|
||||
"coordinator": {
|
||||
"last_update_success": coordinator.last_update_success,
|
||||
"last_update": coordinator.last_update_success_time.isoformat()
|
||||
if coordinator.last_update_success_time
|
||||
else None,
|
||||
"update_interval": coordinator.update_interval.total_seconds(),
|
||||
"last_exception": str(coordinator.last_exception)
|
||||
if coordinator.last_exception
|
||||
else None,
|
||||
},
|
||||
# Device/System information
|
||||
"devices": {
|
||||
device_id: {
|
||||
"name": device.name,
|
||||
"model": device.model,
|
||||
"firmware": device.firmware,
|
||||
"features": device.supported_features,
|
||||
"state": device.state,
|
||||
}
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
},
|
||||
# API information (redacted)
|
||||
"api": {
|
||||
"endpoint": coordinator.client.endpoint,
|
||||
"authenticated": coordinator.client.is_authenticated,
|
||||
"rate_limit_remaining": coordinator.client.rate_limit_remaining,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Diagnostics
|
||||
|
||||
### Test File Structure
|
||||
|
||||
```python
|
||||
"""Test diagnostics."""
|
||||
from homeassistant.core import HomeAssistant
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from tests.components.my_integration import setup_integration
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_api,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
# Verify structure
|
||||
assert "entry" in diagnostics
|
||||
assert "coordinator_data" in diagnostics
|
||||
|
||||
# Verify redaction
|
||||
assert "api_key" not in str(diagnostics)
|
||||
assert "password" not in str(diagnostics)
|
||||
|
||||
# Verify useful data is present
|
||||
assert diagnostics["entry"]["title"] == "My Device"
|
||||
assert diagnostics["coordinator_data"]["devices"]
|
||||
|
||||
|
||||
async def test_device_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test device diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "device_id")}
|
||||
)
|
||||
assert device
|
||||
|
||||
diagnostics = await get_diagnostics_for_device(
|
||||
hass, hass_client, mock_config_entry, device
|
||||
)
|
||||
|
||||
# Verify device-specific data
|
||||
assert diagnostics["device_info"]["id"] == "device_id"
|
||||
assert "entities" in diagnostics
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Coordinator-Based Integration
|
||||
|
||||
```python
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"coordinator": {
|
||||
"last_update_success": coordinator.last_update_success,
|
||||
"data": coordinator.data.to_dict(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Multiple Coordinators
|
||||
|
||||
```python
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = entry.runtime_data
|
||||
|
||||
return {
|
||||
"device_coordinator": data.device_coordinator.data.to_dict(),
|
||||
"status_coordinator": data.status_coordinator.data.to_dict(),
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Hub with Multiple Devices
|
||||
|
||||
```python
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
hub = entry.runtime_data
|
||||
|
||||
return {
|
||||
"hub": {
|
||||
"connected": hub.connected,
|
||||
"version": hub.version,
|
||||
},
|
||||
"devices": {
|
||||
device_id: device.to_dict()
|
||||
for device_id, device in hub.devices.items()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Redact all sensitive information
|
||||
- Include coordinator state and update times
|
||||
- Provide device/system information
|
||||
- Include error messages (if present)
|
||||
- Make data easily readable
|
||||
- Test that redaction works
|
||||
- Include API/connection status
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Include raw passwords, tokens, or API keys
|
||||
- Include precise GPS coordinates
|
||||
- Include personal information (emails, names)
|
||||
- Make diagnostics too large (>1MB)
|
||||
- Include binary data
|
||||
- Assume all fields are present (use .get())
|
||||
- Include sensitive serial numbers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Diagnostics Not Appearing
|
||||
|
||||
Check:
|
||||
1. File named `diagnostics.py` in integration folder
|
||||
2. Function named exactly `async_get_config_entry_diagnostics`
|
||||
3. Proper import of `ConfigEntry` and `HomeAssistant`
|
||||
4. Integration is loaded successfully
|
||||
|
||||
### Redaction Not Working
|
||||
|
||||
Check:
|
||||
1. Using `async_redact_data` from `homeassistant.helpers`
|
||||
2. Field names match exactly (case-sensitive)
|
||||
3. Nested fields use dot notation: `"auth.password"`
|
||||
4. TO_REDACT is a set, not a list
|
||||
|
||||
### Device Diagnostics Not Working
|
||||
|
||||
Check:
|
||||
1. Device has proper identifiers
|
||||
2. Function named exactly `async_get_device_diagnostics`
|
||||
3. Device parameter is `dr.DeviceEntry`
|
||||
4. Proper device lookup logic
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
Diagnostics are required for **Gold tier** integrations:
|
||||
- Must implement config entry diagnostics
|
||||
- Should implement device diagnostics (if applicable)
|
||||
- Must redact all sensitive information
|
||||
- Should provide comprehensive troubleshooting data
|
||||
|
||||
## References
|
||||
|
||||
- Quality Scale Rule: `diagnostics`
|
||||
- Home Assistant Docs: [Integration Diagnostics](https://developers.home-assistant.io/docs/integration_fetching_data)
|
||||
- Helper Functions: `homeassistant.helpers.redact`
|
||||
@@ -0,0 +1,508 @@
|
||||
# Number Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Number entities allow users to control numeric values within a defined range. They're used for settings like volume, brightness, temperature setpoints, or any numeric configuration parameter.
|
||||
|
||||
## Basic Number Implementation
|
||||
|
||||
```python
|
||||
"""Number platform for my_integration."""
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up numbers."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MyNumber(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MyNumber(MyEntity, NumberEntity):
|
||||
"""Representation of a number."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "volume"
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 100
|
||||
_attr_native_step = 1
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the number."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_volume"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.volume
|
||||
return None
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self.coordinator.client.set_volume(self.device_id, int(value))
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Number Properties
|
||||
|
||||
### Core Properties
|
||||
|
||||
```python
|
||||
class MyNumber(NumberEntity):
|
||||
"""Number with all common properties."""
|
||||
|
||||
# Basic identification
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "brightness"
|
||||
_attr_unique_id = "device_123_brightness"
|
||||
|
||||
# Value range and step
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 255
|
||||
_attr_native_step = 1 # or 0.1 for decimals
|
||||
|
||||
# Unit of measurement
|
||||
_attr_native_unit_of_measurement = PERCENTAGE # or other units
|
||||
|
||||
# Display mode
|
||||
_attr_mode = NumberMode.SLIDER # or NumberMode.BOX, NumberMode.AUTO
|
||||
|
||||
# Entity category
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return current value."""
|
||||
return self.device.brightness
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self.device.set_brightness(int(value))
|
||||
```
|
||||
|
||||
### Required Properties
|
||||
|
||||
```python
|
||||
# Minimum value
|
||||
_attr_native_min_value = 0
|
||||
|
||||
# Maximum value
|
||||
_attr_native_max_value = 100
|
||||
|
||||
# Step size (precision)
|
||||
_attr_native_step = 1 # Integers
|
||||
_attr_native_step = 0.1 # One decimal place
|
||||
_attr_native_step = 0.01 # Two decimal places
|
||||
```
|
||||
|
||||
### Current Value
|
||||
|
||||
```python
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
return self.device.current_value
|
||||
|
||||
# Or use attribute
|
||||
_attr_native_value = 50.0
|
||||
```
|
||||
|
||||
### Set Value Method
|
||||
|
||||
```python
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update to new value."""
|
||||
await self.device.set_value(value)
|
||||
# Update state
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Display Mode
|
||||
|
||||
Control how the number is displayed in the UI:
|
||||
|
||||
```python
|
||||
from homeassistant.components.number import NumberMode
|
||||
|
||||
# Slider (default for ranges)
|
||||
_attr_mode = NumberMode.SLIDER
|
||||
|
||||
# Input box (better for precise values or large ranges)
|
||||
_attr_mode = NumberMode.BOX
|
||||
|
||||
# Auto (let HA decide based on range)
|
||||
_attr_mode = NumberMode.AUTO
|
||||
```
|
||||
|
||||
**When to use each**:
|
||||
- `SLIDER`: Small ranges (0-100), settings like volume/brightness
|
||||
- `BOX`: Large ranges, precise values, IDs or codes
|
||||
- `AUTO`: Let Home Assistant decide (default)
|
||||
|
||||
## Device Class
|
||||
|
||||
Use device classes for proper representation:
|
||||
|
||||
```python
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
|
||||
# Common device classes
|
||||
_attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
_attr_device_class = NumberDeviceClass.HUMIDITY
|
||||
_attr_device_class = NumberDeviceClass.VOLTAGE
|
||||
_attr_device_class = NumberDeviceClass.CURRENT
|
||||
_attr_device_class = NumberDeviceClass.POWER
|
||||
_attr_device_class = NumberDeviceClass.BATTERY
|
||||
_attr_device_class = NumberDeviceClass.DISTANCE
|
||||
_attr_device_class = NumberDeviceClass.DURATION
|
||||
```
|
||||
|
||||
## Units of Measurement
|
||||
|
||||
```python
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
|
||||
# Percentage (0-100)
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
# Temperature
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
|
||||
# Time
|
||||
_attr_native_unit_of_measurement = UnitOfTime.SECONDS
|
||||
|
||||
# Custom units
|
||||
_attr_native_unit_of_measurement = "dB" # Decibels
|
||||
```
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple number entities:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.components.number import NumberEntityDescription, NumberMode
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MyNumberDescription(NumberEntityDescription):
|
||||
"""Describes a number."""
|
||||
value_fn: Callable[[MyData], float | None]
|
||||
set_fn: Callable[[MyClient, str, float], Awaitable[None]]
|
||||
|
||||
|
||||
NUMBERS: tuple[MyNumberDescription, ...] = (
|
||||
MyNumberDescription(
|
||||
key="volume",
|
||||
translation_key="volume",
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_step=1,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
mode=NumberMode.SLIDER,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda data: data.volume,
|
||||
set_fn=lambda client, device_id, value: client.set_volume(device_id, int(value)),
|
||||
),
|
||||
MyNumberDescription(
|
||||
key="temperature_setpoint",
|
||||
translation_key="temperature_setpoint",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
native_min_value=16,
|
||||
native_max_value=30,
|
||||
native_step=0.5,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
mode=NumberMode.SLIDER,
|
||||
value_fn=lambda data: data.target_temperature,
|
||||
set_fn=lambda client, device_id, value: client.set_temperature(device_id, value),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MyNumber(MyEntity, NumberEntity):
|
||||
"""Number using entity description."""
|
||||
|
||||
entity_description: MyNumberDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
device_id: str,
|
||||
description: MyNumberDescription,
|
||||
) -> None:
|
||||
"""Initialize the number."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return current value."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return self.entity_description.value_fn(device)
|
||||
return None
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self.entity_description.set_fn(
|
||||
self.coordinator.client,
|
||||
self.device_id,
|
||||
value,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Value Validation
|
||||
|
||||
Home Assistant validates against min/max/step, but you can add custom validation:
|
||||
|
||||
```python
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set value with custom validation."""
|
||||
# Custom validation
|
||||
if value % 5 != 0:
|
||||
raise ValueError("Value must be multiple of 5")
|
||||
|
||||
await self.device.set_value(value)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## State Update Patterns
|
||||
|
||||
### Pattern 1: Optimistic Update
|
||||
|
||||
```python
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set value with optimistic update."""
|
||||
# Update immediately
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
try:
|
||||
await self.device.set_value(value)
|
||||
except DeviceError:
|
||||
# Revert on error
|
||||
await self.coordinator.async_request_refresh()
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 2: Coordinator Refresh
|
||||
|
||||
```python
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set value and refresh."""
|
||||
await self.device.set_value(value)
|
||||
# Get actual value from device
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
### Pattern 3: Direct State Update
|
||||
|
||||
```python
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set value with direct state update."""
|
||||
new_value = await self.device.set_value(value)
|
||||
# Device returns actual value
|
||||
self._attr_native_value = new_value
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Testing Numbers
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```python
|
||||
"""Test numbers."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_numbers(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test number entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### Value Testing
|
||||
|
||||
```python
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_value(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device,
|
||||
) -> None:
|
||||
"""Test setting number value."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Check initial value
|
||||
state = hass.states.get("number.my_device_volume")
|
||||
assert state
|
||||
assert state.state == "50"
|
||||
assert state.attributes["min"] == 0
|
||||
assert state.attributes["max"] == 100
|
||||
assert state.attributes["step"] == 1
|
||||
|
||||
# Set new value
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "number.my_device_volume",
|
||||
ATTR_VALUE: 75,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_device.set_volume.assert_called_once_with(75)
|
||||
|
||||
# Verify state updated
|
||||
state = hass.states.get("number.my_device_volume")
|
||||
assert state.state == "75"
|
||||
```
|
||||
|
||||
## Common Number Types
|
||||
|
||||
### Volume Control
|
||||
|
||||
```python
|
||||
class VolumeNumber(NumberEntity):
|
||||
"""Volume control."""
|
||||
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 100
|
||||
_attr_native_step = 1
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_mode = NumberMode.SLIDER
|
||||
```
|
||||
|
||||
### Temperature Setpoint
|
||||
|
||||
```python
|
||||
class TemperatureNumber(NumberEntity):
|
||||
"""Temperature setpoint."""
|
||||
|
||||
_attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
_attr_native_min_value = 16.0
|
||||
_attr_native_max_value = 30.0
|
||||
_attr_native_step = 0.5
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
_attr_mode = NumberMode.SLIDER
|
||||
```
|
||||
|
||||
### Duration Setting
|
||||
|
||||
```python
|
||||
class DurationNumber(NumberEntity):
|
||||
"""Duration setting."""
|
||||
|
||||
_attr_device_class = NumberDeviceClass.DURATION
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 3600
|
||||
_attr_native_step = 60 # 1 minute steps
|
||||
_attr_native_unit_of_measurement = UnitOfTime.SECONDS
|
||||
_attr_mode = NumberMode.BOX
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Set appropriate min/max/step values
|
||||
- Use device class when available
|
||||
- Use standard units
|
||||
- Set display mode appropriately
|
||||
- Implement unique IDs
|
||||
- Use translation keys
|
||||
- Mark config numbers with entity_category
|
||||
- Handle value updates properly
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Allow invalid ranges (min > max)
|
||||
- Use zero or negative step
|
||||
- Block the event loop
|
||||
- Ignore validation errors
|
||||
- Create numbers without min/max/step
|
||||
- Hardcode entity names
|
||||
- Use for binary values (use switch)
|
||||
- Use for selection from list (use select)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Number Not Appearing
|
||||
|
||||
Check:
|
||||
- [ ] Unique ID is set
|
||||
- [ ] Platform is in PLATFORMS list
|
||||
- [ ] min/max/step are all set
|
||||
- [ ] Entity is added with async_add_entities
|
||||
|
||||
### Value Not Updating
|
||||
|
||||
Check:
|
||||
- [ ] async_set_native_value is called
|
||||
- [ ] Coordinator refresh is working
|
||||
- [ ] native_value returns correct value
|
||||
- [ ] Value is within min/max range
|
||||
|
||||
### UI Shows Wrong Control Type
|
||||
|
||||
Check:
|
||||
- [ ] mode is set correctly
|
||||
- [ ] Range is appropriate for mode
|
||||
- [ ] Step size is reasonable
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, device class, entity category
|
||||
- **Platinum**: Full type hints
|
||||
|
||||
## References
|
||||
|
||||
- [Number Documentation](https://developers.home-assistant.io/docs/core/entity/number)
|
||||
- [Number Integration](https://www.home-assistant.io/integrations/number/)
|
||||
@@ -0,0 +1,520 @@
|
||||
# Select Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Select entities allow users to choose from a predefined list of options. They're used for settings like operation modes, presets, input sources, or any configuration with a fixed set of choices.
|
||||
|
||||
## Basic Select Implementation
|
||||
|
||||
```python
|
||||
"""Select platform for my_integration."""
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up selects."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MySelect(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MySelect(MyEntity, SelectEntity):
|
||||
"""Representation of a select."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "operation_mode"
|
||||
_attr_options = ["auto", "cool", "heat", "fan"]
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the select."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_mode"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current selected option."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.mode
|
||||
return None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.coordinator.client.set_mode(self.device_id, option)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Select Properties
|
||||
|
||||
### Core Properties
|
||||
|
||||
```python
|
||||
class MySelect(SelectEntity):
|
||||
"""Select with all common properties."""
|
||||
|
||||
# Basic identification
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "preset"
|
||||
_attr_unique_id = "device_123_preset"
|
||||
|
||||
# Available options (required)
|
||||
_attr_options = ["comfort", "eco", "away", "sleep"]
|
||||
|
||||
# Entity category
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return current selected option."""
|
||||
return self.device.preset
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the selected option."""
|
||||
await self.device.set_preset(option)
|
||||
```
|
||||
|
||||
### Required Properties and Methods
|
||||
|
||||
```python
|
||||
# List of available options
|
||||
_attr_options = ["option1", "option2", "option3"]
|
||||
|
||||
# Current selected option
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected option."""
|
||||
return self.device.current_mode
|
||||
|
||||
# Or use attribute
|
||||
_attr_current_option = "option1"
|
||||
|
||||
# Method to change option
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.device.set_option(option)
|
||||
```
|
||||
|
||||
## Using Enums for Options
|
||||
|
||||
Recommended pattern for type safety:
|
||||
|
||||
```python
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class OperationMode(StrEnum):
|
||||
"""Operation modes."""
|
||||
AUTO = "auto"
|
||||
COOL = "cool"
|
||||
HEAT = "heat"
|
||||
FAN = "fan"
|
||||
|
||||
|
||||
class MySelect(SelectEntity):
|
||||
"""Select using enum."""
|
||||
|
||||
_attr_options = [mode.value for mode in OperationMode]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return current mode."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.mode
|
||||
return None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set mode."""
|
||||
# Validate option is in enum
|
||||
mode = OperationMode(option)
|
||||
await self.coordinator.client.set_mode(self.device_id, mode)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple select entities:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.components.select import SelectEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MySelectDescription(SelectEntityDescription):
|
||||
"""Describes a select."""
|
||||
current_fn: Callable[[MyData], str | None]
|
||||
select_fn: Callable[[MyClient, str, str], Awaitable[None]]
|
||||
|
||||
|
||||
SELECTS: tuple[MySelectDescription, ...] = (
|
||||
MySelectDescription(
|
||||
key="mode",
|
||||
translation_key="operation_mode",
|
||||
options=["auto", "cool", "heat", "fan"],
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
current_fn=lambda data: data.mode,
|
||||
select_fn=lambda client, device_id, option: client.set_mode(device_id, option),
|
||||
),
|
||||
MySelectDescription(
|
||||
key="preset",
|
||||
translation_key="preset",
|
||||
options=["comfort", "eco", "away", "sleep"],
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
current_fn=lambda data: data.preset,
|
||||
select_fn=lambda client, device_id, option: client.set_preset(device_id, option),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up selects."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MySelect(coordinator, device_id, description)
|
||||
for device_id in coordinator.data.devices
|
||||
for description in SELECTS
|
||||
)
|
||||
|
||||
|
||||
class MySelect(MyEntity, SelectEntity):
|
||||
"""Select using entity description."""
|
||||
|
||||
entity_description: MySelectDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
device_id: str,
|
||||
description: MySelectDescription,
|
||||
) -> None:
|
||||
"""Initialize the select."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return current option."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return self.entity_description.current_fn(device)
|
||||
return None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select option."""
|
||||
await self.entity_description.select_fn(
|
||||
self.coordinator.client,
|
||||
self.device_id,
|
||||
option,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Dynamic Options
|
||||
|
||||
If options change based on device state:
|
||||
|
||||
```python
|
||||
class MyDynamicSelect(SelectEntity):
|
||||
"""Select with dynamic options."""
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return available options based on device state."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.available_modes
|
||||
return []
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return current option."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.current_mode
|
||||
return None
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select option."""
|
||||
await self.device.set_mode(option)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Option Translation
|
||||
|
||||
Use translation keys for user-friendly option labels:
|
||||
|
||||
```json
|
||||
// strings.json
|
||||
{
|
||||
"entity": {
|
||||
"select": {
|
||||
"operation_mode": {
|
||||
"name": "Operation mode",
|
||||
"state": {
|
||||
"auto": "Automatic",
|
||||
"cool": "Cooling",
|
||||
"heat": "Heating",
|
||||
"fan": "Fan only"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
class MySelect(SelectEntity):
|
||||
"""Select with translated options."""
|
||||
|
||||
_attr_translation_key = "operation_mode"
|
||||
_attr_options = ["auto", "cool", "heat", "fan"]
|
||||
```
|
||||
|
||||
## State Update Patterns
|
||||
|
||||
### Pattern 1: Optimistic Update
|
||||
|
||||
```python
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select option with optimistic update."""
|
||||
# Update immediately
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
|
||||
try:
|
||||
await self.device.set_option(option)
|
||||
except DeviceError:
|
||||
# Revert on error
|
||||
await self.coordinator.async_request_refresh()
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 2: Coordinator Refresh
|
||||
|
||||
```python
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select option and refresh."""
|
||||
await self.device.set_option(option)
|
||||
# Get actual option from device
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
### Pattern 3: Direct State Update
|
||||
|
||||
```python
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select option with direct state update."""
|
||||
actual_option = await self.device.set_option(option)
|
||||
# Device returns actual option
|
||||
self._attr_current_option = actual_option
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Testing Selects
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```python
|
||||
"""Test selects."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_selects(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test select entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### Option Selection Testing
|
||||
|
||||
```python
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION
|
||||
|
||||
|
||||
async def test_select_option(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device,
|
||||
) -> None:
|
||||
"""Test selecting an option."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Check initial state
|
||||
state = hass.states.get("select.my_device_mode")
|
||||
assert state
|
||||
assert state.state == "auto"
|
||||
assert state.attributes["options"] == ["auto", "cool", "heat", "fan"]
|
||||
|
||||
# Select new option
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.my_device_mode",
|
||||
ATTR_OPTION: "cool",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_device.set_mode.assert_called_once_with("cool")
|
||||
|
||||
# Verify state updated
|
||||
state = hass.states.get("select.my_device_mode")
|
||||
assert state.state == "cool"
|
||||
```
|
||||
|
||||
## Common Select Types
|
||||
|
||||
### Operation Mode
|
||||
|
||||
```python
|
||||
class ModeSelect(SelectEntity):
|
||||
"""Operation mode select."""
|
||||
|
||||
_attr_translation_key = "operation_mode"
|
||||
_attr_options = ["auto", "cool", "heat", "fan", "dry"]
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
```
|
||||
|
||||
### Preset
|
||||
|
||||
```python
|
||||
class PresetSelect(SelectEntity):
|
||||
"""Preset select."""
|
||||
|
||||
_attr_translation_key = "preset"
|
||||
_attr_options = ["comfort", "eco", "away", "sleep", "boost"]
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
```
|
||||
|
||||
### Input Source
|
||||
|
||||
```python
|
||||
class InputSourceSelect(SelectEntity):
|
||||
"""Input source select."""
|
||||
|
||||
_attr_translation_key = "source"
|
||||
_attr_options = ["hdmi1", "hdmi2", "usb", "bluetooth", "optical"]
|
||||
```
|
||||
|
||||
### Effect/Scene
|
||||
|
||||
```python
|
||||
class EffectSelect(SelectEntity):
|
||||
"""Light effect select."""
|
||||
|
||||
_attr_translation_key = "effect"
|
||||
_attr_options = ["none", "rainbow", "pulse", "strobe", "breathe"]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Use enums for type safety
|
||||
- Provide translation keys for options
|
||||
- Validate selected options
|
||||
- Implement unique IDs
|
||||
- Use entity_category for config selects
|
||||
- Keep option lists reasonable (<20 items)
|
||||
- Use consistent option naming (lowercase, underscores)
|
||||
- Provide clear option translations
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Accept options not in the list
|
||||
- Have too many options (use input_select helper instead)
|
||||
- Block the event loop
|
||||
- Hardcode entity names
|
||||
- Change options list arbitrarily
|
||||
- Use for numeric values (use number entity)
|
||||
- Use for binary choices (use switch)
|
||||
- Have empty options list
|
||||
|
||||
## Select vs. Other Entities
|
||||
|
||||
**Use Select when**:
|
||||
- Fixed list of text options
|
||||
- Modes, presets, or settings
|
||||
- 2-20 options
|
||||
|
||||
**Use Switch when**:
|
||||
- Binary on/off control
|
||||
- Only 2 states
|
||||
|
||||
**Use Number when**:
|
||||
- Numeric range
|
||||
- Continuous values
|
||||
|
||||
**Use Input Select when**:
|
||||
- User-defined options
|
||||
- Need dynamic option list
|
||||
- Helper/template integration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Select Not Appearing
|
||||
|
||||
Check:
|
||||
- [ ] Unique ID is set
|
||||
- [ ] Platform is in PLATFORMS list
|
||||
- [ ] options list is not empty
|
||||
- [ ] Entity is added with async_add_entities
|
||||
|
||||
### Option Not Accepted
|
||||
|
||||
Check:
|
||||
- [ ] Option is in options list (case-sensitive)
|
||||
- [ ] Options list is properly formatted
|
||||
- [ ] async_select_option handles the option
|
||||
|
||||
### Options Not Translating
|
||||
|
||||
Check:
|
||||
- [ ] translation_key is set
|
||||
- [ ] strings.json has state translations
|
||||
- [ ] Option keys match exactly
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, entity category
|
||||
- **Platinum**: Full type hints, use StrEnum for options
|
||||
|
||||
## References
|
||||
|
||||
- [Select Documentation](https://developers.home-assistant.io/docs/core/entity/select)
|
||||
- [Select Integration](https://www.home-assistant.io/integrations/select/)
|
||||
@@ -0,0 +1,560 @@
|
||||
# Sensor Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Sensors are read-only entities that represent measurements, states, or information from devices and services. They display numeric values, strings, timestamps, or other data types.
|
||||
|
||||
## Basic Sensor Implementation
|
||||
|
||||
### Minimal Sensor
|
||||
|
||||
```python
|
||||
"""Sensor platform for my_integration."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MySensor(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MySensor(MyEntity, SensorEntity):
|
||||
"""Representation of a sensor."""
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_temperature"
|
||||
self._attr_translation_key = "temperature"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the sensor value."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.temperature
|
||||
return None
|
||||
```
|
||||
|
||||
## Sensor Properties
|
||||
|
||||
### Core Properties
|
||||
|
||||
```python
|
||||
class MySensor(SensorEntity):
|
||||
"""Sensor with all common properties."""
|
||||
|
||||
# Basic identification
|
||||
_attr_has_entity_name = True # Required
|
||||
_attr_translation_key = "temperature" # For translations
|
||||
_attr_unique_id = "device_123_temp" # Required
|
||||
|
||||
# Device class and units
|
||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
_attr_suggested_display_precision = 1 # Decimal places
|
||||
|
||||
# State class for statistics
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
# Entity category
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC # If diagnostic
|
||||
|
||||
# Availability
|
||||
_attr_entity_registry_enabled_default = False # If noisy/less important
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return sensor value."""
|
||||
return self.device.temperature
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.device_id in self.coordinator.data
|
||||
```
|
||||
|
||||
## Device Classes
|
||||
|
||||
Use device classes for proper representation:
|
||||
|
||||
```python
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
|
||||
# Common device classes
|
||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_device_class = SensorDeviceClass.PRESSURE
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_device_class = SensorDeviceClass.VOLTAGE
|
||||
_attr_device_class = SensorDeviceClass.CURRENT
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_device_class = SensorDeviceClass.MONETARY
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Automatic unit conversion
|
||||
- Proper UI representation
|
||||
- Voice assistant integration
|
||||
- Historical statistics
|
||||
|
||||
## State Classes
|
||||
|
||||
For long-term statistics support:
|
||||
|
||||
```python
|
||||
from homeassistant.components.sensor import SensorStateClass
|
||||
|
||||
# Measurement - value at a point in time
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
# Examples: temperature, humidity, power
|
||||
|
||||
# Total - cumulative value that can increase/decrease
|
||||
_attr_state_class = SensorStateClass.TOTAL
|
||||
# Examples: energy consumed, data transferred
|
||||
# Use with last_reset for resettable totals
|
||||
|
||||
# Total increasing - cumulative value that only increases
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
# Examples: lifetime energy, odometer
|
||||
```
|
||||
|
||||
### When to Use State Classes
|
||||
|
||||
✅ **Use MEASUREMENT for**:
|
||||
- Temperature, humidity, pressure
|
||||
- Current power usage
|
||||
- Instantaneous values
|
||||
|
||||
✅ **Use TOTAL for**:
|
||||
- Daily/monthly energy consumption (resets)
|
||||
- Periodic counters
|
||||
|
||||
✅ **Use TOTAL_INCREASING for**:
|
||||
- Lifetime energy consumption
|
||||
- Monotonically increasing counters
|
||||
|
||||
❌ **Don't use state class for**:
|
||||
- Text/string sensors
|
||||
- Status sensors (enum values)
|
||||
- Non-numeric sensors
|
||||
|
||||
## Unit of Measurement
|
||||
|
||||
### Using Standard Units
|
||||
|
||||
```python
|
||||
from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
UnitOfPower,
|
||||
UnitOfEnergy,
|
||||
PERCENTAGE,
|
||||
)
|
||||
|
||||
# Temperature
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
# Auto-converts to user's preference (°F/°C/K)
|
||||
|
||||
# Power
|
||||
_attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
|
||||
# Energy
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
# Percentage
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
```
|
||||
|
||||
### Custom Units
|
||||
|
||||
```python
|
||||
# For non-standard units
|
||||
_attr_native_unit_of_measurement = "AQI" # Air Quality Index
|
||||
_attr_native_unit_of_measurement = "ppm" # Parts per million
|
||||
```
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple similar sensors, use SensorEntityDescription:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MySensorDescription(SensorEntityDescription):
|
||||
"""Describes a sensor."""
|
||||
value_fn: Callable[[MyData], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[MySensorDescription, ...] = (
|
||||
MySensorDescription(
|
||||
key="temperature",
|
||||
translation_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.temperature,
|
||||
),
|
||||
MySensorDescription(
|
||||
key="humidity",
|
||||
translation_key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.humidity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MySensor(MyEntity, SensorEntity):
|
||||
"""Sensor using entity description."""
|
||||
|
||||
entity_description: MySensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
description: MySensorDescription,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the sensor value."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return self.entity_description.value_fn(device)
|
||||
return None
|
||||
```
|
||||
|
||||
### Lambda Functions in EntityDescription
|
||||
|
||||
When lambdas get long, use proper formatting:
|
||||
|
||||
```python
|
||||
# ❌ Bad - too long
|
||||
SensorEntityDescription(
|
||||
key="temperature",
|
||||
value_fn=lambda data: round(data["temp_value"] * 1.8 + 32, 1) if data.get("temp_value") is not None else None,
|
||||
)
|
||||
|
||||
# ✅ Good - wrapped properly
|
||||
SensorEntityDescription(
|
||||
key="temperature",
|
||||
value_fn=lambda data: (
|
||||
round(data["temp_value"] * 1.8 + 32, 1)
|
||||
if data.get("temp_value") is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Timestamp Sensors
|
||||
|
||||
For datetime values:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
|
||||
class MyTimestampSensor(SensorEntity):
|
||||
"""Timestamp sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return timestamp."""
|
||||
return self.device.last_update
|
||||
```
|
||||
|
||||
## Enum Sensors
|
||||
|
||||
For sensors with fixed set of possible values:
|
||||
|
||||
```python
|
||||
from enum import StrEnum
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
|
||||
class OperationMode(StrEnum):
|
||||
"""Operation modes."""
|
||||
AUTO = "auto"
|
||||
MANUAL = "manual"
|
||||
ECO = "eco"
|
||||
|
||||
|
||||
class MyModeSensor(SensorEntity):
|
||||
"""Mode sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENUM
|
||||
_attr_options = [mode.value for mode in OperationMode]
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return current mode."""
|
||||
return self.device.mode
|
||||
```
|
||||
|
||||
## Entity Category
|
||||
|
||||
Mark diagnostic or configuration sensors:
|
||||
|
||||
```python
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
# Diagnostic sensors (technical info)
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
# Examples: signal strength, uptime, IP address
|
||||
|
||||
# Config sensors (device settings)
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
# Examples: current mode setting, configuration values
|
||||
```
|
||||
|
||||
## Disabled by Default
|
||||
|
||||
For noisy or less important sensors:
|
||||
|
||||
```python
|
||||
class MySignalStrengthSensor(SensorEntity):
|
||||
"""Signal strength sensor - noisy."""
|
||||
|
||||
_attr_entity_registry_enabled_default = False
|
||||
```
|
||||
|
||||
## Dynamic Sensor Addition
|
||||
|
||||
For devices that appear after setup:
|
||||
|
||||
```python
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors with dynamic addition."""
|
||||
coordinator = entry.runtime_data
|
||||
known_devices: set[str] = set()
|
||||
|
||||
@callback
|
||||
def _add_new_devices() -> None:
|
||||
"""Add newly discovered devices."""
|
||||
current_devices = set(coordinator.data.devices.keys())
|
||||
new_devices = current_devices - known_devices
|
||||
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
MySensor(coordinator, device_id)
|
||||
for device_id in new_devices
|
||||
)
|
||||
|
||||
# Initial setup
|
||||
_add_new_devices()
|
||||
|
||||
# Listen for new devices
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_new_devices))
|
||||
```
|
||||
|
||||
## Testing Sensors
|
||||
|
||||
### Test with Snapshots
|
||||
|
||||
```python
|
||||
"""Test sensors."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test sensor entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### Test Sensor Values
|
||||
|
||||
```python
|
||||
async def test_sensor_values(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test sensor values are correct."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("sensor.my_device_temperature")
|
||||
assert state
|
||||
assert state.state == "22.5"
|
||||
assert state.attributes["unit_of_measurement"] == "°C"
|
||||
assert state.attributes["device_class"] == "temperature"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Use device classes when available
|
||||
- Set state classes for statistics
|
||||
- Use standard units of measurement
|
||||
- Implement unique IDs
|
||||
- Use entity descriptions for similar sensors
|
||||
- Mark diagnostic sensors with entity_category
|
||||
- Disable noisy sensors by default
|
||||
- Return None for unknown values
|
||||
- Use translation keys for entity names
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Hardcode entity names
|
||||
- Use string "unavailable" or "unknown" as state
|
||||
- Mix units (always use native_unit_of_measurement)
|
||||
- Create sensors without unique IDs
|
||||
- Poll in sensor update if using coordinator
|
||||
- Block the event loop
|
||||
- Use state class for non-numeric sensors
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Coordinator-Based Sensor
|
||||
|
||||
```python
|
||||
class MySensor(CoordinatorEntity[MyCoordinator], SensorEntity):
|
||||
"""Coordinator-based sensor."""
|
||||
|
||||
_attr_should_poll = False # Coordinator handles updates
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Get value from coordinator data."""
|
||||
return self.coordinator.data.get(self.key)
|
||||
```
|
||||
|
||||
### Pattern 2: Push-Updated Sensor
|
||||
|
||||
```python
|
||||
class MyPushSensor(SensorEntity):
|
||||
"""Push-updated sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to updates."""
|
||||
self.async_on_remove(
|
||||
self.device.subscribe(self._handle_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_update(self, value: float) -> None:
|
||||
"""Handle push update."""
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
### Pattern 3: Calculated Sensor
|
||||
|
||||
```python
|
||||
class MyCalculatedSensor(SensorEntity):
|
||||
"""Calculated from other sensors."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to source sensors."""
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
["sensor.source1", "sensor.source2"],
|
||||
self._handle_update,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_update(self, event: Event) -> None:
|
||||
"""Recalculate when sources change."""
|
||||
# Calculate new value
|
||||
self._attr_native_value = self._calculate()
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sensor Not Appearing
|
||||
|
||||
Check:
|
||||
- [ ] Unique ID is set
|
||||
- [ ] Platform is in PLATFORMS list
|
||||
- [ ] async_setup_entry is called
|
||||
- [ ] Entity is added with async_add_entities
|
||||
|
||||
### Values Not Updating
|
||||
|
||||
Check:
|
||||
- [ ] Coordinator is updating
|
||||
- [ ] Entity is available
|
||||
- [ ] native_value returns correct data
|
||||
- [ ] should_poll is False for coordinator
|
||||
|
||||
### Units Not Converting
|
||||
|
||||
Check:
|
||||
- [ ] Using standard unit constants
|
||||
- [ ] Device class is set correctly
|
||||
- [ ] Unit matches device class
|
||||
|
||||
### Statistics Not Working
|
||||
|
||||
Check:
|
||||
- [ ] State class is set
|
||||
- [ ] Values are numeric
|
||||
- [ ] Device class is appropriate
|
||||
- [ ] Units are consistent
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, device class, entity category
|
||||
- **Platinum**: Full type hints
|
||||
|
||||
## References
|
||||
|
||||
- [Sensor Documentation](https://developers.home-assistant.io/docs/core/entity/sensor)
|
||||
- [Device Classes](https://www.home-assistant.io/integrations/sensor/#device-class)
|
||||
- [State Classes](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes)
|
||||
@@ -0,0 +1,505 @@
|
||||
# Switch Platform Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Switches are entities that can be turned on or off. They represent controllable devices like smart plugs, relays, or any binary control. Unlike binary sensors, switches can be controlled by the user.
|
||||
|
||||
## Basic Switch Implementation
|
||||
|
||||
```python
|
||||
"""Switch platform for my_integration."""
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MyConfigEntry
|
||||
from .coordinator import MyCoordinator
|
||||
from .entity import MyEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MySwitch(coordinator, device_id)
|
||||
for device_id in coordinator.data.devices
|
||||
)
|
||||
|
||||
|
||||
class MySwitch(MyEntity, SwitchEntity):
|
||||
"""Representation of a switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "outlet"
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = f"{device_id}_switch"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if switch is on."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.is_on
|
||||
return None
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self.coordinator.client.turn_on(self.device_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self.coordinator.client.turn_off(self.device_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Switch Properties and Methods
|
||||
|
||||
### Core Properties
|
||||
|
||||
```python
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if entity is on."""
|
||||
return self.device.state
|
||||
|
||||
# Or use attribute
|
||||
_attr_is_on = True # or False, or None
|
||||
```
|
||||
|
||||
### Required Methods
|
||||
|
||||
```python
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.device.turn_on()
|
||||
# Update state
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.device.turn_off()
|
||||
# Update state
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
### Optional Toggle Method
|
||||
|
||||
```python
|
||||
async def async_toggle(self, **kwargs: Any) -> None:
|
||||
"""Toggle the entity."""
|
||||
# Only implement if device has native toggle
|
||||
await self.device.toggle()
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
**Note**: If `async_toggle` is not implemented, Home Assistant will use `async_turn_on`/`async_turn_off` based on current state.
|
||||
|
||||
## Device Class
|
||||
|
||||
Switches can have device classes to indicate their type:
|
||||
|
||||
```python
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.OUTLET
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
```
|
||||
|
||||
Device classes:
|
||||
- `OUTLET` - Smart plug/outlet
|
||||
- `SWITCH` - Generic switch (default)
|
||||
|
||||
## State Update Patterns
|
||||
|
||||
### Pattern 1: Optimistic Update
|
||||
|
||||
For fast UI response:
|
||||
|
||||
```python
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
# Update state immediately (optimistic)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
try:
|
||||
await self.coordinator.client.turn_on(self.device_id)
|
||||
except DeviceError:
|
||||
# Revert on error
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 2: Coordinator Refresh
|
||||
|
||||
Wait for actual state:
|
||||
|
||||
```python
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.coordinator.client.turn_on(self.device_id)
|
||||
# Refresh coordinator to get actual state
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
### Pattern 3: Push Update
|
||||
|
||||
For push-based systems:
|
||||
|
||||
```python
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
# Command device
|
||||
await self.device.turn_on()
|
||||
# State will be updated via push event
|
||||
# No need to call async_write_ha_state()
|
||||
```
|
||||
|
||||
## Entity Descriptions Pattern
|
||||
|
||||
For multiple similar switches:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.components.switch import SwitchEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MySwitchDescription(SwitchEntityDescription):
|
||||
"""Describes a switch."""
|
||||
is_on_fn: Callable[[MyData], bool | None]
|
||||
turn_on_fn: Callable[[MyClient, str], Awaitable[None]]
|
||||
turn_off_fn: Callable[[MyClient, str], Awaitable[None]]
|
||||
|
||||
|
||||
SWITCHES: tuple[MySwitchDescription, ...] = (
|
||||
MySwitchDescription(
|
||||
key="outlet",
|
||||
translation_key="outlet",
|
||||
device_class=SwitchDeviceClass.OUTLET,
|
||||
is_on_fn=lambda data: data.outlet_state,
|
||||
turn_on_fn=lambda client, device_id: client.turn_on_outlet(device_id),
|
||||
turn_off_fn=lambda client, device_id: client.turn_off_outlet(device_id),
|
||||
),
|
||||
MySwitchDescription(
|
||||
key="led",
|
||||
translation_key="led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda data: data.led_enabled,
|
||||
turn_on_fn=lambda client, device_id: client.enable_led(device_id),
|
||||
turn_off_fn=lambda client, device_id: client.disable_led(device_id),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MySwitch(MyEntity, SwitchEntity):
|
||||
"""Switch using entity description."""
|
||||
|
||||
entity_description: MySwitchDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MyCoordinator,
|
||||
device_id: str,
|
||||
description: MySwitchDescription,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return if switch is on."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return self.entity_description.is_on_fn(device)
|
||||
return None
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.entity_description.turn_on_fn(
|
||||
self.coordinator.client,
|
||||
self.device_id,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.entity_description.turn_off_fn(
|
||||
self.coordinator.client,
|
||||
self.device_id,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Configuration Switches
|
||||
|
||||
Switches that control device settings (not physical devices):
|
||||
|
||||
```python
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
class MyConfigSwitch(SwitchEntity):
|
||||
"""Configuration switch."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "led_indicator"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if LED is enabled."""
|
||||
return self.device.led_enabled
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Enable LED indicator."""
|
||||
await self.device.set_led(True)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Disable LED indicator."""
|
||||
await self.device.set_led(False)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Handle errors gracefully:
|
||||
|
||||
```python
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on with error handling."""
|
||||
try:
|
||||
await self.device.turn_on()
|
||||
except DeviceOfflineError as err:
|
||||
# Let entity become unavailable
|
||||
raise HomeAssistantError(f"Device is offline: {err}") from err
|
||||
except DeviceError as err:
|
||||
# Specific error
|
||||
raise HomeAssistantError(f"Failed to turn on: {err}") from err
|
||||
else:
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
## Testing Switches
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```python
|
||||
"""Test switches."""
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_switches(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration,
|
||||
) -> None:
|
||||
"""Test switch entities."""
|
||||
await snapshot_platform(
|
||||
hass,
|
||||
entity_registry,
|
||||
snapshot,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
```
|
||||
|
||||
### Control Testing
|
||||
|
||||
```python
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
||||
|
||||
async def test_switch_on_off(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device,
|
||||
) -> None:
|
||||
"""Test turning switch on and off."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Test initial state
|
||||
state = hass.states.get("switch.my_device_outlet")
|
||||
assert state
|
||||
assert state.state == "off"
|
||||
|
||||
# Turn on
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.my_device_outlet"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_device.turn_on.assert_called_once()
|
||||
|
||||
# Check state updated
|
||||
state = hass.states.get("switch.my_device_outlet")
|
||||
assert state.state == "on"
|
||||
|
||||
# Turn off
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "switch.my_device_outlet"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_device.turn_off.assert_called_once()
|
||||
|
||||
state = hass.states.get("switch.my_device_outlet")
|
||||
assert state.state == "off"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Coordinator-Based Switch
|
||||
|
||||
```python
|
||||
class MySwitch(CoordinatorEntity[MyCoordinator], SwitchEntity):
|
||||
"""Coordinator-based switch."""
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.coordinator.client.turn_on(self.device_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.client.turn_off(self.device_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return if switch is on."""
|
||||
if device := self.coordinator.data.devices.get(self.device_id):
|
||||
return device.is_on
|
||||
return None
|
||||
```
|
||||
|
||||
### Pattern 2: Local State Management
|
||||
|
||||
```python
|
||||
class MyLocalSwitch(SwitchEntity):
|
||||
"""Switch with local state."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_is_on = False
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.device.turn_on()
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.device.turn_off()
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
### Pattern 3: With Additional Control
|
||||
|
||||
```python
|
||||
class MyAdvancedSwitch(SwitchEntity):
|
||||
"""Switch with timer support."""
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on with optional duration."""
|
||||
duration = kwargs.get("duration") # Custom kwarg
|
||||
|
||||
if duration:
|
||||
await self.device.turn_on_for(duration)
|
||||
else:
|
||||
await self.device.turn_on()
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- Implement both turn_on and turn_off
|
||||
- Update state after commands
|
||||
- Handle errors properly
|
||||
- Use coordinator for state management
|
||||
- Implement unique IDs
|
||||
- Use translation keys
|
||||
- Mark config switches with entity_category
|
||||
- Refresh coordinator after commands
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- Block the event loop
|
||||
- Ignore errors silently
|
||||
- Create switches without unique IDs
|
||||
- Mix control and sensing (use separate entities)
|
||||
- Poll unnecessarily
|
||||
- Hardcode entity names
|
||||
- Forget to update state after commands
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Switch Not Responding
|
||||
|
||||
Check:
|
||||
- [ ] turn_on/turn_off methods are async
|
||||
- [ ] Not blocking the event loop
|
||||
- [ ] API client is working
|
||||
- [ ] Errors are being raised properly
|
||||
|
||||
### State Not Updating
|
||||
|
||||
Check:
|
||||
- [ ] async_write_ha_state() is called
|
||||
- [ ] Coordinator refresh is working
|
||||
- [ ] is_on returns correct value
|
||||
- [ ] Push updates are subscribed
|
||||
|
||||
### Switch Appearing as Unavailable
|
||||
|
||||
Check:
|
||||
- [ ] Device connection is working
|
||||
- [ ] Coordinator update is successful
|
||||
- [ ] available property returns True
|
||||
- [ ] Entity is in coordinator.data
|
||||
|
||||
## Quality Scale Considerations
|
||||
|
||||
- **Bronze**: Unique ID required
|
||||
- **Gold**: Entity translations, device class (if applicable)
|
||||
- **Platinum**: Full type hints
|
||||
|
||||
## References
|
||||
|
||||
- [Switch Documentation](https://developers.home-assistant.io/docs/core/entity/switch)
|
||||
- [Switch Integration](https://www.home-assistant.io/integrations/switch/)
|
||||
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Review Home Assistant integration code for quality, best practices, and standards compliance. Use when reviewing pull requests, identifying anti-patterns, checking security vulnerabilities (OWASP), verifying async patterns, ensuring quality scale compliance, or providing comprehensive code feedback.
|
||||
---
|
||||
|
||||
# Code Review Skill for Home Assistant Integrations
|
||||
|
||||
You are an expert Home Assistant code reviewer with deep knowledge of Python, async programming, Home Assistant architecture, and integration best practices.
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
### What to Review
|
||||
✅ **DO review and comment on:**
|
||||
- Architecture and design patterns
|
||||
- Async programming correctness
|
||||
- Error handling and edge cases
|
||||
- Security vulnerabilities (XSS, SQL injection, command injection, etc.)
|
||||
- Performance issues (blocking operations, inefficient loops)
|
||||
- Code organization and clarity
|
||||
- Compliance with Home Assistant patterns
|
||||
- Quality scale requirements
|
||||
- Missing functionality or incomplete implementations
|
||||
|
||||
❌ **DO NOT comment on:**
|
||||
- Missing imports (static analysis catches this)
|
||||
- Code formatting (Ruff handles this)
|
||||
- Minor style issues that linters catch
|
||||
|
||||
### Git Practices During Review
|
||||
⚠️ **CRITICAL**: After review has started:
|
||||
- **DO NOT amend commits**
|
||||
- **DO NOT squash commits**
|
||||
- **DO NOT rebase commits**
|
||||
- Reviewers need to see what changed since their last review
|
||||
|
||||
## Key Review Areas
|
||||
|
||||
### 1. Async Programming Patterns
|
||||
|
||||
#### ✅ Good Async Patterns
|
||||
```python
|
||||
# Proper async I/O
|
||||
data = await client.get_data()
|
||||
|
||||
# Using asyncio.sleep instead of time.sleep
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# Executor for blocking operations
|
||||
result = await hass.async_add_executor_job(blocking_function, args)
|
||||
|
||||
# Gathering async operations
|
||||
results = await asyncio.gather(
|
||||
client.get_temp(),
|
||||
client.get_humidity(),
|
||||
)
|
||||
|
||||
# @callback for event loop safe functions
|
||||
@callback
|
||||
def async_update_callback(self, event):
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
#### ❌ Bad Async Patterns
|
||||
```python
|
||||
# Blocking operations in event loop
|
||||
data = requests.get(url) # ❌ Blocks event loop
|
||||
time.sleep(5) # ❌ Blocks event loop
|
||||
|
||||
# Awaiting in loops (use gather instead)
|
||||
for device in devices:
|
||||
data = await device.get_data() # ❌ Sequential, slow
|
||||
|
||||
# Reusing BleakClient instances
|
||||
await self.client.connect() # ❌ Don't reuse BleakClient
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
#### ✅ Good Error Handling
|
||||
```python
|
||||
# Minimal try blocks, process outside
|
||||
try:
|
||||
data = await device.get_data()
|
||||
except DeviceError as err:
|
||||
_LOGGER.error("Failed to get data: %s", err)
|
||||
return
|
||||
|
||||
# Process data outside try block
|
||||
processed = data.get("value", 0) * 100
|
||||
self._attr_native_value = processed
|
||||
|
||||
# Proper exception types
|
||||
try:
|
||||
await client.connect()
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise ConfigEntryNotReady(f"Timeout connecting to {host}") from ex
|
||||
except AuthError as ex:
|
||||
raise ConfigEntryAuthFailed("Invalid credentials") from ex
|
||||
```
|
||||
|
||||
#### ❌ Bad Error Handling
|
||||
```python
|
||||
# Too much code in try block
|
||||
try:
|
||||
data = await device.get_data()
|
||||
processed = data.get("value", 0) * 100 # ❌ Should be outside
|
||||
self._attr_native_value = processed
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed")
|
||||
|
||||
# Bare exceptions in regular code
|
||||
try:
|
||||
data = await device.get_data()
|
||||
except Exception: # ❌ Too broad (unless in config flow/background task)
|
||||
_LOGGER.error("Failed")
|
||||
|
||||
# Wrong exception type
|
||||
if end_date < start_date:
|
||||
raise ValueError("Invalid dates") # ❌ Should be ServiceValidationError
|
||||
```
|
||||
|
||||
### 3. Security Vulnerabilities
|
||||
|
||||
Check for OWASP Top 10 vulnerabilities:
|
||||
|
||||
```python
|
||||
# ❌ Command Injection
|
||||
os.system(f"ping {user_input}") # DANGEROUS
|
||||
|
||||
# ✅ Safe alternative
|
||||
await hass.async_add_executor_job(
|
||||
subprocess.run,
|
||||
["ping", user_input],
|
||||
check=True
|
||||
)
|
||||
|
||||
# ❌ Exposing secrets in diagnostics
|
||||
return {"api_key": entry.data[CONF_API_KEY]} # DANGEROUS
|
||||
|
||||
# ✅ Safe alternative
|
||||
return async_redact_data(entry.data, {CONF_API_KEY, CONF_PASSWORD})
|
||||
```
|
||||
|
||||
### 4. Configuration Flow Patterns
|
||||
|
||||
#### ✅ Good Config Flow
|
||||
```python
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._test_connection(user_input)
|
||||
except ConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # ✅ Allowed in config flow
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(device_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=device_name,
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Entity Patterns
|
||||
|
||||
#### ✅ Good Entity Patterns
|
||||
```python
|
||||
class MySensor(CoordinatorEntity[MyCoordinator], SensorEntity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "temperature"
|
||||
|
||||
def __init__(self, coordinator: MyCoordinator, device_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{device_id}_temperature"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=coordinator.data[device_id].name,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
if device_data := self.coordinator.data.get(self.device_id):
|
||||
return device_data.temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return super().available and self.device_id in self.coordinator.data
|
||||
```
|
||||
|
||||
### 6. Quality Scale Compliance
|
||||
|
||||
Review manifest.json and quality_scale.yaml:
|
||||
|
||||
```json
|
||||
{
|
||||
"domain": "my_integration",
|
||||
"name": "My Integration",
|
||||
"codeowners": ["@me"],
|
||||
"config_flow": true,
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver"
|
||||
}
|
||||
```
|
||||
|
||||
Check:
|
||||
- [ ] All required Bronze rules implemented or exempted
|
||||
- [ ] Rules match declared quality scale tier
|
||||
- [ ] Valid exemption reasons provided
|
||||
- [ ] manifest.json has all required fields
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### ✅ Good Performance
|
||||
```python
|
||||
# Parallel API calls
|
||||
temp, humidity = await asyncio.gather(
|
||||
api.get_temperature(),
|
||||
api.get_humidity(),
|
||||
)
|
||||
|
||||
# Efficient coordinator usage
|
||||
PARALLEL_UPDATES = 0 # Unlimited for coordinator-based
|
||||
```
|
||||
|
||||
### ❌ Bad Performance
|
||||
```python
|
||||
# Sequential API calls
|
||||
temp = await api.get_temperature()
|
||||
humidity = await api.get_humidity() # ❌ Should use gather
|
||||
|
||||
# User-configurable scan intervals
|
||||
vol.Optional("scan_interval"): cv.positive_int # ❌ Not allowed
|
||||
```
|
||||
|
||||
## Review Process
|
||||
|
||||
When reviewing code:
|
||||
|
||||
1. **Architecture Review**: Does it follow HA patterns?
|
||||
2. **Code Quality**: Are async patterns correct? Is error handling comprehensive?
|
||||
3. **Standards Compliance**: Quality scale requirements met?
|
||||
4. **Performance & Efficiency**: No blocking operations? Efficient API usage?
|
||||
5. **User Experience**: Clear error messages? Proper translations?
|
||||
|
||||
## Providing Feedback
|
||||
|
||||
Structure feedback as:
|
||||
1. **Summary**: Overall assessment
|
||||
2. **Critical Issues**: Must fix before merge
|
||||
3. **Suggestions**: Nice-to-have improvements
|
||||
4. **Positive Notes**: What's done well
|
||||
|
||||
Be specific with file:line references and provide code examples.
|
||||
|
||||
## Reference Files
|
||||
|
||||
For detailed patterns and best practices, see:
|
||||
- `.claude/references/diagnostics.md` - Diagnostics implementation
|
||||
- `.claude/references/sensor.md` - Sensor platform
|
||||
- `.claude/references/binary_sensor.md` - Binary sensor platform
|
||||
- `.claude/references/switch.md` - Switch platform
|
||||
- `.claude/references/button.md` - Button platform
|
||||
- `.claude/references/number.md` - Number platform
|
||||
- `.claude/references/select.md` - Select platform
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: github-pr-reviewer
|
||||
description: Reviews GitHub pull requests and provides feedback comments. This is the top skill to use for reviewing Pull Requests from GitHub.
|
||||
---
|
||||
|
||||
# Review GitHub Pull Request
|
||||
|
||||
## Follow these steps:
|
||||
1. Use 'gh pr view' to get the PR details and description.
|
||||
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||
3. Analyze the code changes for:
|
||||
- Code quality and style consistency
|
||||
- Potential bugs or issues
|
||||
- Performance implications
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
- Documentation updates if needed
|
||||
4. Ensure any existing review comments have been addressed.
|
||||
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
|
||||
|
||||
## Verification:
|
||||
|
||||
- After the review, run parallel subagents for each finding to double check it.
|
||||
- Spawn up to a maximum of 10 parallel subagents at a time.
|
||||
- Gather the results from the subagents and summarize them in the final review comments.
|
||||
|
||||
|
||||
## IMPORTANT:
|
||||
- Just review. DO NOT make any changes
|
||||
- Be constructive and specific in your comments
|
||||
- Suggest improvements where appropriate
|
||||
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||
- No need to run tests or linters, just review the code changes.
|
||||
- No need to highlight things that are already good.
|
||||
|
||||
## Output format:
|
||||
- List specific comments for each file/line that needs attention.
|
||||
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||
- Example output:
|
||||
```
|
||||
Overall assessment: request changes.
|
||||
- [CRITICAL] sensor.py:143 - Memory leak
|
||||
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
|
||||
- [SUGGESTION] test_init.py:45 - Improve x variable name
|
||||
```
|
||||
- Make sure to include the file and line number when possible in the bullet points.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: ha-integration-knowledge
|
||||
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
|
||||
---
|
||||
|
||||
## File Locations
|
||||
- **Integration code**: `./homeassistant/components/<integration_domain>/`
|
||||
- **Integration tests**: `./tests/components/<integration_domain>/`
|
||||
|
||||
## General guidelines
|
||||
|
||||
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
|
||||
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
|
||||
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
- Integrations should not implement fixes or workarounds for limitations in libraries. Instead, the library should be updated to fix the issue.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules
|
||||
- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices.
|
||||
|
||||
Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml`
|
||||
|
||||
### How Rules Apply
|
||||
1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level
|
||||
2. **Bronze Rules**: Always required for any integration with quality scale
|
||||
3. **Higher Tier Rules**: Only apply if integration targets that tier or higher
|
||||
4. **Rule Status**: Check `quality_scale.yaml` in integration folder for:
|
||||
- `done`: Rule implemented
|
||||
- `exempt`: Rule doesn't apply (with reason in comment)
|
||||
- `todo`: Rule needs implementation
|
||||
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
|
||||
@@ -1,6 +0,0 @@
|
||||
# Integration Diagnostics
|
||||
|
||||
Platform exists as `homeassistant/components/<domain>/diagnostics.py`.
|
||||
|
||||
- **Required**: Implement diagnostic data collection
|
||||
- **Security**: Never expose passwords, tokens, or sensitive coordinates
|
||||
@@ -1,21 +0,0 @@
|
||||
# Repairs platform
|
||||
|
||||
Platform exists as `homeassistant/components/<domain>/repairs.py`.
|
||||
|
||||
- **Actionable Issues Required**: All repair issues must be actionable for end users
|
||||
- **Issue Content Requirements**:
|
||||
- Clearly explain what is happening
|
||||
- Provide specific steps users need to take to resolve the issue
|
||||
- Use friendly, helpful language
|
||||
- Include relevant context (device names, error details, etc.)
|
||||
- **String Content Must Include**:
|
||||
- What the problem is
|
||||
- Why it matters
|
||||
- Exact steps to resolve (numbered list when multiple steps)
|
||||
- What to expect after following the steps
|
||||
- **Avoid Vague Instructions**: Don't just say "update firmware" - provide specific steps
|
||||
- **Severity Guidelines**:
|
||||
- `CRITICAL`: Reserved for extreme scenarios only
|
||||
- `ERROR`: Requires immediate user attention
|
||||
- `WARNING`: Indicates future potential breakage
|
||||
- Only create issues for problems users can potentially resolve
|
||||
@@ -0,0 +1,297 @@
|
||||
---
|
||||
name: quality-scale-architect
|
||||
description: Provide architectural guidance and quality scale oversight for Home Assistant integrations. Use when designing integration structure, selecting quality tiers (Bronze/Silver/Gold/Platinum), recommending architectural patterns (coordinator/push/hub), planning quality progression, or advising on integration organization.
|
||||
---
|
||||
|
||||
# Quality Scale Architect for Home Assistant Integrations
|
||||
|
||||
You are an expert Home Assistant integration architect specializing in quality scale systems, best practices, and architectural patterns.
|
||||
|
||||
## Quality Scale System
|
||||
|
||||
### Quality Scale Tiers
|
||||
|
||||
**Bronze** - Basic Requirements (Mandatory for all integrations with quality scale)
|
||||
- ✅ Config flow (UI configuration)
|
||||
- ✅ Entity unique IDs
|
||||
- ✅ Action setup (or exempt)
|
||||
- ✅ Appropriate setup retries
|
||||
- ✅ Reauthentication flow
|
||||
- ✅ Reconfigure flow
|
||||
- ✅ Test coverage
|
||||
|
||||
**Silver** - Enhanced Functionality
|
||||
- All Bronze requirements +
|
||||
- ✅ Entity unavailable tracking
|
||||
- ✅ Parallel updates configuration
|
||||
- ✅ Runtime data storage
|
||||
- ✅ Unique config entry titles
|
||||
|
||||
**Gold** - Advanced Features
|
||||
- All Silver requirements +
|
||||
- ✅ Device registry usage
|
||||
- ✅ Integration diagnostics
|
||||
- ✅ Device diagnostics
|
||||
- ✅ Entity category
|
||||
- ✅ Device class
|
||||
- ✅ Disabled by default (for noisy entities)
|
||||
- ✅ Entity translations
|
||||
- ✅ Exception translations
|
||||
- ✅ Icon translations
|
||||
|
||||
**Platinum** - Highest Quality Standards
|
||||
- All Gold requirements +
|
||||
- ✅ Strict typing (full type hints)
|
||||
- ✅ Async dependencies (no sync-blocking libs)
|
||||
- ✅ WebSession injection
|
||||
- ✅ config_entry parameter in coordinator
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Coordinator-Based Architecture
|
||||
**Use when**: Polling multiple entities from the same API
|
||||
|
||||
```python
|
||||
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: MyClient,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(minutes=5),
|
||||
config_entry=config_entry, # ✅ Pass for Platinum
|
||||
)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> MyData:
|
||||
try:
|
||||
return await self.client.fetch_data()
|
||||
except ApiError as err:
|
||||
raise UpdateFailed(f"Error: {err}") from err
|
||||
```
|
||||
|
||||
### Pattern 2: Push-Based Architecture
|
||||
**Use when**: Device pushes updates (webhooks, MQTT, WebSocket)
|
||||
|
||||
```python
|
||||
class MyEntity(SensorEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
self.async_on_remove(
|
||||
self.hub.subscribe_updates(self._handle_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_update(self, data: dict) -> None:
|
||||
self._attr_native_value = data["value"]
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
|
||||
### Pattern 3: Hub with Discovery
|
||||
**Use when**: Hub device with multiple discoverable endpoints
|
||||
|
||||
```python
|
||||
@callback
|
||||
def _check_new_devices() -> None:
|
||||
"""Check for new devices."""
|
||||
current = set(coordinator.data.devices.keys())
|
||||
new = current - known_devices
|
||||
|
||||
if new:
|
||||
known_devices.update(new)
|
||||
async_dispatcher_send(hass, f"{DOMAIN}_new_device", new)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_new_devices))
|
||||
```
|
||||
|
||||
## Architectural Decision Guide
|
||||
|
||||
### Choosing Integration Type
|
||||
|
||||
**Device Integration** (`"integration_type": "device"`)
|
||||
- Physical or virtual devices
|
||||
- Example: Smart plugs, thermostats, cameras
|
||||
|
||||
**Hub Integration** (`"integration_type": "hub"`)
|
||||
- Central hub controlling multiple devices
|
||||
- Example: Philips Hue bridge, Z-Wave controller
|
||||
|
||||
**Service Integration** (`"integration_type": "service"`)
|
||||
- Cloud services, APIs
|
||||
- Example: Weather services, notification platforms
|
||||
|
||||
**Helper Integration** (`"integration_type": "helper"`)
|
||||
- Utility integrations
|
||||
- Example: Template, group, automation helpers
|
||||
|
||||
### Choosing IoT Class
|
||||
|
||||
```json
|
||||
{
|
||||
"iot_class": "cloud_polling", // API polling
|
||||
"iot_class": "cloud_push", // Cloud webhooks/MQTT
|
||||
"iot_class": "local_polling", // Local device polling
|
||||
"iot_class": "local_push", // Local device push
|
||||
"iot_class": "calculated" // No external data
|
||||
}
|
||||
```
|
||||
|
||||
## Quality Scale Progression Strategy
|
||||
|
||||
### Starting Bronze (Minimum Viable Integration)
|
||||
|
||||
**Essential Components**:
|
||||
```
|
||||
homeassistant/components/my_integration/
|
||||
├── __init__.py # async_setup_entry, async_unload_entry
|
||||
├── manifest.json # Required fields, quality_scale: "bronze"
|
||||
├── const.py # DOMAIN constant
|
||||
├── config_flow.py # UI configuration with reauth/reconfigure
|
||||
├── sensor.py # Platform with unique IDs
|
||||
├── strings.json # Translations
|
||||
└── quality_scale.yaml # Rule tracking
|
||||
|
||||
tests/components/my_integration/
|
||||
├── conftest.py # Test fixtures
|
||||
├── test_config_flow.py # 100% coverage
|
||||
└── test_sensor.py # Entity tests
|
||||
```
|
||||
|
||||
**Bronze Checklist**:
|
||||
- [ ] Config flow with UI setup
|
||||
- [ ] Reauthentication flow
|
||||
- [ ] Reconfigure flow
|
||||
- [ ] All entities have unique IDs
|
||||
- [ ] Proper setup error handling
|
||||
- [ ] >95% test coverage
|
||||
- [ ] 100% config flow coverage
|
||||
|
||||
### Progressing to Silver
|
||||
|
||||
**Add**:
|
||||
- Entity unavailability tracking
|
||||
- Runtime data storage (not hass.data)
|
||||
- Parallel updates configuration
|
||||
- Unique entry titles
|
||||
|
||||
```python
|
||||
# Store in runtime_data (Silver requirement)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Entity availability (Silver requirement)
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return super().available and self.device_id in self.coordinator.data
|
||||
|
||||
# Parallel updates (Silver requirement)
|
||||
PARALLEL_UPDATES = 0 # For coordinator-based
|
||||
```
|
||||
|
||||
### Progressing to Gold
|
||||
|
||||
**Add**:
|
||||
- Device registry entries
|
||||
- Integration & device diagnostics
|
||||
- Entity categories, device classes
|
||||
- Entity translations
|
||||
- Exception translations
|
||||
- Icon translations
|
||||
|
||||
```python
|
||||
# Device info (Gold requirement)
|
||||
_attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
name=device.name,
|
||||
manufacturer="Manufacturer",
|
||||
model="Model",
|
||||
)
|
||||
|
||||
# Diagnostics (Gold requirement)
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: MyConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"runtime": entry.runtime_data.to_dict(),
|
||||
}
|
||||
```
|
||||
|
||||
### Progressing to Platinum
|
||||
|
||||
**Add**:
|
||||
- Comprehensive type hints (py.typed)
|
||||
- Async-only dependencies
|
||||
- WebSession injection support
|
||||
|
||||
```python
|
||||
# Type hints (Platinum requirement)
|
||||
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
|
||||
|
||||
# WebSession injection (Platinum requirement)
|
||||
client = MyClient(
|
||||
host=entry.data[CONF_HOST],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
# Pass config_entry to coordinator (Platinum requirement)
|
||||
coordinator = MyCoordinator(hass, client, entry)
|
||||
```
|
||||
|
||||
## Common Architectural Questions
|
||||
|
||||
### Q: Should I use a coordinator?
|
||||
**Use coordinator when**:
|
||||
- Polling API for multiple entities
|
||||
- Want efficient data sharing
|
||||
- Need coordinated updates
|
||||
|
||||
**Don't use coordinator when**:
|
||||
- Push-based updates (use callbacks)
|
||||
- Single entity integration
|
||||
- Each entity has independent data source
|
||||
|
||||
### Q: Where should I store runtime data?
|
||||
```python
|
||||
# ✅ GOOD - Use runtime_data (Silver+)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# ❌ BAD - Don't use hass.data
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
```
|
||||
|
||||
### Q: When should I create devices vs. just entities?
|
||||
**Create devices when**:
|
||||
- Representing physical/virtual devices
|
||||
- Multiple entities belong to same device
|
||||
- Want grouped device management
|
||||
|
||||
**Just entities when**:
|
||||
- Service integration (no physical device)
|
||||
- Single entity integration
|
||||
- Calculated/helper entities
|
||||
|
||||
## Reference Files
|
||||
|
||||
For detailed implementation guidance, see:
|
||||
- `.claude/references/diagnostics.md` - Diagnostics implementation
|
||||
- `.claude/references/sensor.md` - Sensor platform
|
||||
- `.claude/references/binary_sensor.md` - Binary sensor platform
|
||||
- `.claude/references/switch.md` - Switch platform
|
||||
- `.claude/references/button.md` - Button platform
|
||||
- `.claude/references/number.md` - Number platform
|
||||
- `.claude/references/select.md` - Select platform
|
||||
|
||||
## Your Task
|
||||
|
||||
When providing architectural guidance:
|
||||
|
||||
1. **Understand Requirements**: What is the integration type? What data needs exposure? Polling or push? What quality tier?
|
||||
2. **Recommend Architecture**: Suggest appropriate patterns, identify required components, explain decisions
|
||||
3. **Quality Scale Guidance**: Recommend starting tier, identify applicable rules, suggest progression path
|
||||
4. **Implementation Plan**: Outline file structure, identify key components, suggest implementation order
|
||||
5. **Best Practices**: Performance considerations, maintainability tips, common pitfalls to avoid
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
name: testing
|
||||
description: Write, run, and fix tests for Home Assistant integrations. Use when writing comprehensive test coverage (>95%), running pytest, fixing failing tests, updating snapshots, or following HA testing patterns. Specializes in modern fixture patterns, config flow testing (100% coverage), entity snapshot testing, and mocking external APIs.
|
||||
---
|
||||
|
||||
# Testing Skill for Home Assistant Integrations
|
||||
|
||||
You are an expert Home Assistant integration test engineer specializing in writing comprehensive, maintainable tests that follow Home Assistant conventions and best practices.
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Coverage Requirements
|
||||
- **Minimum Coverage**: 95% for all modules
|
||||
- **Config Flow**: 100% coverage required for all paths
|
||||
- **Location**: Tests go in `tests/components/{domain}/`
|
||||
|
||||
### Test File Organization
|
||||
```
|
||||
tests/components/my_integration/
|
||||
├── __init__.py
|
||||
├── conftest.py # Fixtures and test setup
|
||||
├── test_config_flow.py # Config flow tests (100% coverage)
|
||||
├── test_sensor.py # Sensor platform tests
|
||||
├── test_init.py # Integration setup tests
|
||||
└── snapshots/ # Generated snapshot files
|
||||
```
|
||||
|
||||
## Modern Fixture Setup Pattern
|
||||
|
||||
Always use this pattern for integration tests:
|
||||
|
||||
```python
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import Platform
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="My Integration",
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "127.0.0.1", CONF_API_KEY: "test_key"},
|
||||
unique_id="device_unique_id",
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device_api() -> Generator[MagicMock]:
|
||||
"""Return a mocked device API."""
|
||||
with patch("homeassistant.components.my_integration.MyDeviceAPI", autospec=True) as api_mock:
|
||||
api = api_mock.return_value
|
||||
api.get_data.return_value = MyDeviceData.from_json(
|
||||
load_fixture("device_data.json", DOMAIN)
|
||||
)
|
||||
yield api
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device_api: MagicMock,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
```
|
||||
|
||||
## Entity Testing with Snapshots
|
||||
|
||||
Use snapshot testing for entity verification:
|
||||
|
||||
```python
|
||||
from syrupy import SnapshotAssertion
|
||||
from homeassistant.helpers import entity_registry as er, device_registry as dr
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the sensor entities."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
# Verify entities are assigned to device
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "device_unique_id")}
|
||||
)
|
||||
assert device_entry
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry.device_id == device_entry.id
|
||||
```
|
||||
|
||||
## Config Flow Testing (100% Coverage Required)
|
||||
|
||||
Test ALL paths in config flow:
|
||||
|
||||
```python
|
||||
async def test_user_flow_success(hass, mock_api):
|
||||
"""Test successful user flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=TEST_USER_INPUT
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "My Device"
|
||||
assert result["data"] == TEST_USER_INPUT
|
||||
|
||||
async def test_flow_connection_error(hass, mock_api_error):
|
||||
"""Test connection error handling."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=TEST_USER_INPUT
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
async def test_flow_duplicate_entry(hass, mock_config_entry, mock_api):
|
||||
"""Test duplicate entry prevention."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=TEST_USER_INPUT
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Integration-Specific Tests (Recommended)
|
||||
```bash
|
||||
pytest ./tests/components/<integration_domain> \
|
||||
--cov=homeassistant.components.<integration_domain> \
|
||||
--cov-report term-missing \
|
||||
--durations-min=1 \
|
||||
--durations=0 \
|
||||
--numprocesses=auto
|
||||
```
|
||||
|
||||
### Quick Test of Changed Files
|
||||
```bash
|
||||
pytest --timeout=10 --picked
|
||||
```
|
||||
|
||||
### Update Test Snapshots
|
||||
```bash
|
||||
pytest ./tests/components/<integration_domain> --snapshot-update
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT**: After using `--snapshot-update`:
|
||||
1. Run tests again WITHOUT the flag to verify snapshots
|
||||
2. Review the snapshot changes carefully
|
||||
3. Don't commit snapshot updates without verification
|
||||
|
||||
## Critical Testing Rules
|
||||
|
||||
### NEVER Do These Things
|
||||
- ❌ Don't access `hass.data` directly in tests
|
||||
- ❌ Don't test entities in isolation without integration setup
|
||||
- ❌ Don't forget to mock external dependencies
|
||||
|
||||
### ALWAYS Do These Things
|
||||
- ✅ Use proper integration setup through fixtures
|
||||
- ✅ Mock all external APIs
|
||||
- ✅ Test through the integration's public interface
|
||||
- ✅ Use snapshot testing for entities
|
||||
- ✅ Achieve 100% config flow coverage
|
||||
- ✅ Achieve >95% overall coverage
|
||||
|
||||
## Reference Files
|
||||
|
||||
For detailed implementation guidance, see:
|
||||
- `.claude/references/sensor.md` - Sensor platform patterns
|
||||
- `.claude/references/binary_sensor.md` - Binary sensor patterns
|
||||
- `.claude/references/switch.md` - Switch platform patterns
|
||||
- `.claude/references/button.md` - Button platform patterns
|
||||
- `.claude/references/number.md` - Number platform patterns
|
||||
- `.claude/references/select.md` - Select platform patterns
|
||||
+1
-5
@@ -22,7 +22,6 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/calendar/**
|
||||
- homeassistant/components/camera/**
|
||||
- homeassistant/components/climate/**
|
||||
- homeassistant/components/conversation/**
|
||||
- homeassistant/components/cover/**
|
||||
- homeassistant/components/date/**
|
||||
- homeassistant/components/datetime/**
|
||||
@@ -34,9 +33,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/humidifier/**
|
||||
- homeassistant/components/image/**
|
||||
- homeassistant/components/image_processing/**
|
||||
- homeassistant/components/infrared/**
|
||||
- homeassistant/components/lawn_mower/**
|
||||
- homeassistant/components/radio_frequency/**
|
||||
- homeassistant/components/light/**
|
||||
- homeassistant/components/lock/**
|
||||
- homeassistant/components/media_player/**
|
||||
@@ -56,7 +53,6 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
- homeassistant/components/valve/**
|
||||
- homeassistant/components/wake_word/**
|
||||
- homeassistant/components/water_heater/**
|
||||
- homeassistant/components/weather/**
|
||||
|
||||
@@ -74,6 +70,7 @@ components: &components
|
||||
- homeassistant/components/cloud/**
|
||||
- homeassistant/components/config/**
|
||||
- homeassistant/components/configurator/**
|
||||
- homeassistant/components/conversation/**
|
||||
- homeassistant/components/demo/**
|
||||
- homeassistant/components/device_automation/**
|
||||
- homeassistant/components/dhcp/**
|
||||
@@ -94,7 +91,6 @@ components: &components
|
||||
- homeassistant/components/input_number/**
|
||||
- homeassistant/components/input_select/**
|
||||
- homeassistant/components/input_text/**
|
||||
- homeassistant/components/labs/**
|
||||
- homeassistant/components/logbook/**
|
||||
- homeassistant/components/logger/**
|
||||
- homeassistant/components/lovelace/**
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
||||
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"PYTHONASYNCIODEBUG": "1"
|
||||
},
|
||||
"features": {
|
||||
// Node feature required for Claude Code until fixed https://github.com/anthropics/devcontainer-features/issues/28
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
// Port 5683 udp is used by Shelly integration
|
||||
@@ -37,8 +40,7 @@
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
@@ -60,13 +62,7 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"[json][jsonc][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"json.schemas": [
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../.claude/skills
|
||||
+2
-3
@@ -14,12 +14,11 @@ Dockerfile.dev linguist-language=Dockerfile
|
||||
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
Dockerfile linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
|
||||
machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
requirements_all.txt linguist-generated=true
|
||||
requirements_test_all.txt linguist-generated=true
|
||||
requirements_test_pre_commit.txt linguist-generated=true
|
||||
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||
.github/workflows/*.lock.yml linguist-generated=true
|
||||
|
||||
@@ -80,7 +80,7 @@ If the code communicates with devices, web services, or third-party tools:
|
||||
Updated and included derived files by running: `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||
Updated by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.
|
||||
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Cache and install APT packages
|
||||
description: >-
|
||||
Wraps awalsh128/cache-apt-pkgs-action with the workarounds Home Assistant CI
|
||||
needs. Removes the conflicting Microsoft apt source before any apt run, and
|
||||
points the dynamic linker at the host's multiarch lib subdirectories so
|
||||
shared libraries that rely on update-alternatives or postinst-managed paths
|
||||
(eg libblas, liblapack pulled in by ffmpeg) stay reachable since the upstream
|
||||
action does not execute postinst scripts on cache restore.
|
||||
|
||||
inputs:
|
||||
packages:
|
||||
description: Space-delimited list of apt packages to install.
|
||||
required: true
|
||||
version:
|
||||
description: Cache version. Bump to invalidate the cache.
|
||||
required: false
|
||||
default: "1"
|
||||
execute_install_scripts:
|
||||
description: >-
|
||||
Pass-through to awalsh128/cache-apt-pkgs-action. Postinst scripts are not
|
||||
actually cached by the upstream action, so this is largely a no-op today.
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Remove conflicting Microsoft apt source
|
||||
shell: bash
|
||||
run: sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
|
||||
- name: Install apt packages via cache
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||
with:
|
||||
packages: ${{ inputs.packages }}
|
||||
version: ${{ inputs.version }}
|
||||
execute_install_scripts: ${{ inputs.execute_install_scripts }}
|
||||
- name: Refresh dynamic linker cache
|
||||
shell: bash
|
||||
run: |
|
||||
# awalsh128/cache-apt-pkgs-action does not run postinst scripts on
|
||||
# cache restore, so update-alternatives symlinks (eg the one libblas
|
||||
# creates at /usr/lib/<multiarch>/libblas.so.3) are never produced.
|
||||
# Add every /usr/lib/<multiarch> subdirectory that holds shared
|
||||
# libraries to the ldconfig search path so the dynamic linker still
|
||||
# finds them. Use dpkg-architecture to derive the host's multiarch
|
||||
# tuple so this works on non-x86_64 runners too.
|
||||
multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
|
||||
find "/usr/lib/${multiarch}" -mindepth 2 -maxdepth 2 \
|
||||
-name '*.so.*' -printf '%h\n' \
|
||||
| sort -u \
|
||||
| sudo tee /etc/ld.so.conf.d/zzz-cache-apt-extras.conf > /dev/null
|
||||
sudo ldconfig
|
||||
+1168
-36
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,3 @@ updates:
|
||||
labels:
|
||||
- dependency
|
||||
- github_actions
|
||||
cooldown:
|
||||
default-days: 7
|
||||
ignore:
|
||||
# Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump.
|
||||
- dependency-name: "github/gh-aw-actions/**"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
applyTo: "homeassistant/components/**, tests/components/**"
|
||||
excludeAgent: "cloud-agent"
|
||||
---
|
||||
|
||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||
|
||||
|
||||
## File Locations
|
||||
- **Integration code**: `./homeassistant/components/<integration_domain>/`
|
||||
- **Integration tests**: `./tests/components/<integration_domain>/`
|
||||
|
||||
## General guidelines
|
||||
|
||||
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
|
||||
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
|
||||
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
- Integrations should not implement fixes or workarounds for limitations in libraries. Instead, the library should be updated to fix the issue.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules
|
||||
- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices.
|
||||
|
||||
Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml`
|
||||
|
||||
### How Rules Apply
|
||||
1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level
|
||||
2. **Bronze Rules**: Always required for any integration with quality scale
|
||||
3. **Higher Tier Rules**: Only apply if integration targets that tier or higher
|
||||
4. **Rule Status**: Check `quality_scale.yaml` in integration folder for:
|
||||
- `done`: Rule implemented
|
||||
- `exempt`: Rule doesn't apply (with reason in comment)
|
||||
- `todo`: Rule needs implementation
|
||||
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
|
||||
@@ -1,245 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
|
||||
"enabledManagers": [
|
||||
"pep621",
|
||||
"pip_requirements",
|
||||
"pre-commit",
|
||||
"dockerfile",
|
||||
"custom.regex",
|
||||
"homeassistant-manifest"
|
||||
],
|
||||
|
||||
"pre-commit": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"pip_requirements": {
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)requirements[\\w_-]*\\.txt$/",
|
||||
"/(^|/)homeassistant/package_constraints\\.txt$/"
|
||||
]
|
||||
},
|
||||
|
||||
"dockerfile": {
|
||||
"managerFilePatterns": ["/^Dockerfile$/"]
|
||||
},
|
||||
|
||||
"homeassistant-manifest": {
|
||||
"managerFilePatterns": [
|
||||
"/^homeassistant/components/[^/]+/manifest\\.json$/"
|
||||
]
|
||||
},
|
||||
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update ruff required-version in pyproject.toml",
|
||||
"managerFilePatterns": ["/^pyproject\\.toml$/"],
|
||||
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
|
||||
"depNameTemplate": "ruff",
|
||||
"datasourceTemplate": "pypi"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update go2rtc RECOMMENDED_VERSION in const.py alongside the Dockerfile pin",
|
||||
"managerFilePatterns": ["/^homeassistant/components/go2rtc/const\\.py$/"],
|
||||
"matchStrings": ["RECOMMENDED_VERSION = \"(?<currentValue>[\\d.]+)\""],
|
||||
"depNameTemplate": "ghcr.io/alexxit/go2rtc",
|
||||
"datasourceTemplate": "docker"
|
||||
}
|
||||
],
|
||||
|
||||
"minimumReleaseAge": "7 days",
|
||||
"prConcurrentLimit": 10,
|
||||
"prHourlyLimit": 2,
|
||||
"schedule": ["before 6am"],
|
||||
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessageAction": "Update",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"commitMessageExtra": "to {{newVersion}}",
|
||||
|
||||
"automerge": false,
|
||||
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": false
|
||||
},
|
||||
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Deny all by default — allowlist below re-enables specific packages",
|
||||
"matchPackageNames": ["*"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Core runtime dependencies (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"aiohttp",
|
||||
"aiohttp-fast-zlib",
|
||||
"aiohttp_cors",
|
||||
"aiohttp-asyncmdnsresolver",
|
||||
"yarl",
|
||||
"httpx",
|
||||
"requests",
|
||||
"urllib3",
|
||||
"certifi",
|
||||
"orjson",
|
||||
"PyYAML",
|
||||
"Jinja2",
|
||||
"cryptography",
|
||||
"pyOpenSSL",
|
||||
"PyJWT",
|
||||
"SQLAlchemy",
|
||||
"Pillow",
|
||||
"attrs",
|
||||
"uv",
|
||||
"voluptuous",
|
||||
"voluptuous-serialize",
|
||||
"voluptuous-openapi",
|
||||
"zeroconf"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency", "core"]
|
||||
},
|
||||
{
|
||||
"description": "Common Python utilities (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"astral",
|
||||
"atomicwrites-homeassistant",
|
||||
"audioop-lts",
|
||||
"awesomeversion",
|
||||
"bcrypt",
|
||||
"ciso8601",
|
||||
"cronsim",
|
||||
"defusedxml",
|
||||
"fnv-hash-fast",
|
||||
"getmac",
|
||||
"ical",
|
||||
"ifaddr",
|
||||
"lru-dict",
|
||||
"mutagen",
|
||||
"propcache",
|
||||
"pyserial",
|
||||
"python-slugify",
|
||||
"PyTurboJPEG",
|
||||
"securetar",
|
||||
"standard-aifc",
|
||||
"standard-telnetlib",
|
||||
"ulid-transform",
|
||||
"unidiff",
|
||||
"url-normalize",
|
||||
"xmltodict"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Home Assistant ecosystem packages (core-maintained, no cooldown)",
|
||||
"matchPackageNames": [
|
||||
"hassil",
|
||||
"home-assistant-bluetooth",
|
||||
"home-assistant-frontend",
|
||||
"home-assistant-intents",
|
||||
"infrared-protocols",
|
||||
"rf-protocols"
|
||||
],
|
||||
"enabled": true,
|
||||
"minimumReleaseAge": null,
|
||||
"labels": ["dependency", "core"]
|
||||
},
|
||||
{
|
||||
"description": "Test dependencies (allowlisted)",
|
||||
"matchPackageNames": [
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"pytest-aiohttp",
|
||||
"pytest-cov",
|
||||
"pytest-freezer",
|
||||
"pytest-github-actions-annotate-failures",
|
||||
"pytest-socket",
|
||||
"pytest-sugar",
|
||||
"pytest-timeout",
|
||||
"pytest-unordered",
|
||||
"pytest-picked",
|
||||
"pytest-xdist",
|
||||
"pylint",
|
||||
"pylint-per-file-ignores",
|
||||
"astroid",
|
||||
"coverage",
|
||||
"freezegun",
|
||||
"syrupy",
|
||||
"respx",
|
||||
"requests-mock",
|
||||
"ruff",
|
||||
"codespell",
|
||||
"yamllint",
|
||||
"zizmor"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "For types-* stubs, only allow patch updates. Major/minor bumps track the upstream runtime package version and must be manually coordinated with the corresponding pin.",
|
||||
"matchPackageNames": ["/^types-/"],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Pre-commit hook repos (allowlisted, matched by owner/repo)",
|
||||
"matchPackageNames": [
|
||||
"astral-sh/ruff-pre-commit",
|
||||
"codespell-project/codespell",
|
||||
"adrienverge/yamllint",
|
||||
"zizmorcore/zizmor-pre-commit"
|
||||
],
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Docker allowlist (ghcr.io exposes no release timestamps so the global cooldown needs to be bypassed)",
|
||||
"matchPackageNames": ["ghcr.io/alexxit/go2rtc"],
|
||||
"enabled": true,
|
||||
"minimumReleaseAge": null,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Group ruff pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["astral-sh/ruff-pre-commit", "ruff"],
|
||||
"groupName": "ruff",
|
||||
"groupSlug": "ruff"
|
||||
},
|
||||
{
|
||||
"description": "Group codespell pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["codespell-project/codespell", "codespell"],
|
||||
"groupName": "codespell",
|
||||
"groupSlug": "codespell"
|
||||
},
|
||||
{
|
||||
"description": "Group yamllint pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["adrienverge/yamllint", "yamllint"],
|
||||
"groupName": "yamllint",
|
||||
"groupSlug": "yamllint"
|
||||
},
|
||||
{
|
||||
"description": "Group zizmor pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["zizmorcore/zizmor-pre-commit", "zizmor"],
|
||||
"groupName": "zizmor",
|
||||
"groupSlug": "zizmor"
|
||||
},
|
||||
{
|
||||
"description": "Group pylint with astroid (their versions are linked and must move together)",
|
||||
"matchPackageNames": ["pylint", "astroid"],
|
||||
"groupName": "pylint",
|
||||
"groupSlug": "pylint"
|
||||
},
|
||||
{
|
||||
"description": "Group go2rtc Dockerfile pin with const.py RECOMMENDED_VERSION into one PR",
|
||||
"matchPackageNames": ["ghcr.io/alexxit/go2rtc"],
|
||||
"groupName": "go2rtc",
|
||||
"groupSlug": "go2rtc"
|
||||
}
|
||||
]
|
||||
}
|
||||
+154
-155
@@ -10,51 +10,45 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.05.0"
|
||||
BASE_IMAGE_VERSION: "2025.12.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
init:
|
||||
name: Initialize build
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
channel: ${{ steps.version.outputs.channel }}
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
architectures: ${{ env.ARCHITECTURES }}
|
||||
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@master
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
uses: home-assistant/actions/helpers/version@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version@master
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
with:
|
||||
ignore-dev: true
|
||||
|
||||
@@ -69,14 +63,14 @@ jobs:
|
||||
- name: Download Translations
|
||||
run: python3 -m script.translations download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Archive translations
|
||||
shell: bash
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -88,27 +82,25 @@ jobs:
|
||||
needs: init
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
include:
|
||||
- arch: amd64
|
||||
os: ubuntu-24.04
|
||||
os: ubuntu-latest
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -119,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
@@ -128,23 +120,22 @@ jobs:
|
||||
workflow_conclusion: success
|
||||
name: package
|
||||
|
||||
- name: Set up Python
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Adjust nightly version
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
shell: bash
|
||||
env:
|
||||
UV_PRERELEASE: allow
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
python3 -m pip install "$(grep '^uv' < requirements.txt)"
|
||||
uv pip install packaging tomli
|
||||
uv pip install .
|
||||
python3 script/version_bump.py nightly --set-nightly-version "${VERSION}"
|
||||
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
||||
|
||||
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
|
||||
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}"
|
||||
@@ -174,11 +165,11 @@ jobs:
|
||||
sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
|
||||
homeassistant/package_constraints.txt
|
||||
|
||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
|
||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
||||
fi
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -190,36 +181,84 @@ jobs:
|
||||
- name: Write meta info file
|
||||
shell: bash
|
||||
run: |
|
||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- &install_cosign
|
||||
name: Install Cosign
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Build variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify base image signature
|
||||
run: |
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
|
||||
"${{ steps.vars.outputs.base_image }}"
|
||||
|
||||
- name: Verify cache image signature
|
||||
id: cache
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
|
||||
"${{ steps.vars.outputs.cache_image }}"
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build-args: |
|
||||
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
|
||||
cache-gha: false
|
||||
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
|
||||
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
|
||||
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
|
||||
image-tags: ${{ needs.init.outputs.version }}
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ steps.vars.outputs.platform }}
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
|
||||
build-args: |
|
||||
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
||||
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
labels: |
|
||||
io.hass.arch=${{ matrix.arch }}
|
||||
io.hass.version=${{ needs.init.outputs.version }}
|
||||
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
|
||||
org.opencontainers.image.version=${{ needs.init.outputs.version }}
|
||||
|
||||
- name: Sign image
|
||||
run: |
|
||||
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
|
||||
|
||||
build_machine:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
machine:
|
||||
- generic-x86-64
|
||||
- intel-nuc
|
||||
- khadas-vim3
|
||||
- odroid-c2
|
||||
- odroid-c4
|
||||
@@ -232,55 +271,37 @@ jobs:
|
||||
- raspberrypi5-64
|
||||
- yellow
|
||||
- green
|
||||
include:
|
||||
# Default: aarch64 on native ARM runner
|
||||
- arch: aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
# Overrides for amd64 machines
|
||||
- machine: generic-x86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
- machine: qemux86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Compute extra tags
|
||||
id: tags
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
if [[ "${VERSION}" =~ d ]]; then
|
||||
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${VERSION}" =~ b ]]; then
|
||||
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
|
||||
# Create general tags
|
||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
else
|
||||
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
|
||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build machine image
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build-args: |
|
||||
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
cache-gha: false
|
||||
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
context: machine/
|
||||
cosign-base-identity: "https://github.com/home-assistant/core/.*"
|
||||
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
file: machine/${{ matrix.machine }}
|
||||
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
|
||||
image-tags: |
|
||||
${{ needs.init.outputs.version }}
|
||||
${{ steps.tags.outputs.extra_tags }}
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# home-assistant/builder doesn't support sha pinning
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2025.11.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
--target /data/machine \
|
||||
--cosign \
|
||||
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
@@ -288,23 +309,19 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
with:
|
||||
name: ${{ secrets.GIT_NAME }}
|
||||
email: ${{ secrets.GIT_EMAIL }}
|
||||
token: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Update version file
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
@@ -314,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Update version file (stable -> beta)
|
||||
if: needs.init.outputs.channel == 'stable'
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
@@ -323,34 +340,31 @@ jobs:
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
publish_container:
|
||||
name: Publish to ${{ matrix.registry }}
|
||||
name: Publish meta container for ${{ matrix.registry }}
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
- *install_cosign
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -358,17 +372,14 @@ jobs:
|
||||
|
||||
- name: Verify architecture image signatures
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Verifying ${arch} image signature..."
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
echo "✓ All images verified successfully"
|
||||
|
||||
@@ -380,7 +391,7 @@ jobs:
|
||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ matrix.registry }}/home-assistant
|
||||
sep-tags: ","
|
||||
@@ -394,24 +405,21 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
|
||||
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
# Use imagetools to copy image blobs directly between registries
|
||||
# This preserves provenance/attestations and seems to be much faster than pull/push
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Copying ${arch} image to DockerHub..."
|
||||
for attempt in 1 2 3; do
|
||||
if docker buildx imagetools create \
|
||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"; then
|
||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
||||
break
|
||||
fi
|
||||
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||
@@ -421,28 +429,23 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${VERSION}"
|
||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
|
||||
- name: Create and push multi-arch manifests
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
REGISTRY: ${{ matrix.registry }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
# Build list of architecture images dynamically
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
ARCH_IMAGES=()
|
||||
for arch in $ARCHS; do
|
||||
ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
||||
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
||||
done
|
||||
|
||||
# Build list of all tags for single manifest creation
|
||||
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
TAG_ARGS=()
|
||||
IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
||||
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS+=("--tag" "${tag}")
|
||||
done
|
||||
@@ -466,22 +469,20 @@ jobs:
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
id-token: write # For PyPI trusted publishing
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -499,7 +500,7 @@ jobs:
|
||||
python -m build
|
||||
|
||||
- name: Upload package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
@@ -507,10 +508,10 @@ jobs:
|
||||
name: Build and test hassfest image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
attestations: write # For build provenance attestation
|
||||
id-token: write # For build provenance attestation
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
needs: ["init"]
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
env:
|
||||
@@ -518,19 +519,17 @@ jobs:
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -538,12 +537,12 @@ jobs:
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
|
||||
- name: Run hassfest against core
|
||||
run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
||||
run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
|
||||
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -552,7 +551,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Check requirements (deterministic)
|
||||
|
||||
# Stage 1 of the Check requirements pipeline.
|
||||
#
|
||||
# Runs the deterministic Python checks and uploads the structured
|
||||
# results as an artifact. Stage 2 (the agentic workflow defined in
|
||||
# `check-requirements.md`) consumes the artifact on completion.
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
# Auto-trigger on PRs that touch tracked requirement files is disabled
|
||||
# for now while we iterate — testing the workflow_run handoff to the
|
||||
# agentic stage is hard with an auto-trigger. Re-enable once the chain
|
||||
# has been validated end-to-end.
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
# paths:
|
||||
# - "**/requirements*.txt"
|
||||
# - "homeassistant/package_constraints.txt"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pull_request_number:
|
||||
description: "Pull request number to (re-)check"
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deterministic:
|
||||
name: Run deterministic requirement checks
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # To fetch the PR diff via gh CLI
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Install script dependencies
|
||||
run: pip install -r script/check_requirements/requirements.txt
|
||||
- name: Collect PR diff
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
mkdir -p deterministic
|
||||
gh pr diff "${PR_NUMBER}" > deterministic/pr.diff
|
||||
- name: Run deterministic checks
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
python -m script.check_requirements \
|
||||
--pr-number "${PR_NUMBER}" \
|
||||
--diff deterministic/pr.diff \
|
||||
--output deterministic/results.json
|
||||
- name: Upload deterministic-results artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: deterministic/results.json
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,378 +0,0 @@
|
||||
---
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Check requirements (deterministic)"]
|
||||
types: [completed]
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
network:
|
||||
allowed:
|
||||
- python
|
||||
tools:
|
||||
web-fetch: {}
|
||||
github:
|
||||
toolsets: [default, actions]
|
||||
min-integrity: unapproved
|
||||
safe-outputs:
|
||||
add-comment:
|
||||
max: 1
|
||||
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
|
||||
needs:
|
||||
- extract_pr_number
|
||||
jobs:
|
||||
extract_pr_number:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||
steps:
|
||||
- name: Download deterministic-results artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract PR number from artifact
|
||||
id: extract
|
||||
run: |
|
||||
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
|
||||
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download deterministic-results artifact
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/gh-aw/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
post-steps:
|
||||
- name: Verify agent produced an add_comment safe-output
|
||||
if: always() && github.event.workflow_run.conclusion == 'success'
|
||||
run: |
|
||||
OUTPUT=/tmp/gh-aw/agent_output.json
|
||||
if [ ! -f "${OUTPUT}" ]; then
|
||||
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '"add_comment"' "${OUTPUT}"; then
|
||||
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
|
||||
echo "Agent output:"
|
||||
cat "${OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
description: >
|
||||
Resolves the deterministic-stage artifact's NEEDS_AGENT checks for changed
|
||||
Python package requirements on PRs targeting the core repo, then posts the
|
||||
final review comment. Triggered by completion of the deterministic workflow.
|
||||
Reads the uploaded artifact from disk, replaces placeholders for any check
|
||||
whose status is `needs_agent`, and posts the merged comment using the PR
|
||||
number recorded inside the artifact itself. Each check kind has a dedicated
|
||||
instruction section below; if the artifact contains a check kind that does
|
||||
not have a section here, the agent fails hard rather than guess.
|
||||
---
|
||||
|
||||
# Check requirements (AW)
|
||||
|
||||
You are a code review assistant for the Home Assistant project. The
|
||||
deterministic stage has already evaluated every check it can on its own
|
||||
and produced an artifact containing the PR number, per-package check
|
||||
results, and a pre-rendered comment with placeholders. **Your only job is
|
||||
to read that artifact, resolve any `needs_agent` checks, and post the
|
||||
final comment.**
|
||||
|
||||
## Step 1 — Read the deterministic-stage artifact
|
||||
|
||||
The deterministic stage uploaded its results to the runner at
|
||||
`/tmp/gh-aw/deterministic/results.json`.
|
||||
|
||||
The JSON has this shape:
|
||||
|
||||
- `pr_number` — the PR being checked. The `add_comment` safe-output is
|
||||
already targeted at this PR (a pre-job extracts `pr_number` from the
|
||||
artifact and the workflow wires it into the safe-output config via
|
||||
`needs.extract_pr_number.outputs.pr_number`), so **you do not need to
|
||||
set `item_number` yourself** — just emit `add_comment` with the
|
||||
rendered body.
|
||||
- `needs_agent` — `true` iff any package's check needs resolution.
|
||||
- `packages[]` — one entry per changed package. Each entry has:
|
||||
- `name`, `old_version` (`null` for a newly added package; otherwise the
|
||||
previous pin), `new_version`, `repo_url`, `publisher_kind`.
|
||||
- `checks` — a dict keyed by **check kind** (string). Each value has a
|
||||
`status` (`pass`, `warn`, `fail`, or `needs_agent`) and `details`.
|
||||
- `rendered_comment` — the final PR comment body, already rendered. For
|
||||
every check whose status is `needs_agent` it contains two placeholders
|
||||
you must replace:
|
||||
- `{{CHECK_CELL:<pkg-name>:<check-kind>}}` — one cell of the summary
|
||||
table. Replace with exactly one of `✅`, `⚠️`, `❌`.
|
||||
- `{{CHECK_DETAIL:<pkg-name>:<check-kind>}}` — the body of one bullet
|
||||
in the package's `<details>` block. Replace with
|
||||
`<icon> <one-line explanation>` (the bullet's leading
|
||||
`- **<label>**:` is already rendered — replace only the placeholder).
|
||||
|
||||
You **must not** modify any other content in `rendered_comment`. Do not
|
||||
re-evaluate checks that already have a deterministic status. Do not add
|
||||
or remove packages.
|
||||
|
||||
## Step 2 — Resolve each `needs_agent` check
|
||||
|
||||
For each `package` in `packages`:
|
||||
|
||||
For each `(check_kind, result)` in `package.checks` where
|
||||
`result.status == "needs_agent"`:
|
||||
|
||||
1. Look up `## Check kind: <check_kind>` in the **Check instructions**
|
||||
section below.
|
||||
2. **If no matching section exists**: emit a single `add_comment` whose
|
||||
body is:
|
||||
|
||||
```
|
||||
<!-- requirements-check -->
|
||||
## Check requirements
|
||||
|
||||
❌ Internal error: the deterministic artifact contains a check kind
|
||||
(`<check_kind>` on package `<pkg-name>`) that this workflow has no
|
||||
instructions for. Update `.github/workflows/check-requirements.md`
|
||||
to add a matching `## Check kind: <check_kind>` section, or remove
|
||||
the kind from the deterministic stage.
|
||||
```
|
||||
|
||||
Then stop. **Do not improvise** a verdict for an unknown check kind.
|
||||
3. Otherwise, follow the instructions in that section. They tell you
|
||||
which icon (✅/⚠️/❌) and one-line explanation to produce.
|
||||
|
||||
## Step 3 — Post the comment
|
||||
|
||||
1. Replace every `{{CHECK_CELL:…}}` and `{{CHECK_DETAIL:…}}` placeholder
|
||||
in `rendered_comment` with the resolved value.
|
||||
2. Emit the resulting markdown using `add_comment` — set `body` to the
|
||||
merged `rendered_comment` verbatim (the leading
|
||||
`<!-- requirements-check -->` marker must be preserved). The PR
|
||||
target is already set by the workflow; do not pass `item_number`.
|
||||
|
||||
If the artifact's top-level `needs_agent` is `false` (no checks need
|
||||
you), emit `rendered_comment` unchanged.
|
||||
|
||||
## Check instructions
|
||||
|
||||
### Check kind: `repo_public`
|
||||
|
||||
Verify that the package's source repository is publicly reachable.
|
||||
|
||||
1. Read `package.repo_url`.
|
||||
2. Use the `web-fetch` tool to GET that URL.
|
||||
3. Decide the verdict:
|
||||
- HTTP 200, returns a public repository page → ✅
|
||||
`<repo_url> is publicly accessible.`
|
||||
- HTTP 4xx/5xx, or the response redirects to a login / sign-in page →
|
||||
❌ `Source repository at <repo_url> is not publicly accessible.
|
||||
Home Assistant requires all dependencies to have publicly available
|
||||
source code.`
|
||||
- Any other inconclusive result → ⚠️ with a one-line description.
|
||||
|
||||
If `repo_public` resolves to ❌ for a package, **also** mark that
|
||||
package's `release_pipeline` and `async_blocking` cells/details as `—`
|
||||
(em dash) and explain `Skipped because the source repository is not
|
||||
publicly accessible.` — neither check can be performed without a public
|
||||
repo.
|
||||
|
||||
### Check kind: `pr_link`
|
||||
|
||||
Verify the PR description contains the right link for the change.
|
||||
|
||||
1. Fetch the PR body via the GitHub MCP tool, using the `pr_number`
|
||||
field from the artifact.
|
||||
2. Extract all URLs from the body.
|
||||
3. For a **new package** (`package.old_version` is `null`):
|
||||
- The PR body must contain a URL that points at `package.repo_url`
|
||||
(any sub-path of the same `owner/repo` on the same host is
|
||||
acceptable). A PyPI link is **not** sufficient.
|
||||
- ✅ if such a URL is present.
|
||||
- ❌ otherwise:
|
||||
`PR description must link to the source repository at <repo_url>.
|
||||
A PyPI page link is not sufficient.`
|
||||
4. For a **version bump** (`package.old_version` is not `null`):
|
||||
- The PR body must contain a URL on the same host as
|
||||
`package.repo_url` that references **both** `package.old_version`
|
||||
and `package.new_version` (e.g. a GitHub compare URL
|
||||
`compare/vX...vY`, a release / changelog URL containing both
|
||||
versions, etc.).
|
||||
- ✅ if such a URL is present and the versions match the actual bump.
|
||||
- ❌ otherwise:
|
||||
`PR description should link to a changelog or compare URL on
|
||||
<repo_url> that mentions both <old_version> and <new_version>.`
|
||||
|
||||
### Check kind: `release_pipeline`
|
||||
|
||||
Inspect the upstream project's release / publish CI pipeline.
|
||||
|
||||
For each package needing inspection, determine the source repository
|
||||
host from `package.repo_url`, then apply the corresponding checklist.
|
||||
|
||||
#### GitHub repositories (`github.com`)
|
||||
|
||||
1. List workflows: `GET /repos/{owner}/{repo}/actions/workflows`.
|
||||
2. Identify any workflow whose name or filename suggests publishing to
|
||||
PyPI (`release`, `publish`, `pypi`, or `deploy`).
|
||||
3. Fetch the workflow file and check:
|
||||
- **Trigger sanity**: triggered by `push` to tags,
|
||||
`release: published`, or `workflow_run` on a release job —
|
||||
**not** solely `workflow_dispatch` with no environment-protection
|
||||
guard.
|
||||
- **OIDC / Trusted Publisher**: look for `id-token: write` and one of
|
||||
`pypa/gh-action-pypi-publish`, `actions/attest-build-provenance`,
|
||||
or `TWINE_PASSWORD` from a static `secrets.PYPI_TOKEN`.
|
||||
- **No manual upload bypass**: no ungated `twine upload` or
|
||||
`pip upload`.
|
||||
4. Verdict:
|
||||
- ✅ if OIDC + sane triggers + no bypass.
|
||||
- ⚠️ if static token but version bump, or details unclear.
|
||||
- ❌ if static token on a new package, or only-manual triggers with
|
||||
no environment protection.
|
||||
|
||||
#### GitLab repositories (`gitlab.com` or self-hosted GitLab)
|
||||
|
||||
1. Resolve the project ID via
|
||||
`GET https://gitlab.com/api/v4/projects/{url-encoded-namespace-and-name}`.
|
||||
2. Fetch `.gitlab-ci.yml` via
|
||||
`GET https://gitlab.com/api/v4/projects/{id}/repository/files/.gitlab-ci.yml/raw?ref=HEAD`.
|
||||
3. Apply the same conceptual checks: tag-only / protected-branch
|
||||
triggers, GitLab OIDC `id_tokens` or CI/CD protected `PYPI_TOKEN`, no
|
||||
ungated `twine upload`. Same verdict rules as GitHub.
|
||||
|
||||
#### Other code hosting providers (Bitbucket, Codeberg, Gitea, Sourcehut, …)
|
||||
|
||||
1. Use `web-fetch` to retrieve any visible CI configuration
|
||||
(`.circleci/config.yml`, `Jenkinsfile`, `azure-pipelines.yml`,
|
||||
`bitbucket-pipelines.yml`, `.builds/*.yml`).
|
||||
2. Apply the conceptual checks: automated triggers, CI-injected
|
||||
credentials, no manual `twine upload`.
|
||||
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
|
||||
inspected; hosting provider is not GitHub or GitLab.`
|
||||
|
||||
### Check kind: `async_blocking`
|
||||
|
||||
Verify whether the dependency performs blocking I/O inside async code
|
||||
paths. Home Assistant runs on a single asyncio event loop, so a library
|
||||
that exposes an `async` surface must not call blocking APIs from inside
|
||||
its `async def` functions — that stalls the whole loop. A purely sync
|
||||
library is fine: Home Assistant integrations are expected to wrap such
|
||||
calls in an executor.
|
||||
|
||||
**Two modes — pick by inspecting `package.old_version`:**
|
||||
|
||||
- `old_version` is `null` → **new package**: review the *entire current
|
||||
source tree*. Nothing about this dependency has been vetted before.
|
||||
- `old_version` is a string → **version bump**: review only the *diff
|
||||
between `old_version` and `new_version`*. The previous version was
|
||||
already accepted, so blocking calls that were present in
|
||||
`old_version` are not regressions; report only what `new_version`
|
||||
introduces.
|
||||
|
||||
#### Step 1 — Decide whether the library exposes an async surface
|
||||
|
||||
Use the `github` MCP tool (for `github.com` repos) or `web-fetch`
|
||||
(other hosts) on `package.repo_url`. Always inspect the tag /
|
||||
ref matching `new_version` (e.g. `v{new_version}` or `{new_version}`).
|
||||
|
||||
- Locate the top-level package directory (usually named after the
|
||||
import name, often equal or close to `package.name`).
|
||||
- Check `pyproject.toml` / `setup.py` / `setup.cfg` / `README*` for
|
||||
async indicators (`Framework :: AsyncIO` trove classifier, `asyncio`
|
||||
/ `aiohttp` / `httpx` / `anyio` in dependencies, an async usage
|
||||
example in the README).
|
||||
- Grep the package source for `async def`. A handful of `async def`
|
||||
entries in the public modules is enough to treat the library as
|
||||
having an async surface.
|
||||
|
||||
If the library is **sync-only** (no `async def` in its public modules
|
||||
and no async framework dependency) → ✅
|
||||
`Sync-only library; Home Assistant integrations must wrap calls in an
|
||||
executor.` *This verdict is the same in both modes.*
|
||||
|
||||
#### Step 2a — Mode: new package (`old_version` is `null`)
|
||||
|
||||
Inspect **every `async def` in the public modules** for blocking
|
||||
patterns. Walk transitively into helpers the async functions call.
|
||||
|
||||
#### Step 2b — Mode: version bump (`old_version` is a string)
|
||||
|
||||
Fetch the diff between the two tags and review **only changed lines**:
|
||||
|
||||
- GitHub: `GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag}` via
|
||||
the `github` MCP tool, or
|
||||
`https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff`
|
||||
via `web-fetch`. Try the common tag formats in order until one
|
||||
resolves: `v{version}`, `{version}`, `release-{version}`.
|
||||
- GitLab: `https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff`.
|
||||
- Other hosts: use the project's equivalent compare URL via
|
||||
`web-fetch`.
|
||||
|
||||
If neither tag format resolves on the host, fall back to a full review
|
||||
(Step 2a) and mention in the detail that the diff was unavailable.
|
||||
|
||||
When reviewing the diff, only flag blocking patterns that appear in
|
||||
**added lines** *inside or reachable from* an `async def`. A blocking
|
||||
call that existed in `old_version` and is unchanged is not a regression
|
||||
for this bump.
|
||||
|
||||
#### Step 3 — Blocking patterns to look for
|
||||
|
||||
In both modes, the patterns to flag inside `async def` bodies are:
|
||||
|
||||
- Sync HTTP: `requests.`, `urllib.request`, `urllib3.` direct use,
|
||||
`http.client.`, sync `httpx.Client(` / `httpx.get(` (NOT the
|
||||
`AsyncClient`), `pycurl`.
|
||||
- `time.sleep(` (must be `await asyncio.sleep(`).
|
||||
- Sync sockets: bare `socket.socket` reads/writes, `ssl.wrap_socket`,
|
||||
blocking `select.select`.
|
||||
- File I/O: `open(` / `pathlib.Path.read_*` / `.write_*` for
|
||||
non-trivial sizes (small one-shot reads during import are
|
||||
acceptable; reads/writes on the request path are not — prefer
|
||||
`aiofiles` / executor).
|
||||
- Sync DB drivers used directly: `sqlite3`, `psycopg2`, `pymysql`,
|
||||
`pymongo` (sync client), `redis.Redis` (sync client).
|
||||
- `subprocess.run` / `subprocess.call` / `os.system` (must be
|
||||
`asyncio.create_subprocess_*`).
|
||||
|
||||
A call that is clearly dispatched to an executor
|
||||
(`run_in_executor`, `asyncio.to_thread`, `anyio.to_thread.run_sync`)
|
||||
does NOT count as blocking.
|
||||
|
||||
#### Step 4 — Verdict
|
||||
|
||||
- ✅ — no offending blocking pattern in the surface being reviewed
|
||||
(whole tree for a new package, added lines for a bump). For a bump,
|
||||
phrase the detail as `No new blocking calls introduced in
|
||||
{old_version} → {new_version}.`.
|
||||
- ⚠️ — blocking calls exist only in sync helpers that the async API
|
||||
does not call, or only on a clearly non-hot path (e.g. one-shot
|
||||
setup before the event loop is running). Cite at least one
|
||||
`<file>:<line>` and explain why it is not on the hot path.
|
||||
- ❌ — a blocking call is reachable from an `async def` that is part
|
||||
of the public API on the request / polling path (for a bump: the
|
||||
call was introduced or moved onto the hot path by this version).
|
||||
Cite the offending `<file>:<line>` as a clickable link on the repo
|
||||
host so the contributor can jump to it.
|
||||
|
||||
## Notes
|
||||
|
||||
- Be constructive and helpful. Reference the inspected workflow / CI
|
||||
file by URL where useful so the contributor can fix the issue.
|
||||
- The dedup of the requirements-check comment is handled by gh-aw's
|
||||
`add_comment` safe-output via the `<!-- requirements-check -->`
|
||||
marker on the first line of `rendered_comment`.
|
||||
- If the deterministic workflow concluded with a non-success status,
|
||||
this workflow's `if:` guard on `Download deterministic-results
|
||||
artifact` skipped the download. If you find no file at
|
||||
`/tmp/gh-aw/deterministic/results.json`, emit nothing — the post-step
|
||||
verification is also gated and will not complain.
|
||||
+528
-740
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,6 @@ on:
|
||||
schedule:
|
||||
- cron: "30 18 * * 4"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -17,22 +15,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
actions: read # To read workflow information for CodeQL
|
||||
contents: read # To check out the repository
|
||||
security-events: write # To upload CodeQL results
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -5,23 +5,18 @@ on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-duplicates:
|
||||
name: Detect duplicate issues
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on and label issues
|
||||
models: read # For AI-based duplicate detection
|
||||
|
||||
steps:
|
||||
- name: Check if integration label was added and extract details
|
||||
id: extract
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
// Debug: Log the event payload
|
||||
@@ -118,7 +113,7 @@ jobs:
|
||||
- name: Fetch similar issues
|
||||
id: fetch_similar
|
||||
if: steps.extract.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
||||
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
||||
@@ -236,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
@@ -285,7 +280,7 @@ jobs:
|
||||
- name: Post duplicate detection results
|
||||
id: post_results
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
||||
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
||||
|
||||
@@ -5,23 +5,18 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-language:
|
||||
name: Detect non-English issues
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
models: read # For AI-based language detection
|
||||
|
||||
steps:
|
||||
- name: Check issue language
|
||||
id: detect_language
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
@@ -62,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
@@ -95,7 +90,7 @@ jobs:
|
||||
|
||||
- name: Process non-English issues
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
||||
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
||||
|
||||
@@ -5,20 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
name: Lock inactive threads
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To lock issues
|
||||
pull-requests: write # To lock pull requests
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
with:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"owner": "check-executables-have-shebangs",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$",
|
||||
"regexp": "^(.+):\\s(.+)$",
|
||||
"file": 1,
|
||||
"message": 2
|
||||
}
|
||||
|
||||
@@ -5,44 +5,14 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To read CODEOWNERS file
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
@@ -6,20 +6,10 @@ on:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: Mark stale issues and PRs
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To label and close stale issues
|
||||
pull-requests: write # To label and close stale PRs
|
||||
steps:
|
||||
# The 60 day stale policy for PRs
|
||||
# Used for:
|
||||
@@ -27,7 +17,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues (-1)
|
||||
- name: 60 days stale PRs policy
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
@@ -55,11 +45,11 @@ jobs:
|
||||
- name: Generate app token
|
||||
id: token
|
||||
# Pinned to a specific version of the action for security reasons
|
||||
# v3.2.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
# v1.7.0
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
with:
|
||||
app-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
|
||||
private-key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
|
||||
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
|
||||
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
|
||||
|
||||
# The 90 day stale policy for issues
|
||||
# Used for:
|
||||
@@ -67,7 +57,7 @@ jobs:
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: 90 days stale issues
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
days-before-stale: 90
|
||||
@@ -97,7 +87,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
only-labels: "needs-more-information"
|
||||
|
||||
@@ -9,11 +9,8 @@ on:
|
||||
paths:
|
||||
- "**strings.json"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
@@ -22,17 +19,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Upload Translations
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
run: |
|
||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||
python3 -m script.translations upload
|
||||
|
||||
@@ -16,7 +16,8 @@ on:
|
||||
- "requirements.txt"
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
permissions: {}
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
@@ -28,16 +29,15 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- &checkout
|
||||
name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
|
||||
- name: Create Python virtual environment
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Create requirements_diff file
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" =~ (schedule|workflow_dispatch) ]]; then
|
||||
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
||||
touch requirements_diff.txt
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -106,8 +106,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp314"]
|
||||
matrix: &matrix-build
|
||||
abi: ["cp313", "cp314"]
|
||||
arch: ["amd64", "aarch64"]
|
||||
include:
|
||||
- arch: amd64
|
||||
@@ -115,18 +115,17 @@ jobs:
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- &download-env-file
|
||||
name: Download env_file
|
||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- &download-requirements-diff
|
||||
name: Download requirements_diff
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -137,12 +136,12 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
|
||||
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
||||
@@ -157,32 +156,16 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp314"]
|
||||
arch: ["amd64", "aarch64"]
|
||||
include:
|
||||
- arch: amd64
|
||||
os: ubuntu-latest
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
matrix: *matrix-build
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: env_file
|
||||
- *download-env-file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
- *download-requirements-diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@@ -195,15 +178,15 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: *home-assistant-wheels
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"
|
||||
requirements: "requirements_all.txt"
|
||||
|
||||
@@ -142,6 +142,5 @@ pytest_buckets.txt
|
||||
|
||||
# AI tooling
|
||||
.claude/settings.local.json
|
||||
.claude/worktrees/
|
||||
.serena/
|
||||
|
||||
|
||||
+5
-20
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.13
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
@@ -8,7 +8,7 @@ repos:
|
||||
- id: ruff-format
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.2
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
@@ -17,13 +17,6 @@ repos:
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args:
|
||||
- --pedantic
|
||||
exclude: ^\.github/workflows/.*\.lock\.yml$
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
@@ -37,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.38.0
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
@@ -46,15 +39,14 @@ repos:
|
||||
- id: prettier
|
||||
additional_dependencies:
|
||||
- prettier@3.6.2
|
||||
- prettier-plugin-sort-json@4.2.0
|
||||
exclude: ^\.github/workflows/.*\.lock\.yml$
|
||||
- prettier-plugin-sort-json@4.1.1
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run `python-typing-update` hook manually from time to time
|
||||
# to update python typing syntax.
|
||||
# Will require manual work, before submitting changes!
|
||||
# prek run --hook-stage manual python-typing-update --all-files
|
||||
# pre-commit run --hook-stage manual python-typing-update --all-files
|
||||
- id: python-typing-update
|
||||
stages: [manual]
|
||||
args:
|
||||
@@ -89,13 +81,6 @@ repos:
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
||||
- id: gen_copilot_instructions
|
||||
name: gen_copilot_instructions
|
||||
entry: script/run-in-env.sh python3 -m script.gen_copilot_instructions
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(AGENTS\.md|\.claude/skills/(?!github-pr-reviewer/).+/SKILL\.md|\.github/copilot-instructions\.md|script/gen_copilot_instructions\.py)$
|
||||
- id: hassfest
|
||||
name: hassfest
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.14.5
|
||||
3.13
|
||||
|
||||
+2
-59
@@ -46,16 +46,13 @@ homeassistant.components.accuweather.*
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.acmeda.*
|
||||
homeassistant.components.actiontec.*
|
||||
homeassistant.components.actron_air.*
|
||||
homeassistant.components.adax.*
|
||||
homeassistant.components.adguard.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.ai_task.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airobot.*
|
||||
homeassistant.components.airos.*
|
||||
homeassistant.components.airq.*
|
||||
homeassistant.components.airthings.*
|
||||
@@ -86,7 +83,6 @@ homeassistant.components.androidtv_remote.*
|
||||
homeassistant.components.anel_pwrctrl.*
|
||||
homeassistant.components.anova.*
|
||||
homeassistant.components.anthemav.*
|
||||
homeassistant.components.anthropic.*
|
||||
homeassistant.components.apache_kafka.*
|
||||
homeassistant.components.apcupsd.*
|
||||
homeassistant.components.api.*
|
||||
@@ -124,6 +120,7 @@ homeassistant.components.blueprint.*
|
||||
homeassistant.components.bluesound.*
|
||||
homeassistant.components.bluetooth.*
|
||||
homeassistant.components.bluetooth_adapters.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
homeassistant.components.bond.*
|
||||
homeassistant.components.bosch_alarm.*
|
||||
homeassistant.components.braviatv.*
|
||||
@@ -131,15 +128,12 @@ homeassistant.components.bring.*
|
||||
homeassistant.components.brother.*
|
||||
homeassistant.components.browser.*
|
||||
homeassistant.components.bryant_evolution.*
|
||||
homeassistant.components.bsblan.*
|
||||
homeassistant.components.bthome.*
|
||||
homeassistant.components.button.*
|
||||
homeassistant.components.calendar.*
|
||||
homeassistant.components.cambridge_audio.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.casper_glow.*
|
||||
homeassistant.components.centriconnect.*
|
||||
homeassistant.components.cert_expiry.*
|
||||
homeassistant.components.clickatell.*
|
||||
homeassistant.components.clicksend.*
|
||||
@@ -156,7 +150,6 @@ homeassistant.components.counter.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.cpuspeed.*
|
||||
homeassistant.components.crownstone.*
|
||||
homeassistant.components.data_grand_lyon.*
|
||||
homeassistant.components.date.*
|
||||
homeassistant.components.datetime.*
|
||||
homeassistant.components.deako.*
|
||||
@@ -177,11 +170,9 @@ homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.downloader.*
|
||||
homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
homeassistant.components.duco.*
|
||||
homeassistant.components.dunehd.*
|
||||
homeassistant.components.duotecno.*
|
||||
homeassistant.components.easyenergy.*
|
||||
@@ -216,9 +207,7 @@ homeassistant.components.firefly_iii.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flexit_bacnet.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.folder_watcher.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
homeassistant.components.freshr.*
|
||||
homeassistant.components.fritz.*
|
||||
homeassistant.components.fritzbox.*
|
||||
homeassistant.components.fritzbox_callmonitor.*
|
||||
@@ -226,13 +215,11 @@ homeassistant.components.fronius.*
|
||||
homeassistant.components.frontend.*
|
||||
homeassistant.components.fujitsu_fglair.*
|
||||
homeassistant.components.fully_kiosk.*
|
||||
homeassistant.components.fumis.*
|
||||
homeassistant.components.fyta.*
|
||||
homeassistant.components.generic_hygrostat.*
|
||||
homeassistant.components.generic_thermostat.*
|
||||
homeassistant.components.geo_location.*
|
||||
homeassistant.components.geocaching.*
|
||||
homeassistant.components.ghost.*
|
||||
homeassistant.components.gios.*
|
||||
homeassistant.components.github.*
|
||||
homeassistant.components.glances.*
|
||||
@@ -250,11 +237,9 @@ homeassistant.components.gpsd.*
|
||||
homeassistant.components.greeneye_monitor.*
|
||||
homeassistant.components.group.*
|
||||
homeassistant.components.guardian.*
|
||||
homeassistant.components.guntamatic.*
|
||||
homeassistant.components.habitica.*
|
||||
homeassistant.components.hardkernel.*
|
||||
homeassistant.components.hardware.*
|
||||
homeassistant.components.hdfury.*
|
||||
homeassistant.components.heos.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
@@ -280,15 +265,12 @@ homeassistant.components.homekit_controller.storage
|
||||
homeassistant.components.homekit_controller.utils
|
||||
homeassistant.components.homewizard.*
|
||||
homeassistant.components.homeworks.*
|
||||
homeassistant.components.hr_energy_qube.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.humidifier.*
|
||||
homeassistant.components.husqvarna_automower.*
|
||||
homeassistant.components.huum.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.hypontech.*
|
||||
homeassistant.components.ibeacon.*
|
||||
homeassistant.components.idasen_desk.*
|
||||
homeassistant.components.image.*
|
||||
@@ -298,14 +280,11 @@ homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.immich.*
|
||||
homeassistant.components.incomfort.*
|
||||
homeassistant.components.indevolt.*
|
||||
homeassistant.components.inels.*
|
||||
homeassistant.components.infrared.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.intelliclima.*
|
||||
homeassistant.components.intent.*
|
||||
homeassistant.components.intent_script.*
|
||||
homeassistant.components.ios.*
|
||||
@@ -313,7 +292,6 @@ homeassistant.components.iotty.*
|
||||
homeassistant.components.ipp.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.iron_os.*
|
||||
homeassistant.components.isal.*
|
||||
homeassistant.components.islamic_prayer_times.*
|
||||
homeassistant.components.isy994.*
|
||||
homeassistant.components.jellyfin.*
|
||||
@@ -324,7 +302,6 @@ homeassistant.components.knocki.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.kulersky.*
|
||||
homeassistant.components.labs.*
|
||||
homeassistant.components.lacrosse.*
|
||||
homeassistant.components.lacrosse_view.*
|
||||
homeassistant.components.lamarzocco.*
|
||||
@@ -336,11 +313,8 @@ homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.lg_tv_rs232.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.liebherr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linkplay.*
|
||||
@@ -356,10 +330,8 @@ homeassistant.components.lookin.*
|
||||
homeassistant.components.lovelace.*
|
||||
homeassistant.components.luftdaten.*
|
||||
homeassistant.components.lunatone.*
|
||||
homeassistant.components.lutron.*
|
||||
homeassistant.components.madvr.*
|
||||
homeassistant.components.manual.*
|
||||
homeassistant.components.marantz_infrared.*
|
||||
homeassistant.components.mastodon.*
|
||||
homeassistant.components.matrix.*
|
||||
homeassistant.components.matter.*
|
||||
@@ -389,7 +361,7 @@ homeassistant.components.my.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.myuplink.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.namecheapdns.*
|
||||
homeassistant.components.nanoleaf.*
|
||||
homeassistant.components.nasweb.*
|
||||
homeassistant.components.neato.*
|
||||
homeassistant.components.nest.*
|
||||
@@ -403,7 +375,6 @@ homeassistant.components.no_ip.*
|
||||
homeassistant.components.nordpool.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.nrgkick.*
|
||||
homeassistant.components.ntfy.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
@@ -411,13 +382,11 @@ homeassistant.components.ohme.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
homeassistant.components.onedrive_for_business.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.open_router.*
|
||||
homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openevse.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
@@ -425,13 +394,9 @@ homeassistant.components.opnsense.*
|
||||
homeassistant.components.opower.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.otp.*
|
||||
homeassistant.components.ouman_eh_800.*
|
||||
homeassistant.components.overkiz.*
|
||||
homeassistant.components.overseerr.*
|
||||
homeassistant.components.ovhcloud_ai_endpoints.*
|
||||
homeassistant.components.p1_monitor.*
|
||||
homeassistant.components.paj_gps.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.paperless_ngx.*
|
||||
homeassistant.components.peblar.*
|
||||
@@ -442,16 +407,13 @@ homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.ping.*
|
||||
homeassistant.components.plugwise.*
|
||||
homeassistant.components.pooldose.*
|
||||
homeassistant.components.portainer.*
|
||||
homeassistant.components.powerfox.*
|
||||
homeassistant.components.powerfox_local.*
|
||||
homeassistant.components.powerwall.*
|
||||
homeassistant.components.private_ble_device.*
|
||||
homeassistant.components.prometheus.*
|
||||
homeassistant.components.proximity.*
|
||||
homeassistant.components.prusalink.*
|
||||
homeassistant.components.ptdevices.*
|
||||
homeassistant.components.pure_energie.*
|
||||
homeassistant.components.purpleair.*
|
||||
homeassistant.components.pushbullet.*
|
||||
@@ -465,13 +427,10 @@ homeassistant.components.radarr.*
|
||||
homeassistant.components.radio_browser.*
|
||||
homeassistant.components.rainforest_raven.*
|
||||
homeassistant.components.rainmachine.*
|
||||
homeassistant.components.random.*
|
||||
homeassistant.components.raspberry_pi.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.recovery_mode.*
|
||||
homeassistant.components.redgtech.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.remote_calendar.*
|
||||
@@ -494,16 +453,13 @@ homeassistant.components.rss_feed_template.*
|
||||
homeassistant.components.russound_rio.*
|
||||
homeassistant.components.ruuvi_gateway.*
|
||||
homeassistant.components.ruuvitag_ble.*
|
||||
homeassistant.components.samsung_infrared.*
|
||||
homeassistant.components.samsungtv.*
|
||||
homeassistant.components.saunum.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.schedule.*
|
||||
homeassistant.components.schlage.*
|
||||
homeassistant.components.scrape.*
|
||||
homeassistant.components.script.*
|
||||
homeassistant.components.search.*
|
||||
homeassistant.components.season.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
@@ -530,7 +486,6 @@ homeassistant.components.smtp.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
homeassistant.components.spaceapi.*
|
||||
homeassistant.components.speedtestdotnet.*
|
||||
homeassistant.components.spotify.*
|
||||
homeassistant.components.sql.*
|
||||
@@ -555,7 +510,6 @@ homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.system_health.*
|
||||
homeassistant.components.system_log.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.systemnexa2.*
|
||||
homeassistant.components.tag.*
|
||||
homeassistant.components.tailscale.*
|
||||
homeassistant.components.tailwind.*
|
||||
@@ -566,9 +520,6 @@ homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.telegram_bot.*
|
||||
homeassistant.components.teleinfo.*
|
||||
homeassistant.components.teltonika.*
|
||||
homeassistant.components.teslemetry.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
homeassistant.components.threshold.*
|
||||
@@ -592,27 +543,21 @@ homeassistant.components.trafikverket_train.*
|
||||
homeassistant.components.trafikverket_weatherstation.*
|
||||
homeassistant.components.transmission.*
|
||||
homeassistant.components.trend.*
|
||||
homeassistant.components.trmnl.*
|
||||
homeassistant.components.tts.*
|
||||
homeassistant.components.twentemilieu.*
|
||||
homeassistant.components.unifi.*
|
||||
homeassistant.components.unifi_access.*
|
||||
homeassistant.components.unifiprotect.*
|
||||
homeassistant.components.upcloud.*
|
||||
homeassistant.components.update.*
|
||||
homeassistant.components.uptime.*
|
||||
homeassistant.components.uptime_kuma.*
|
||||
homeassistant.components.uptimerobot.*
|
||||
homeassistant.components.usage_prediction.*
|
||||
homeassistant.components.usb.*
|
||||
homeassistant.components.uvc.*
|
||||
homeassistant.components.vacuum.*
|
||||
homeassistant.components.vallox.*
|
||||
homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.velux.*
|
||||
homeassistant.components.victron_gx.*
|
||||
homeassistant.components.vistapool.*
|
||||
homeassistant.components.vivotek.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
@@ -625,7 +570,6 @@ homeassistant.components.water_heater.*
|
||||
homeassistant.components.watts.*
|
||||
homeassistant.components.watttime.*
|
||||
homeassistant.components.weather.*
|
||||
homeassistant.components.web_rtc.*
|
||||
homeassistant.components.webhook.*
|
||||
homeassistant.components.webostv.*
|
||||
homeassistant.components.websocket_api.*
|
||||
@@ -642,7 +586,6 @@ homeassistant.components.yale_smart_alarm.*
|
||||
homeassistant.components.yalexs_ble.*
|
||||
homeassistant.components.youtube.*
|
||||
homeassistant.components.zeroconf.*
|
||||
homeassistant.components.zinvolt.*
|
||||
homeassistant.components.zodiac.*
|
||||
homeassistant.components.zone.*
|
||||
homeassistant.components.zwave_js.*
|
||||
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
"python.testing.pytestEnabled": false,
|
||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
// Pyright is too pedantic for Home Assistant
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
},
|
||||
|
||||
Vendored
+7
-7
@@ -45,7 +45,7 @@
|
||||
{
|
||||
"label": "Ruff",
|
||||
"type": "shell",
|
||||
"command": "prek run ruff-check --all-files",
|
||||
"command": "pre-commit run ruff-check --all-files",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
@@ -57,9 +57,9 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Prek",
|
||||
"label": "Pre-commit",
|
||||
"type": "shell",
|
||||
"command": "prek run --show-diff-on-failure",
|
||||
"command": "pre-commit run --show-diff-on-failure",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
@@ -120,7 +120,7 @@
|
||||
{
|
||||
"label": "Generate Requirements",
|
||||
"type": "shell",
|
||||
"command": "${command:python.interpreterPath} -m script.gen_requirements_all",
|
||||
"command": "./script/gen_requirements_all.py",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@@ -132,7 +132,7 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all production Requirements",
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_all.txt",
|
||||
"group": {
|
||||
@@ -146,9 +146,9 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all (test & production) Requirements",
|
||||
"label": "Install all Test Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
|
||||
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
ignore: |
|
||||
tests/fixtures/core/config/yaml_errors/
|
||||
.github/workflows/*.lock.yml
|
||||
rules:
|
||||
braces:
|
||||
level: error
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||
|
||||
## Git Commit Guidelines
|
||||
|
||||
- **Do NOT amend, squash, or rebase commits that have already been pushed to the PR branch after the PR is opened** - Reviewers need to follow the commit history, as well as see what changed since their last review
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- When opening a pull request, use the repository's PR template (`.github/PULL_REQUEST_TEMPLATE.md`). NEVER REMOVE ANYTHING from the template.
|
||||
- Do not remove checkboxes that are not checked — leave all unchecked checkboxes in place so reviewers can see which options were not selected.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
- Home Assistant officially supports Python 3.14 as its minimum version. Do not flag syntax or features that require Python 3.14 as issues, and do not suggest workarounds for older Python versions.
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue.
|
||||
- Python 3.14 evaluates annotations lazily (PEP 649). Forward references in annotations do not need to be quoted — annotations can reference names defined later in the module without quoting them or using `from __future__ import annotations`. Do not flag unquoted forward references in annotations as issues.
|
||||
|
||||
## Testing
|
||||
|
||||
- Use `uv run pytest` to run tests
|
||||
- After modifying `strings.json` for an integration, regenerate the English translation file before running tests: `.venv/bin/python3 -m script.translations develop --integration <integration_name>`. Tests load translations from the generated `translations/en.json`, not directly from `strings.json`.
|
||||
- When writing or modifying tests, ensure all test function parameters have type annotations.
|
||||
- Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
|
||||
- Prefer `@pytest.mark.usefixtures` over arguments, if the argument is not going to be used.
|
||||
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
||||
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
||||
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
||||
|
||||
## Good practices
|
||||
|
||||
- Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
- When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
|
||||
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
|
||||
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why — non-obvious constraints, surprising behavior, or workarounds — never what.
|
||||
Generated
+73
-246
@@ -15,7 +15,7 @@
|
||||
.yamllint @home-assistant/core
|
||||
pyproject.toml @home-assistant/core
|
||||
requirements_test.txt @home-assistant/core
|
||||
/.devcontainer/ @home-assistant/core @edenhaus
|
||||
/.devcontainer/ @home-assistant/core
|
||||
/.github/ @home-assistant/core
|
||||
/.vscode/ @home-assistant/core
|
||||
/homeassistant/*.py @home-assistant/core
|
||||
@@ -37,13 +37,6 @@ build.json @home-assistant/supervisor
|
||||
# Other code
|
||||
/homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
# Agent Configurations
|
||||
AGENTS.md @home-assistant/core
|
||||
CLAUDE.md @home-assistant/core
|
||||
/.agent/ @home-assistant/core
|
||||
/.claude/ @home-assistant/core
|
||||
/.gemini/ @home-assistant/core
|
||||
|
||||
# Integrations
|
||||
/homeassistant/components/abode/ @shred86
|
||||
/tests/components/abode/ @shred86
|
||||
@@ -68,8 +61,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/components/ai_task/ @home-assistant/core
|
||||
/tests/components/ai_task/ @home-assistant/core
|
||||
/homeassistant/components/aidot/ @s1eedz @HongBryan
|
||||
/tests/components/aidot/ @s1eedz @HongBryan
|
||||
/homeassistant/components/air_quality/ @home-assistant/core
|
||||
/tests/components/air_quality/ @home-assistant/core
|
||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||
@@ -195,10 +186,7 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/auth/ @home-assistant/core
|
||||
/homeassistant/components/automation/ @home-assistant/core
|
||||
/tests/components/automation/ @home-assistant/core
|
||||
/homeassistant/components/autoskope/ @mcisk
|
||||
/tests/components/autoskope/ @mcisk
|
||||
/homeassistant/components/avea/ @pattyland
|
||||
/tests/components/avea/ @pattyland
|
||||
/homeassistant/components/awair/ @ahayworth @ricohageman
|
||||
/tests/components/awair/ @ahayworth @ricohageman
|
||||
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||
@@ -224,20 +212,18 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/balboa/ @garbled1 @natekspencer
|
||||
/homeassistant/components/bang_olufsen/ @mj23000
|
||||
/tests/components/bang_olufsen/ @mj23000
|
||||
/homeassistant/components/battery/ @home-assistant/core
|
||||
/tests/components/battery/ @home-assistant/core
|
||||
/homeassistant/components/bayesian/ @HarvsG
|
||||
/tests/components/bayesian/ @HarvsG
|
||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||
/homeassistant/components/binary_sensor/ @home-assistant/core
|
||||
/tests/components/binary_sensor/ @home-assistant/core
|
||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||
/homeassistant/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||
/tests/components/blebox/ @bbx-a @swistakm
|
||||
/homeassistant/components/blink/ @fronzbot
|
||||
/tests/components/blink/ @fronzbot
|
||||
/homeassistant/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
@@ -248,14 +234,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/bluetooth/ @bdraco
|
||||
/homeassistant/components/bluetooth_adapters/ @bdraco
|
||||
/tests/components/bluetooth_adapters/ @bdraco
|
||||
/homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
|
||||
/tests/components/bosch_alarm/ @mag1024 @sanjay900
|
||||
/homeassistant/components/bosch_shc/ @tschamm
|
||||
/tests/components/bosch_shc/ @tschamm
|
||||
/homeassistant/components/brands/ @home-assistant/core
|
||||
/tests/components/brands/ @home-assistant/core
|
||||
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
||||
/tests/components/braviatv/ @bieniu @Drafteed
|
||||
/homeassistant/components/bring/ @miaucl @tr4nt0r
|
||||
@@ -285,22 +271,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/cambridge_audio/ @noahhusby
|
||||
/homeassistant/components/camera/ @home-assistant/core
|
||||
/tests/components/camera/ @home-assistant/core
|
||||
/homeassistant/components/casper_glow/ @mikeodr
|
||||
/tests/components/casper_glow/ @mikeodr
|
||||
/homeassistant/components/cast/ @emontnemery
|
||||
/tests/components/cast/ @emontnemery
|
||||
/homeassistant/components/ccm15/ @ocalvo
|
||||
/tests/components/ccm15/ @ocalvo
|
||||
/homeassistant/components/centriconnect/ @gresrun
|
||||
/tests/components/centriconnect/ @gresrun
|
||||
/homeassistant/components/cert_expiry/ @jjlawren
|
||||
/tests/components/cert_expiry/ @jjlawren
|
||||
/homeassistant/components/chacon_dio/ @cnico
|
||||
/tests/components/chacon_dio/ @cnico
|
||||
/homeassistant/components/chess_com/ @joostlek
|
||||
/tests/components/chess_com/ @joostlek
|
||||
/homeassistant/components/cielo_home/ @ihsan-cielo @mudasar-cielo
|
||||
/tests/components/cielo_home/ @ihsan-cielo @mudasar-cielo
|
||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||
@@ -310,8 +288,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/cloud/ @home-assistant/cloud
|
||||
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
|
||||
/tests/components/cloudflare/ @ludeeus @ctalkington
|
||||
/homeassistant/components/cloudflare_r2/ @corrreia
|
||||
/tests/components/cloudflare_r2/ @corrreia
|
||||
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/tests/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/homeassistant/components/coinbase/ @tombrien
|
||||
@@ -352,8 +328,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/cync/ @Kinachi249
|
||||
/homeassistant/components/daikin/ @fredrike
|
||||
/tests/components/daikin/ @fredrike
|
||||
/homeassistant/components/data_grand_lyon/ @Crocmagnon
|
||||
/tests/components/data_grand_lyon/ @Crocmagnon
|
||||
/homeassistant/components/date/ @home-assistant/core
|
||||
/tests/components/date/ @home-assistant/core
|
||||
/homeassistant/components/datetime/ @home-assistant/core
|
||||
@@ -371,8 +345,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/deluge/ @tkdrob
|
||||
/homeassistant/components/demo/ @home-assistant/core
|
||||
/tests/components/demo/ @home-assistant/core
|
||||
/homeassistant/components/denon_rs232/ @balloob
|
||||
/tests/components/denon_rs232/ @balloob
|
||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||
@@ -407,10 +379,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dlna_dms/ @chishm
|
||||
/homeassistant/components/dnsip/ @gjohansson-ST
|
||||
/tests/components/dnsip/ @gjohansson-ST
|
||||
/homeassistant/components/door/ @home-assistant/core
|
||||
/tests/components/door/ @home-assistant/core
|
||||
/homeassistant/components/doorbell/ @home-assistant/core
|
||||
/tests/components/doorbell/ @home-assistant/core
|
||||
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
||||
@@ -421,8 +389,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dropbox/ @bdr99
|
||||
/tests/components/dropbox/ @bdr99
|
||||
/homeassistant/components/droplet/ @sarahseidman
|
||||
/tests/components/droplet/ @sarahseidman
|
||||
/homeassistant/components/dsmr/ @Robbie1221
|
||||
@@ -431,18 +397,16 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/duckdns/ @tr4nt0r
|
||||
/tests/components/duckdns/ @tr4nt0r
|
||||
/homeassistant/components/duco/ @ronaldvdmeer
|
||||
/tests/components/duco/ @ronaldvdmeer
|
||||
/homeassistant/components/duke_energy/ @hunterjm
|
||||
/tests/components/duke_energy/ @hunterjm
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192
|
||||
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
/homeassistant/components/dynalite/ @ziv1234
|
||||
/tests/components/dynalite/ @ziv1234
|
||||
/homeassistant/components/eafm/ @Jc2k
|
||||
/tests/components/eafm/ @Jc2k
|
||||
/homeassistant/components/earn_e_p1/ @Miggets7
|
||||
/tests/components/earn_e_p1/ @Miggets7
|
||||
/homeassistant/components/easyenergy/ @klaasnicolaas
|
||||
/tests/components/easyenergy/ @klaasnicolaas
|
||||
/homeassistant/components/ecoforest/ @pjanuario
|
||||
@@ -504,8 +468,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||
/homeassistant/components/epic_games_store/ @Quentame
|
||||
/tests/components/epic_games_store/ @Quentame
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
/tests/components/epion/ @lhgravendeel
|
||||
/homeassistant/components/epson/ @pszafer
|
||||
@@ -520,8 +484,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/essent/ @jaapp
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/eurotronic_cometblue/ @rikroe
|
||||
/tests/components/eurotronic_cometblue/ @rikroe
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
/tests/components/event/ @home-assistant/core
|
||||
/homeassistant/components/evohome/ @zxdavb
|
||||
@@ -581,18 +543,18 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
||||
/tests/components/foscam/ @Foscam-wangzhengyu
|
||||
/homeassistant/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/tests/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
/tests/components/freedompro/ @stefano055415
|
||||
/homeassistant/components/freshr/ @SierraNL
|
||||
/tests/components/freshr/ @SierraNL
|
||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
||||
/tests/components/fressnapf_tracker/ @eifinger
|
||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||
/tests/components/fritzbox/ @mib1185 @flabbamann
|
||||
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
|
||||
/tests/components/fritzbox_callmonitor/ @cdce8p
|
||||
/homeassistant/components/fronius/ @farmio
|
||||
/tests/components/fronius/ @farmio
|
||||
/homeassistant/components/frontend/ @home-assistant/frontend
|
||||
@@ -603,18 +565,12 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/fujitsu_fglair/ @crevetor
|
||||
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||
/tests/components/fully_kiosk/ @cgarwood
|
||||
/homeassistant/components/fumis/ @frenck
|
||||
/tests/components/fumis/ @frenck
|
||||
/homeassistant/components/fyta/ @dontinelli
|
||||
/tests/components/fyta/ @dontinelli
|
||||
/homeassistant/components/garage_door/ @home-assistant/core
|
||||
/tests/components/garage_door/ @home-assistant/core
|
||||
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
||||
/tests/components/garages_amsterdam/ @klaasnicolaas
|
||||
/homeassistant/components/gardena_bluetooth/ @elupus
|
||||
/tests/components/gardena_bluetooth/ @elupus
|
||||
/homeassistant/components/gate/ @home-assistant/core
|
||||
/tests/components/gate/ @home-assistant/core
|
||||
/homeassistant/components/gdacs/ @exxamalte
|
||||
/tests/components/gdacs/ @exxamalte
|
||||
/homeassistant/components/generic/ @davet2001
|
||||
@@ -637,8 +593,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/geonetnz_quakes/ @exxamalte
|
||||
/homeassistant/components/geonetnz_volcano/ @exxamalte
|
||||
/tests/components/geonetnz_volcano/ @exxamalte
|
||||
/homeassistant/components/ghost/ @johnonolan
|
||||
/tests/components/ghost/ @johnonolan
|
||||
/homeassistant/components/gios/ @bieniu
|
||||
/tests/components/gios/ @bieniu
|
||||
/homeassistant/components/github/ @timmo001 @ludeeus
|
||||
@@ -687,8 +641,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/gpsd/ @fabaff @jrieger
|
||||
/homeassistant/components/gree/ @cmroche
|
||||
/tests/components/gree/ @cmroche
|
||||
/homeassistant/components/green_planet_energy/ @petschni
|
||||
/tests/components/green_planet_energy/ @petschni
|
||||
/homeassistant/components/greeneye_monitor/ @jkeljo
|
||||
/tests/components/greeneye_monitor/ @jkeljo
|
||||
/homeassistant/components/group/ @home-assistant/core
|
||||
@@ -697,8 +649,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/growatt_server/ @johanzander
|
||||
/homeassistant/components/guardian/ @bachya
|
||||
/tests/components/guardian/ @bachya
|
||||
/homeassistant/components/guntamatic/ @JensTimmerman
|
||||
/tests/components/guntamatic/ @JensTimmerman
|
||||
/homeassistant/components/habitica/ @tr4nt0r
|
||||
/tests/components/habitica/ @tr4nt0r
|
||||
/homeassistant/components/hanna/ @bestycame
|
||||
@@ -716,8 +666,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
/tests/components/hdmi_cec/ @inytar
|
||||
/homeassistant/components/heatmiser/ @andylockran
|
||||
/homeassistant/components/hegel/ @boazca
|
||||
/tests/components/hegel/ @boazca
|
||||
/homeassistant/components/heos/ @andrewsayre
|
||||
/tests/components/heos/ @andrewsayre
|
||||
/homeassistant/components/here_travel_time/ @eifinger
|
||||
@@ -761,20 +709,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/homekit_controller/ @Jc2k @bdraco
|
||||
/homeassistant/components/homematic/ @pvizeli
|
||||
/tests/components/homematic/ @pvizeli
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th @lackas
|
||||
/tests/components/homematicip_cloud/ @hahn-th @lackas
|
||||
/homeassistant/components/homevolt/ @danielhiversen @liudger
|
||||
/tests/components/homevolt/ @danielhiversen @liudger
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th
|
||||
/tests/components/homematicip_cloud/ @hahn-th
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/honeywell_string_lights/ @balloob
|
||||
/tests/components/honeywell_string_lights/ @balloob
|
||||
/homeassistant/components/hr_energy_qube/ @MattieGit
|
||||
/tests/components/hr_energy_qube/ @MattieGit
|
||||
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
|
||||
/tests/components/html5/ @alexyao2015 @tr4nt0r
|
||||
/homeassistant/components/html5/ @alexyao2015
|
||||
/tests/components/html5/ @alexyao2015
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
/tests/components/http/ @home-assistant/core
|
||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||
@@ -787,8 +729,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/huisbaasje/ @dennisschroer
|
||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
||||
/homeassistant/components/humidity/ @home-assistant/core
|
||||
/tests/components/humidity/ @home-assistant/core
|
||||
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||
@@ -803,8 +743,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
||||
/homeassistant/components/hyperion/ @dermotduffy
|
||||
/tests/components/hyperion/ @dermotduffy
|
||||
/homeassistant/components/hypontech/ @jcisio
|
||||
/tests/components/hypontech/ @jcisio
|
||||
/homeassistant/components/ialarm/ @RyuzakiKK
|
||||
/tests/components/ialarm/ @RyuzakiKK
|
||||
/homeassistant/components/iammeter/ @lewei50
|
||||
@@ -814,14 +752,10 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/icloud/ @Quentame @nzapponi
|
||||
/homeassistant/components/idasen_desk/ @abmantis
|
||||
/tests/components/idasen_desk/ @abmantis
|
||||
/homeassistant/components/idrive_e2/ @patrickvorgers
|
||||
/tests/components/idrive_e2/ @patrickvorgers
|
||||
/homeassistant/components/igloohome/ @keithle888
|
||||
/tests/components/igloohome/ @keithle888
|
||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||
/tests/components/ign_sismologia/ @exxamalte
|
||||
/homeassistant/components/illuminance/ @home-assistant/core
|
||||
/tests/components/illuminance/ @home-assistant/core
|
||||
/homeassistant/components/image/ @home-assistant/core
|
||||
/tests/components/image/ @home-assistant/core
|
||||
/homeassistant/components/image_processing/ @home-assistant/core
|
||||
@@ -840,14 +774,10 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
/tests/components/incomfort/ @jbouwh
|
||||
/homeassistant/components/indevolt/ @xirt
|
||||
/tests/components/indevolt/ @xirt
|
||||
/homeassistant/components/inels/ @epdevlab
|
||||
/tests/components/inels/ @epdevlab
|
||||
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
|
||||
/tests/components/influxdb/ @mdegat01 @Robbie1221
|
||||
/homeassistant/components/infrared/ @home-assistant/core
|
||||
/tests/components/infrared/ @home-assistant/core
|
||||
/homeassistant/components/influxdb/ @mdegat01
|
||||
/tests/components/influxdb/ @mdegat01
|
||||
/homeassistant/components/inkbird/ @bdraco
|
||||
/tests/components/inkbird/ @bdraco
|
||||
/homeassistant/components/input_boolean/ @home-assistant/core
|
||||
@@ -862,12 +792,10 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/input_select/ @home-assistant/core
|
||||
/homeassistant/components/input_text/ @home-assistant/core
|
||||
/tests/components/input_text/ @home-assistant/core
|
||||
/homeassistant/components/insteon/ @teharris1 @ssyrell
|
||||
/tests/components/insteon/ @teharris1 @ssyrell
|
||||
/homeassistant/components/insteon/ @teharris1
|
||||
/tests/components/insteon/ @teharris1
|
||||
/homeassistant/components/integration/ @dgomes
|
||||
/tests/components/integration/ @dgomes
|
||||
/homeassistant/components/intelliclima/ @dvdinth
|
||||
/tests/components/intelliclima/ @dvdinth
|
||||
/homeassistant/components/intellifire/ @jeeftor
|
||||
/tests/components/intellifire/ @jeeftor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
@@ -919,8 +847,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley
|
||||
/tests/components/jvc_projector/ @SteveEasley
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
/tests/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
/homeassistant/components/kaiterra/ @Michsior14
|
||||
/homeassistant/components/kaleidescape/ @SteveEasley
|
||||
/tests/components/kaleidescape/ @SteveEasley
|
||||
@@ -933,8 +861,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/keyboard_remote/ @bendavid @lanrat
|
||||
/homeassistant/components/keymitt_ble/ @spycle
|
||||
/tests/components/keymitt_ble/ @spycle
|
||||
/homeassistant/components/kiosker/ @Claeysson
|
||||
/tests/components/kiosker/ @Claeysson
|
||||
/homeassistant/components/kitchen_sink/ @home-assistant/core
|
||||
/tests/components/kitchen_sink/ @home-assistant/core
|
||||
/homeassistant/components/kmtronic/ @dgomes
|
||||
@@ -945,6 +871,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
/tests/components/kostal_plenticore/ @stegm
|
||||
/homeassistant/components/kraken/ @eifinger
|
||||
@@ -981,22 +909,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/letpot/ @jpelgrom
|
||||
/tests/components/letpot/ @jpelgrom
|
||||
/homeassistant/components/lg_infrared/ @abmantis
|
||||
/tests/components/lg_infrared/ @abmantis
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/homeassistant/components/lg_tv_rs232/ @balloob
|
||||
/tests/components/lg_tv_rs232/ @balloob
|
||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||
/tests/components/libre_hardware_monitor/ @Sab44
|
||||
/homeassistant/components/lichess/ @aryanhasgithub
|
||||
/tests/components/lichess/ @aryanhasgithub
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/liebherr/ @mettolen
|
||||
/tests/components/liebherr/ @mettolen
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
@@ -1022,8 +942,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/logbook/ @home-assistant/core
|
||||
/homeassistant/components/logger/ @home-assistant/core
|
||||
/tests/components/logger/ @home-assistant/core
|
||||
/homeassistant/components/lojack/ @devinslick
|
||||
/tests/components/lojack/ @devinslick
|
||||
/homeassistant/components/london_underground/ @jpbede
|
||||
/tests/components/london_underground/ @jpbede
|
||||
/homeassistant/components/lookin/ @ANMalko @bdraco
|
||||
@@ -1047,8 +965,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/lyric/ @timmo001
|
||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||
/tests/components/madvr/ @iloveicedgreentea
|
||||
/homeassistant/components/marantz_infrared/ @balloob
|
||||
/tests/components/marantz_infrared/ @balloob
|
||||
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/homeassistant/components/matrix/ @PaarthShah
|
||||
@@ -1082,8 +998,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/met/ @danielhiversen
|
||||
/homeassistant/components/met_eireann/ @DylanGore
|
||||
/tests/components/met_eireann/ @DylanGore
|
||||
/homeassistant/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_lt/ @xE1H
|
||||
/tests/components/meteo_lt/ @xE1H
|
||||
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
||||
@@ -1101,12 +1017,10 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/mill/ @danielhiversen
|
||||
/homeassistant/components/min_max/ @gjohansson-ST
|
||||
/tests/components/min_max/ @gjohansson-ST
|
||||
/homeassistant/components/minecraft_server/ @elmurato @zachdeibert
|
||||
/tests/components/minecraft_server/ @elmurato @zachdeibert
|
||||
/homeassistant/components/minecraft_server/ @elmurato
|
||||
/tests/components/minecraft_server/ @elmurato
|
||||
/homeassistant/components/minio/ @tkislan
|
||||
/tests/components/minio/ @tkislan
|
||||
/homeassistant/components/mitsubishi_comfort/ @nikolairahimi
|
||||
/tests/components/mitsubishi_comfort/ @nikolairahimi
|
||||
/homeassistant/components/moat/ @bdraco
|
||||
/tests/components/moat/ @bdraco
|
||||
/homeassistant/components/mobile_app/ @home-assistant/core
|
||||
@@ -1117,8 +1031,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/modern_forms/ @wonderslug
|
||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/moisture/ @home-assistant/core
|
||||
/tests/components/moisture/ @home-assistant/core
|
||||
/homeassistant/components/monarch_money/ @jeeftor
|
||||
/tests/components/monarch_money/ @jeeftor
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
@@ -1129,8 +1041,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/moon/ @fabaff @frenck
|
||||
/homeassistant/components/mopeka/ @bdraco
|
||||
/tests/components/mopeka/ @bdraco
|
||||
/homeassistant/components/motion/ @home-assistant/core
|
||||
/tests/components/motion/ @home-assistant/core
|
||||
/homeassistant/components/motion_blinds/ @starkillerOG
|
||||
/tests/components/motion_blinds/ @starkillerOG
|
||||
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
|
||||
@@ -1142,8 +1052,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
/homeassistant/components/mta/ @OnFreund
|
||||
/tests/components/mta/ @OnFreund
|
||||
/homeassistant/components/mullvad/ @meichthys
|
||||
/tests/components/mullvad/ @meichthys
|
||||
/homeassistant/components/music_assistant/ @music-assistant @arturpragacz
|
||||
@@ -1152,8 +1060,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/mutesync/ @currentoor
|
||||
/homeassistant/components/my/ @home-assistant/core
|
||||
/tests/components/my/ @home-assistant/core
|
||||
/homeassistant/components/myneomitis/ @l-pr
|
||||
/tests/components/myneomitis/ @l-pr
|
||||
/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
|
||||
/tests/components/mysensors/ @MartinHjelmare @functionpointer
|
||||
/homeassistant/components/mystrom/ @fabaff
|
||||
@@ -1162,23 +1068,21 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/myuplink/ @pajzo @astrandb
|
||||
/homeassistant/components/nam/ @bieniu
|
||||
/tests/components/nam/ @bieniu
|
||||
/homeassistant/components/namecheapdns/ @tr4nt0r
|
||||
/tests/components/namecheapdns/ @tr4nt0r
|
||||
/homeassistant/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4
|
||||
/tests/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4
|
||||
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
|
||||
/tests/components/nanoleaf/ @milanmeu @joostlek
|
||||
/homeassistant/components/nasweb/ @nasWebio
|
||||
/tests/components/nasweb/ @nasWebio
|
||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||
/homeassistant/components/ness_alarm/ @nickw444 @poshy163
|
||||
/tests/components/ness_alarm/ @nickw444 @poshy163
|
||||
/homeassistant/components/ness_alarm/ @nickw444
|
||||
/tests/components/ness_alarm/ @nickw444
|
||||
/homeassistant/components/nest/ @allenporter
|
||||
/tests/components/nest/ @allenporter
|
||||
/homeassistant/components/netatmo/ @cgtobi
|
||||
/tests/components/netatmo/ @cgtobi
|
||||
/homeassistant/components/netdata/ @fabaff
|
||||
/homeassistant/components/netgear/ @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear_lte/ @tkdrob
|
||||
/tests/components/netgear_lte/ @tkdrob
|
||||
/homeassistant/components/network/ @home-assistant/core
|
||||
@@ -1218,10 +1122,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/notify_events/ @matrozov @papajojo
|
||||
/homeassistant/components/notion/ @bachya
|
||||
/tests/components/notion/ @bachya
|
||||
/homeassistant/components/novy_cooker_hood/ @piitaya
|
||||
/tests/components/novy_cooker_hood/ @piitaya
|
||||
/homeassistant/components/nrgkick/ @andijakl
|
||||
/tests/components/nrgkick/ @andijakl
|
||||
/homeassistant/components/nsw_fuel_station/ @nickw444
|
||||
/tests/components/nsw_fuel_station/ @nickw444
|
||||
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
@@ -1246,8 +1146,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/nzbget/ @chriscla
|
||||
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
||||
/tests/components/obihai/ @dshokouhi @ejpenney
|
||||
/homeassistant/components/occupancy/ @home-assistant/core
|
||||
/tests/components/occupancy/ @home-assistant/core
|
||||
/homeassistant/components/octoprint/ @rfleming71
|
||||
/tests/components/octoprint/ @rfleming71
|
||||
/homeassistant/components/ohmconnect/ @robbiet480
|
||||
@@ -1256,30 +1154,22 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/ollama/ @synesthesiam
|
||||
/tests/components/ollama/ @synesthesiam
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/omie/ @luuuis
|
||||
/tests/components/omie/ @luuuis
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onedrive/ @zweckj
|
||||
/tests/components/onedrive/ @zweckj
|
||||
/homeassistant/components/onedrive_for_business/ @zweckj
|
||||
/tests/components/onedrive_for_business/ @zweckj
|
||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||
/tests/components/onewire/ @garbled1 @epenet
|
||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||
/homeassistant/components/onvif/ @jterrace
|
||||
/tests/components/onvif/ @jterrace
|
||||
/homeassistant/components/onvif/ @hunterjm @jterrace
|
||||
/tests/components/onvif/ @hunterjm @jterrace
|
||||
/homeassistant/components/open_meteo/ @frenck
|
||||
/tests/components/open_meteo/ @frenck
|
||||
/homeassistant/components/open_router/ @joostlek @ab3lson
|
||||
/tests/components/open_router/ @joostlek @ab3lson
|
||||
/homeassistant/components/openai_conversation/ @Shulyaka
|
||||
/tests/components/openai_conversation/ @Shulyaka
|
||||
/homeassistant/components/opendisplay/ @g4bri3lDev
|
||||
/tests/components/opendisplay/ @g4bri3lDev
|
||||
/homeassistant/components/open_router/ @joostlek
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w @firstof9
|
||||
@@ -1292,8 +1182,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/openhome/ @bazwilliams
|
||||
/homeassistant/components/openrgb/ @felipecrs
|
||||
/tests/components/openrgb/ @felipecrs
|
||||
/homeassistant/components/opensensemap/ @AlCalzone
|
||||
/tests/components/opensensemap/ @AlCalzone
|
||||
/homeassistant/components/opensky/ @joostlek
|
||||
/tests/components/opensky/ @joostlek
|
||||
/homeassistant/components/opentherm_gw/ @mvn23
|
||||
@@ -1302,8 +1190,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/openuv/ @bachya
|
||||
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||
/homeassistant/components/opnsense/ @HarlemSquirrel @Snuffy2
|
||||
/tests/components/opnsense/ @HarlemSquirrel @Snuffy2
|
||||
/homeassistant/components/opnsense/ @mtreinish
|
||||
/tests/components/opnsense/ @mtreinish
|
||||
/homeassistant/components/opower/ @tronikos
|
||||
/tests/components/opower/ @tronikos
|
||||
/homeassistant/components/oralb/ @bdraco @Lash-L
|
||||
@@ -1313,22 +1201,16 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/osoenergy/ @osohotwateriot
|
||||
/homeassistant/components/otbr/ @home-assistant/core
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ouman_eh_800/ @Markus98
|
||||
/tests/components/ouman_eh_800/ @Markus98
|
||||
/homeassistant/components/ourgroceries/ @OnFreund
|
||||
/tests/components/ourgroceries/ @OnFreund
|
||||
/homeassistant/components/overkiz/ @imicknl
|
||||
/tests/components/overkiz/ @imicknl
|
||||
/homeassistant/components/overseerr/ @joostlek @AmGarera
|
||||
/tests/components/overseerr/ @joostlek @AmGarera
|
||||
/homeassistant/components/ovhcloud_ai_endpoints/ @Crocmagnon
|
||||
/tests/components/ovhcloud_ai_endpoints/ @Crocmagnon
|
||||
/homeassistant/components/ovo_energy/ @timmo001
|
||||
/tests/components/ovo_energy/ @timmo001
|
||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||
/tests/components/p1_monitor/ @klaasnicolaas
|
||||
/homeassistant/components/paj_gps/ @skipperro
|
||||
/tests/components/paj_gps/ @skipperro
|
||||
/homeassistant/components/palazzetti/ @dotvav
|
||||
/tests/components/palazzetti/ @dotvav
|
||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||
@@ -1353,8 +1235,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pi_hole/ @shenxn
|
||||
/homeassistant/components/picnic/ @corneyl @codesalatdev
|
||||
/tests/components/picnic/ @corneyl @codesalatdev
|
||||
/homeassistant/components/picotts/ @rooggiieerr
|
||||
/tests/components/picotts/ @rooggiieerr
|
||||
/homeassistant/components/ping/ @jpbede
|
||||
/tests/components/ping/ @jpbede
|
||||
/homeassistant/components/plaato/ @JohNan
|
||||
@@ -1373,16 +1253,10 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/poolsense/ @haemishkyd
|
||||
/homeassistant/components/portainer/ @erwindouna
|
||||
/tests/components/portainer/ @erwindouna
|
||||
/homeassistant/components/power/ @home-assistant/core
|
||||
/tests/components/power/ @home-assistant/core
|
||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||
/tests/components/powerfox/ @klaasnicolaas
|
||||
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
||||
/tests/components/powerfox_local/ @klaasnicolaas
|
||||
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/homeassistant/components/prana/ @prana-dev-official
|
||||
/tests/components/prana/ @prana-dev-official
|
||||
/homeassistant/components/private_ble_device/ @Jc2k
|
||||
/tests/components/private_ble_device/ @Jc2k
|
||||
/homeassistant/components/probe_plus/ @pantherale0
|
||||
@@ -1397,12 +1271,9 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/prosegur/ @dgomes
|
||||
/homeassistant/components/proximity/ @mib1185
|
||||
/tests/components/proximity/ @mib1185
|
||||
/homeassistant/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||
/homeassistant/components/ps4/ @ktnrg45
|
||||
/tests/components/ps4/ @ktnrg45
|
||||
/homeassistant/components/ptdevices/ @ParemTech-Inc @frogman85978
|
||||
/tests/components/ptdevices/ @ParemTech-Inc @frogman85978
|
||||
/homeassistant/components/pterodactyl/ @elmurato
|
||||
/tests/components/pterodactyl/ @elmurato
|
||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||
@@ -1417,8 +1288,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pushover/ @engrbm87
|
||||
/homeassistant/components/pvoutput/ @frenck
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/pyload/ @tr4nt0r
|
||||
/tests/components/pyload/ @tr4nt0r
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
@@ -1446,8 +1317,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/radarr/ @tkdrob
|
||||
/homeassistant/components/radio_browser/ @frenck
|
||||
/tests/components/radio_browser/ @frenck
|
||||
/homeassistant/components/radio_frequency/ @home-assistant/core
|
||||
/tests/components/radio_frequency/ @home-assistant/core
|
||||
/homeassistant/components/radiotherm/ @vinnyfuria
|
||||
/tests/components/radiotherm/ @vinnyfuria
|
||||
/homeassistant/components/rainbird/ @konikvranik @allenporter
|
||||
@@ -1473,8 +1342,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/recorder/ @home-assistant/core
|
||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/redgtech/ @jonhsady @luan-nvg
|
||||
/tests/components/redgtech/ @jonhsady @luan-nvg
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
@@ -1516,8 +1383,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/roku/ @ctalkington
|
||||
/homeassistant/components/romy/ @xeniter
|
||||
/tests/components/romy/ @xeniter
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/homeassistant/components/roon/ @pavoni
|
||||
/tests/components/roon/ @pavoni
|
||||
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
|
||||
@@ -1540,10 +1407,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
/homeassistant/components/samsung_infrared/ @lmaertin
|
||||
/tests/components/samsung_infrared/ @lmaertin
|
||||
/homeassistant/components/samsungtv/ @chemelli74
|
||||
/tests/components/samsungtv/ @chemelli74
|
||||
/homeassistant/components/samsungtv/ @chemelli74 @epenet
|
||||
/tests/components/samsungtv/ @chemelli74 @epenet
|
||||
/homeassistant/components/sanix/ @tomaszsluszniak
|
||||
/tests/components/sanix/ @tomaszsluszniak
|
||||
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||
@@ -1639,8 +1504,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||
/homeassistant/components/smappee/ @bsmappee
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smarla/ @explicatis @johannes-exp
|
||||
/tests/components/smarla/ @explicatis @johannes-exp
|
||||
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
|
||||
/tests/components/smarla/ @explicatis @rlint-explicatis
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @joostlek
|
||||
@@ -1666,8 +1531,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solarman/ @solarmanpv
|
||||
/tests/components/solarman/ @solarmanpv
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
@@ -1685,7 +1548,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||
/homeassistant/components/splunk/ @Bre77
|
||||
/tests/components/splunk/ @Bre77
|
||||
/homeassistant/components/spotify/ @frenck @joostlek
|
||||
/tests/components/spotify/ @frenck @joostlek
|
||||
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
@@ -1696,6 +1558,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/srp_energy/ @briglx
|
||||
/homeassistant/components/starline/ @anonym-tsk
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/starlink/ @boswelja
|
||||
/tests/components/starlink/ @boswelja
|
||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/homeassistant/components/steam_online/ @tkdrob
|
||||
@@ -1741,15 +1605,13 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/syncthing/ @zhulik
|
||||
/homeassistant/components/syncthru/ @nielstron
|
||||
/tests/components/syncthru/ @nielstron
|
||||
/homeassistant/components/synology_dsm/ @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @Quentame @mib1185
|
||||
/homeassistant/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/homeassistant/components/synology_srm/ @aerialls
|
||||
/homeassistant/components/system_bridge/ @timmo001
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/systemnexa2/ @konsulten
|
||||
/tests/components/systemnexa2/ @konsulten
|
||||
/homeassistant/components/tado/ @erwindouna
|
||||
/tests/components/tado/ @erwindouna
|
||||
/homeassistant/components/tag/ @home-assistant/core
|
||||
@@ -1773,14 +1635,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/tedee/ @patrickhilker @zweckj
|
||||
/homeassistant/components/telegram_bot/ @hanwg
|
||||
/tests/components/telegram_bot/ @hanwg
|
||||
/homeassistant/components/teleinfo/ @esciara
|
||||
/tests/components/teleinfo/ @esciara
|
||||
/homeassistant/components/tellduslive/ @fredrike
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
/tests/components/teltonika/ @karlbeecken
|
||||
/homeassistant/components/temperature/ @home-assistant/core
|
||||
/tests/components/temperature/ @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
@@ -1793,6 +1649,7 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/tessie/ @Bre77
|
||||
/homeassistant/components/text/ @home-assistant/core
|
||||
/tests/components/text/ @home-assistant/core
|
||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
||||
/homeassistant/components/thermobeacon/ @bdraco
|
||||
/tests/components/thermobeacon/ @bdraco
|
||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||
@@ -1826,8 +1683,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||
/homeassistant/components/totalconnect/ @austinmroczek
|
||||
/tests/components/totalconnect/ @austinmroczek
|
||||
/homeassistant/components/touchline/ @mnordseth
|
||||
/tests/components/touchline/ @mnordseth
|
||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||
/tests/components/touchline_sl/ @jnsgruk
|
||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||
@@ -1848,16 +1703,12 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/trafikverket_train/ @gjohansson-ST
|
||||
/homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST
|
||||
/tests/components/trafikverket_weatherstation/ @gjohansson-ST
|
||||
/homeassistant/components/trane/ @bdraco
|
||||
/tests/components/trane/ @bdraco
|
||||
/homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
|
||||
/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
|
||||
/homeassistant/components/trend/ @jpbede
|
||||
/tests/components/trend/ @jpbede
|
||||
/homeassistant/components/triggercmd/ @rvmey
|
||||
/tests/components/triggercmd/ @rvmey
|
||||
/homeassistant/components/trmnl/ @joostlek
|
||||
/tests/components/trmnl/ @joostlek
|
||||
/homeassistant/components/tts/ @home-assistant/core
|
||||
/tests/components/tts/ @home-assistant/core
|
||||
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
||||
@@ -1868,17 +1719,11 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
||||
/homeassistant/components/twitch/ @joostlek
|
||||
/tests/components/twitch/ @joostlek
|
||||
/homeassistant/components/uhoo/ @getuhoo @joshsmonta
|
||||
/tests/components/uhoo/ @getuhoo @joshsmonta
|
||||
/homeassistant/components/ukraine_alarm/ @PaulAnnekov
|
||||
/tests/components/ukraine_alarm/ @PaulAnnekov
|
||||
/homeassistant/components/unifi/ @Kane610
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_access/ @imhotep @RaHehl
|
||||
/tests/components/unifi_access/ @imhotep @RaHehl
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifi_discovery/ @RaHehl
|
||||
/tests/components/unifi_discovery/ @RaHehl
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @RaHehl
|
||||
/tests/components/unifiprotect/ @RaHehl
|
||||
@@ -1917,8 +1762,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/vegehub/ @thulrus
|
||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||
/tests/components/velbus/ @Cereal2nd @brefra
|
||||
/homeassistant/components/velux/ @Julius2342 @pawlizio @wollew
|
||||
/tests/components/velux/ @Julius2342 @pawlizio @wollew
|
||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
||||
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||
/tests/components/venstar/ @garbled1 @jhollowe
|
||||
/homeassistant/components/versasense/ @imstevenxyz
|
||||
@@ -1926,18 +1771,14 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/homeassistant/components/vicare/ @CFenner @lackas
|
||||
/tests/components/vicare/ @CFenner @lackas
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/victron_ble/ @rajlaud
|
||||
/tests/components/victron_ble/ @rajlaud
|
||||
/homeassistant/components/victron_gx/ @tomer-w
|
||||
/tests/components/victron_gx/ @tomer-w
|
||||
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
||||
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
/tests/components/vilfo/ @ManneW
|
||||
/homeassistant/components/vistapool/ @fdebrus
|
||||
/tests/components/vistapool/ @fdebrus
|
||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||
/tests/components/vivotek/ @HarlemSquirrel
|
||||
/homeassistant/components/vizio/ @raman325
|
||||
@@ -1965,7 +1806,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/water_heater/ @home-assistant/core
|
||||
/tests/components/water_heater/ @home-assistant/core
|
||||
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
|
||||
/tests/components/waterfurnace/ @sdague @masterkoppa
|
||||
/homeassistant/components/watergate/ @adam-the-hero
|
||||
/tests/components/watergate/ @adam-the-hero
|
||||
/homeassistant/components/watson_tts/ @rutkai
|
||||
@@ -1995,8 +1835,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/webostv/ @thecode
|
||||
/homeassistant/components/websocket_api/ @home-assistant/core
|
||||
/tests/components/websocket_api/ @home-assistant/core
|
||||
/homeassistant/components/weheat/ @barryvdh
|
||||
/tests/components/weheat/ @barryvdh
|
||||
/homeassistant/components/weheat/ @jesperraemaekers
|
||||
/tests/components/weheat/ @jesperraemaekers
|
||||
/homeassistant/components/wemo/ @esev
|
||||
/tests/components/wemo/ @esev
|
||||
/homeassistant/components/whirlpool/ @abmantis @mkmer
|
||||
@@ -2005,35 +1845,29 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/whois/ @frenck
|
||||
/homeassistant/components/wiffi/ @mampfes
|
||||
/tests/components/wiffi/ @mampfes
|
||||
/homeassistant/components/wiim/ @Linkplay2020
|
||||
/tests/components/wiim/ @Linkplay2020
|
||||
/homeassistant/components/wilight/ @leofig-rj
|
||||
/tests/components/wilight/ @leofig-rj
|
||||
/homeassistant/components/window/ @home-assistant/core
|
||||
/tests/components/window/ @home-assistant/core
|
||||
/homeassistant/components/wirelesstag/ @sergeymaysak
|
||||
/homeassistant/components/withings/ @joostlek
|
||||
/tests/components/withings/ @joostlek
|
||||
/homeassistant/components/wiz/ @sbidy @arturpragacz
|
||||
/tests/components/wiz/ @sbidy @arturpragacz
|
||||
/homeassistant/components/wled/ @frenck @mik-laj
|
||||
/tests/components/wled/ @frenck @mik-laj
|
||||
/homeassistant/components/wled/ @frenck
|
||||
/tests/components/wled/ @frenck
|
||||
/homeassistant/components/wmspro/ @mback2k
|
||||
/tests/components/wmspro/ @mback2k
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @EnjoyingM
|
||||
/tests/components/wolflink/ @adamkrol93 @EnjoyingM
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
||||
/tests/components/wolflink/ @adamkrol93 @mtielen
|
||||
/homeassistant/components/workday/ @fabaff @gjohansson-ST
|
||||
/tests/components/workday/ @fabaff @gjohansson-ST
|
||||
/homeassistant/components/worldclock/ @fabaff
|
||||
/tests/components/worldclock/ @fabaff
|
||||
/homeassistant/components/ws66i/ @ssaenger
|
||||
/tests/components/ws66i/ @ssaenger
|
||||
/homeassistant/components/wsdot/ @ucodery
|
||||
/tests/components/wsdot/ @ucodery
|
||||
/homeassistant/components/wyoming/ @synesthesiam
|
||||
/tests/components/wyoming/ @synesthesiam
|
||||
/homeassistant/components/xbox/ @tr4nt0r
|
||||
/tests/components/xbox/ @tr4nt0r
|
||||
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
|
||||
/tests/components/xbox/ @hunterjm @tr4nt0r
|
||||
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
|
||||
@@ -2042,8 +1876,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
|
||||
/homeassistant/components/xiaomi_tv/ @simse
|
||||
/homeassistant/components/xmpp/ @fabaff @flowolf
|
||||
/homeassistant/components/xthings_cloud/ @XthingsJacobs
|
||||
/tests/components/xthings_cloud/ @XthingsJacobs
|
||||
/homeassistant/components/yale/ @bdraco
|
||||
/tests/components/yale/ @bdraco
|
||||
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
|
||||
@@ -2062,8 +1894,6 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/yi/ @bachya
|
||||
/homeassistant/components/yolink/ @matrixd2
|
||||
/tests/components/yolink/ @matrixd2
|
||||
/homeassistant/components/yoto/ @cdnninja @piitaya
|
||||
/tests/components/yoto/ @cdnninja @piitaya
|
||||
/homeassistant/components/youless/ @gjong
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/youtube/ @joostlek
|
||||
@@ -2076,20 +1906,17 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/zeroconf/ @bdraco
|
||||
/homeassistant/components/zerproc/ @emlove
|
||||
/tests/components/zerproc/ @emlove
|
||||
/homeassistant/components/zeversolar/ @kvanzuijlen @mhuiskes
|
||||
/tests/components/zeversolar/ @kvanzuijlen @mhuiskes
|
||||
/homeassistant/components/zeversolar/ @kvanzuijlen
|
||||
/tests/components/zeversolar/ @kvanzuijlen
|
||||
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/homeassistant/components/zimi/ @markhannon
|
||||
/tests/components/zimi/ @markhannon
|
||||
/homeassistant/components/zinvolt/ @joostlek
|
||||
/tests/components/zinvolt/ @joostlek
|
||||
/homeassistant/components/zodiac/ @JulienTant
|
||||
/tests/components/zodiac/ @JulienTant
|
||||
/homeassistant/components/zone/ @home-assistant/core
|
||||
/tests/components/zone/ @home-assistant/core
|
||||
/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi
|
||||
/tests/components/zoneminder/ @rohankapoorcom @nabbi
|
||||
/homeassistant/components/zwave_js/ @home-assistant/z-wave
|
||||
/tests/components/zwave_js/ @home-assistant/z-wave
|
||||
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS
|
||||
|
||||
Generated
+14
-11
@@ -1,5 +1,4 @@
|
||||
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
|
||||
# Partly generated by hassfest.
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
ARG BUILD_FROM
|
||||
@@ -11,6 +10,7 @@ LABEL \
|
||||
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
|
||||
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.source="https://github.com/home-assistant/core" \
|
||||
org.opencontainers.image.title="Home Assistant" \
|
||||
org.opencontainers.image.url="https://www.home-assistant.io/"
|
||||
|
||||
@@ -20,22 +20,25 @@ ENV \
|
||||
UV_SYSTEM_PYTHON=true \
|
||||
UV_NO_CACHE=true
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:1.9.14@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:f394f6329f5389a4c9a7fc54b09fdec9621bbb78bf7a672b973440bbdfb02241 /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv at the version pinned in the requirements file
|
||||
&& pip3 install --no-cache-dir "uv==$(awk -F'==' '/^uv==/{print $2}' homeassistant/requirements.txt)" \
|
||||
&& uv pip install \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.9.17
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
--no-build \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
@@ -49,7 +52,7 @@ RUN \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
|
||||
COPY . homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
-e ./homeassistant \
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
@@ -53,9 +52,6 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
|
||||
# Claude Code native install
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
|
||||
@@ -10,7 +10,6 @@ coverage:
|
||||
target: auto
|
||||
threshold: 1
|
||||
paths:
|
||||
- homeassistant/components/*/backup.py
|
||||
- homeassistant/components/*/config_flow.py
|
||||
- homeassistant/components/*/device_action.py
|
||||
- homeassistant/components/*/device_condition.py
|
||||
@@ -29,7 +28,6 @@ coverage:
|
||||
target: 100
|
||||
threshold: 0
|
||||
paths:
|
||||
- homeassistant/components/*/backup.py
|
||||
- homeassistant/components/*/config_flow.py
|
||||
- homeassistant/components/*/device_action.py
|
||||
- homeassistant/components/*/device_condition.py
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Start Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import suppress
|
||||
import faulthandler
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
@@ -73,12 +75,10 @@ async def auth_manager_from_config(
|
||||
provider_hash[key] = provider
|
||||
|
||||
if isinstance(provider, HassAuthProvider):
|
||||
# Can be removed in 2026.7 with the legacy mode of
|
||||
# homeassistant auth provider.
|
||||
# We need to initialize the provider to create the repair
|
||||
# if needed as otherwise the provider will be initialized
|
||||
# on first use, which could be rare as users don't
|
||||
# frequently change auth settings
|
||||
# Can be removed in 2026.7 with the legacy mode of homeassistant auth provider
|
||||
# We need to initialize the provider to create the repair if needed as otherwise
|
||||
# the provider will be initialized on first use, which could be rare as users
|
||||
# don't frequently change auth settings
|
||||
await provider.async_initialize()
|
||||
|
||||
if module_configs:
|
||||
@@ -134,7 +134,7 @@ class AuthManagerFlowManager(
|
||||
"""
|
||||
flow = cast(LoginFlow, flow)
|
||||
|
||||
if result["type"] is not FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] != FlowResultType.CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Storage for auth models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import hmac
|
||||
import itertools
|
||||
|
||||
@@ -5,31 +5,25 @@ we can cache the result of the decode of valid tokens
|
||||
to speed up the process.
|
||||
"""
|
||||
|
||||
from collections.abc import Container, Iterable, Sequence
|
||||
from datetime import timedelta
|
||||
from functools import lru_cache
|
||||
from typing import Any, override
|
||||
from __future__ import annotations
|
||||
|
||||
from jwt import DecodeError, PyJWK, PyJWS, PyJWT
|
||||
from jwt.algorithms import AllowedPublicKeys
|
||||
from jwt.types import Options
|
||||
from datetime import timedelta
|
||||
from functools import lru_cache, partial
|
||||
from typing import Any
|
||||
|
||||
from jwt import DecodeError, PyJWS, PyJWT
|
||||
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
JWT_TOKEN_CACHE_SIZE = 16
|
||||
MAX_TOKEN_SIZE = 8192
|
||||
|
||||
_NO_VERIFY_OPTIONS = Options(
|
||||
verify_signature=False,
|
||||
verify_exp=False,
|
||||
verify_nbf=False,
|
||||
verify_iat=False,
|
||||
verify_aud=False,
|
||||
verify_iss=False,
|
||||
verify_sub=False,
|
||||
verify_jti=False,
|
||||
require=[],
|
||||
)
|
||||
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
|
||||
|
||||
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
|
||||
"require": []
|
||||
}
|
||||
_NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
|
||||
|
||||
|
||||
class _PyJWSWithLoadCache(PyJWS):
|
||||
@@ -44,6 +38,9 @@ class _PyJWSWithLoadCache(PyJWS):
|
||||
return super()._load(jwt)
|
||||
|
||||
|
||||
_jws = _PyJWSWithLoadCache()
|
||||
|
||||
|
||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
||||
def _decode_payload(json_payload: str) -> dict[str, Any]:
|
||||
"""Decode the payload from a JWS dictionary."""
|
||||
@@ -59,12 +56,21 @@ def _decode_payload(json_payload: str) -> dict[str, Any]:
|
||||
class _PyJWTWithVerify(PyJWT):
|
||||
"""PyJWT with a fast decode implementation."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the PyJWT instance."""
|
||||
# We require exp and iat claims to be present
|
||||
super().__init__(Options(require=["exp", "iat"]))
|
||||
# Override the _jws instance with our cached version
|
||||
self._jws = _PyJWSWithLoadCache()
|
||||
def decode_payload(
|
||||
self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Decode a JWT's payload."""
|
||||
if len(jwt) > MAX_TOKEN_SIZE:
|
||||
# Avoid caching impossible tokens
|
||||
raise DecodeError("Token too large")
|
||||
return _decode_payload(
|
||||
_jws.decode_complete(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
algorithms=algorithms,
|
||||
options=options,
|
||||
)["payload"]
|
||||
)
|
||||
|
||||
def verify_and_decode(
|
||||
self,
|
||||
@@ -73,70 +79,37 @@ class _PyJWTWithVerify(PyJWT):
|
||||
algorithms: list[str],
|
||||
issuer: str | None = None,
|
||||
leeway: float | timedelta = 0,
|
||||
options: Options | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify a JWT's signature and claims."""
|
||||
return self.decode(
|
||||
merged_options = {**_VERIFY_OPTIONS, **(options or {})}
|
||||
payload = self.decode_payload(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
options=merged_options,
|
||||
algorithms=algorithms,
|
||||
)
|
||||
# These should never be missing since we verify them
|
||||
# but this is an additional safeguard to make sure
|
||||
# nothing slips through.
|
||||
assert "exp" in payload, "exp claim is required"
|
||||
assert "iat" in payload, "iat claim is required"
|
||||
self._validate_claims(
|
||||
payload=payload,
|
||||
options=merged_options,
|
||||
issuer=issuer,
|
||||
leeway=leeway,
|
||||
options=options,
|
||||
)
|
||||
|
||||
@override
|
||||
def decode(
|
||||
self,
|
||||
jwt: str | bytes,
|
||||
key: AllowedPublicKeys | PyJWK | str | bytes = "",
|
||||
algorithms: Sequence[str] | None = None,
|
||||
options: Options | None = None,
|
||||
verify: bool | None = None,
|
||||
detached_payload: bytes | None = None,
|
||||
audience: str | Iterable[str] | None = None,
|
||||
subject: str | None = None,
|
||||
issuer: str | Container[str] | None = None,
|
||||
leeway: float | timedelta = 0,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Decode a JWT, verifying the signature and claims."""
|
||||
if len(jwt) > MAX_TOKEN_SIZE:
|
||||
# Avoid caching impossible tokens
|
||||
raise DecodeError("Token too large")
|
||||
return super().decode(
|
||||
jwt=jwt,
|
||||
key=key,
|
||||
algorithms=algorithms,
|
||||
options=options,
|
||||
verify=verify,
|
||||
detached_payload=detached_payload,
|
||||
audience=audience,
|
||||
subject=subject,
|
||||
issuer=issuer,
|
||||
leeway=leeway,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@override
|
||||
def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]:
|
||||
return _decode_payload(decoded["payload"])
|
||||
return payload
|
||||
|
||||
|
||||
_jwt = _PyJWTWithVerify()
|
||||
verify_and_decode = _jwt.verify_and_decode
|
||||
|
||||
|
||||
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
|
||||
def unverified_hs256_token_decode(jwt: str) -> dict[str, Any]:
|
||||
"""Decode a JWT without verifying the signature."""
|
||||
return _jwt.decode(
|
||||
jwt=jwt,
|
||||
key="",
|
||||
algorithms=["HS256"],
|
||||
options=_NO_VERIFY_OPTIONS,
|
||||
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
|
||||
partial(
|
||||
_jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"unverified_hs256_token_decode",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Pluggable auth modules for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Example auth module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
Sending HOTP through notify service
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Auth models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import secrets
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -12,9 +13,6 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .util import test_all
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..models import User
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
||||
|
||||
__all__ = [
|
||||
@@ -24,21 +22,10 @@ __all__ = [
|
||||
"PermissionLookup",
|
||||
"PolicyPermissions",
|
||||
"PolicyType",
|
||||
"filter_entity_ids_by_permission",
|
||||
"merge_policies",
|
||||
]
|
||||
|
||||
|
||||
def filter_entity_ids_by_permission(
|
||||
user: User, entity_ids: Iterable[str], key: str
|
||||
) -> list[str]:
|
||||
"""Filter entity IDs to those the user can access for the given policy key."""
|
||||
if user.is_admin or user.permissions.access_all_entities(key):
|
||||
return list(entity_ids)
|
||||
check_entity = user.permissions.check_entity
|
||||
return [entity_id for entity_id in entity_ids if check_entity(entity_id, key)]
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Entity permissions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Permission for events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from homeassistant.const import (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Merging of policies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from .types import CategoryType, PolicyType
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Models for permissions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import attr
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Helpers to deal with permissions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import cast
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import types
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Auth provider that validates credentials via an external command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Home Assistant auth provider."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections.abc import Mapping
|
||||
@@ -120,10 +122,9 @@ class Data:
|
||||
if self.normalize_username(username, force_normalize=True) != username:
|
||||
logging.getLogger(__name__).warning(
|
||||
(
|
||||
"Home Assistant auth provider is running in"
|
||||
" legacy mode because we detected usernames"
|
||||
" that are normalized (lowercase and without"
|
||||
" spaces). Please change the username: '%s'."
|
||||
"Home Assistant auth provider is running in legacy mode "
|
||||
"because we detected usernames that are normalized (lowercase and without spaces)."
|
||||
" Please change the username: '%s'."
|
||||
),
|
||||
username,
|
||||
)
|
||||
@@ -140,9 +141,7 @@ class Data:
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="homeassistant_provider_not_normalized_usernames",
|
||||
translation_placeholders={
|
||||
"usernames": (
|
||||
f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"'
|
||||
)
|
||||
"usernames": f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"'
|
||||
},
|
||||
learn_more_url="homeassistant://config/users",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Example auth provider."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import hmac
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Home Assistant module to handle restoring backups."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -37,6 +40,17 @@ class RestoreBackupFileContent:
|
||||
restore_homeassistant: bool
|
||||
|
||||
|
||||
def password_to_key(password: str) -> bytes:
|
||||
"""Generate a AES Key from password.
|
||||
|
||||
Matches the implementation in supervisor.backups.utils.password_to_key.
|
||||
"""
|
||||
key: bytes = password.encode()
|
||||
for _ in range(100):
|
||||
key = hashlib.sha256(key).digest()
|
||||
return key[:16]
|
||||
|
||||
|
||||
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||
"""Return the contents of the restore backup file."""
|
||||
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
||||
@@ -60,10 +74,7 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
||||
|
||||
|
||||
def _clear_configuration_directory(config_dir: Path, keep: Iterable[str]) -> None:
|
||||
"""Delete all files and directories in the config dir.
|
||||
|
||||
Entries in the keep list are preserved.
|
||||
"""
|
||||
"""Delete all files and directories in the config directory except entries in the keep list."""
|
||||
keep_paths = [config_dir.joinpath(path) for path in keep]
|
||||
entries_to_remove = sorted(
|
||||
entry for entry in config_dir.iterdir() if entry not in keep_paths
|
||||
@@ -85,14 +96,15 @@ def _extract_backup(
|
||||
"""Extract the backup file to the config directory."""
|
||||
with (
|
||||
TemporaryDirectory() as tempdir,
|
||||
securetar.SecureTarArchive(
|
||||
securetar.SecureTarFile(
|
||||
restore_content.backup_file_path,
|
||||
gzip=False,
|
||||
mode="r",
|
||||
) as ostf,
|
||||
):
|
||||
ostf.tar.extractall(
|
||||
ostf.extractall(
|
||||
path=Path(tempdir, "extracted"),
|
||||
members=securetar.secure_path(ostf.tar),
|
||||
members=securetar.secure_path(ostf),
|
||||
filter="fully_trusted",
|
||||
)
|
||||
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||
@@ -104,8 +116,7 @@ def _extract_backup(
|
||||
)
|
||||
) > HA_VERSION:
|
||||
raise ValueError(
|
||||
f"You need at least Home Assistant version"
|
||||
f" {backup_meta_version} to restore this backup"
|
||||
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
||||
)
|
||||
|
||||
with securetar.SecureTarFile(
|
||||
@@ -115,7 +126,10 @@ def _extract_backup(
|
||||
f"homeassistant.tar{'.gz' if backup_meta['compressed'] else ''}",
|
||||
),
|
||||
gzip=backup_meta["compressed"],
|
||||
password=restore_content.password,
|
||||
key=password_to_key(restore_content.password)
|
||||
if restore_content.password is not None
|
||||
else None,
|
||||
mode="r",
|
||||
) as istf:
|
||||
istf.extractall(
|
||||
path=Path(tempdir, "homeassistant"),
|
||||
|
||||
+63
-82
@@ -1,5 +1,7 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
import contextlib
|
||||
@@ -17,8 +19,7 @@ from time import monotonic
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# Import cryptography early since import openssl is not thread-safe
|
||||
# _frozen_importlib._DeadlockError: deadlock detected by
|
||||
# _ModuleLock('cryptography.hazmat.backends.openssl.backend')
|
||||
# _frozen_importlib._DeadlockError: deadlock detected by _ModuleLock('cryptography.hazmat.backends.openssl.backend')
|
||||
import cryptography.hazmat.backends.openssl.backend # noqa: F401
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
@@ -66,10 +67,12 @@ from .const import (
|
||||
BASE_PLATFORMS,
|
||||
FORMAT_DATETIME,
|
||||
KEY_DATA_LOGGING as DATA_LOGGING,
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
REQUIRED_NEXT_PYTHON_VER,
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
)
|
||||
from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
@@ -166,14 +169,10 @@ FRONTEND_INTEGRATIONS = {
|
||||
# visible in frontend
|
||||
"frontend",
|
||||
}
|
||||
# Stage 0 is divided into substages. Each substage has a name,
|
||||
# a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it
|
||||
# could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it
|
||||
# does not freeze other timeouts.
|
||||
# If we add timeouts to the frontend substages, we should make sure
|
||||
# they don't apply in recovery mode.
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
||||
@@ -213,7 +212,6 @@ DEFAULT_INTEGRATIONS = {
|
||||
"analytics", # Needed for onboarding
|
||||
"application_credentials",
|
||||
"backup",
|
||||
"brands",
|
||||
"frontend",
|
||||
"hardware",
|
||||
"labs",
|
||||
@@ -239,31 +237,9 @@ DEFAULT_INTEGRATIONS = {
|
||||
"input_text",
|
||||
"schedule",
|
||||
"timer",
|
||||
#
|
||||
# Base platforms:
|
||||
# Note: Calendar and todo are not included to prevent them from registering
|
||||
# their frontend panels when there are no calendar or todo integrations.
|
||||
*(BASE_PLATFORMS - {"calendar", "todo"}),
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"air_quality",
|
||||
"battery",
|
||||
"door",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"power",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
# These integrations are set up if recovery mode is activated.
|
||||
"backup",
|
||||
"cloud",
|
||||
"frontend",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||
@@ -458,57 +434,32 @@ def _init_blocking_io_modules_in_executor() -> None:
|
||||
is_docker_env()
|
||||
|
||||
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
"""Load the registries and modules that will do blocking I/O.
|
||||
|
||||
Return whether loading succeeded.
|
||||
"""
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
"""Load the registries and modules that will do blocking I/O."""
|
||||
if DATA_REGISTRIES_LOADED in hass.data:
|
||||
return True
|
||||
|
||||
return
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
entity.async_setup(hass)
|
||||
frame.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
translation.async_setup(hass)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
device_registry.async_setup(hass)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
except UnsupportedStorageVersionError as err:
|
||||
# If we're already in recovery mode, we don't want to handle the exception
|
||||
# and activate recovery mode again, as that would lead to an infinite loop.
|
||||
if recovery:
|
||||
raise
|
||||
|
||||
_LOGGER.error(
|
||||
"Storage file %s was created by a newer version of Home Assistant"
|
||||
" (storage version %s > %s); activating recovery mode; on-disk data"
|
||||
" is preserved; upgrade Home Assistant or restore from a backup",
|
||||
err.storage_key,
|
||||
err.found_version,
|
||||
err.max_supported_version,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass)),
|
||||
create_eager_task(category_registry.async_load(hass)),
|
||||
create_eager_task(device_registry.async_load(hass)),
|
||||
create_eager_task(entity_registry.async_load(hass)),
|
||||
create_eager_task(floor_registry.async_load(hass)),
|
||||
create_eager_task(issue_registry.async_load(hass)),
|
||||
create_eager_task(label_registry.async_load(hass)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
||||
async def async_from_config_dict(
|
||||
@@ -525,9 +476,7 @@ async def async_from_config_dict(
|
||||
# Prime custom component cache early so we know if registry entries are tied
|
||||
# to a custom integration
|
||||
await loader.async_get_custom_components(hass)
|
||||
|
||||
if not await async_load_base_functionality(hass):
|
||||
return None
|
||||
await async_load_base_functionality(hass)
|
||||
|
||||
# Set up core.
|
||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||
@@ -567,6 +516,38 @@ async def async_from_config_dict(
|
||||
|
||||
stop = monotonic()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
|
||||
|
||||
if (
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE
|
||||
and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER
|
||||
):
|
||||
current_python_version = ".".join(str(x) for x in sys.version_info[:3])
|
||||
required_python_version = ".".join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Support for the running Python version %s is deprecated and "
|
||||
"will be removed in Home Assistant %s; "
|
||||
"Please upgrade Python to %s"
|
||||
),
|
||||
current_python_version,
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
required_python_version,
|
||||
)
|
||||
issue_registry.async_create_issue(
|
||||
hass,
|
||||
core.DOMAIN,
|
||||
f"python_version_{required_python_version}",
|
||||
is_fixable=False,
|
||||
severity=issue_registry.IssueSeverity.WARNING,
|
||||
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
translation_key="python_version",
|
||||
translation_placeholders={
|
||||
"current_python_version": current_python_version,
|
||||
"required_python_version": required_python_version,
|
||||
"breaks_in_ha_version": REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
},
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "american_standard",
|
||||
"name": "American Standard",
|
||||
"integrations": ["nexia", "trane"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "bega",
|
||||
"name": "BEGA",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"integrations": ["cloudflare", "cloudflare_r2"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "denon",
|
||||
"name": "Denon",
|
||||
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
|
||||
"integrations": ["denon", "denonavr", "heos"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "heatit",
|
||||
"name": "Heatit",
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "heiman",
|
||||
"name": "Heiman",
|
||||
"iot_standards": ["matter", "zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "honeywell",
|
||||
"name": "Honeywell",
|
||||
"integrations": ["lyric", "evohome", "honeywell", "honeywell_string_lights"]
|
||||
"integrations": ["lyric", "evohome", "honeywell"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"domain": "leviton",
|
||||
"name": "Leviton",
|
||||
"integrations": ["decora_wifi"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": [
|
||||
"lg_infrared",
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
"webostv"
|
||||
]
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "marantz",
|
||||
"name": "Marantz",
|
||||
"integrations": ["marantz", "marantz_infrared"]
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
"microsoft",
|
||||
"msteams",
|
||||
"onedrive",
|
||||
"onedrive_for_business",
|
||||
"xbox"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "mitsubishi",
|
||||
"name": "Mitsubishi",
|
||||
"integrations": ["melcloud", "mitsubishi_comfort"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "powerfox",
|
||||
"name": "Powerfox",
|
||||
"integrations": ["powerfox", "powerfox_local"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "samsung",
|
||||
"name": "Samsung",
|
||||
"integrations": ["familyhub", "samsung_infrared", "samsungtv", "syncthru"]
|
||||
"integrations": ["familyhub", "samsungtv", "syncthru"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "sensereo",
|
||||
"name": "Sensereo",
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "trane",
|
||||
"name": "Trane",
|
||||
"integrations": ["nexia", "trane"]
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
{
|
||||
"domain": "ubiquiti",
|
||||
"name": "Ubiquiti",
|
||||
"integrations": [
|
||||
"airos",
|
||||
"unifi",
|
||||
"unifi_access",
|
||||
"unifi_direct",
|
||||
"unifi_discovery",
|
||||
"unifiled",
|
||||
"unifiprotect"
|
||||
]
|
||||
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "ubisys",
|
||||
"name": "Ubisys",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "victron",
|
||||
"name": "Victron",
|
||||
"integrations": ["victron_gx", "victron_ble", "victron_remote_monitoring"]
|
||||
"integrations": ["victron_ble", "victron_remote_monitoring"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "zunzunbee",
|
||||
"name": "Zunzunbee",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Support for the Abode Security System."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
@@ -65,16 +67,13 @@ class AbodeSystem:
|
||||
logout_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
type AbodeConfigEntry = ConfigEntry[AbodeSystem]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Abode integration from a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -100,54 +99,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> boo
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex
|
||||
|
||||
entry.runtime_data = AbodeSystem(abode, polling)
|
||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await setup_hass_events(hass, entry)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass, entry)
|
||||
await setup_hass_events(hass)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _shutdown_client(abode: Abode) -> None:
|
||||
"""Shutdown client."""
|
||||
abode.events.stop()
|
||||
abode.logout()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
await hass.async_add_executor_job(_shutdown_client, entry.runtime_data.abode)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout)
|
||||
|
||||
if logout_listener := entry.runtime_data.logout_listener:
|
||||
logout_listener()
|
||||
hass.data[DOMAIN].logout_listener()
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def setup_hass_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
|
||||
async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
def logout(event: Event) -> None:
|
||||
"""Logout of Abode."""
|
||||
if not entry.runtime_data.polling:
|
||||
entry.runtime_data.abode.events.stop()
|
||||
if not hass.data[DOMAIN].polling:
|
||||
hass.data[DOMAIN].abode.events.stop()
|
||||
|
||||
entry.runtime_data.abode.logout()
|
||||
hass.data[DOMAIN].abode.logout()
|
||||
LOGGER.info("Logged out of Abode")
|
||||
|
||||
if not entry.runtime_data.polling:
|
||||
await hass.async_add_executor_job(entry.runtime_data.abode.events.start)
|
||||
if not hass.data[DOMAIN].polling:
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
|
||||
|
||||
entry.runtime_data.logout_listener = hass.bus.async_listen_once(
|
||||
hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, logout
|
||||
)
|
||||
|
||||
|
||||
def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
|
||||
def setup_abode_events(hass: HomeAssistant) -> None:
|
||||
"""Event callbacks."""
|
||||
|
||||
def event_callback(event: str, event_json: dict[str, str]) -> None:
|
||||
@@ -184,6 +178,6 @@ def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
|
||||
]
|
||||
|
||||
for event in events:
|
||||
entry.runtime_data.abode.events.add_event_callback(
|
||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||
event, partial(event_callback, event)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Support for Abode Security System alarm control panels."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jaraco.abode.devices.alarm import Alarm
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
@@ -7,20 +9,22 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeConfigEntry
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AbodeConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode alarm control panel device."""
|
||||
data = entry.runtime_data
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
async_add_entities(
|
||||
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user