Compare commits

..

1 Commits

Author SHA1 Message Date
ludeeus
374cb0e69d Add host and add-on resource usage to support package download 2026-01-16 07:45:13 +00:00
9075 changed files with 188778 additions and 696287 deletions

View File

@@ -1 +0,0 @@
../.claude/skills/

View File

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

View File

@@ -1,39 +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.
## 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.

View File

@@ -1,44 +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.
- "potato" is a forbidden word for an integration and should never be used.
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
## 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

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ base_platforms: &base_platforms
- homeassistant/components/calendar/** - homeassistant/components/calendar/**
- homeassistant/components/camera/** - homeassistant/components/camera/**
- homeassistant/components/climate/** - homeassistant/components/climate/**
- homeassistant/components/conversation/**
- homeassistant/components/cover/** - homeassistant/components/cover/**
- homeassistant/components/date/** - homeassistant/components/date/**
- homeassistant/components/datetime/** - homeassistant/components/datetime/**
@@ -34,9 +33,7 @@ base_platforms: &base_platforms
- homeassistant/components/humidifier/** - homeassistant/components/humidifier/**
- homeassistant/components/image/** - homeassistant/components/image/**
- homeassistant/components/image_processing/** - homeassistant/components/image_processing/**
- homeassistant/components/infrared/**
- homeassistant/components/lawn_mower/** - homeassistant/components/lawn_mower/**
- homeassistant/components/radio_frequency/**
- homeassistant/components/light/** - homeassistant/components/light/**
- homeassistant/components/lock/** - homeassistant/components/lock/**
- homeassistant/components/media_player/** - homeassistant/components/media_player/**
@@ -56,7 +53,6 @@ base_platforms: &base_platforms
- homeassistant/components/update/** - homeassistant/components/update/**
- homeassistant/components/vacuum/** - homeassistant/components/vacuum/**
- homeassistant/components/valve/** - homeassistant/components/valve/**
- homeassistant/components/wake_word/**
- homeassistant/components/water_heater/** - homeassistant/components/water_heater/**
- homeassistant/components/weather/** - homeassistant/components/weather/**
@@ -74,6 +70,7 @@ components: &components
- homeassistant/components/cloud/** - homeassistant/components/cloud/**
- homeassistant/components/config/** - homeassistant/components/config/**
- homeassistant/components/configurator/** - homeassistant/components/configurator/**
- homeassistant/components/conversation/**
- homeassistant/components/demo/** - homeassistant/components/demo/**
- homeassistant/components/device_automation/** - homeassistant/components/device_automation/**
- homeassistant/components/dhcp/** - homeassistant/components/dhcp/**
@@ -94,7 +91,6 @@ components: &components
- homeassistant/components/input_number/** - homeassistant/components/input_number/**
- homeassistant/components/input_select/** - homeassistant/components/input_select/**
- homeassistant/components/input_text/** - homeassistant/components/input_text/**
- homeassistant/components/labs/**
- homeassistant/components/logbook/** - homeassistant/components/logbook/**
- homeassistant/components/logger/** - homeassistant/components/logger/**
- homeassistant/components/lovelace/** - homeassistant/components/lovelace/**

View File

@@ -8,6 +8,9 @@
"PYTHONASYNCIODEBUG": "1" "PYTHONASYNCIODEBUG": "1"
}, },
"features": { "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": {} "ghcr.io/devcontainers/features/github-cli:1": {}
}, },
// Port 5683 udp is used by Shelly integration // Port 5683 udp is used by Shelly integration
@@ -60,13 +63,7 @@
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff" "editor.defaultFormatter": "charliermarsh.ruff"
}, },
"[json]": { "[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"json.schemas": [ "json.schemas": [

View File

@@ -1 +0,0 @@
../.claude/skills

1
.gitattributes vendored
View File

@@ -16,7 +16,6 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true mypy.ini linguist-generated=true
requirements.txt linguist-generated=true requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true requirements_all.txt linguist-generated=true

View File

@@ -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`. Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`. - [ ] New or updated dependencies have been added to `requirements_all.txt`.
Updated by running `python3 -m script.gen_requirements_all`. 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. This project is very active and we have a high turnover of pull requests.

File diff suppressed because it is too large Load Diff

View File

@@ -9,5 +9,3 @@ updates:
labels: labels:
- dependency - dependency
- github_actions - github_actions
cooldown:
default-days: 7

216
.github/renovate.json vendored
View File

@@ -1,216 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"enabledManagers": [
"pep621",
"pip_requirements",
"pre-commit",
"regex",
"homeassistant-manifest"
],
"pre-commit": {
"enabled": true
},
"pip_requirements": {
"managerFilePatterns": [
"/(^|/)requirements[\\w_-]*\\.txt$/",
"/(^|/)homeassistant/package_constraints\\.txt$/"
]
},
"homeassistant-manifest": {
"managerFilePatterns": [
"/^homeassistant/components/[^/]+/manifest\\.json$/"
]
},
"regexManagers": [
{
"description": "Update ruff required-version in pyproject.toml",
"managerFilePatterns": ["/^pyproject\\.toml$/"],
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
"depNameTemplate": "ruff",
"datasourceTemplate": "pypi"
}
],
"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",
"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"
],
"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": "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"
}
]
}

View File

@@ -10,51 +10,45 @@ on:
env: env:
BUILD_TYPE: core BUILD_TYPE: core
DEFAULT_PYTHON: "3.13"
PIP_TIMEOUT: 60 PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true" UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker # Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.02.0" BASE_IMAGE_VERSION: "2025.12.0"
ARCHITECTURES: '["amd64", "aarch64"]' ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
init: init:
name: Initialize build name: Initialize build
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read # To check out the repository
outputs: outputs:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }} channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }} architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Python - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: 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 - name: Get version
id: version id: version
uses: home-assistant/actions/helpers/version@master # zizmor: ignore[unpinned-uses] uses: home-assistant/actions/helpers/version@master
with: with:
type: ${{ env.BUILD_TYPE }} type: ${{ env.BUILD_TYPE }}
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses] uses: home-assistant/actions/helpers/verify-version@master
with: with:
ignore-dev: true ignore-dev: true
@@ -69,14 +63,14 @@ jobs:
- name: Download Translations - name: Download Translations
run: python3 -m script.translations download run: python3 -m script.translations download
env: env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env] LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Archive translations - name: Archive translations
shell: bash shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz
@@ -88,27 +82,25 @@ jobs:
needs: init needs: init
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
permissions: permissions:
contents: read # To check out the repository contents: read
packages: write # To push to GHCR packages: write
id-token: write # For cosign signing id-token: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
include: include:
- arch: amd64 - arch: amd64
os: ubuntu-24.04 os: ubuntu-latest
- arch: aarch64 - arch: aarch64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20 uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend repo: home-assistant/frontend
@@ -119,7 +111,7 @@ jobs:
- name: Download nightly wheels of intents - name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20 uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package repo: OHF-Voice/intents-package
@@ -128,23 +120,22 @@ jobs:
workflow_conclusion: success workflow_conclusion: success
name: package name: package
- name: Set up Python - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version-file: ".python-version" python-version: ${{ env.DEFAULT_PYTHON }}
- name: Adjust nightly version - name: Adjust nightly version
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
shell: bash shell: bash
env: env:
UV_PRERELEASE: allow UV_PRERELEASE: allow
VERSION: ${{ needs.init.outputs.version }}
run: | run: |
python3 -m pip install "$(grep '^uv' < requirements.txt)" python3 -m pip install "$(grep '^uv' < requirements.txt)"
uv pip install packaging tomli uv pip install packaging tomli
uv pip install . 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 if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" 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]}|" \ sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
homeassistant/package_constraints.txt 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 fi
- name: Download translations - name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: translations name: translations
@@ -190,36 +181,84 @@ jobs:
- name: Write meta info file - name: Write meta info file
shell: bash shell: bash
run: | 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 - 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: with:
arch: ${{ matrix.arch }} context: .
build-args: | file: ./Dockerfile
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }} platforms: ${{ steps.vars.outputs.platform }}
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 }}
push: true 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: build_machine:
name: Build ${{ matrix.machine }} machine core image name: Build ${{ matrix.machine }} machine core image
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"] needs: ["init", "build_base"]
runs-on: ${{ matrix.runs-on }} runs-on: ubuntu-latest
permissions: permissions:
contents: read # To check out the repository contents: read
packages: write # To push to GHCR packages: write
id-token: write # For cosign signing id-token: write
strategy: strategy:
matrix: matrix:
machine: machine:
- generic-x86-64 - generic-x86-64
- intel-nuc
- khadas-vim3 - khadas-vim3
- odroid-c2 - odroid-c2
- odroid-c4 - odroid-c4
@@ -232,55 +271,37 @@ jobs:
- raspberrypi5-64 - raspberrypi5-64
- yellow - yellow
- green - 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: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Compute extra tags - name: Set build additional args
id: tags
shell: bash
env:
VERSION: ${{ needs.init.outputs.version }}
run: | run: |
if [[ "${VERSION}" =~ d ]]; then # Create general tags
echo "extra_tags=dev" >> "$GITHUB_OUTPUT" if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
elif [[ "${VERSION}" =~ b ]]; then echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
echo "extra_tags=beta" >> "$GITHUB_OUTPUT" elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else else
echo "extra_tags=stable" >> "$GITHUB_OUTPUT" echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi fi
- name: Build machine image - name: Login to GitHub Container Registry
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
arch: ${{ matrix.arch }} registry: ghcr.io
build-args: | username: ${{ github.repository_owner }}
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }} password: ${{ secrets.GITHUB_TOKEN }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }} # home-assistant/builder doesn't support sha pinning
context: machine/ - name: Build base image
cosign-base-identity: "https://github.com/home-assistant/core/.*" uses: home-assistant/builder@2025.11.0
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }} with:
file: machine/${{ matrix.machine }} args: |
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant $BUILD_ARGS \
image-tags: | --target /data/machine \
${{ needs.init.outputs.version }} --cosign \
${{ steps.tags.outputs.extra_tags }} --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
push: true
version: ${{ needs.init.outputs.version }}
publish_ha: publish_ha:
name: Publish version files name: Publish version files
@@ -288,23 +309,19 @@ jobs:
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
needs: ["init", "build_machine"] needs: ["init", "build_machine"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses] uses: home-assistant/actions/helpers/git-init@master
with: with:
name: ${{ secrets.GIT_NAME }} name: ${{ secrets.GIT_NAME }}
email: ${{ secrets.GIT_EMAIL }} email: ${{ secrets.GIT_EMAIL }}
token: ${{ secrets.GIT_TOKEN }} token: ${{ secrets.GIT_TOKEN }}
- name: Update version file - 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: with:
key: "homeassistant[]" key: "homeassistant[]"
key-description: "Home Assistant Core" key-description: "Home Assistant Core"
@@ -314,7 +331,7 @@ jobs:
- name: Update version file (stable -> beta) - name: Update version file (stable -> beta)
if: needs.init.outputs.channel == 'stable' 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: with:
key: "homeassistant[]" key: "homeassistant[]"
key-description: "Home Assistant Core" key-description: "Home Assistant Core"
@@ -329,28 +346,25 @@ jobs:
needs: ["init", "build_base"] needs: ["init", "build_base"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read # To check out the repository contents: read
packages: write # To push to GHCR packages: write
id-token: write # For cosign signing id-token: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
- name: Install Cosign - *install_cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: "v2.5.3"
- name: Login to DockerHub - name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant' if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -358,17 +372,14 @@ jobs:
- name: Verify architecture image signatures - name: Verify architecture image signatures
shell: bash shell: bash
env:
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
VERSION: ${{ needs.init.outputs.version }}
run: | run: |
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]') ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do for arch in $ARCHS; do
echo "Verifying ${arch} image signature..." echo "Verifying ${arch} image signature..."
cosign verify \ cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp https://github.com/home-assistant/core/.* \ --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 done
echo "✓ All images verified successfully" echo "✓ All images verified successfully"
@@ -380,7 +391,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev # 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata - name: Generate Docker metadata
id: meta id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: ${{ matrix.registry }}/home-assistant images: ${{ matrix.registry }}/home-assistant
sep-tags: "," 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') }} 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 - 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 - name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant' if: matrix.registry == 'docker.io/homeassistant'
shell: bash shell: bash
env:
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
VERSION: ${{ needs.init.outputs.version }}
run: | run: |
# Use imagetools to copy image blobs directly between registries # Use imagetools to copy image blobs directly between registries
# This preserves provenance/attestations and seems to be much faster than pull/push # 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 for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..." echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do for attempt in 1 2 3; do
if docker buildx imagetools create \ if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" \ --tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"; then "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break break
fi fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..." echo "Attempt ${attempt} failed, retrying in 10 seconds..."
@@ -421,28 +429,23 @@ jobs:
exit 1 exit 1
fi fi
done done
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done done
- name: Create and push multi-arch manifests - name: Create and push multi-arch manifests
shell: bash shell: bash
env:
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
REGISTRY: ${{ matrix.registry }}
VERSION: ${{ needs.init.outputs.version }}
META_TAGS: ${{ steps.meta.outputs.tags }}
run: | run: |
# Build list of architecture images dynamically # Build list of architecture images dynamically
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]') ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
ARCH_IMAGES=() ARCH_IMAGES=()
for arch in $ARCHS; do for arch in $ARCHS; do
ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}") ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
done done
# Build list of all tags for single manifest creation # Build list of all tags for single manifest creation
# Note: Using sep-tags=',' in metadata-action for easier parsing # Note: Using sep-tags=',' in metadata-action for easier parsing
TAG_ARGS=() TAG_ARGS=()
IFS=',' read -ra TAGS <<< "${META_TAGS}" IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
for tag in "${TAGS[@]}"; do for tag in "${TAGS[@]}"; do
TAG_ARGS+=("--tag" "${tag}") TAG_ARGS+=("--tag" "${tag}")
done done
@@ -466,22 +469,20 @@ jobs:
needs: ["init", "build_base"] needs: ["init", "build_base"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read # To check out the repository contents: read
id-token: write # For PyPI trusted publishing id-token: write
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Python - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version-file: ".python-version" python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations - name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: translations name: translations
@@ -499,7 +500,7 @@ jobs:
python -m build python -m build
- name: Upload package to PyPI - 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: with:
skip-existing: true skip-existing: true
@@ -507,10 +508,10 @@ jobs:
name: Build and test hassfest image name: Build and test hassfest image
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read # To check out the repository contents: read
packages: write # To push to GHCR packages: write
attestations: write # For build provenance attestation attestations: write
id-token: write # For build provenance attestation id-token: write
needs: ["init"] needs: ["init"]
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
env: env:
@@ -518,19 +519,17 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }} HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@@ -538,12 +537,12 @@ jobs:
tags: ${{ env.HASSFEST_IMAGE_TAG }} tags: ${{ env.HASSFEST_IMAGE_TAG }}
- name: Run hassfest against core - 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 - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push id: push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@@ -552,7 +551,7 @@ jobs:
- name: Generate artifact attestation - name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' 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: with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }} subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,6 @@ on:
schedule: schedule:
- cron: "30 18 * * 4" - cron: "30 18 * * 4"
permissions: {}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -17,22 +15,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 360 timeout-minutes: 360
permissions: permissions:
actions: read # To read workflow information for CodeQL actions: read
contents: read # To check out the repository contents: read
security-events: write # To upload CodeQL results security-events: write
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with: with:
category: "/language:python" category: "/language:python"

View File

@@ -5,23 +5,18 @@ on:
issues: issues:
types: [labeled] types: [labeled]
permissions: {} permissions:
issues: write
concurrency: models: read
group: ${{ github.workflow }}-${{ github.event.issue.number }}
jobs: jobs:
detect-duplicates: detect-duplicates:
name: Detect duplicate issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # To comment on and label issues
models: read # For AI-based duplicate detection
steps: steps:
- name: Check if integration label was added and extract details - name: Check if integration label was added and extract details
id: extract id: extract
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
script: | script: |
// Debug: Log the event payload // Debug: Log the event payload
@@ -118,7 +113,7 @@ jobs:
- name: Fetch similar issues - name: Fetch similar issues
id: fetch_similar id: fetch_similar
if: steps.extract.outputs.should_continue == 'true' if: steps.extract.outputs.should_continue == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env: env:
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }} INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }} CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
@@ -236,7 +231,7 @@ jobs:
- name: Detect duplicates using AI - name: Detect duplicates using AI
id: ai_detection id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7 uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with: with:
model: openai/gpt-4o model: openai/gpt-4o
system-prompt: | system-prompt: |
@@ -285,7 +280,7 @@ jobs:
- name: Post duplicate detection results - name: Post duplicate detection results
id: post_results id: post_results
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' 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: env:
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }} AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }} SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}

View File

@@ -5,23 +5,18 @@ on:
issues: issues:
types: [opened] types: [opened]
permissions: {} permissions:
issues: write
concurrency: models: read
group: ${{ github.workflow }}-${{ github.event.issue.number }}
jobs: jobs:
detect-language: detect-language:
name: Detect non-English issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # To comment on, label, and close issues
models: read # For AI-based language detection
steps: steps:
- name: Check issue language - name: Check issue language
id: detect_language id: detect_language
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env: env:
ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_TITLE: ${{ github.event.issue.title }}
@@ -62,7 +57,7 @@ jobs:
- name: Detect language using AI - name: Detect language using AI
id: ai_language_detection id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true' if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7 uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with: with:
model: openai/gpt-4o-mini model: openai/gpt-4o-mini
system-prompt: | system-prompt: |
@@ -95,7 +90,7 @@ jobs:
- name: Process non-English issues - name: Process non-English issues
if: steps.detect_language.outputs.should_continue == 'true' 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: env:
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }} AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }} ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}

View File

@@ -5,20 +5,10 @@ on:
schedule: schedule:
- cron: "0 * * * *" - cron: "0 * * * *"
permissions: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs: jobs:
lock: lock:
name: Lock inactive threads
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # To lock issues
pull-requests: write # To lock pull requests
steps: steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with: with:

View File

@@ -4,7 +4,7 @@
"owner": "check-executables-have-shebangs", "owner": "check-executables-have-shebangs",
"pattern": [ "pattern": [
{ {
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$", "regexp": "^(.+):\\s(.+)$",
"file": 1, "file": 1,
"message": 2 "message": 2
} }

View File

@@ -5,44 +5,14 @@ on:
issues: issues:
types: [opened] types: [opened]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
jobs: 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: check-authorization:
name: Check authorization
runs-on: ubuntu-latest 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) # Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task' if: github.event.issue.type.name == 'Task'
steps: steps:
- name: Check if user is authorized - name: Check if user is authorized
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
script: | script: |
const issueAuthor = context.payload.issue.user.login; const issueAuthor = context.payload.issue.user.login;

View File

@@ -6,20 +6,10 @@ on:
- cron: "0 * * * *" - cron: "0 * * * *"
workflow_dispatch: workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs: jobs:
stale: stale:
name: Mark stale issues and PRs
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # To label and close stale issues
pull-requests: write # To label and close stale PRs
steps: steps:
# The 60 day stale policy for PRs # The 60 day stale policy for PRs
# Used for: # Used for:
@@ -27,7 +17,7 @@ jobs:
# - No PRs marked as no-stale # - No PRs marked as no-stale
# - No issues (-1) # - No issues (-1)
- name: 60 days stale PRs policy - name: 60 days stale PRs policy
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60 days-before-stale: 60
@@ -58,8 +48,8 @@ jobs:
# v1.7.0 # v1.7.0
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
with: with:
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env] app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env] private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
# The 90 day stale policy for issues # The 90 day stale policy for issues
# Used for: # Used for:
@@ -67,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted # - No issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: 90 days stale issues - name: 90 days stale issues
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ steps.token.outputs.token }} repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90 days-before-stale: 90
@@ -97,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted # - No Issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: Needs more information stale issues policy - name: Needs more information stale issues policy
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
repo-token: ${{ steps.token.outputs.token }} repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information" only-labels: "needs-more-information"

View File

@@ -9,11 +9,8 @@ on:
paths: paths:
- "**strings.json" - "**strings.json"
permissions: {} env:
DEFAULT_PYTHON: "3.13"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs: jobs:
upload: upload:
@@ -22,17 +19,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Python - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version-file: ".python-version" python-version: ${{ env.DEFAULT_PYTHON }}
- name: Upload Translations - name: Upload Translations
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
run: | run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
python3 -m script.translations upload python3 -m script.translations upload

View File

@@ -16,7 +16,8 @@ on:
- "requirements.txt" - "requirements.txt"
- "script/gen_requirements_all.py" - "script/gen_requirements_all.py"
permissions: {} env:
DEFAULT_PYTHON: "3.13"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}} group: ${{ github.workflow }}-${{ github.ref_name}}
@@ -28,16 +29,15 @@ jobs:
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - &checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 name: Checkout the repository
with: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
persist-credentials: false
- name: Set up Python - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version-file: ".python-version" python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Create Python virtual environment - name: Create Python virtual environment
@@ -50,7 +50,7 @@ jobs:
- name: Create requirements_diff file - name: Create requirements_diff file
run: | run: |
if [[ "${GITHUB_EVENT_NAME}" =~ (schedule|workflow_dispatch) ]]; then if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
touch requirements_diff.txt touch requirements_diff.txt
else else
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
@@ -74,7 +74,7 @@ jobs:
) > .env_file ) > .env_file
- name: Upload 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: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
@@ -82,7 +82,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload requirements_diff - name: Upload requirements_diff
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: *actions-upload-artifact
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
@@ -94,7 +94,7 @@ jobs:
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels - name: Upload requirements_all_wheels
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: *actions-upload-artifact
with: with:
name: requirements_all_wheels name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt path: ./requirements_all_wheels_*.txt
@@ -106,8 +106,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix: &matrix-build
abi: ["cp314"] abi: ["cp313", "cp314"]
arch: ["amd64", "aarch64"] arch: ["amd64", "aarch64"]
include: include:
- arch: amd64 - arch: amd64
@@ -115,18 +115,17 @@ jobs:
- arch: aarch64 - arch: aarch64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
steps: steps:
- name: Checkout the repository - *checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download env_file - &download-env-file
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 name: Download env_file
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: env_file name: env_file
- name: Download requirements_diff - &download-requirements-diff
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 name: Download requirements_diff
uses: *actions-download-artifact
with: with:
name: requirements_diff name: requirements_diff
@@ -137,12 +136,12 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0 uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env] wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev" apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
@@ -157,32 +156,16 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix: *matrix-build
abi: ["cp314"]
arch: ["amd64", "aarch64"]
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps: steps:
- name: Checkout the repository - *checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download env_file - *download-env-file
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: env_file
- name: Download requirements_diff - *download-requirements-diff
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_diff
- name: Download requirements_all_wheels - name: Download requirements_all_wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: *actions-download-artifact
with: with:
name: requirements_all_wheels name: requirements_all_wheels
@@ -195,15 +178,15 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0 uses: *home-assistant-wheels
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env] wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true 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" 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 skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt" requirements: "requirements_all.txt"

1
.gitignore vendored
View File

@@ -142,6 +142,5 @@ pytest_buckets.txt
# AI tooling # AI tooling
.claude/settings.local.json .claude/settings.local.json
.claude/worktrees/
.serena/ .serena/

View File

@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10 rev: v0.13.0
hooks: hooks:
- id: ruff-check - id: ruff-check
args: args:
@@ -8,7 +8,7 @@ repos:
- id: ruff-format - id: ruff-format
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$ files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.2 rev: v2.4.1
hooks: hooks:
- id: codespell - id: codespell
args: args:
@@ -17,12 +17,6 @@ repos:
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json, html] exclude_types: [csv, json, html]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ 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
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v6.0.0
hooks: hooks:
@@ -36,7 +30,7 @@ repos:
- --branch=master - --branch=master
- --branch=rc - --branch=rc
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.38.0 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
@@ -87,13 +81,6 @@ repos:
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ 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 - id: hassfest
name: hassfest name: hassfest
entry: script/run-in-env.sh python3 -m script.hassfest entry: script/run-in-env.sh python3 -m script.hassfest

View File

@@ -1 +1 @@
3.14.3 3.13

View File

@@ -46,16 +46,13 @@ homeassistant.components.accuweather.*
homeassistant.components.acer_projector.* homeassistant.components.acer_projector.*
homeassistant.components.acmeda.* homeassistant.components.acmeda.*
homeassistant.components.actiontec.* homeassistant.components.actiontec.*
homeassistant.components.actron_air.*
homeassistant.components.adax.* homeassistant.components.adax.*
homeassistant.components.adguard.* homeassistant.components.adguard.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.ai_task.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*
homeassistant.components.airgradient.* homeassistant.components.airgradient.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.airnow.* homeassistant.components.airnow.*
homeassistant.components.airobot.*
homeassistant.components.airos.* homeassistant.components.airos.*
homeassistant.components.airq.* homeassistant.components.airq.*
homeassistant.components.airthings.* homeassistant.components.airthings.*
@@ -86,7 +83,6 @@ homeassistant.components.androidtv_remote.*
homeassistant.components.anel_pwrctrl.* homeassistant.components.anel_pwrctrl.*
homeassistant.components.anova.* homeassistant.components.anova.*
homeassistant.components.anthemav.* homeassistant.components.anthemav.*
homeassistant.components.anthropic.*
homeassistant.components.apache_kafka.* homeassistant.components.apache_kafka.*
homeassistant.components.apcupsd.* homeassistant.components.apcupsd.*
homeassistant.components.api.* homeassistant.components.api.*
@@ -124,6 +120,7 @@ homeassistant.components.blueprint.*
homeassistant.components.bluesound.* homeassistant.components.bluesound.*
homeassistant.components.bluetooth.* homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.* homeassistant.components.bluetooth_adapters.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.* homeassistant.components.bond.*
homeassistant.components.bosch_alarm.* homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.* homeassistant.components.braviatv.*
@@ -131,14 +128,12 @@ homeassistant.components.bring.*
homeassistant.components.brother.* homeassistant.components.brother.*
homeassistant.components.browser.* homeassistant.components.browser.*
homeassistant.components.bryant_evolution.* homeassistant.components.bryant_evolution.*
homeassistant.components.bsblan.*
homeassistant.components.bthome.* homeassistant.components.bthome.*
homeassistant.components.button.* homeassistant.components.button.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
homeassistant.components.cambridge_audio.* homeassistant.components.cambridge_audio.*
homeassistant.components.camera.* homeassistant.components.camera.*
homeassistant.components.canary.* homeassistant.components.canary.*
homeassistant.components.casper_glow.*
homeassistant.components.cert_expiry.* homeassistant.components.cert_expiry.*
homeassistant.components.clickatell.* homeassistant.components.clickatell.*
homeassistant.components.clicksend.* homeassistant.components.clicksend.*
@@ -175,11 +170,9 @@ homeassistant.components.dnsip.*
homeassistant.components.doorbird.* homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.* homeassistant.components.dormakaba_dkey.*
homeassistant.components.downloader.* homeassistant.components.downloader.*
homeassistant.components.dropbox.*
homeassistant.components.droplet.* homeassistant.components.droplet.*
homeassistant.components.dsmr.* homeassistant.components.dsmr.*
homeassistant.components.duckdns.* homeassistant.components.duckdns.*
homeassistant.components.duco.*
homeassistant.components.dunehd.* homeassistant.components.dunehd.*
homeassistant.components.duotecno.* homeassistant.components.duotecno.*
homeassistant.components.easyenergy.* homeassistant.components.easyenergy.*
@@ -214,9 +207,7 @@ homeassistant.components.firefly_iii.*
homeassistant.components.fitbit.* homeassistant.components.fitbit.*
homeassistant.components.flexit_bacnet.* homeassistant.components.flexit_bacnet.*
homeassistant.components.flux_led.* homeassistant.components.flux_led.*
homeassistant.components.folder_watcher.*
homeassistant.components.forecast_solar.* homeassistant.components.forecast_solar.*
homeassistant.components.freshr.*
homeassistant.components.fritz.* homeassistant.components.fritz.*
homeassistant.components.fritzbox.* homeassistant.components.fritzbox.*
homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fritzbox_callmonitor.*
@@ -224,13 +215,11 @@ homeassistant.components.fronius.*
homeassistant.components.frontend.* homeassistant.components.frontend.*
homeassistant.components.fujitsu_fglair.* homeassistant.components.fujitsu_fglair.*
homeassistant.components.fully_kiosk.* homeassistant.components.fully_kiosk.*
homeassistant.components.fumis.*
homeassistant.components.fyta.* homeassistant.components.fyta.*
homeassistant.components.generic_hygrostat.* homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.* homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
homeassistant.components.geocaching.* homeassistant.components.geocaching.*
homeassistant.components.ghost.*
homeassistant.components.gios.* homeassistant.components.gios.*
homeassistant.components.github.* homeassistant.components.github.*
homeassistant.components.glances.* homeassistant.components.glances.*
@@ -251,7 +240,6 @@ homeassistant.components.guardian.*
homeassistant.components.habitica.* homeassistant.components.habitica.*
homeassistant.components.hardkernel.* homeassistant.components.hardkernel.*
homeassistant.components.hardware.* homeassistant.components.hardware.*
homeassistant.components.hdfury.*
homeassistant.components.heos.* homeassistant.components.heos.*
homeassistant.components.here_travel_time.* homeassistant.components.here_travel_time.*
homeassistant.components.history.* homeassistant.components.history.*
@@ -277,15 +265,12 @@ homeassistant.components.homekit_controller.storage
homeassistant.components.homekit_controller.utils homeassistant.components.homekit_controller.utils
homeassistant.components.homewizard.* homeassistant.components.homewizard.*
homeassistant.components.homeworks.* homeassistant.components.homeworks.*
homeassistant.components.hr_energy_qube.*
homeassistant.components.http.* homeassistant.components.http.*
homeassistant.components.huawei_lte.* homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.* homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.* homeassistant.components.husqvarna_automower.*
homeassistant.components.huum.*
homeassistant.components.hydrawise.* homeassistant.components.hydrawise.*
homeassistant.components.hyperion.* homeassistant.components.hyperion.*
homeassistant.components.hypontech.*
homeassistant.components.ibeacon.* homeassistant.components.ibeacon.*
homeassistant.components.idasen_desk.* homeassistant.components.idasen_desk.*
homeassistant.components.image.* homeassistant.components.image.*
@@ -296,12 +281,10 @@ homeassistant.components.imgw_pib.*
homeassistant.components.immich.* homeassistant.components.immich.*
homeassistant.components.incomfort.* homeassistant.components.incomfort.*
homeassistant.components.inels.* homeassistant.components.inels.*
homeassistant.components.infrared.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
homeassistant.components.input_text.* homeassistant.components.input_text.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.intelliclima.*
homeassistant.components.intent.* homeassistant.components.intent.*
homeassistant.components.intent_script.* homeassistant.components.intent_script.*
homeassistant.components.ios.* homeassistant.components.ios.*
@@ -309,7 +292,6 @@ homeassistant.components.iotty.*
homeassistant.components.ipp.* homeassistant.components.ipp.*
homeassistant.components.iqvia.* homeassistant.components.iqvia.*
homeassistant.components.iron_os.* homeassistant.components.iron_os.*
homeassistant.components.isal.*
homeassistant.components.islamic_prayer_times.* homeassistant.components.islamic_prayer_times.*
homeassistant.components.isy994.* homeassistant.components.isy994.*
homeassistant.components.jellyfin.* homeassistant.components.jellyfin.*
@@ -320,7 +302,6 @@ homeassistant.components.knocki.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.* homeassistant.components.kraken.*
homeassistant.components.kulersky.* homeassistant.components.kulersky.*
homeassistant.components.labs.*
homeassistant.components.lacrosse.* homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.* homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.* homeassistant.components.lamarzocco.*
@@ -332,10 +313,8 @@ homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.* homeassistant.components.led_ble.*
homeassistant.components.lektrico.* homeassistant.components.lektrico.*
homeassistant.components.letpot.* homeassistant.components.letpot.*
homeassistant.components.lg_infrared.*
homeassistant.components.libre_hardware_monitor.* homeassistant.components.libre_hardware_monitor.*
homeassistant.components.lidarr.* homeassistant.components.lidarr.*
homeassistant.components.liebherr.*
homeassistant.components.lifx.* homeassistant.components.lifx.*
homeassistant.components.light.* homeassistant.components.light.*
homeassistant.components.linkplay.* homeassistant.components.linkplay.*
@@ -351,7 +330,6 @@ homeassistant.components.lookin.*
homeassistant.components.lovelace.* homeassistant.components.lovelace.*
homeassistant.components.luftdaten.* homeassistant.components.luftdaten.*
homeassistant.components.lunatone.* homeassistant.components.lunatone.*
homeassistant.components.lutron.*
homeassistant.components.madvr.* homeassistant.components.madvr.*
homeassistant.components.manual.* homeassistant.components.manual.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
@@ -383,7 +361,7 @@ homeassistant.components.my.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.myuplink.* homeassistant.components.myuplink.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.namecheapdns.* homeassistant.components.nanoleaf.*
homeassistant.components.nasweb.* homeassistant.components.nasweb.*
homeassistant.components.neato.* homeassistant.components.neato.*
homeassistant.components.nest.* homeassistant.components.nest.*
@@ -397,7 +375,6 @@ homeassistant.components.no_ip.*
homeassistant.components.nordpool.* homeassistant.components.nordpool.*
homeassistant.components.notify.* homeassistant.components.notify.*
homeassistant.components.notion.* homeassistant.components.notion.*
homeassistant.components.nrgkick.*
homeassistant.components.ntfy.* homeassistant.components.ntfy.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.nut.* homeassistant.components.nut.*
@@ -405,13 +382,11 @@ homeassistant.components.ohme.*
homeassistant.components.onboarding.* homeassistant.components.onboarding.*
homeassistant.components.oncue.* homeassistant.components.oncue.*
homeassistant.components.onedrive.* homeassistant.components.onedrive.*
homeassistant.components.onedrive_for_business.*
homeassistant.components.onewire.* homeassistant.components.onewire.*
homeassistant.components.onkyo.* homeassistant.components.onkyo.*
homeassistant.components.open_meteo.* homeassistant.components.open_meteo.*
homeassistant.components.open_router.* homeassistant.components.open_router.*
homeassistant.components.openai_conversation.* homeassistant.components.openai_conversation.*
homeassistant.components.openevse.*
homeassistant.components.openexchangerates.* homeassistant.components.openexchangerates.*
homeassistant.components.opensky.* homeassistant.components.opensky.*
homeassistant.components.openuv.* homeassistant.components.openuv.*
@@ -419,7 +394,6 @@ homeassistant.components.opnsense.*
homeassistant.components.opower.* homeassistant.components.opower.*
homeassistant.components.oralb.* homeassistant.components.oralb.*
homeassistant.components.otbr.* homeassistant.components.otbr.*
homeassistant.components.otp.*
homeassistant.components.overkiz.* homeassistant.components.overkiz.*
homeassistant.components.overseerr.* homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.* homeassistant.components.p1_monitor.*
@@ -436,7 +410,6 @@ homeassistant.components.plugwise.*
homeassistant.components.pooldose.* homeassistant.components.pooldose.*
homeassistant.components.portainer.* homeassistant.components.portainer.*
homeassistant.components.powerfox.* homeassistant.components.powerfox.*
homeassistant.components.powerfox_local.*
homeassistant.components.powerwall.* homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.* homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.* homeassistant.components.prometheus.*
@@ -455,13 +428,10 @@ homeassistant.components.radarr.*
homeassistant.components.radio_browser.* homeassistant.components.radio_browser.*
homeassistant.components.rainforest_raven.* homeassistant.components.rainforest_raven.*
homeassistant.components.rainmachine.* homeassistant.components.rainmachine.*
homeassistant.components.random.*
homeassistant.components.raspberry_pi.* homeassistant.components.raspberry_pi.*
homeassistant.components.rdw.* homeassistant.components.rdw.*
homeassistant.components.recollect_waste.* homeassistant.components.recollect_waste.*
homeassistant.components.recorder.* homeassistant.components.recorder.*
homeassistant.components.recovery_mode.*
homeassistant.components.redgtech.*
homeassistant.components.remember_the_milk.* homeassistant.components.remember_the_milk.*
homeassistant.components.remote.* homeassistant.components.remote.*
homeassistant.components.remote_calendar.* homeassistant.components.remote_calendar.*
@@ -485,14 +455,12 @@ homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.* homeassistant.components.ruuvitag_ble.*
homeassistant.components.samsungtv.* homeassistant.components.samsungtv.*
homeassistant.components.saunum.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.schedule.* homeassistant.components.schedule.*
homeassistant.components.schlage.* homeassistant.components.schlage.*
homeassistant.components.scrape.* homeassistant.components.scrape.*
homeassistant.components.script.* homeassistant.components.script.*
homeassistant.components.search.* homeassistant.components.search.*
homeassistant.components.season.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensibo.* homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.* homeassistant.components.sensirion_ble.*
@@ -519,7 +487,6 @@ homeassistant.components.smtp.*
homeassistant.components.snooz.* homeassistant.components.snooz.*
homeassistant.components.solarlog.* homeassistant.components.solarlog.*
homeassistant.components.sonarr.* homeassistant.components.sonarr.*
homeassistant.components.spaceapi.*
homeassistant.components.speedtestdotnet.* homeassistant.components.speedtestdotnet.*
homeassistant.components.spotify.* homeassistant.components.spotify.*
homeassistant.components.sql.* homeassistant.components.sql.*
@@ -544,7 +511,6 @@ homeassistant.components.synology_dsm.*
homeassistant.components.system_health.* homeassistant.components.system_health.*
homeassistant.components.system_log.* homeassistant.components.system_log.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.systemnexa2.*
homeassistant.components.tag.* homeassistant.components.tag.*
homeassistant.components.tailscale.* homeassistant.components.tailscale.*
homeassistant.components.tailwind.* homeassistant.components.tailwind.*
@@ -555,8 +521,6 @@ homeassistant.components.tcp.*
homeassistant.components.technove.* homeassistant.components.technove.*
homeassistant.components.tedee.* homeassistant.components.tedee.*
homeassistant.components.telegram_bot.* homeassistant.components.telegram_bot.*
homeassistant.components.teleinfo.*
homeassistant.components.teslemetry.*
homeassistant.components.text.* homeassistant.components.text.*
homeassistant.components.thethingsnetwork.* homeassistant.components.thethingsnetwork.*
homeassistant.components.threshold.* homeassistant.components.threshold.*
@@ -580,26 +544,21 @@ homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.* homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.transmission.* homeassistant.components.transmission.*
homeassistant.components.trend.* homeassistant.components.trend.*
homeassistant.components.trmnl.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.twentemilieu.* homeassistant.components.twentemilieu.*
homeassistant.components.unifi.* homeassistant.components.unifi.*
homeassistant.components.unifi_access.*
homeassistant.components.unifiprotect.* homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.* homeassistant.components.upcloud.*
homeassistant.components.update.* homeassistant.components.update.*
homeassistant.components.uptime.* homeassistant.components.uptime.*
homeassistant.components.uptime_kuma.* homeassistant.components.uptime_kuma.*
homeassistant.components.uptimerobot.* homeassistant.components.uptimerobot.*
homeassistant.components.usage_prediction.*
homeassistant.components.usb.* homeassistant.components.usb.*
homeassistant.components.uvc.* homeassistant.components.uvc.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*
homeassistant.components.vallox.* homeassistant.components.vallox.*
homeassistant.components.valve.* homeassistant.components.valve.*
homeassistant.components.velbus.* homeassistant.components.velbus.*
homeassistant.components.velux.*
homeassistant.components.victron_gx.*
homeassistant.components.vivotek.* homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.* homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.* homeassistant.components.vodafone_station.*
@@ -612,7 +571,6 @@ homeassistant.components.water_heater.*
homeassistant.components.watts.* homeassistant.components.watts.*
homeassistant.components.watttime.* homeassistant.components.watttime.*
homeassistant.components.weather.* homeassistant.components.weather.*
homeassistant.components.web_rtc.*
homeassistant.components.webhook.* homeassistant.components.webhook.*
homeassistant.components.webostv.* homeassistant.components.webostv.*
homeassistant.components.websocket_api.* homeassistant.components.websocket_api.*
@@ -629,7 +587,6 @@ homeassistant.components.yale_smart_alarm.*
homeassistant.components.yalexs_ble.* homeassistant.components.yalexs_ble.*
homeassistant.components.youtube.* homeassistant.components.youtube.*
homeassistant.components.zeroconf.* homeassistant.components.zeroconf.*
homeassistant.components.zinvolt.*
homeassistant.components.zodiac.* homeassistant.components.zodiac.*
homeassistant.components.zone.* homeassistant.components.zone.*
homeassistant.components.zwave_js.* homeassistant.components.zwave_js.*

View File

@@ -1,27 +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
## Development Commands
.vscode/tasks.json contains useful commands used for development.
## Python Syntax Notes
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue since Home Assistant officially supports Python 3.14.
## Testing
When writing or modifying tests, ensure all test function parameters have type annotations.
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
## 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.

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
.github/copilot-instructions.md

View File

@@ -1 +1 @@
AGENTS.md .github/copilot-instructions.md

252
CODEOWNERS generated
View File

@@ -15,7 +15,7 @@
.yamllint @home-assistant/core .yamllint @home-assistant/core
pyproject.toml @home-assistant/core pyproject.toml @home-assistant/core
requirements_test.txt @home-assistant/core requirements_test.txt @home-assistant/core
/.devcontainer/ @home-assistant/core @edenhaus /.devcontainer/ @home-assistant/core
/.github/ @home-assistant/core /.github/ @home-assistant/core
/.vscode/ @home-assistant/core /.vscode/ @home-assistant/core
/homeassistant/*.py @home-assistant/core /homeassistant/*.py @home-assistant/core
@@ -37,13 +37,6 @@ build.json @home-assistant/supervisor
# Other code # Other code
/homeassistant/scripts/check_config.py @kellerza /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 # Integrations
/homeassistant/components/abode/ @shred86 /homeassistant/components/abode/ @shred86
/tests/components/abode/ @shred86 /tests/components/abode/ @shred86
@@ -193,8 +186,6 @@ CLAUDE.md @home-assistant/core
/tests/components/auth/ @home-assistant/core /tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core /homeassistant/components/automation/ @home-assistant/core
/tests/components/automation/ @home-assistant/core /tests/components/automation/ @home-assistant/core
/homeassistant/components/autoskope/ @mcisk
/tests/components/autoskope/ @mcisk
/homeassistant/components/avea/ @pattyland /homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @ricohageman /homeassistant/components/awair/ @ahayworth @ricohageman
/tests/components/awair/ @ahayworth @ricohageman /tests/components/awair/ @ahayworth @ricohageman
@@ -221,16 +212,14 @@ CLAUDE.md @home-assistant/core
/tests/components/balboa/ @garbled1 @natekspencer /tests/components/balboa/ @garbled1 @natekspencer
/homeassistant/components/bang_olufsen/ @mj23000 /homeassistant/components/bang_olufsen/ @mj23000
/tests/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 /homeassistant/components/bayesian/ @HarvsG
/tests/components/bayesian/ @HarvsG /tests/components/bayesian/ @HarvsG
/homeassistant/components/beewi_smartclim/ @alemuro /homeassistant/components/beewi_smartclim/ @alemuro
/homeassistant/components/binary_sensor/ @home-assistant/core /homeassistant/components/binary_sensor/ @home-assistant/core
/tests/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria /homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @swistakm @bkobus-bbx /homeassistant/components/blebox/ @bbx-a @swistakm
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx /tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blink/ @fronzbot /homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot /tests/components/blink/ @fronzbot
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23 /homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
@@ -245,14 +234,14 @@ CLAUDE.md @home-assistant/core
/tests/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco
/homeassistant/components/bluetooth_adapters/ @bdraco /homeassistant/components/bluetooth_adapters/ @bdraco
/tests/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 /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900 /homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900 /tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm /homeassistant/components/bosch_shc/ @tschamm
/tests/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 /homeassistant/components/braviatv/ @bieniu @Drafteed
/tests/components/braviatv/ @bieniu @Drafteed /tests/components/braviatv/ @bieniu @Drafteed
/homeassistant/components/bring/ @miaucl @tr4nt0r /homeassistant/components/bring/ @miaucl @tr4nt0r
@@ -282,8 +271,6 @@ CLAUDE.md @home-assistant/core
/tests/components/cambridge_audio/ @noahhusby /tests/components/cambridge_audio/ @noahhusby
/homeassistant/components/camera/ @home-assistant/core /homeassistant/components/camera/ @home-assistant/core
/tests/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 /homeassistant/components/cast/ @emontnemery
/tests/components/cast/ @emontnemery /tests/components/cast/ @emontnemery
/homeassistant/components/ccm15/ @ocalvo /homeassistant/components/ccm15/ @ocalvo
@@ -292,8 +279,6 @@ CLAUDE.md @home-assistant/core
/tests/components/cert_expiry/ @jjlawren /tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico /homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico /tests/components/chacon_dio/ @cnico
/homeassistant/components/chess_com/ @joostlek
/tests/components/chess_com/ @joostlek
/homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl
@@ -303,8 +288,6 @@ CLAUDE.md @home-assistant/core
/tests/components/cloud/ @home-assistant/cloud /tests/components/cloud/ @home-assistant/cloud
/homeassistant/components/cloudflare/ @ludeeus @ctalkington /homeassistant/components/cloudflare/ @ludeeus @ctalkington
/tests/components/cloudflare/ @ludeeus @ctalkington /tests/components/cloudflare/ @ludeeus @ctalkington
/homeassistant/components/cloudflare_r2/ @corrreia
/tests/components/cloudflare_r2/ @corrreia
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99 /homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
/tests/components/co2signal/ @jpbede @VIKTORVAV99 /tests/components/co2signal/ @jpbede @VIKTORVAV99
/homeassistant/components/coinbase/ @tombrien /homeassistant/components/coinbase/ @tombrien
@@ -362,8 +345,6 @@ CLAUDE.md @home-assistant/core
/tests/components/deluge/ @tkdrob /tests/components/deluge/ @tkdrob
/homeassistant/components/demo/ @home-assistant/core /homeassistant/components/demo/ @home-assistant/core
/tests/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 /homeassistant/components/denonavr/ @ol-iver @starkillerOG
/tests/components/denonavr/ @ol-iver @starkillerOG /tests/components/denonavr/ @ol-iver @starkillerOG
/homeassistant/components/derivative/ @afaucogney @karwosts /homeassistant/components/derivative/ @afaucogney @karwosts
@@ -398,10 +379,6 @@ CLAUDE.md @home-assistant/core
/tests/components/dlna_dms/ @chishm /tests/components/dlna_dms/ @chishm
/homeassistant/components/dnsip/ @gjohansson-ST /homeassistant/components/dnsip/ @gjohansson-ST
/tests/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 /homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery /homeassistant/components/dormakaba_dkey/ @emontnemery
@@ -412,8 +389,6 @@ CLAUDE.md @home-assistant/core
/tests/components/dremel_3d_printer/ @tkdrob /tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dropbox/ @bdr99
/tests/components/dropbox/ @bdr99
/homeassistant/components/droplet/ @sarahseidman /homeassistant/components/droplet/ @sarahseidman
/tests/components/droplet/ @sarahseidman /tests/components/droplet/ @sarahseidman
/homeassistant/components/dsmr/ @Robbie1221 /homeassistant/components/dsmr/ @Robbie1221
@@ -422,18 +397,16 @@ CLAUDE.md @home-assistant/core
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duckdns/ @tr4nt0r /homeassistant/components/duckdns/ @tr4nt0r
/tests/components/duckdns/ @tr4nt0r /tests/components/duckdns/ @tr4nt0r
/homeassistant/components/duco/ @ronaldvdmeer /homeassistant/components/duke_energy/ @hunterjm
/tests/components/duco/ @ronaldvdmeer /tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 /tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/homeassistant/components/dynalite/ @ziv1234 /homeassistant/components/dynalite/ @ziv1234
/tests/components/dynalite/ @ziv1234 /tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k /homeassistant/components/eafm/ @Jc2k
/tests/components/eafm/ @Jc2k /tests/components/eafm/ @Jc2k
/homeassistant/components/earn_e_p1/ @Miggets7
/tests/components/earn_e_p1/ @Miggets7
/homeassistant/components/easyenergy/ @klaasnicolaas /homeassistant/components/easyenergy/ @klaasnicolaas
/tests/components/easyenergy/ @klaasnicolaas /tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecoforest/ @pjanuario /homeassistant/components/ecoforest/ @pjanuario
@@ -495,8 +468,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 @roberty99 /homeassistant/components/ephember/ @ttroy50 @roberty99
/homeassistant/components/epic_games_store/ @Quentame /homeassistant/components/epic_games_store/ @hacf-fr @Quentame
/tests/components/epic_games_store/ @Quentame /tests/components/epic_games_store/ @hacf-fr @Quentame
/homeassistant/components/epion/ @lhgravendeel /homeassistant/components/epion/ @lhgravendeel
/tests/components/epion/ @lhgravendeel /tests/components/epion/ @lhgravendeel
/homeassistant/components/epson/ @pszafer /homeassistant/components/epson/ @pszafer
@@ -511,8 +484,6 @@ CLAUDE.md @home-assistant/core
/tests/components/essent/ @jaapp /tests/components/essent/ @jaapp
/homeassistant/components/eufylife_ble/ @bdr99 /homeassistant/components/eufylife_ble/ @bdr99
/tests/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 /homeassistant/components/event/ @home-assistant/core
/tests/components/event/ @home-assistant/core /tests/components/event/ @home-assistant/core
/homeassistant/components/evohome/ @zxdavb /homeassistant/components/evohome/ @zxdavb
@@ -572,18 +543,18 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/fortios/ @kimfrellsen /homeassistant/components/fortios/ @kimfrellsen
/homeassistant/components/foscam/ @Foscam-wangzhengyu /homeassistant/components/foscam/ @Foscam-wangzhengyu
/tests/components/foscam/ @Foscam-wangzhengyu /tests/components/foscam/ @Foscam-wangzhengyu
/homeassistant/components/freebox/ @hacf-fr/reviewers @Quentame /homeassistant/components/freebox/ @hacf-fr @Quentame
/tests/components/freebox/ @hacf-fr/reviewers @Quentame /tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415 /homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415 /tests/components/freedompro/ @stefano055415
/homeassistant/components/freshr/ @SierraNL
/tests/components/freshr/ @SierraNL
/homeassistant/components/fressnapf_tracker/ @eifinger /homeassistant/components/fressnapf_tracker/ @eifinger
/tests/components/fressnapf_tracker/ @eifinger /tests/components/fressnapf_tracker/ @eifinger
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185 /homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185 /tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann /homeassistant/components/fritzbox/ @mib1185 @flabbamann
/tests/components/fritzbox/ @mib1185 @flabbamann /tests/components/fritzbox/ @mib1185 @flabbamann
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
/tests/components/fritzbox_callmonitor/ @cdce8p
/homeassistant/components/fronius/ @farmio /homeassistant/components/fronius/ @farmio
/tests/components/fronius/ @farmio /tests/components/fronius/ @farmio
/homeassistant/components/frontend/ @home-assistant/frontend /homeassistant/components/frontend/ @home-assistant/frontend
@@ -594,18 +565,12 @@ CLAUDE.md @home-assistant/core
/tests/components/fujitsu_fglair/ @crevetor /tests/components/fujitsu_fglair/ @crevetor
/homeassistant/components/fully_kiosk/ @cgarwood /homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fumis/ @frenck
/tests/components/fumis/ @frenck
/homeassistant/components/fyta/ @dontinelli /homeassistant/components/fyta/ @dontinelli
/tests/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 /homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus /homeassistant/components/gardena_bluetooth/ @elupus
/tests/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 /homeassistant/components/gdacs/ @exxamalte
/tests/components/gdacs/ @exxamalte /tests/components/gdacs/ @exxamalte
/homeassistant/components/generic/ @davet2001 /homeassistant/components/generic/ @davet2001
@@ -628,8 +593,6 @@ CLAUDE.md @home-assistant/core
/tests/components/geonetnz_quakes/ @exxamalte /tests/components/geonetnz_quakes/ @exxamalte
/homeassistant/components/geonetnz_volcano/ @exxamalte /homeassistant/components/geonetnz_volcano/ @exxamalte
/tests/components/geonetnz_volcano/ @exxamalte /tests/components/geonetnz_volcano/ @exxamalte
/homeassistant/components/ghost/ @johnonolan
/tests/components/ghost/ @johnonolan
/homeassistant/components/gios/ @bieniu /homeassistant/components/gios/ @bieniu
/tests/components/gios/ @bieniu /tests/components/gios/ @bieniu
/homeassistant/components/github/ @timmo001 @ludeeus /homeassistant/components/github/ @timmo001 @ludeeus
@@ -678,8 +641,6 @@ CLAUDE.md @home-assistant/core
/tests/components/gpsd/ @fabaff @jrieger /tests/components/gpsd/ @fabaff @jrieger
/homeassistant/components/gree/ @cmroche /homeassistant/components/gree/ @cmroche
/tests/components/gree/ @cmroche /tests/components/gree/ @cmroche
/homeassistant/components/green_planet_energy/ @petschni
/tests/components/green_planet_energy/ @petschni
/homeassistant/components/greeneye_monitor/ @jkeljo /homeassistant/components/greeneye_monitor/ @jkeljo
/tests/components/greeneye_monitor/ @jkeljo /tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core /homeassistant/components/group/ @home-assistant/core
@@ -705,8 +666,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/hdmi_cec/ @inytar /homeassistant/components/hdmi_cec/ @inytar
/tests/components/hdmi_cec/ @inytar /tests/components/hdmi_cec/ @inytar
/homeassistant/components/heatmiser/ @andylockran /homeassistant/components/heatmiser/ @andylockran
/homeassistant/components/hegel/ @boazca
/tests/components/hegel/ @boazca
/homeassistant/components/heos/ @andrewsayre /homeassistant/components/heos/ @andrewsayre
/tests/components/heos/ @andrewsayre /tests/components/heos/ @andrewsayre
/homeassistant/components/here_travel_time/ @eifinger /homeassistant/components/here_travel_time/ @eifinger
@@ -750,20 +709,14 @@ CLAUDE.md @home-assistant/core
/tests/components/homekit_controller/ @Jc2k @bdraco /tests/components/homekit_controller/ @Jc2k @bdraco
/homeassistant/components/homematic/ @pvizeli /homeassistant/components/homematic/ @pvizeli
/tests/components/homematic/ @pvizeli /tests/components/homematic/ @pvizeli
/homeassistant/components/homematicip_cloud/ @hahn-th @lackas /homeassistant/components/homematicip_cloud/ @hahn-th
/tests/components/homematicip_cloud/ @hahn-th @lackas /tests/components/homematicip_cloud/ @hahn-th
/homeassistant/components/homevolt/ @danielhiversen @liudger
/tests/components/homevolt/ @danielhiversen @liudger
/homeassistant/components/homewizard/ @DCSBL /homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer /homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman @mkmer /tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/honeywell_string_lights/ @balloob /homeassistant/components/html5/ @alexyao2015
/tests/components/honeywell_string_lights/ @balloob /tests/components/html5/ @alexyao2015
/homeassistant/components/hr_energy_qube/ @MattieGit
/tests/components/hr_energy_qube/ @MattieGit
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
/tests/components/html5/ @alexyao2015 @tr4nt0r
/homeassistant/components/http/ @home-assistant/core /homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core /tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle /homeassistant/components/huawei_lte/ @scop @fphammerle
@@ -776,8 +729,6 @@ CLAUDE.md @home-assistant/core
/tests/components/huisbaasje/ @dennisschroer /tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka /homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
/tests/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 /homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock /tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555 /homeassistant/components/husqvarna_automower/ @Thomas55555
@@ -792,8 +743,6 @@ CLAUDE.md @home-assistant/core
/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan /tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
/homeassistant/components/hyperion/ @dermotduffy /homeassistant/components/hyperion/ @dermotduffy
/tests/components/hyperion/ @dermotduffy /tests/components/hyperion/ @dermotduffy
/homeassistant/components/hypontech/ @jcisio
/tests/components/hypontech/ @jcisio
/homeassistant/components/ialarm/ @RyuzakiKK /homeassistant/components/ialarm/ @RyuzakiKK
/tests/components/ialarm/ @RyuzakiKK /tests/components/ialarm/ @RyuzakiKK
/homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iammeter/ @lewei50
@@ -803,14 +752,10 @@ CLAUDE.md @home-assistant/core
/tests/components/icloud/ @Quentame @nzapponi /tests/components/icloud/ @Quentame @nzapponi
/homeassistant/components/idasen_desk/ @abmantis /homeassistant/components/idasen_desk/ @abmantis
/tests/components/idasen_desk/ @abmantis /tests/components/idasen_desk/ @abmantis
/homeassistant/components/idrive_e2/ @patrickvorgers
/tests/components/idrive_e2/ @patrickvorgers
/homeassistant/components/igloohome/ @keithle888 /homeassistant/components/igloohome/ @keithle888
/tests/components/igloohome/ @keithle888 /tests/components/igloohome/ @keithle888
/homeassistant/components/ign_sismologia/ @exxamalte /homeassistant/components/ign_sismologia/ @exxamalte
/tests/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 /homeassistant/components/image/ @home-assistant/core
/tests/components/image/ @home-assistant/core /tests/components/image/ @home-assistant/core
/homeassistant/components/image_processing/ @home-assistant/core /homeassistant/components/image_processing/ @home-assistant/core
@@ -829,14 +774,10 @@ CLAUDE.md @home-assistant/core
/tests/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh /homeassistant/components/incomfort/ @jbouwh
/tests/components/incomfort/ @jbouwh /tests/components/incomfort/ @jbouwh
/homeassistant/components/indevolt/ @xirt
/tests/components/indevolt/ @xirt
/homeassistant/components/inels/ @epdevlab /homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab /tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221 /homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01 @Robbie1221 /tests/components/influxdb/ @mdegat01
/homeassistant/components/infrared/ @home-assistant/core
/tests/components/infrared/ @home-assistant/core
/homeassistant/components/inkbird/ @bdraco /homeassistant/components/inkbird/ @bdraco
/tests/components/inkbird/ @bdraco /tests/components/inkbird/ @bdraco
/homeassistant/components/input_boolean/ @home-assistant/core /homeassistant/components/input_boolean/ @home-assistant/core
@@ -855,8 +796,6 @@ CLAUDE.md @home-assistant/core
/tests/components/insteon/ @teharris1 /tests/components/insteon/ @teharris1
/homeassistant/components/integration/ @dgomes /homeassistant/components/integration/ @dgomes
/tests/components/integration/ @dgomes /tests/components/integration/ @dgomes
/homeassistant/components/intelliclima/ @dvdinth
/tests/components/intelliclima/ @dvdinth
/homeassistant/components/intellifire/ @jeeftor /homeassistant/components/intellifire/ @jeeftor
/tests/components/intellifire/ @jeeftor /tests/components/intellifire/ @jeeftor
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz /homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
@@ -908,8 +847,8 @@ CLAUDE.md @home-assistant/core
/tests/components/jewish_calendar/ @tsvi /tests/components/jewish_calendar/ @tsvi
/homeassistant/components/justnimbus/ @kvanzuijlen /homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen /tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley /homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
/tests/components/jvc_projector/ @SteveEasley /tests/components/jvc_projector/ @SteveEasley @msavazzi
/homeassistant/components/kaiterra/ @Michsior14 /homeassistant/components/kaiterra/ @Michsior14
/homeassistant/components/kaleidescape/ @SteveEasley /homeassistant/components/kaleidescape/ @SteveEasley
/tests/components/kaleidescape/ @SteveEasley /tests/components/kaleidescape/ @SteveEasley
@@ -922,8 +861,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/keyboard_remote/ @bendavid @lanrat /homeassistant/components/keyboard_remote/ @bendavid @lanrat
/homeassistant/components/keymitt_ble/ @spycle /homeassistant/components/keymitt_ble/ @spycle
/tests/components/keymitt_ble/ @spycle /tests/components/keymitt_ble/ @spycle
/homeassistant/components/kiosker/ @Claeysson
/tests/components/kiosker/ @Claeysson
/homeassistant/components/kitchen_sink/ @home-assistant/core /homeassistant/components/kitchen_sink/ @home-assistant/core
/tests/components/kitchen_sink/ @home-assistant/core /tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes /homeassistant/components/kmtronic/ @dgomes
@@ -972,20 +909,14 @@ CLAUDE.md @home-assistant/core
/tests/components/lektrico/ @lektrico /tests/components/lektrico/ @lektrico
/homeassistant/components/letpot/ @jpelgrom /homeassistant/components/letpot/ @jpelgrom
/tests/components/letpot/ @jpelgrom /tests/components/letpot/ @jpelgrom
/homeassistant/components/lg_infrared/ @home-assistant/core
/tests/components/lg_infrared/ @home-assistant/core
/homeassistant/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98 /tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration /homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration /tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/libre_hardware_monitor/ @Sab44 /homeassistant/components/libre_hardware_monitor/ @Sab44
/tests/components/libre_hardware_monitor/ @Sab44 /tests/components/libre_hardware_monitor/ @Sab44
/homeassistant/components/lichess/ @aryanhasgithub
/tests/components/lichess/ @aryanhasgithub
/homeassistant/components/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob
/homeassistant/components/liebherr/ @mettolen
/tests/components/liebherr/ @mettolen
/homeassistant/components/lifx/ @Djelibeybi /homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi /tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core /homeassistant/components/light/ @home-assistant/core
@@ -1011,8 +942,6 @@ CLAUDE.md @home-assistant/core
/tests/components/logbook/ @home-assistant/core /tests/components/logbook/ @home-assistant/core
/homeassistant/components/logger/ @home-assistant/core /homeassistant/components/logger/ @home-assistant/core
/tests/components/logger/ @home-assistant/core /tests/components/logger/ @home-assistant/core
/homeassistant/components/lojack/ @devinslick
/tests/components/lojack/ @devinslick
/homeassistant/components/london_underground/ @jpbede /homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede /tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco /homeassistant/components/lookin/ @ANMalko @bdraco
@@ -1069,8 +998,8 @@ CLAUDE.md @home-assistant/core
/tests/components/met/ @danielhiversen /tests/components/met/ @danielhiversen
/homeassistant/components/met_eireann/ @DylanGore /homeassistant/components/met_eireann/ @DylanGore
/tests/components/met_eireann/ @DylanGore /tests/components/met_eireann/ @DylanGore
/homeassistant/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame /homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
/tests/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame /tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
/homeassistant/components/meteo_lt/ @xE1H /homeassistant/components/meteo_lt/ @xE1H
/tests/components/meteo_lt/ @xE1H /tests/components/meteo_lt/ @xE1H
/homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoalarm/ @rolfberkenbosch
@@ -1088,8 +1017,8 @@ CLAUDE.md @home-assistant/core
/tests/components/mill/ @danielhiversen /tests/components/mill/ @danielhiversen
/homeassistant/components/min_max/ @gjohansson-ST /homeassistant/components/min_max/ @gjohansson-ST
/tests/components/min_max/ @gjohansson-ST /tests/components/min_max/ @gjohansson-ST
/homeassistant/components/minecraft_server/ @elmurato @zachdeibert /homeassistant/components/minecraft_server/ @elmurato
/tests/components/minecraft_server/ @elmurato @zachdeibert /tests/components/minecraft_server/ @elmurato
/homeassistant/components/minio/ @tkislan /homeassistant/components/minio/ @tkislan
/tests/components/minio/ @tkislan /tests/components/minio/ @tkislan
/homeassistant/components/moat/ @bdraco /homeassistant/components/moat/ @bdraco
@@ -1102,8 +1031,6 @@ CLAUDE.md @home-assistant/core
/tests/components/modern_forms/ @wonderslug /tests/components/modern_forms/ @wonderslug
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n /homeassistant/components/moehlenhoff_alpha2/ @j-a-n
/tests/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 /homeassistant/components/monarch_money/ @jeeftor
/tests/components/monarch_money/ @jeeftor /tests/components/monarch_money/ @jeeftor
/homeassistant/components/monoprice/ @etsinko @OnFreund /homeassistant/components/monoprice/ @etsinko @OnFreund
@@ -1114,8 +1041,6 @@ CLAUDE.md @home-assistant/core
/tests/components/moon/ @fabaff @frenck /tests/components/moon/ @fabaff @frenck
/homeassistant/components/mopeka/ @bdraco /homeassistant/components/mopeka/ @bdraco
/tests/components/mopeka/ @bdraco /tests/components/mopeka/ @bdraco
/homeassistant/components/motion/ @home-assistant/core
/tests/components/motion/ @home-assistant/core
/homeassistant/components/motion_blinds/ @starkillerOG /homeassistant/components/motion_blinds/ @starkillerOG
/tests/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy /homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
@@ -1127,8 +1052,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco /homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco /tests/components/mqtt/ @emontnemery @jbouwh @bdraco
/homeassistant/components/msteams/ @peroyvind /homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mta/ @OnFreund
/tests/components/mta/ @OnFreund
/homeassistant/components/mullvad/ @meichthys /homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys
/homeassistant/components/music_assistant/ @music-assistant @arturpragacz /homeassistant/components/music_assistant/ @music-assistant @arturpragacz
@@ -1137,8 +1060,6 @@ CLAUDE.md @home-assistant/core
/tests/components/mutesync/ @currentoor /tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core /homeassistant/components/my/ @home-assistant/core
/tests/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 /homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
/tests/components/mysensors/ @MartinHjelmare @functionpointer /tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff /homeassistant/components/mystrom/ @fabaff
@@ -1149,21 +1070,21 @@ CLAUDE.md @home-assistant/core
/tests/components/nam/ @bieniu /tests/components/nam/ @bieniu
/homeassistant/components/namecheapdns/ @tr4nt0r /homeassistant/components/namecheapdns/ @tr4nt0r
/tests/components/namecheapdns/ @tr4nt0r /tests/components/namecheapdns/ @tr4nt0r
/homeassistant/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4 /homeassistant/components/nanoleaf/ @milanmeu @joostlek
/tests/components/nanoleaf/ @milanmeu @joostlek @loebi-ch @JaspervRijbroek @jonathanrobichaud4 /tests/components/nanoleaf/ @milanmeu @joostlek
/homeassistant/components/nasweb/ @nasWebio /homeassistant/components/nasweb/ @nasWebio
/tests/components/nasweb/ @nasWebio /tests/components/nasweb/ @nasWebio
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul /homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul /tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/homeassistant/components/ness_alarm/ @nickw444 @poshy163 /homeassistant/components/ness_alarm/ @nickw444
/tests/components/ness_alarm/ @nickw444 @poshy163 /tests/components/ness_alarm/ @nickw444
/homeassistant/components/nest/ @allenporter /homeassistant/components/nest/ @allenporter
/tests/components/nest/ @allenporter /tests/components/nest/ @allenporter
/homeassistant/components/netatmo/ @cgtobi /homeassistant/components/netatmo/ @cgtobi
/tests/components/netatmo/ @cgtobi /tests/components/netatmo/ @cgtobi
/homeassistant/components/netdata/ @fabaff /homeassistant/components/netdata/ @fabaff
/homeassistant/components/netgear/ @Quentame @starkillerOG /homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
/tests/components/netgear/ @Quentame @starkillerOG /tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
/homeassistant/components/netgear_lte/ @tkdrob /homeassistant/components/netgear_lte/ @tkdrob
/tests/components/netgear_lte/ @tkdrob /tests/components/netgear_lte/ @tkdrob
/homeassistant/components/network/ @home-assistant/core /homeassistant/components/network/ @home-assistant/core
@@ -1203,8 +1124,6 @@ CLAUDE.md @home-assistant/core
/tests/components/notify_events/ @matrozov @papajojo /tests/components/notify_events/ @matrozov @papajojo
/homeassistant/components/notion/ @bachya /homeassistant/components/notion/ @bachya
/tests/components/notion/ @bachya /tests/components/notion/ @bachya
/homeassistant/components/nrgkick/ @andijakl
/tests/components/nrgkick/ @andijakl
/homeassistant/components/nsw_fuel_station/ @nickw444 /homeassistant/components/nsw_fuel_station/ @nickw444
/tests/components/nsw_fuel_station/ @nickw444 /tests/components/nsw_fuel_station/ @nickw444
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte /homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
@@ -1229,8 +1148,6 @@ CLAUDE.md @home-assistant/core
/tests/components/nzbget/ @chriscla /tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney /homeassistant/components/obihai/ @dshokouhi @ejpenney
/tests/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 /homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71 /tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480 /homeassistant/components/ohmconnect/ @robbiet480
@@ -1245,22 +1162,16 @@ CLAUDE.md @home-assistant/core
/tests/components/ondilo_ico/ @JeromeHXP /tests/components/ondilo_ico/ @JeromeHXP
/homeassistant/components/onedrive/ @zweckj /homeassistant/components/onedrive/ @zweckj
/tests/components/onedrive/ @zweckj /tests/components/onedrive/ @zweckj
/homeassistant/components/onedrive_for_business/ @zweckj
/tests/components/onedrive_for_business/ @zweckj
/homeassistant/components/onewire/ @garbled1 @epenet /homeassistant/components/onewire/ @garbled1 @epenet
/tests/components/onewire/ @garbled1 @epenet /tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz @eclair4151 /homeassistant/components/onkyo/ @arturpragacz @eclair4151
/tests/components/onkyo/ @arturpragacz @eclair4151 /tests/components/onkyo/ @arturpragacz @eclair4151
/homeassistant/components/onvif/ @jterrace /homeassistant/components/onvif/ @hunterjm @jterrace
/tests/components/onvif/ @jterrace /tests/components/onvif/ @hunterjm @jterrace
/homeassistant/components/open_meteo/ @frenck /homeassistant/components/open_meteo/ @frenck
/tests/components/open_meteo/ @frenck /tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek @ab3lson /homeassistant/components/open_router/ @joostlek
/tests/components/open_router/ @joostlek @ab3lson /tests/components/open_router/ @joostlek
/homeassistant/components/openai_conversation/ @Shulyaka
/tests/components/openai_conversation/ @Shulyaka
/homeassistant/components/opendisplay/ @g4bri3lDev
/tests/components/opendisplay/ @g4bri3lDev
/homeassistant/components/openerz/ @misialq /homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq /tests/components/openerz/ @misialq
/homeassistant/components/openevse/ @c00w @firstof9 /homeassistant/components/openevse/ @c00w @firstof9
@@ -1281,8 +1192,8 @@ CLAUDE.md @home-assistant/core
/tests/components/openuv/ @bachya /tests/components/openuv/ @bachya
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck /homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck /tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/homeassistant/components/opnsense/ @HarlemSquirrel @Snuffy2 /homeassistant/components/opnsense/ @mtreinish
/tests/components/opnsense/ @HarlemSquirrel @Snuffy2 /tests/components/opnsense/ @mtreinish
/homeassistant/components/opower/ @tronikos /homeassistant/components/opower/ @tronikos
/tests/components/opower/ @tronikos /tests/components/opower/ @tronikos
/homeassistant/components/oralb/ @bdraco @Lash-L /homeassistant/components/oralb/ @bdraco @Lash-L
@@ -1326,8 +1237,6 @@ CLAUDE.md @home-assistant/core
/tests/components/pi_hole/ @shenxn /tests/components/pi_hole/ @shenxn
/homeassistant/components/picnic/ @corneyl @codesalatdev /homeassistant/components/picnic/ @corneyl @codesalatdev
/tests/components/picnic/ @corneyl @codesalatdev /tests/components/picnic/ @corneyl @codesalatdev
/homeassistant/components/picotts/ @rooggiieerr
/tests/components/picotts/ @rooggiieerr
/homeassistant/components/ping/ @jpbede /homeassistant/components/ping/ @jpbede
/tests/components/ping/ @jpbede /tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan /homeassistant/components/plaato/ @JohNan
@@ -1346,16 +1255,10 @@ CLAUDE.md @home-assistant/core
/tests/components/poolsense/ @haemishkyd /tests/components/poolsense/ @haemishkyd
/homeassistant/components/portainer/ @erwindouna /homeassistant/components/portainer/ @erwindouna
/tests/components/portainer/ @erwindouna /tests/components/portainer/ @erwindouna
/homeassistant/components/power/ @home-assistant/core
/tests/components/power/ @home-assistant/core
/homeassistant/components/powerfox/ @klaasnicolaas /homeassistant/components/powerfox/ @klaasnicolaas
/tests/components/powerfox/ @klaasnicolaas /tests/components/powerfox/ @klaasnicolaas
/homeassistant/components/powerfox_local/ @klaasnicolaas
/tests/components/powerfox_local/ @klaasnicolaas
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson /homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
/tests/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 /homeassistant/components/private_ble_device/ @Jc2k
/tests/components/private_ble_device/ @Jc2k /tests/components/private_ble_device/ @Jc2k
/homeassistant/components/probe_plus/ @pantherale0 /homeassistant/components/probe_plus/ @pantherale0
@@ -1370,8 +1273,7 @@ CLAUDE.md @home-assistant/core
/tests/components/prosegur/ @dgomes /tests/components/prosegur/ @dgomes
/homeassistant/components/proximity/ @mib1185 /homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185 /tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech /homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
/homeassistant/components/ps4/ @ktnrg45 /homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45 /tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato /homeassistant/components/pterodactyl/ @elmurato
@@ -1417,8 +1319,6 @@ CLAUDE.md @home-assistant/core
/tests/components/radarr/ @tkdrob /tests/components/radarr/ @tkdrob
/homeassistant/components/radio_browser/ @frenck /homeassistant/components/radio_browser/ @frenck
/tests/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 /homeassistant/components/radiotherm/ @vinnyfuria
/tests/components/radiotherm/ @vinnyfuria /tests/components/radiotherm/ @vinnyfuria
/homeassistant/components/rainbird/ @konikvranik @allenporter /homeassistant/components/rainbird/ @konikvranik @allenporter
@@ -1444,8 +1344,6 @@ CLAUDE.md @home-assistant/core
/tests/components/recorder/ @home-assistant/core /tests/components/recorder/ @home-assistant/core
/homeassistant/components/recovery_mode/ @home-assistant/core /homeassistant/components/recovery_mode/ @home-assistant/core
/tests/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 /homeassistant/components/refoss/ @ashionky
/tests/components/refoss/ @ashionky /tests/components/refoss/ @ashionky
/homeassistant/components/rehlko/ @bdraco @peterager /homeassistant/components/rehlko/ @bdraco @peterager
@@ -1608,8 +1506,8 @@ CLAUDE.md @home-assistant/core
/tests/components/sma/ @kellerza @rklomp @erwindouna /tests/components/sma/ @kellerza @rklomp @erwindouna
/homeassistant/components/smappee/ @bsmappee /homeassistant/components/smappee/ @bsmappee
/tests/components/smappee/ @bsmappee /tests/components/smappee/ @bsmappee
/homeassistant/components/smarla/ @explicatis @johannes-exp /homeassistant/components/smarla/ @explicatis @rlint-explicatis
/tests/components/smarla/ @explicatis @johannes-exp /tests/components/smarla/ @explicatis @rlint-explicatis
/homeassistant/components/smart_meter_texas/ @grahamwetzler /homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler /tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek /homeassistant/components/smartthings/ @joostlek
@@ -1635,8 +1533,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solaredge_local/ @drobtravels @scheric
/homeassistant/components/solarlog/ @Ernst79 @dontinelli /homeassistant/components/solarlog/ @Ernst79 @dontinelli
/tests/components/solarlog/ @Ernst79 @dontinelli /tests/components/solarlog/ @Ernst79 @dontinelli
/homeassistant/components/solarman/ @solarmanpv
/tests/components/solarman/ @solarmanpv
/homeassistant/components/solax/ @squishykid @Darsstar /homeassistant/components/solax/ @squishykid @Darsstar
/tests/components/solax/ @squishykid @Darsstar /tests/components/solax/ @squishykid @Darsstar
/homeassistant/components/soma/ @ratsept /homeassistant/components/soma/ @ratsept
@@ -1654,7 +1550,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 /homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87 /tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
/homeassistant/components/splunk/ @Bre77 /homeassistant/components/splunk/ @Bre77
/tests/components/splunk/ @Bre77
/homeassistant/components/spotify/ @frenck @joostlek /homeassistant/components/spotify/ @frenck @joostlek
/tests/components/spotify/ @frenck @joostlek /tests/components/spotify/ @frenck @joostlek
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira /homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
@@ -1665,6 +1560,8 @@ CLAUDE.md @home-assistant/core
/tests/components/srp_energy/ @briglx /tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk /homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk /tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST /homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
/tests/components/statistics/ @ThomDietrich @gjohansson-ST /tests/components/statistics/ @ThomDietrich @gjohansson-ST
/homeassistant/components/steam_online/ @tkdrob /homeassistant/components/steam_online/ @tkdrob
@@ -1710,15 +1607,13 @@ CLAUDE.md @home-assistant/core
/tests/components/syncthing/ @zhulik /tests/components/syncthing/ @zhulik
/homeassistant/components/syncthru/ @nielstron /homeassistant/components/syncthru/ @nielstron
/tests/components/syncthru/ @nielstron /tests/components/syncthru/ @nielstron
/homeassistant/components/synology_dsm/ @Quentame @mib1185 /homeassistant/components/synology_dsm/ @hacf-fr @Quentame @mib1185
/tests/components/synology_dsm/ @Quentame @mib1185 /tests/components/synology_dsm/ @hacf-fr @Quentame @mib1185
/homeassistant/components/synology_srm/ @aerialls /homeassistant/components/synology_srm/ @aerialls
/homeassistant/components/system_bridge/ @timmo001 /homeassistant/components/system_bridge/ @timmo001
/tests/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001
/homeassistant/components/systemmonitor/ @gjohansson-ST /homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST /tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/systemnexa2/ @konsulten
/tests/components/systemnexa2/ @konsulten
/homeassistant/components/tado/ @erwindouna /homeassistant/components/tado/ @erwindouna
/tests/components/tado/ @erwindouna /tests/components/tado/ @erwindouna
/homeassistant/components/tag/ @home-assistant/core /homeassistant/components/tag/ @home-assistant/core
@@ -1742,14 +1637,8 @@ CLAUDE.md @home-assistant/core
/tests/components/tedee/ @patrickhilker @zweckj /tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/telegram_bot/ @hanwg /homeassistant/components/telegram_bot/ @hanwg
/tests/components/telegram_bot/ @hanwg /tests/components/telegram_bot/ @hanwg
/homeassistant/components/teleinfo/ @esciara
/tests/components/teleinfo/ @esciara
/homeassistant/components/tellduslive/ @fredrike /homeassistant/components/tellduslive/ @fredrike
/tests/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 /homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @home-assistant/core /tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_fleet/ @Bre77
@@ -1762,6 +1651,7 @@ CLAUDE.md @home-assistant/core
/tests/components/tessie/ @Bre77 /tests/components/tessie/ @Bre77
/homeassistant/components/text/ @home-assistant/core /homeassistant/components/text/ @home-assistant/core
/tests/components/text/ @home-assistant/core /tests/components/text/ @home-assistant/core
/homeassistant/components/tfiac/ @fredrike @mellado
/homeassistant/components/thermobeacon/ @bdraco /homeassistant/components/thermobeacon/ @bdraco
/tests/components/thermobeacon/ @bdraco /tests/components/thermobeacon/ @bdraco
/homeassistant/components/thermopro/ @bdraco @h3ss /homeassistant/components/thermopro/ @bdraco @h3ss
@@ -1795,8 +1685,6 @@ CLAUDE.md @home-assistant/core
/tests/components/tomorrowio/ @raman325 @lymanepp /tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek /homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek
/homeassistant/components/touchline/ @mnordseth
/tests/components/touchline/ @mnordseth
/homeassistant/components/touchline_sl/ @jnsgruk /homeassistant/components/touchline_sl/ @jnsgruk
/tests/components/touchline_sl/ @jnsgruk /tests/components/touchline_sl/ @jnsgruk
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696 /homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
@@ -1817,16 +1705,12 @@ CLAUDE.md @home-assistant/core
/tests/components/trafikverket_train/ @gjohansson-ST /tests/components/trafikverket_train/ @gjohansson-ST
/homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST /homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST
/tests/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 /homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp /tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/homeassistant/components/trend/ @jpbede /homeassistant/components/trend/ @jpbede
/tests/components/trend/ @jpbede /tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey /homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey /tests/components/triggercmd/ @rvmey
/homeassistant/components/trmnl/ @joostlek
/tests/components/trmnl/ @joostlek
/homeassistant/components/tts/ @home-assistant/core /homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core /tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver /homeassistant/components/tuya/ @Tuya @zlinoliver
@@ -1837,17 +1721,11 @@ CLAUDE.md @home-assistant/core
/tests/components/twinkly/ @dr1rrb @Robbie1221 @Olen /tests/components/twinkly/ @dr1rrb @Robbie1221 @Olen
/homeassistant/components/twitch/ @joostlek /homeassistant/components/twitch/ @joostlek
/tests/components/twitch/ @joostlek /tests/components/twitch/ @joostlek
/homeassistant/components/uhoo/ @getuhoo @joshsmonta
/tests/components/uhoo/ @getuhoo @joshsmonta
/homeassistant/components/ukraine_alarm/ @PaulAnnekov /homeassistant/components/ukraine_alarm/ @PaulAnnekov
/tests/components/ukraine_alarm/ @PaulAnnekov /tests/components/ukraine_alarm/ @PaulAnnekov
/homeassistant/components/unifi/ @Kane610 /homeassistant/components/unifi/ @Kane610
/tests/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_direct/ @tofuSCHNITZEL
/homeassistant/components/unifi_discovery/ @RaHehl
/tests/components/unifi_discovery/ @RaHehl
/homeassistant/components/unifiled/ @florisvdk /homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl /homeassistant/components/unifiprotect/ @RaHehl
/tests/components/unifiprotect/ @RaHehl /tests/components/unifiprotect/ @RaHehl
@@ -1886,8 +1764,8 @@ CLAUDE.md @home-assistant/core
/tests/components/vegehub/ @thulrus /tests/components/vegehub/ @thulrus
/homeassistant/components/velbus/ @Cereal2nd @brefra /homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra /tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @pawlizio @wollew /homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @pawlizio @wollew /tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe /homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe /tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz /homeassistant/components/versasense/ @imstevenxyz
@@ -1895,12 +1773,10 @@ CLAUDE.md @home-assistant/core
/tests/components/version/ @ludeeus /tests/components/version/ @ludeeus
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven /homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven /tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
/homeassistant/components/vicare/ @CFenner @lackas /homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner @lackas /tests/components/vicare/ @CFenner
/homeassistant/components/victron_ble/ @rajlaud /homeassistant/components/victron_ble/ @rajlaud
/tests/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 /homeassistant/components/victron_remote_monitoring/ @AndyTempel
/tests/components/victron_remote_monitoring/ @AndyTempel /tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW /homeassistant/components/vilfo/ @ManneW
@@ -1932,7 +1808,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/water_heater/ @home-assistant/core /homeassistant/components/water_heater/ @home-assistant/core
/tests/components/water_heater/ @home-assistant/core /tests/components/water_heater/ @home-assistant/core
/homeassistant/components/waterfurnace/ @sdague @masterkoppa /homeassistant/components/waterfurnace/ @sdague @masterkoppa
/tests/components/waterfurnace/ @sdague @masterkoppa
/homeassistant/components/watergate/ @adam-the-hero /homeassistant/components/watergate/ @adam-the-hero
/tests/components/watergate/ @adam-the-hero /tests/components/watergate/ @adam-the-hero
/homeassistant/components/watson_tts/ @rutkai /homeassistant/components/watson_tts/ @rutkai
@@ -1962,8 +1837,8 @@ CLAUDE.md @home-assistant/core
/tests/components/webostv/ @thecode /tests/components/webostv/ @thecode
/homeassistant/components/websocket_api/ @home-assistant/core /homeassistant/components/websocket_api/ @home-assistant/core
/tests/components/websocket_api/ @home-assistant/core /tests/components/websocket_api/ @home-assistant/core
/homeassistant/components/weheat/ @barryvdh /homeassistant/components/weheat/ @jesperraemaekers
/tests/components/weheat/ @barryvdh /tests/components/weheat/ @jesperraemaekers
/homeassistant/components/wemo/ @esev /homeassistant/components/wemo/ @esev
/tests/components/wemo/ @esev /tests/components/wemo/ @esev
/homeassistant/components/whirlpool/ @abmantis @mkmer /homeassistant/components/whirlpool/ @abmantis @mkmer
@@ -1972,35 +1847,29 @@ CLAUDE.md @home-assistant/core
/tests/components/whois/ @frenck /tests/components/whois/ @frenck
/homeassistant/components/wiffi/ @mampfes /homeassistant/components/wiffi/ @mampfes
/tests/components/wiffi/ @mampfes /tests/components/wiffi/ @mampfes
/homeassistant/components/wiim/ @Linkplay2020
/tests/components/wiim/ @Linkplay2020
/homeassistant/components/wilight/ @leofig-rj /homeassistant/components/wilight/ @leofig-rj
/tests/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/wirelesstag/ @sergeymaysak
/homeassistant/components/withings/ @joostlek /homeassistant/components/withings/ @joostlek
/tests/components/withings/ @joostlek /tests/components/withings/ @joostlek
/homeassistant/components/wiz/ @sbidy @arturpragacz /homeassistant/components/wiz/ @sbidy @arturpragacz
/tests/components/wiz/ @sbidy @arturpragacz /tests/components/wiz/ @sbidy @arturpragacz
/homeassistant/components/wled/ @frenck @mik-laj /homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck @mik-laj /tests/components/wled/ @frenck
/homeassistant/components/wmspro/ @mback2k /homeassistant/components/wmspro/ @mback2k
/tests/components/wmspro/ @mback2k /tests/components/wmspro/ @mback2k
/homeassistant/components/wolflink/ @adamkrol93 @EnjoyingM /homeassistant/components/wolflink/ @adamkrol93 @mtielen
/tests/components/wolflink/ @adamkrol93 @EnjoyingM /tests/components/wolflink/ @adamkrol93 @mtielen
/homeassistant/components/workday/ @fabaff @gjohansson-ST /homeassistant/components/workday/ @fabaff @gjohansson-ST
/tests/components/workday/ @fabaff @gjohansson-ST /tests/components/workday/ @fabaff @gjohansson-ST
/homeassistant/components/worldclock/ @fabaff /homeassistant/components/worldclock/ @fabaff
/tests/components/worldclock/ @fabaff /tests/components/worldclock/ @fabaff
/homeassistant/components/ws66i/ @ssaenger /homeassistant/components/ws66i/ @ssaenger
/tests/components/ws66i/ @ssaenger /tests/components/ws66i/ @ssaenger
/homeassistant/components/wsdot/ @ucodery
/tests/components/wsdot/ @ucodery
/homeassistant/components/wyoming/ @synesthesiam /homeassistant/components/wyoming/ @synesthesiam
/tests/components/wyoming/ @synesthesiam /tests/components/wyoming/ @synesthesiam
/homeassistant/components/xbox/ @tr4nt0r /homeassistant/components/xbox/ @hunterjm @tr4nt0r
/tests/components/xbox/ @tr4nt0r /tests/components/xbox/ @hunterjm @tr4nt0r
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi /homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
/tests/components/xiaomi_aqara/ @danielhiversen @syssi /tests/components/xiaomi_aqara/ @danielhiversen @syssi
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79 /homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
@@ -2045,14 +1914,11 @@ CLAUDE.md @home-assistant/core
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES /tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/homeassistant/components/zimi/ @markhannon /homeassistant/components/zimi/ @markhannon
/tests/components/zimi/ @markhannon /tests/components/zimi/ @markhannon
/homeassistant/components/zinvolt/ @joostlek
/tests/components/zinvolt/ @joostlek
/homeassistant/components/zodiac/ @JulienTant /homeassistant/components/zodiac/ @JulienTant
/tests/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant
/homeassistant/components/zone/ @home-assistant/core /homeassistant/components/zone/ @home-assistant/core
/tests/components/zone/ @home-assistant/core /tests/components/zone/ @home-assistant/core
/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi /homeassistant/components/zoneminder/ @rohankapoorcom @nabbi
/tests/components/zoneminder/ @rohankapoorcom @nabbi
/homeassistant/components/zwave_js/ @home-assistant/z-wave /homeassistant/components/zwave_js/ @home-assistant/z-wave
/tests/components/zwave_js/ @home-assistant/z-wave /tests/components/zwave_js/ @home-assistant/z-wave
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS /homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS

23
Dockerfile generated
View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
# Automatically generated by hassfest. # Automatically generated by hassfest.
# #
# To update, run python3 -m script.hassfest -p docker # To update, run python3 -m script.hassfest -p docker
@@ -11,6 +10,7 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \ 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.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \ 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.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/" org.opencontainers.image.url="https://www.home-assistant.io/"
@@ -20,22 +20,25 @@ ENV \
UV_SYSTEM_PYTHON=true \ UV_SYSTEM_PYTHON=true \
UV_NO_CACHE=true UV_NO_CACHE=true
WORKDIR /usr/src
# Home Assistant S6-Overlay # Home Assistant S6-Overlay
COPY rootfs / COPY rootfs /
# Add go2rtc binary # Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc@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 \ RUN \
# Verify go2rtc can be executed # Verify go2rtc can be executed
go2rtc --version \ go2rtc --version \
# Install uv at the version pinned in the requirements file # Install uv
&& pip3 install --no-cache-dir "uv==$(awk -F'==' '/^uv==/{print $2}' homeassistant/requirements.txt)" \ && pip3 install uv==0.9.17
&& uv pip install \
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 \ --no-build \
-r homeassistant/requirements.txt -r homeassistant/requirements.txt
@@ -49,7 +52,7 @@ RUN \
-r homeassistant/requirements_all.txt -r homeassistant/requirements_all.txt
## Setup Home Assistant Core ## Setup Home Assistant Core
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/ COPY . homeassistant/
RUN \ RUN \
uv pip install \ uv pip install \
-e ./homeassistant \ -e ./homeassistant \

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
FROM mcr.microsoft.com/vscode/devcontainers/base:debian FROM mcr.microsoft.com/vscode/devcontainers/base:debian
SHELL ["/bin/bash", "-o", "pipefail", "-c"] 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 \ --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 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 WORKDIR /workspaces
# Set the default shell to bash instead of sh # Set the default shell to bash instead of sh

View File

@@ -10,7 +10,6 @@ coverage:
target: auto target: auto
threshold: 1 threshold: 1
paths: paths:
- homeassistant/components/*/backup.py
- homeassistant/components/*/config_flow.py - homeassistant/components/*/config_flow.py
- homeassistant/components/*/device_action.py - homeassistant/components/*/device_action.py
- homeassistant/components/*/device_condition.py - homeassistant/components/*/device_condition.py
@@ -29,7 +28,6 @@ coverage:
target: 100 target: 100
threshold: 0 threshold: 0
paths: paths:
- homeassistant/components/*/backup.py
- homeassistant/components/*/config_flow.py - homeassistant/components/*/config_flow.py
- homeassistant/components/*/device_action.py - homeassistant/components/*/device_action.py
- homeassistant/components/*/device_condition.py - homeassistant/components/*/device_condition.py

View File

@@ -7,31 +7,23 @@ to speed up the process.
from __future__ import annotations from __future__ import annotations
from collections.abc import Container, Iterable, Sequence
from datetime import timedelta from datetime import timedelta
from functools import lru_cache from functools import lru_cache, partial
from typing import Any, override from typing import Any
from jwt import DecodeError, PyJWK, PyJWS, PyJWT from jwt import DecodeError, PyJWS, PyJWT
from jwt.algorithms import AllowedPublicKeys
from jwt.types import Options
from homeassistant.util.json import json_loads from homeassistant.util.json import json_loads
JWT_TOKEN_CACHE_SIZE = 16 JWT_TOKEN_CACHE_SIZE = 16
MAX_TOKEN_SIZE = 8192 MAX_TOKEN_SIZE = 8192
_NO_VERIFY_OPTIONS = Options( _VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
verify_signature=False,
verify_exp=False, _VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
verify_nbf=False, "require": []
verify_iat=False, }
verify_aud=False, _NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
verify_iss=False,
verify_sub=False,
verify_jti=False,
require=[],
)
class _PyJWSWithLoadCache(PyJWS): class _PyJWSWithLoadCache(PyJWS):
@@ -46,6 +38,9 @@ class _PyJWSWithLoadCache(PyJWS):
return super()._load(jwt) return super()._load(jwt)
_jws = _PyJWSWithLoadCache()
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE) @lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
def _decode_payload(json_payload: str) -> dict[str, Any]: def _decode_payload(json_payload: str) -> dict[str, Any]:
"""Decode the payload from a JWS dictionary.""" """Decode the payload from a JWS dictionary."""
@@ -61,12 +56,21 @@ def _decode_payload(json_payload: str) -> dict[str, Any]:
class _PyJWTWithVerify(PyJWT): class _PyJWTWithVerify(PyJWT):
"""PyJWT with a fast decode implementation.""" """PyJWT with a fast decode implementation."""
def __init__(self) -> None: def decode_payload(
"""Initialize the PyJWT instance.""" self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
# We require exp and iat claims to be present ) -> dict[str, Any]:
super().__init__(Options(require=["exp", "iat"])) """Decode a JWT's payload."""
# Override the _jws instance with our cached version if len(jwt) > MAX_TOKEN_SIZE:
self._jws = _PyJWSWithLoadCache() # 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( def verify_and_decode(
self, self,
@@ -75,70 +79,37 @@ class _PyJWTWithVerify(PyJWT):
algorithms: list[str], algorithms: list[str],
issuer: str | None = None, issuer: str | None = None,
leeway: float | timedelta = 0, leeway: float | timedelta = 0,
options: Options | None = None, options: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Verify a JWT's signature and claims.""" """Verify a JWT's signature and claims."""
return self.decode( merged_options = {**_VERIFY_OPTIONS, **(options or {})}
payload = self.decode_payload(
jwt=jwt, jwt=jwt,
key=key, key=key,
options=merged_options,
algorithms=algorithms, 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, issuer=issuer,
leeway=leeway, leeway=leeway,
options=options,
) )
return payload
@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"])
_jwt = _PyJWTWithVerify() _jwt = _PyJWTWithVerify()
verify_and_decode = _jwt.verify_and_decode verify_and_decode = _jwt.verify_and_decode
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
partial(
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE) _jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
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,
) )
)
__all__ = [ __all__ = [
"unverified_hs256_token_decode", "unverified_hs256_token_decode",

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
import hashlib
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
@@ -39,6 +40,17 @@ class RestoreBackupFileContent:
restore_homeassistant: bool 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: def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
"""Return the contents of the restore backup file.""" """Return the contents of the restore backup file."""
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE) instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
@@ -84,14 +96,15 @@ def _extract_backup(
"""Extract the backup file to the config directory.""" """Extract the backup file to the config directory."""
with ( with (
TemporaryDirectory() as tempdir, TemporaryDirectory() as tempdir,
securetar.SecureTarArchive( securetar.SecureTarFile(
restore_content.backup_file_path, restore_content.backup_file_path,
gzip=False,
mode="r", mode="r",
) as ostf, ) as ostf,
): ):
ostf.tar.extractall( ostf.extractall(
path=Path(tempdir, "extracted"), path=Path(tempdir, "extracted"),
members=securetar.secure_path(ostf.tar), members=securetar.secure_path(ostf),
filter="fully_trusted", filter="fully_trusted",
) )
backup_meta_file = Path(tempdir, "extracted", "backup.json") backup_meta_file = Path(tempdir, "extracted", "backup.json")
@@ -113,7 +126,10 @@ def _extract_backup(
f"homeassistant.tar{'.gz' if backup_meta['compressed'] else ''}", f"homeassistant.tar{'.gz' if backup_meta['compressed'] else ''}",
), ),
gzip=backup_meta["compressed"], 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: ) as istf:
istf.extractall( istf.extractall(
path=Path(tempdir, "homeassistant"), path=Path(tempdir, "homeassistant"),

View File

@@ -67,10 +67,12 @@ from .const import (
BASE_PLATFORMS, BASE_PLATFORMS,
FORMAT_DATETIME, FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING, KEY_DATA_LOGGING as DATA_LOGGING,
REQUIRED_NEXT_PYTHON_HA_RELEASE,
REQUIRED_NEXT_PYTHON_VER,
SIGNAL_BOOTSTRAP_INTEGRATIONS, SIGNAL_BOOTSTRAP_INTEGRATIONS,
) )
from .core_config import async_process_ha_core_config from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError from .exceptions import HomeAssistantError
from .helpers import ( from .helpers import (
area_registry, area_registry,
category_registry, category_registry,
@@ -210,7 +212,6 @@ DEFAULT_INTEGRATIONS = {
"analytics", # Needed for onboarding "analytics", # Needed for onboarding
"application_credentials", "application_credentials",
"backup", "backup",
"brands",
"frontend", "frontend",
"hardware", "hardware",
"labs", "labs",
@@ -236,31 +237,9 @@ DEFAULT_INTEGRATIONS = {
"input_text", "input_text",
"schedule", "schedule",
"timer", "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 = { DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
# These integrations are set up if recovery mode is activated. # These integrations are set up if recovery mode is activated.
"backup",
"cloud",
"frontend", "frontend",
} }
DEFAULT_INTEGRATIONS_SUPERVISOR = { DEFAULT_INTEGRATIONS_SUPERVISOR = {
@@ -455,57 +434,32 @@ def _init_blocking_io_modules_in_executor() -> None:
is_docker_env() is_docker_env()
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool: async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
"""Load the registries and modules that will do blocking I/O. """Load the registries and modules that will do blocking I/O."""
Return whether loading succeeded.
"""
if DATA_REGISTRIES_LOADED in hass.data: if DATA_REGISTRIES_LOADED in hass.data:
return True return
hass.data[DATA_REGISTRIES_LOADED] = None hass.data[DATA_REGISTRIES_LOADED] = None
entity.async_setup(hass) entity.async_setup(hass)
frame.async_setup(hass) frame.async_setup(hass)
template.async_setup(hass) template.async_setup(hass)
translation.async_setup(hass) translation.async_setup(hass)
await asyncio.gather(
recovery = hass.config.recovery_mode create_eager_task(get_internal_store_manager(hass).async_initialize()),
device_registry.async_setup(hass) create_eager_task(area_registry.async_load(hass)),
try: create_eager_task(category_registry.async_load(hass)),
await asyncio.gather( create_eager_task(device_registry.async_load(hass)),
create_eager_task(get_internal_store_manager(hass).async_initialize()), create_eager_task(entity_registry.async_load(hass)),
create_eager_task(area_registry.async_load(hass, load_empty=recovery)), create_eager_task(floor_registry.async_load(hass)),
create_eager_task(category_registry.async_load(hass, load_empty=recovery)), create_eager_task(issue_registry.async_load(hass)),
create_eager_task(device_registry.async_load(hass, load_empty=recovery)), create_eager_task(label_registry.async_load(hass)),
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)), hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)), create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)), create_eager_task(restore_state.async_load(hass)),
create_eager_task(label_registry.async_load(hass, load_empty=recovery)), create_eager_task(hass.config_entries.async_initialize()),
hass.async_add_executor_job(_init_blocking_io_modules_in_executor), create_eager_task(async_get_system_info(hass)),
create_eager_task(template.async_load_custom_templates(hass)), create_eager_task(condition.async_setup(hass)),
create_eager_task(restore_state.async_load(hass, load_empty=recovery)), create_eager_task(trigger.async_setup(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)),
)
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
async def async_from_config_dict( async def async_from_config_dict(
@@ -522,9 +476,7 @@ async def async_from_config_dict(
# Prime custom component cache early so we know if registry entries are tied # Prime custom component cache early so we know if registry entries are tied
# to a custom integration # to a custom integration
await loader.async_get_custom_components(hass) await loader.async_get_custom_components(hass)
await async_load_base_functionality(hass)
if not await async_load_base_functionality(hass):
return None
# Set up core. # Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
@@ -564,6 +516,38 @@ async def async_from_config_dict(
stop = monotonic() stop = monotonic()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start) _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 return hass

View File

@@ -1,5 +0,0 @@
{
"domain": "american_standard",
"name": "American Standard",
"integrations": ["nexia", "trane"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "bega",
"name": "BEGA",
"iot_standards": ["zigbee"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "cloudflare",
"name": "Cloudflare",
"integrations": ["cloudflare", "cloudflare_r2"]
}

View File

@@ -1,5 +1,5 @@
{ {
"domain": "denon", "domain": "denon",
"name": "Denon", "name": "Denon",
"integrations": ["denon", "denonavr", "denon_rs232", "heos"] "integrations": ["denon", "denonavr", "heos"]
} }

View File

@@ -1,5 +0,0 @@
{
"domain": "heatit",
"name": "Heatit",
"iot_standards": ["zwave"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "heiman",
"name": "Heiman",
"iot_standards": ["matter", "zigbee"]
}

View File

@@ -1,5 +1,5 @@
{ {
"domain": "honeywell", "domain": "honeywell",
"name": "Honeywell", "name": "Honeywell",
"integrations": ["lyric", "evohome", "honeywell", "honeywell_string_lights"] "integrations": ["lyric", "evohome", "honeywell"]
} }

View File

@@ -1,6 +1,5 @@
{ {
"domain": "leviton", "domain": "leviton",
"name": "Leviton", "name": "Leviton",
"integrations": ["decora_wifi"],
"iot_standards": ["zwave"] "iot_standards": ["zwave"]
} }

View File

@@ -1,11 +1,5 @@
{ {
"domain": "lg", "domain": "lg",
"name": "LG", "name": "LG",
"integrations": [ "integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
"lg_infrared",
"lg_netcast",
"lg_soundbar",
"lg_thinq",
"webostv"
]
} }

View File

@@ -13,7 +13,6 @@
"microsoft", "microsoft",
"msteams", "msteams",
"onedrive", "onedrive",
"onedrive_for_business",
"xbox" "xbox"
] ]
} }

View File

@@ -1,5 +0,0 @@
{
"domain": "powerfox",
"name": "Powerfox",
"integrations": ["powerfox", "powerfox_local"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "trane",
"name": "Trane",
"integrations": ["nexia", "trane"]
}

View File

@@ -1,13 +1,5 @@
{ {
"domain": "ubiquiti", "domain": "ubiquiti",
"name": "Ubiquiti", "name": "Ubiquiti",
"integrations": [ "integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
"airos",
"unifi",
"unifi_access",
"unifi_direct",
"unifi_discovery",
"unifiled",
"unifiprotect"
]
} }

View File

@@ -1,5 +0,0 @@
{
"domain": "ubisys",
"name": "Ubisys",
"iot_standards": ["zigbee"]
}

View File

@@ -1,5 +1,5 @@
{ {
"domain": "victron", "domain": "victron",
"name": "Victron", "name": "Victron",
"integrations": ["victron_gx", "victron_ble", "victron_remote_monitoring"] "integrations": ["victron_ble", "victron_remote_monitoring"]
} }

View File

@@ -67,16 +67,13 @@ class AbodeSystem:
logout_listener: CALLBACK_TYPE | None = None logout_listener: CALLBACK_TYPE | None = None
type AbodeConfigEntry = ConfigEntry[AbodeSystem]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component.""" """Set up the Abode component."""
async_setup_services(hass) async_setup_services(hass)
return True 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.""" """Set up Abode integration from a config entry."""
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
@@ -102,54 +99,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> boo
except (AbodeException, ConnectTimeout, HTTPError) as ex: except (AbodeException, ConnectTimeout, HTTPError) as ex:
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from 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 hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await setup_hass_events(hass, entry) await setup_hass_events(hass)
await hass.async_add_executor_job(setup_abode_events, hass, entry) await hass.async_add_executor_job(setup_abode_events, hass)
return True return True
def _shutdown_client(abode: Abode) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Shutdown client."""
abode.events.stop()
abode.logout()
async def async_unload_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 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: hass.data[DOMAIN].logout_listener()
logout_listener() hass.data.pop(DOMAIN)
return unload_ok 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.""" """Home Assistant start and stop callbacks."""
def logout(event: Event) -> None: def logout(event: Event) -> None:
"""Logout of Abode.""" """Logout of Abode."""
if not entry.runtime_data.polling: if not hass.data[DOMAIN].polling:
entry.runtime_data.abode.events.stop() hass.data[DOMAIN].abode.events.stop()
entry.runtime_data.abode.logout() hass.data[DOMAIN].abode.logout()
LOGGER.info("Logged out of Abode") LOGGER.info("Logged out of Abode")
if not entry.runtime_data.polling: if not hass.data[DOMAIN].polling:
await hass.async_add_executor_job(entry.runtime_data.abode.events.start) 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 EVENT_HOMEASSISTANT_STOP, logout
) )
def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None: def setup_abode_events(hass: HomeAssistant) -> None:
"""Event callbacks.""" """Event callbacks."""
def event_callback(event: str, event_json: dict[str, str]) -> None: def event_callback(event: str, event_json: dict[str, str]) -> None:
@@ -186,6 +178,6 @@ def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
] ]
for event in events: 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) event, partial(event_callback, event)
) )

View File

@@ -9,20 +9,22 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState, AlarmControlPanelState,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode alarm control panel device.""" """Set up Abode alarm control panel device."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
) )

View File

@@ -10,21 +10,23 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode binary sensor devices.""" """Set up Abode binary sensor devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
device_types = [ device_types = [
"connectivity", "connectivity",

View File

@@ -12,13 +12,14 @@ import requests
from requests.models import Response from requests.models import Response
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import AbodeConfigEntry, AbodeSystem from . import AbodeSystem
from .const import LOGGER from .const import DOMAIN, LOGGER
from .entity import AbodeDevice from .entity import AbodeDevice
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
@@ -26,11 +27,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode camera devices.""" """Set up Abode camera devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeCamera(data, device, timeline.CAPTURE_IMAGE) AbodeCamera(data, device, timeline.CAPTURE_IMAGE)

View File

@@ -64,7 +64,7 @@ class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
else: else:
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
except ConnectTimeout, HTTPError: except (ConnectTimeout, HTTPError):
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
if errors: if errors:

View File

@@ -1,7 +1,5 @@
"""Constants for the Abode Security System component.""" """Constants for the Abode Security System component."""
from __future__ import annotations
import logging import logging
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)

View File

@@ -5,20 +5,22 @@ from typing import Any
from jaraco.abode.devices.cover import Cover from jaraco.abode.devices.cover import Cover
from homeassistant.components.cover import CoverEntity from homeassistant.components.cover import CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode cover devices.""" """Set up Abode cover devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeCover(data, device) AbodeCover(data, device)

View File

@@ -29,7 +29,7 @@ class AbodeEntity(Entity):
self._update_connection_status, self._update_connection_status,
) )
self._data.entity_ids.add(self.entity_id) self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from Abode connection status updates.""" """Unsubscribe from Abode connection status updates."""

View File

@@ -16,20 +16,22 @@ from homeassistant.components.light import (
ColorMode, ColorMode,
LightEntity, LightEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode light devices.""" """Set up Abode light devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeLight(data, device) AbodeLight(data, device)
@@ -98,7 +100,7 @@ class AbodeLight(AbodeDevice, LightEntity):
return _hs return _hs
@property @property
def color_mode(self) -> ColorMode: def color_mode(self) -> str | None:
"""Return the color mode of the light.""" """Return the color mode of the light."""
if self._device.is_dimmable and self._device.is_color_capable: if self._device.is_dimmable and self._device.is_color_capable:
if self.hs_color is not None: if self.hs_color is not None:
@@ -109,7 +111,7 @@ class AbodeLight(AbodeDevice, LightEntity):
return ColorMode.ONOFF return ColorMode.ONOFF
@property @property
def supported_color_modes(self) -> set[ColorMode]: def supported_color_modes(self) -> set[str] | None:
"""Flag supported color modes.""" """Flag supported color modes."""
if self._device.is_dimmable and self._device.is_color_capable: if self._device.is_dimmable and self._device.is_color_capable:
return {ColorMode.COLOR_TEMP, ColorMode.HS} return {ColorMode.COLOR_TEMP, ColorMode.HS}

View File

@@ -5,20 +5,22 @@ from typing import Any
from jaraco.abode.devices.lock import Lock from jaraco.abode.devices.lock import Lock
from homeassistant.components.lock import LockEntity from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode lock devices.""" """Set up Abode lock devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeLock(data, device) AbodeLock(data, device)

View File

@@ -9,6 +9,6 @@
}, },
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"], "loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==6.4.0"], "requirements": ["jaraco.abode==6.2.1"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -12,13 +12,14 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry, AbodeSystem from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeDevice from .entity import AbodeDevice
ABODE_TEMPERATURE_UNIT_HA_UNIT = { ABODE_TEMPERATURE_UNIT_HA_UNIT = {
@@ -39,7 +40,6 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription( AbodeSensorDescription(
key="temperature", key="temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
device.temp_unit device.temp_unit
], ],
@@ -48,14 +48,12 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription( AbodeSensorDescription(
key="humidity", key="humidity",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda _: PERCENTAGE, native_unit_of_measurement_fn=lambda _: PERCENTAGE,
value_fn=lambda device: cast(float, device.humidity), value_fn=lambda device: cast(float, device.humidity),
), ),
AbodeSensorDescription( AbodeSensorDescription(
key="lux", key="lux",
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda _: LIGHT_LUX, native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
value_fn=lambda device: cast(float, device.lux), value_fn=lambda device: cast(float, device.lux),
), ),
@@ -64,11 +62,11 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode sensor devices.""" """Set up Abode sensor devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeSensor(data, device, description) AbodeSensor(data, device, description)

View File

@@ -2,21 +2,19 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from jaraco.abode.exceptions import Exception as AbodeException from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
if TYPE_CHECKING: SERVICE_SETTINGS = "change_setting"
from . import AbodeConfigEntry, AbodeSystem SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_SETTING = "setting" ATTR_SETTING = "setting"
ATTR_VALUE = "value" ATTR_VALUE = "value"
@@ -31,21 +29,13 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def _get_abode_system(hass: HomeAssistant) -> AbodeSystem:
"""Return the Abode system for the loaded config entry."""
entries: list[AbodeConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError("Abode integration is not loaded")
return entries[0].runtime_data
def _change_setting(call: ServiceCall) -> None: def _change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting.""" """Change an Abode system setting."""
setting = call.data[ATTR_SETTING] setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE] value = call.data[ATTR_VALUE]
try: try:
_get_abode_system(call.hass).abode.set_setting(setting, value) call.hass.data[DOMAIN].abode.set_setting(setting, value)
except AbodeException as ex: except AbodeException as ex:
LOGGER.warning(ex) LOGGER.warning(ex)
@@ -56,7 +46,7 @@ def _capture_image(call: ServiceCall) -> None:
target_entities = [ target_entities = [
entity_id entity_id
for entity_id in _get_abode_system(call.hass).entity_ids for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids if entity_id in entity_ids
] ]
@@ -71,7 +61,7 @@ def _trigger_automation(call: ServiceCall) -> None:
target_entities = [ target_entities = [
entity_id entity_id
for entity_id in _get_abode_system(call.hass).entity_ids for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids if entity_id in entity_ids
] ]
@@ -85,13 +75,16 @@ def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services.""" """Home Assistant services."""
hass.services.async_register( hass.services.async_register(
DOMAIN, "change_setting", _change_setting, schema=CHANGE_SETTING_SCHEMA DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, "capture_image", _capture_image, schema=CAPTURE_IMAGE_SCHEMA DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, "trigger_automation", _trigger_automation, schema=AUTOMATION_SCHEMA DOMAIN,
SERVICE_TRIGGER_AUTOMATION,
_trigger_automation,
schema=AUTOMATION_SCHEMA,
) )

View File

@@ -7,11 +7,13 @@ from typing import Any, cast
from jaraco.abode.devices.switch import Switch from jaraco.abode.devices.switch import Switch
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeConfigEntry from . import AbodeSystem
from .const import DOMAIN
from .entity import AbodeAutomation, AbodeDevice from .entity import AbodeAutomation, AbodeDevice
DEVICE_TYPES = ["switch", "valve"] DEVICE_TYPES = ["switch", "valve"]
@@ -19,11 +21,11 @@ DEVICE_TYPES = ["switch", "valve"]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AbodeConfigEntry, entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Abode switch devices.""" """Set up Abode switch devices."""
data = entry.runtime_data data: AbodeSystem = hass.data[DOMAIN]
entities: list[SwitchEntity] = [ entities: list[SwitchEntity] = [
AbodeSwitch(data, device) AbodeSwitch(data, device)

View File

@@ -7,7 +7,7 @@ import logging
from accuweather import AccuWeather from accuweather import AccuWeather
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.const import CONF_API_KEY, Platform from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@@ -72,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
for day in range(5): for day in range(5):
unique_id = f"{location_key}-ozone-{day}" unique_id = f"{location_key}-ozone-{day}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id): if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
_LOGGER.debug("Removing ozone sensor entity %s", entity_id) _LOGGER.debug("Removing ozone sensor entity %s", entity_id)
ent_reg.async_remove(entity_id) ent_reg.async_remove(entity_id)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from asyncio import timeout from asyncio import timeout
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError from aiohttp import ClientError
@@ -12,7 +12,7 @@ from aiohttp.client_exceptions import ClientConnectorError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -43,7 +43,7 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
longitude=user_input[CONF_LONGITUDE], longitude=user_input[CONF_LONGITUDE],
) )
await accuweather.async_get_location() await accuweather.async_get_location()
except ApiError, ClientConnectorError, TimeoutError, ClientError: except (ApiError, ClientConnectorError, TimeoutError, ClientError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidApiKeyError: except InvalidApiKeyError:
errors[CONF_API_KEY] = "invalid_api_key" errors[CONF_API_KEY] = "invalid_api_key"
@@ -55,11 +55,8 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
) )
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
if TYPE_CHECKING:
assert accuweather.location_name is not None
return self.async_create_entry( return self.async_create_entry(
title=accuweather.location_name, data=user_input title=user_input[CONF_NAME], data=user_input
) )
return self.async_show_form( return self.async_show_form(
@@ -73,6 +70,9 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Optional( vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude, ): cv.longitude,
vol.Optional(
CONF_NAME, default=self.hass.config.location_name
): str,
} }
), ),
errors=errors, errors=errors,
@@ -104,7 +104,7 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
longitude=self._longitude, longitude=self._longitude,
) )
await accuweather.async_get_location() await accuweather.async_get_location()
except ApiError, ClientConnectorError, TimeoutError, ClientError: except (ApiError, ClientConnectorError, TimeoutError, ClientError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidApiKeyError: except InvalidApiKeyError:
errors["base"] = "invalid_api_key" errors["base"] = "invalid_api_key"

View File

@@ -64,7 +64,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
"""Initialize.""" """Initialize."""
self.accuweather = accuweather self.accuweather = accuweather
self.location_key = accuweather.location_key self.location_key = accuweather.location_key
name = config_entry.data.get(CONF_NAME) or config_entry.title name = config_entry.data[CONF_NAME]
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.location_key is not None assert self.location_key is not None
@@ -122,7 +122,7 @@ class AccuWeatherForecastDataUpdateCoordinator(
self.accuweather = accuweather self.accuweather = accuweather
self.location_key = accuweather.location_key self.location_key = accuweather.location_key
self._fetch_method = fetch_method self._fetch_method = fetch_method
name = config_entry.data.get(CONF_NAME) or config_entry.title name = config_entry.data[CONF_NAME]
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.location_key is not None assert self.location_key is not None

View File

@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["accuweather"], "loggers": ["accuweather"],
"requirements": ["accuweather==5.1.0"] "requirements": ["accuweather==5.0.0"]
} }

View File

@@ -30,8 +30,6 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
) )
return { return {
"can_reach_server": system_health.async_check_can_reach_url( "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
hass, str(ENDPOINT)
),
"remaining_requests": remaining_requests, "remaining_requests": remaining_requests,
} }

View File

@@ -191,7 +191,7 @@ class AccuWeatherEntity(
{ {
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"], ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"), ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE], ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE], ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][ ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][

View File

@@ -6,11 +6,10 @@ from typing import Final
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
CONF_READ_TIMEOUT: Final = "timeout"
CONF_WRITE_TIMEOUT: Final = "write_timeout" CONF_WRITE_TIMEOUT: Final = "write_timeout"
DEFAULT_NAME: Final = "Acer Projector" DEFAULT_NAME: Final = "Acer Projector"
DEFAULT_READ_TIMEOUT: Final = 1 DEFAULT_TIMEOUT: Final = 1
DEFAULT_WRITE_TIMEOUT: Final = 1 DEFAULT_WRITE_TIMEOUT: Final = 1
ECO_MODE: Final = "ECO Mode" ECO_MODE: Final = "ECO Mode"

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["serialx==1.4.1"] "requirements": ["pyserial==3.5"]
} }

View File

@@ -6,7 +6,7 @@ import logging
import re import re
from typing import Any from typing import Any
from serialx import Serial, SerialException import serial
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import ( from homeassistant.components.switch import (
@@ -16,22 +16,21 @@ from homeassistant.components.switch import (
from homeassistant.const import ( from homeassistant.const import (
CONF_FILENAME, CONF_FILENAME,
CONF_NAME, CONF_NAME,
CONF_TIMEOUT,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import ( from .const import (
CMD_DICT, CMD_DICT,
CONF_READ_TIMEOUT,
CONF_WRITE_TIMEOUT, CONF_WRITE_TIMEOUT,
DEFAULT_NAME, DEFAULT_NAME,
DEFAULT_READ_TIMEOUT, DEFAULT_TIMEOUT,
DEFAULT_WRITE_TIMEOUT, DEFAULT_WRITE_TIMEOUT,
ECO_MODE, ECO_MODE,
ICON, ICON,
@@ -46,7 +45,7 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_FILENAME): cv.isdevice, vol.Required(CONF_FILENAME): cv.isdevice,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_READ_TIMEOUT, default=DEFAULT_READ_TIMEOUT): cv.positive_int, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional( vol.Optional(
CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT
): cv.positive_int, ): cv.positive_int,
@@ -63,10 +62,10 @@ def setup_platform(
"""Connect with serial port and return Acer Projector.""" """Connect with serial port and return Acer Projector."""
serial_port = config[CONF_FILENAME] serial_port = config[CONF_FILENAME]
name = config[CONF_NAME] name = config[CONF_NAME]
read_timeout = config[CONF_READ_TIMEOUT] timeout = config[CONF_TIMEOUT]
write_timeout = config[CONF_WRITE_TIMEOUT] write_timeout = config[CONF_WRITE_TIMEOUT]
add_entities([AcerSwitch(serial_port, name, read_timeout, write_timeout)], True) add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True)
class AcerSwitch(SwitchEntity): class AcerSwitch(SwitchEntity):
@@ -78,14 +77,14 @@ class AcerSwitch(SwitchEntity):
self, self,
serial_port: str, serial_port: str,
name: str, name: str,
read_timeout: int, timeout: int,
write_timeout: int, write_timeout: int,
) -> None: ) -> None:
"""Init of the Acer projector.""" """Init of the Acer projector."""
self.serial = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout
)
self._serial_port = serial_port self._serial_port = serial_port
self._read_timeout = read_timeout
self._write_timeout = write_timeout
self._attr_name = name self._attr_name = name
self._attributes = { self._attributes = {
LAMP_HOURS: STATE_UNKNOWN, LAMP_HOURS: STATE_UNKNOWN,
@@ -95,26 +94,22 @@ class AcerSwitch(SwitchEntity):
def _write_read(self, msg: str) -> str: def _write_read(self, msg: str) -> str:
"""Write to the projector and read the return.""" """Write to the projector and read the return."""
ret = ""
# Sometimes the projector won't answer for no reason or the projector # Sometimes the projector won't answer for no reason or the projector
# was disconnected during runtime. # was disconnected during runtime.
# This way the projector can be reconnected and will still work # This way the projector can be reconnected and will still work
try: try:
with Serial.from_url( if not self.serial.is_open:
self._serial_port, self.serial.open()
read_timeout=self._read_timeout, self.serial.write(msg.encode("utf-8"))
write_timeout=self._write_timeout, # Size is an experience value there is no real limit.
) as serial: # AFAIK there is no limit and no end character so we will usually
serial.write(msg.encode("utf-8")) # need to wait for timeout
ret = self.serial.read_until(size=20).decode("utf-8")
# Size is an experience value there is no real limit. except serial.SerialException:
# AFAIK there is no limit and no end character so we will usually _LOGGER.error("Problem communicating with %s", self._serial_port)
# need to wait for timeout self.serial.close()
return serial.read_until(size=20).decode("utf-8") return ret
except (OSError, SerialException, TimeoutError) as exc:
raise HomeAssistantError(
f"Problem communicating with {self._serial_port}"
) from exc
def _write_read_format(self, msg: str) -> str: def _write_read_format(self, msg: str) -> str:
"""Write msg, obtain answer and format output.""" """Write msg, obtain answer and format output."""

View File

@@ -1 +1 @@
"""The Actiontec integration.""" """The actiontec component."""

View File

@@ -1,7 +1,11 @@
"""The Actron Air integration.""" """The Actron Air integration."""
from actron_neo_api import ActronAirAPI, ActronAirAPIError, ActronAirAuthError from actron_neo_api import (
from actron_neo_api.models.system import ActronAirSystemInfo ActronAirACSystem,
ActronAirAPI,
ActronAirAPIError,
ActronAirAuthError,
)
from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -21,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
"""Set up Actron Air integration from a config entry.""" """Set up Actron Air integration from a config entry."""
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN]) api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirSystemInfo] = [] systems: list[ActronAirACSystem] = []
try: try:
systems = await api.get_ac_systems() systems = await api.get_ac_systems()
@@ -32,17 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
translation_key="auth_error", translation_key="auth_error",
) from err ) from err
except ActronAirAPIError as err: except ActronAirAPIError as err:
raise ConfigEntryNotReady( raise ConfigEntryNotReady from err
translation_domain=DOMAIN,
translation_key="setup_connection_error",
) from err
system_coordinators: dict[str, ActronAirSystemCoordinator] = {} system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems: for system in systems:
coordinator = ActronAirSystemCoordinator(hass, entry, api, system) coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
_LOGGER.debug("Setting up coordinator for system: %s", system.serial) _LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
system_coordinators[system.serial] = coordinator system_coordinators[system["serial"]] = coordinator
entry.runtime_data = ActronAirRuntimeData( entry.runtime_data = ActronAirRuntimeData(
api=api, api=api,

View File

@@ -15,12 +15,10 @@ from homeassistant.components.climate import (
) )
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, ActronAirZoneEntity, actron_air_command from .entity import ActronAirAcEntity, ActronAirZoneEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -138,27 +136,20 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
"""Return the target temperature.""" """Return the target temperature."""
return self._status.user_aircon_settings.temperature_setpoint_cool_c return self._status.user_aircon_settings.temperature_setpoint_cool_c
@actron_air_command
async def async_set_fan_mode(self, fan_mode: str) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set a new fan mode.""" """Set a new fan mode."""
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR[fan_mode] api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode) await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode.""" """Set the HVAC mode."""
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR[hvac_mode] ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
await self._status.ac_system.set_system_mode(ac_mode) await self._status.ac_system.set_system_mode(ac_mode)
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature.""" """Set the temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: temp = kwargs.get(ATTR_TEMPERATURE)
raise ServiceValidationError( await self._status.user_aircon_settings.set_temperature(temperature=temp)
translation_domain=DOMAIN,
translation_key="temperature_missing",
)
await self._status.user_aircon_settings.set_temperature(temperature=temperature)
class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity): class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
@@ -218,18 +209,11 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
"""Return the target temperature.""" """Return the target temperature."""
return self._zone.temperature_setpoint_cool_c return self._zone.temperature_setpoint_cool_c
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode.""" """Set the HVAC mode."""
is_enabled = hvac_mode != HVACMode.OFF is_enabled = hvac_mode != HVACMode.OFF
await self._zone.enable(is_enabled) await self._zone.enable(is_enabled)
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature.""" """Set the temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="temperature_missing",
)
await self._zone.set_temperature(temperature=temperature)

View File

@@ -23,7 +23,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
self._user_code: str = "" self._user_code: str = ""
self._verification_uri: str = "" self._verification_uri: str = ""
self._expires_minutes: str = "30" self._expires_minutes: str = "30"
self.login_task: asyncio.Task[None] | None = None self.login_task: asyncio.Task | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@@ -38,10 +38,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("OAuth2 flow failed: %s", err) _LOGGER.error("OAuth2 flow failed: %s", err)
return self.async_abort(reason="oauth2_error") return self.async_abort(reason="oauth2_error")
self._device_code = device_code_response.device_code self._device_code = device_code_response["device_code"]
self._user_code = device_code_response.user_code self._user_code = device_code_response["user_code"]
self._verification_uri = device_code_response.verification_uri_complete self._verification_uri = device_code_response["verification_uri_complete"]
self._expires_minutes = str(device_code_response.expires_in // 60) self._expires_minutes = str(device_code_response["expires_in"] // 60)
async def _wait_for_authorization() -> None: async def _wait_for_authorization() -> None:
"""Wait for the user to authorize the device.""" """Wait for the user to authorize the device."""
@@ -94,7 +94,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error getting user info: %s", err) _LOGGER.error("Error getting user info: %s", err)
return self.async_abort(reason="oauth2_error") return self.async_abort(reason="oauth2_error")
unique_id = user_data.sub unique_id = str(user_data["id"])
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
# Check if this is a reauth flow # Check if this is a reauth flow
@@ -107,7 +107,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=user_data.email, title=user_data["email"],
data={CONF_API_TOKEN: self._api.refresh_token_value}, data={CONF_API_TOKEN: self._api.refresh_token_value},
) )
@@ -120,7 +120,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="timeout", step_id="timeout",
) )
self.login_task = None del self.login_task
return await self.async_step_user() return await self.async_step_user()
async def async_step_reauth( async def async_step_reauth(

View File

@@ -6,12 +6,12 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from actron_neo_api import ( from actron_neo_api import (
ActronAirACSystem,
ActronAirAPI, ActronAirAPI,
ActronAirAPIError, ActronAirAPIError,
ActronAirAuthError, ActronAirAuthError,
ActronAirStatus, ActronAirStatus,
) )
from actron_neo_api.models.system import ActronAirSystemInfo
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -38,7 +38,7 @@ class ActronAirRuntimeData:
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData] type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]): class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
"""System coordinator for Actron Air integration.""" """System coordinator for Actron Air integration."""
def __init__( def __init__(
@@ -46,7 +46,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
hass: HomeAssistant, hass: HomeAssistant,
entry: ActronAirConfigEntry, entry: ActronAirConfigEntry,
api: ActronAirAPI, api: ActronAirAPI,
system: ActronAirSystemInfo, system: ActronAirACSystem,
) -> None: ) -> None:
"""Initialize the coordinator.""" """Initialize the coordinator."""
super().__init__( super().__init__(
@@ -57,7 +57,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
config_entry=entry, config_entry=entry,
) )
self.system = system self.system = system
self.serial_number = system.serial self.serial_number = system["serial"]
self.api = api self.api = api
self.status = self.api.state_manager.get_status(self.serial_number) self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
@@ -78,14 +78,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
translation_placeholders={"error": repr(err)}, translation_placeholders={"error": repr(err)},
) from err ) from err
status = self.api.state_manager.get_status(self.serial_number) self.status = self.api.state_manager.get_status(self.serial_number)
if status is None:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": "Status not available"},
)
self.status = status
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
return self.status return self.status

View File

@@ -1,35 +0,0 @@
"""Diagnostics support for Actron Air."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
from .coordinator import ActronAirConfigEntry
TO_REDACT = {CONF_API_TOKEN, "master_serial", "serial_number", "serial"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: dict[int, Any] = {}
for idx, coordinator in enumerate(entry.runtime_data.system_coordinators.values()):
coordinators[idx] = {
"system": async_redact_data(
coordinator.system.model_dump(mode="json"), TO_REDACT
),
"status": async_redact_data(
coordinator.data.model_dump(mode="json", exclude={"last_known_state"}),
TO_REDACT,
),
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"coordinators": coordinators,
}

View File

@@ -1,12 +1,7 @@
"""Base entity classes for Actron Air integration.""" """Base entity classes for Actron Air integration."""
from collections.abc import Callable, Coroutine from actron_neo_api import ActronAirZone
from functools import wraps
from typing import Any, Concatenate
from actron_neo_api import ActronAirAPIError, ActronAirZone
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -14,31 +9,6 @@ from .const import DOMAIN
from .coordinator import ActronAirSystemCoordinator from .coordinator import ActronAirSystemCoordinator
def actron_air_command[_EntityT: ActronAirEntity, **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorator for Actron Air API calls.
Handles ActronAirAPIError exceptions, and requests a coordinator update
to update the status of the devices as soon as possible.
"""
@wraps(func)
async def wrapper(self: _EntityT, /, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap API calls with exception handling."""
try:
await func(self, *args, **kwargs)
except ActronAirAPIError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"error": str(err)},
) from err
self.coordinator.async_set_updated_data(self.coordinator.data)
return wrapper
class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]): class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]):
"""Base class for Actron Air entities.""" """Base class for Actron Air entities."""

View File

@@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/actron_air", "documentation": "https://www.home-assistant.io/integrations/actron_air",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "quality_scale": "bronze",
"requirements": ["actron-neo-api==0.5.5"] "requirements": ["actron-neo-api==0.4.1"]
} }

View File

@@ -26,7 +26,7 @@ rules:
unique-config-entry: done unique-config-entry: done
# Silver # Silver
action-exceptions: done action-exceptions: todo
config-entry-unloading: done config-entry-unloading: done
docs-configuration-parameters: docs-configuration-parameters:
status: exempt status: exempt
@@ -37,11 +37,11 @@ rules:
log-when-unavailable: done log-when-unavailable: done
parallel-updates: done parallel-updates: done
reauthentication-flow: done reauthentication-flow: done
test-coverage: done test-coverage: todo
# Gold # Gold
devices: done devices: done
diagnostics: done diagnostics: todo
discovery-update-info: discovery-update-info:
status: exempt status: exempt
comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update. comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update.
@@ -54,12 +54,18 @@ rules:
docs-troubleshooting: done docs-troubleshooting: done
docs-use-cases: done docs-use-cases: done
dynamic-devices: todo dynamic-devices: todo
entity-category: done entity-category:
entity-device-class: todo status: exempt
entity-disabled-by-default: todo comment: This integration does not use entity categories.
entity-translations: done entity-device-class:
exception-translations: done status: exempt
icon-translations: done comment: This integration does not use entity device classes.
entity-disabled-by-default:
status: exempt
comment: Not required for this integration at this stage.
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo reconfiguration-flow: todo
repair-issues: repair-issues:
status: exempt status: exempt
@@ -69,4 +75,4 @@ rules:
# Platinum # Platinum
async-dependency: done async-dependency: done
inject-websession: todo inject-websession: todo
strict-typing: done strict-typing: todo

View File

@@ -49,18 +49,9 @@
} }
}, },
"exceptions": { "exceptions": {
"api_error": {
"message": "Failed to communicate with Actron Air device: {error}"
},
"auth_error": { "auth_error": {
"message": "Authentication failed, please reauthenticate" "message": "Authentication failed, please reauthenticate"
}, },
"setup_connection_error": {
"message": "Failed to connect to the Actron Air API"
},
"temperature_missing": {
"message": "Provide a temperature value when adjusting the climate entity."
},
"update_error": { "update_error": {
"message": "An error occurred while retrieving data from the Actron Air API: {error}" "message": "An error occurred while retrieving data from the Actron Air API: {error}"
} }

View File

@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, actron_air_command from .entity import ActronAirAcEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -29,42 +29,30 @@ SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
key="away_mode", key="away_mode",
translation_key="away_mode", translation_key="away_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode, is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
set_fn=lambda coordinator, enabled: ( set_fn=lambda coordinator,
coordinator.data.user_aircon_settings.set_away_mode(enabled) enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
),
), ),
ActronAirSwitchEntityDescription( ActronAirSwitchEntityDescription(
key="continuous_fan", key="continuous_fan",
translation_key="continuous_fan", translation_key="continuous_fan",
is_on_fn=lambda coordinator: ( is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
coordinator.data.user_aircon_settings.continuous_fan_enabled set_fn=lambda coordinator,
), enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
set_fn=lambda coordinator, enabled: (
coordinator.data.user_aircon_settings.set_continuous_mode(enabled)
),
), ),
ActronAirSwitchEntityDescription( ActronAirSwitchEntityDescription(
key="quiet_mode", key="quiet_mode",
translation_key="quiet_mode", translation_key="quiet_mode",
is_on_fn=lambda coordinator: ( is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
coordinator.data.user_aircon_settings.quiet_mode_enabled set_fn=lambda coordinator,
), enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
set_fn=lambda coordinator, enabled: (
coordinator.data.user_aircon_settings.set_quiet_mode(enabled)
),
), ),
ActronAirSwitchEntityDescription( ActronAirSwitchEntityDescription(
key="turbo_mode", key="turbo_mode",
translation_key="turbo_mode", translation_key="turbo_mode",
is_on_fn=lambda coordinator: ( is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
coordinator.data.user_aircon_settings.turbo_enabled set_fn=lambda coordinator,
), enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
set_fn=lambda coordinator, enabled: ( is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
coordinator.data.user_aircon_settings.set_turbo_mode(enabled)
),
is_supported_fn=lambda coordinator: (
coordinator.data.user_aircon_settings.turbo_supported
),
), ),
) )
@@ -105,12 +93,10 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
"""Return true if the switch is on.""" """Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator) return self.entity_description.is_on_fn(self.coordinator)
@actron_air_command
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
await self.entity_description.set_fn(self.coordinator, True) await self.entity_description.set_fn(self.coordinator, True)
@actron_air_command
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off.""" """Turn the switch off."""
await self.entity_description.set_fn(self.coordinator, False) await self.entity_description.set_fn(self.coordinator, False)

40
homeassistant/components/adax/climate.py Executable file → Normal file
View File

@@ -168,57 +168,29 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
temperature = self._attr_target_temperature or self._attr_min_temp temperature = self._attr_target_temperature or self._attr_min_temp
await self._adax_data_handler.set_target_temperature(temperature) await self._adax_data_handler.set_target_temperature(temperature)
self._attr_target_temperature = temperature
self._attr_icon = "mdi:radiator"
elif hvac_mode == HVACMode.OFF: elif hvac_mode == HVACMode.OFF:
await self._adax_data_handler.set_target_temperature(0) await self._adax_data_handler.set_target_temperature(0)
self._attr_icon = "mdi:radiator-off"
else:
# Ignore unsupported HVAC modes to avoid desynchronizing entity state
# from the physical device.
return
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return return
if self._attr_hvac_mode == HVACMode.HEAT: await self._adax_data_handler.set_target_temperature(temperature)
await self._adax_data_handler.set_target_temperature(temperature)
self._attr_target_temperature = temperature @callback
self.async_write_ha_state() def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
def _update_hvac_attributes(self) -> None:
"""Update hvac mode and temperatures from coordinator data.
The coordinator reports a target temperature of 0 when the heater is
turned off. In that case, only the hvac mode and icon are updated and
the previous non-zero target temperature is preserved. When the
reported target temperature is non-zero, the stored target temperature
is updated to match the coordinator value.
"""
if data := self.coordinator.data: if data := self.coordinator.data:
self._attr_current_temperature = data["current_temperature"] self._attr_current_temperature = data["current_temperature"]
self._attr_available = self._attr_current_temperature is not None
if (target_temp := data["target_temperature"]) == 0: if (target_temp := data["target_temperature"]) == 0:
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off" self._attr_icon = "mdi:radiator-off"
if self._attr_target_temperature is None: if target_temp == 0:
self._attr_target_temperature = self._attr_min_temp self._attr_target_temperature = self._attr_min_temp
else: else:
self._attr_hvac_mode = HVACMode.HEAT self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator" self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp self._attr_target_temperature = target_temp
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_hvac_attributes()
super()._handle_coordinator_update() super()._handle_coordinator_update()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._update_hvac_attributes()

View File

@@ -87,7 +87,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=data_schema, data_schema=data_schema,
) )
wifi_ssid = user_input[WIFI_SSID] wifi_ssid = user_input[WIFI_SSID].replace(" ", "")
wifi_pswd = user_input[WIFI_PSWD].replace(" ", "") wifi_pswd = user_input[WIFI_PSWD].replace(" ", "")
configurator = adax_local.AdaxConfig(wifi_ssid, wifi_pswd) configurator = adax_local.AdaxConfig(wifi_ssid, wifi_pswd)

View File

@@ -20,10 +20,9 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_FORCE, CONF_FORCE,
@@ -46,7 +45,6 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
{vol.Optional(CONF_FORCE, default=False): cv.boolean} {vol.Optional(CONF_FORCE, default=False): cv.boolean}
) )
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE] PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
type AdGuardConfigEntry = ConfigEntry[AdGuardData] type AdGuardConfigEntry = ConfigEntry[AdGuardData]
@@ -59,69 +57,6 @@ class AdGuardData:
version: str version: str
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
def _get_adguard_instances(hass: HomeAssistant) -> list[AdGuardHome]:
"""Get the AdGuardHome instances."""
entries: list[AdGuardConfigEntry] = hass.config_entries.async_loaded_entries(
DOMAIN
)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
return [entry.runtime_data.client for entry in entries]
async def add_url(call: ServiceCall) -> None:
"""Service call to add a new filter subscription to AdGuard Home."""
for adguard in _get_adguard_instances(call.hass):
await adguard.filtering.add_url(
allowlist=False, name=call.data[CONF_NAME], url=call.data[CONF_URL]
)
async def remove_url(call: ServiceCall) -> None:
"""Service call to remove a filter subscription from AdGuard Home."""
for adguard in _get_adguard_instances(call.hass):
await adguard.filtering.remove_url(allowlist=False, url=call.data[CONF_URL])
async def enable_url(call: ServiceCall) -> None:
"""Service call to enable a filter subscription in AdGuard Home."""
for adguard in _get_adguard_instances(call.hass):
await adguard.filtering.enable_url(allowlist=False, url=call.data[CONF_URL])
async def disable_url(call: ServiceCall) -> None:
"""Service call to disable a filter subscription in AdGuard Home."""
for adguard in _get_adguard_instances(call.hass):
await adguard.filtering.disable_url(
allowlist=False, url=call.data[CONF_URL]
)
async def refresh(call: ServiceCall) -> None:
"""Service call to refresh the filter subscriptions in AdGuard Home."""
for adguard in _get_adguard_instances(call.hass):
await adguard.filtering.refresh(
allowlist=False, force=call.data[CONF_FORCE]
)
hass.services.async_register(
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
"""Set up AdGuard Home from a config entry.""" """Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
@@ -144,9 +79,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def add_url(call: ServiceCall) -> None:
"""Service call to add a new filter subscription to AdGuard Home."""
await adguard.filtering.add_url(
allowlist=False, name=call.data[CONF_NAME], url=call.data[CONF_URL]
)
async def remove_url(call: ServiceCall) -> None:
"""Service call to remove a filter subscription from AdGuard Home."""
await adguard.filtering.remove_url(allowlist=False, url=call.data[CONF_URL])
async def enable_url(call: ServiceCall) -> None:
"""Service call to enable a filter subscription in AdGuard Home."""
await adguard.filtering.enable_url(allowlist=False, url=call.data[CONF_URL])
async def disable_url(call: ServiceCall) -> None:
"""Service call to disable a filter subscription in AdGuard Home."""
await adguard.filtering.disable_url(allowlist=False, url=call.data[CONF_URL])
async def refresh(call: ServiceCall) -> None:
"""Service call to refresh the filter subscriptions in AdGuard Home."""
await adguard.filtering.refresh(allowlist=False, force=call.data[CONF_FORCE])
hass.services.async_register(
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
"""Unload AdGuard Home config entry.""" """Unload AdGuard Home config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if not hass.config_entries.async_loaded_entries(DOMAIN):
# This is the last loaded instance of AdGuard, deregister any services
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
return unload_ok

View File

@@ -107,7 +107,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_hassio( async def async_step_hassio(
self, discovery_info: HassioServiceInfo self, discovery_info: HassioServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Prepare configuration for a Hass.io AdGuard Home app. """Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
""" """

View File

@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance.""" """Return device information about this AdGuard Home instance."""
if self._entry.source == SOURCE_HASSIO: if self._entry.source == SOURCE_HASSIO:
config_url = "homeassistant://app/a0d7b954_adguard" config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
elif self.adguard.tls: elif self.adguard.tls:
config_url = f"https://{self.adguard.host}:{self.adguard.port}" config_url = f"https://{self.adguard.host}:{self.adguard.port}"
else: else:

View File

@@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the app: {addon}?", "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?",
"title": "AdGuard Home via Home Assistant app" "title": "AdGuard Home via Home Assistant add-on"
}, },
"user": { "user": {
"data": { "data": {
@@ -76,11 +76,6 @@
} }
} }
}, },
"exceptions": {
"config_entry_not_loaded": {
"message": "Config entry not loaded."
}
},
"services": { "services": {
"add_url": { "add_url": {
"description": "Adds a new filter subscription to AdGuard Home.", "description": "Adds a new filter subscription to AdGuard Home.",

View File

@@ -9,13 +9,9 @@ import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode, ColorMode,
LightEntity, LightEntity,
filter_supported_color_modes,
) )
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -28,20 +24,13 @@ from .entity import AdsEntity
from .hub import AdsHub from .hub import AdsHub
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness" CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
CONF_ADS_VAR_COLOR_TEMP_KELVIN = "adsvar_color_temp_kelvin"
CONF_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin"
CONF_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin"
STATE_KEY_BRIGHTNESS = "brightness" STATE_KEY_BRIGHTNESS = "brightness"
STATE_KEY_COLOR_TEMP_KELVIN = "color_temp_kelvin"
DEFAULT_NAME = "ADS Light" DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_ADS_VAR): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
vol.Optional(CONF_ADS_VAR_COLOR_TEMP_KELVIN): cv.string,
vol.Optional(CONF_MIN_COLOR_TEMP_KELVIN): cv.positive_int,
vol.Optional(CONF_MAX_COLOR_TEMP_KELVIN): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
} }
) )
@@ -58,24 +47,9 @@ def setup_platform(
ads_var_enable: str = config[CONF_ADS_VAR] ads_var_enable: str = config[CONF_ADS_VAR]
ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS) ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS)
ads_var_color_temp_kelvin: str | None = config.get(CONF_ADS_VAR_COLOR_TEMP_KELVIN)
min_color_temp_kelvin: int | None = config.get(CONF_MIN_COLOR_TEMP_KELVIN)
max_color_temp_kelvin: int | None = config.get(CONF_MAX_COLOR_TEMP_KELVIN)
name: str = config[CONF_NAME] name: str = config[CONF_NAME]
add_entities( add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
[
AdsLight(
ads_hub,
ads_var_enable,
ads_var_brightness,
ads_var_color_temp_kelvin,
min_color_temp_kelvin,
max_color_temp_kelvin,
name,
)
]
)
class AdsLight(AdsEntity, LightEntity): class AdsLight(AdsEntity, LightEntity):
@@ -86,40 +60,18 @@ class AdsLight(AdsEntity, LightEntity):
ads_hub: AdsHub, ads_hub: AdsHub,
ads_var_enable: str, ads_var_enable: str,
ads_var_brightness: str | None, ads_var_brightness: str | None,
ads_var_color_temp_kelvin: str | None,
min_color_temp_kelvin: int | None,
max_color_temp_kelvin: int | None,
name: str, name: str,
) -> None: ) -> None:
"""Initialize AdsLight entity.""" """Initialize AdsLight entity."""
super().__init__(ads_hub, name, ads_var_enable) super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None self._state_dict[STATE_KEY_BRIGHTNESS] = None
self._state_dict[STATE_KEY_COLOR_TEMP_KELVIN] = None
self._ads_var_brightness = ads_var_brightness self._ads_var_brightness = ads_var_brightness
self._ads_var_color_temp_kelvin = ads_var_color_temp_kelvin
# Determine supported color modes
color_modes = {ColorMode.ONOFF}
if ads_var_brightness is not None: if ads_var_brightness is not None:
color_modes.add(ColorMode.BRIGHTNESS) self._attr_color_mode = ColorMode.BRIGHTNESS
if ads_var_color_temp_kelvin is not None: self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
color_modes.add(ColorMode.COLOR_TEMP) else:
self._attr_color_mode = ColorMode.ONOFF
self._attr_supported_color_modes = filter_supported_color_modes(color_modes) self._attr_supported_color_modes = {ColorMode.ONOFF}
self._attr_color_mode = next(iter(self._attr_supported_color_modes))
# Set color temperature range (static config values take precedence over defaults)
if ads_var_color_temp_kelvin is not None:
self._attr_min_color_temp_kelvin = (
min_color_temp_kelvin
if min_color_temp_kelvin is not None
else DEFAULT_MIN_KELVIN
)
self._attr_max_color_temp_kelvin = (
max_color_temp_kelvin
if max_color_temp_kelvin is not None
else DEFAULT_MAX_KELVIN
)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register device notification.""" """Register device notification."""
@@ -132,23 +84,11 @@ class AdsLight(AdsEntity, LightEntity):
STATE_KEY_BRIGHTNESS, STATE_KEY_BRIGHTNESS,
) )
if self._ads_var_color_temp_kelvin is not None:
await self.async_initialize_device(
self._ads_var_color_temp_kelvin,
pyads.PLCTYPE_UINT,
STATE_KEY_COLOR_TEMP_KELVIN,
)
@property @property
def brightness(self) -> int | None: def brightness(self) -> int | None:
"""Return the brightness of the light (0..255).""" """Return the brightness of the light (0..255)."""
return self._state_dict[STATE_KEY_BRIGHTNESS] return self._state_dict[STATE_KEY_BRIGHTNESS]
@property
def color_temp_kelvin(self) -> int | None:
"""Return the color temperature in Kelvin."""
return self._state_dict[STATE_KEY_COLOR_TEMP_KELVIN]
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if the entity is on.""" """Return True if the entity is on."""
@@ -157,8 +97,6 @@ class AdsLight(AdsEntity, LightEntity):
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the light on or set a specific dimmer value.""" """Turn the light on or set a specific dimmer value."""
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL) self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL)
if self._ads_var_brightness is not None and brightness is not None: if self._ads_var_brightness is not None and brightness is not None:
@@ -166,11 +104,6 @@ class AdsLight(AdsEntity, LightEntity):
self._ads_var_brightness, brightness, pyads.PLCTYPE_UINT self._ads_var_brightness, brightness, pyads.PLCTYPE_UINT
) )
if self._ads_var_color_temp_kelvin is not None and color_temp is not None:
self._ads_hub.write_by_name(
self._ads_var_color_temp_kelvin, color_temp, pyads.PLCTYPE_UINT
)
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the light off.""" """Turn the light off."""
self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL) self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL)

View File

@@ -1,17 +1,23 @@
"""Advantage Air climate integration.""" """Advantage Air climate integration."""
from advantage_air import advantage_air from datetime import timedelta
import logging
from advantage_air import ApiError, advantage_air
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN from .const import ADVANTAGE_AIR_RETRY
from .coordinator import AdvantageAirCoordinator, AdvantageAirDataConfigEntry from .models import AdvantageAirData
from .services import async_setup_services
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
ADVANTAGE_AIR_SYNC_INTERVAL = 15
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.CLIMATE, Platform.CLIMATE,
@@ -23,13 +29,8 @@ PLATFORMS = [
Platform.UPDATE, Platform.UPDATE,
] ]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) _LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry( async def async_setup_entry(
@@ -45,10 +46,27 @@ async def async_setup_entry(
retry=ADVANTAGE_AIR_RETRY, retry=ADVANTAGE_AIR_RETRY,
) )
coordinator = AdvantageAirCoordinator(hass, entry, api) async def async_get():
try:
return await api.async_get()
except ApiError as err:
raise UpdateFailed(err) from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name="Advantage Air",
update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator entry.runtime_data = AdvantageAirData(coordinator, api)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry from . import AdvantageAirDataConfigEntry
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -24,23 +24,19 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AdvantageAir Binary Sensor platform.""" """Set up AdvantageAir Binary Sensor platform."""
coordinator = config_entry.runtime_data instance = config_entry.runtime_data
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
if aircons := coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items(): for ac_key, ac_device in aircons.items():
entities.append(AdvantageAirFilter(coordinator, ac_key)) entities.append(AdvantageAirFilter(instance, ac_key))
for zone_key, zone in ac_device["zones"].items(): for zone_key, zone in ac_device["zones"].items():
# Only add motion sensor when motion is enabled # Only add motion sensor when motion is enabled
if zone["motionConfig"] >= 2: if zone["motionConfig"] >= 2:
entities.append( entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key))
AdvantageAirZoneMotion(coordinator, ac_key, zone_key)
)
# Only add MyZone if it is available # Only add MyZone if it is available
if zone["type"] != 0: if zone["type"] != 0:
entities.append( entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key))
AdvantageAirZoneMyZone(coordinator, ac_key, zone_key)
)
async_add_entities(entities) async_add_entities(entities)
@@ -51,9 +47,9 @@ class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_name = "Filter" _attr_name = "Filter"
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None: def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an Advantage Air Filter sensor.""" """Initialize an Advantage Air Filter sensor."""
super().__init__(coordinator, ac_key) super().__init__(instance, ac_key)
self._attr_unique_id += "-filter" self._attr_unique_id += "-filter"
@property @property
@@ -67,11 +63,9 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.MOTION _attr_device_class = BinarySensorDeviceClass.MOTION
def __init__( def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Motion sensor.""" """Initialize an Advantage Air Zone Motion sensor."""
super().__init__(coordinator, ac_key, zone_key) super().__init__(instance, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} motion" self._attr_name = f"{self._zone['name']} motion"
self._attr_unique_id += "-motion" self._attr_unique_id += "-motion"
@@ -87,11 +81,9 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity):
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__( def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone MyZone sensor.""" """Initialize an Advantage Air Zone MyZone sensor."""
super().__init__(coordinator, ac_key, zone_key) super().__init__(instance, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} myZone" self._attr_name = f"{self._zone['name']} myZone"
self._attr_unique_id += "-myzone" self._attr_unique_id += "-myzone"

View File

@@ -31,8 +31,8 @@ from .const import (
ADVANTAGE_AIR_STATE_ON, ADVANTAGE_AIR_STATE_ON,
ADVANTAGE_AIR_STATE_OPEN, ADVANTAGE_AIR_STATE_OPEN,
) )
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
ADVANTAGE_AIR_HVAC_MODES = { ADVANTAGE_AIR_HVAC_MODES = {
"heat": HVACMode.HEAT, "heat": HVACMode.HEAT,
@@ -90,16 +90,16 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AdvantageAir climate platform.""" """Set up AdvantageAir climate platform."""
coordinator = config_entry.runtime_data instance = config_entry.runtime_data
entities: list[ClimateEntity] = [] entities: list[ClimateEntity] = []
if aircons := coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items(): for ac_key, ac_device in aircons.items():
entities.append(AdvantageAirAC(coordinator, ac_key)) entities.append(AdvantageAirAC(instance, ac_key))
for zone_key, zone in ac_device["zones"].items(): for zone_key, zone in ac_device["zones"].items():
# Only add zone climate control when zone is in temperature control # Only add zone climate control when zone is in temperature control
if zone["type"] > 0: if zone["type"] > 0:
entities.append(AdvantageAirZone(coordinator, ac_key, zone_key)) entities.append(AdvantageAirZone(instance, ac_key, zone_key))
async_add_entities(entities) async_add_entities(entities)
@@ -114,9 +114,9 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
_attr_name = None _attr_name = None
_support_preset = ClimateEntityFeature(0) _support_preset = ClimateEntityFeature(0)
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None: def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit.""" """Initialize an AdvantageAir AC unit."""
super().__init__(coordinator, ac_key) super().__init__(instance, ac_key)
self._attr_preset_modes = [ADVANTAGE_AIR_MYZONE] self._attr_preset_modes = [ADVANTAGE_AIR_MYZONE]
@@ -282,11 +282,9 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
_attr_max_temp = 32 _attr_max_temp = 32
_attr_min_temp = 16 _attr_min_temp = 16
def __init__( def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an AdvantageAir Zone control.""" """Initialize an AdvantageAir Zone control."""
super().__init__(coordinator, ac_key, zone_key) super().__init__(instance, ac_key, zone_key)
self._attr_name = self._zone["name"] self._attr_name = self._zone["name"]
@property @property

View File

@@ -1,59 +0,0 @@
"""Coordinator for the Advantage Air integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from advantage_air import ApiError, advantage_air
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
ADVANTAGE_AIR_SYNC_INTERVAL = 15
REQUEST_REFRESH_DELAY = 0.5
_LOGGER = logging.getLogger(__name__)
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirCoordinator]
class AdvantageAirCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Advantage Air coordinator."""
config_entry: AdvantageAirDataConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry,
api: advantage_air,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="Advantage Air",
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
self.api = api
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from the API."""
try:
return await self.api.async_get()
except ApiError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err

View File

@@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -26,24 +26,24 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AdvantageAir cover platform.""" """Set up AdvantageAir cover platform."""
coordinator = config_entry.runtime_data instance = config_entry.runtime_data
entities: list[CoverEntity] = [] entities: list[CoverEntity] = []
if aircons := coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items(): for ac_key, ac_device in aircons.items():
for zone_key, zone in ac_device["zones"].items(): for zone_key, zone in ac_device["zones"].items():
# Only add zone vent controls when zone in vent control mode. # Only add zone vent controls when zone in vent control mode.
if zone["type"] == 0: if zone["type"] == 0:
entities.append(AdvantageAirZoneVent(coordinator, ac_key, zone_key)) entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
if things := coordinator.data.get("myThings"): if things := instance.coordinator.data.get("myThings"):
for thing in things["things"].values(): for thing in things["things"].values():
if thing["channelDipState"] in [1, 2]: # 1 = "Blind", 2 = "Blind 2" if thing["channelDipState"] in [1, 2]: # 1 = "Blind", 2 = "Blind 2"
entities.append( entities.append(
AdvantageAirThingCover(coordinator, thing, CoverDeviceClass.BLIND) AdvantageAirThingCover(instance, thing, CoverDeviceClass.BLIND)
) )
elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door" elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door"
entities.append( entities.append(
AdvantageAirThingCover(coordinator, thing, CoverDeviceClass.GARAGE) AdvantageAirThingCover(instance, thing, CoverDeviceClass.GARAGE)
) )
async_add_entities(entities) async_add_entities(entities)
@@ -58,11 +58,9 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity):
| CoverEntityFeature.SET_POSITION | CoverEntityFeature.SET_POSITION
) )
def __init__( def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Vent.""" """Initialize an Advantage Air Zone Vent."""
super().__init__(coordinator, ac_key, zone_key) super().__init__(instance, ac_key, zone_key)
self._attr_name = self._zone["name"] self._attr_name = self._zone["name"]
@property @property
@@ -108,12 +106,12 @@ class AdvantageAirThingCover(AdvantageAirThingEntity, CoverEntity):
def __init__( def __init__(
self, self,
coordinator: AdvantageAirCoordinator, instance: AdvantageAirData,
thing: dict[str, Any], thing: dict[str, Any],
device_class: CoverDeviceClass, device_class: CoverDeviceClass,
) -> None: ) -> None:
"""Initialize an Advantage Air Things Cover.""" """Initialize an Advantage Air Things Cover."""
super().__init__(coordinator, thing) super().__init__(instance, thing)
self._attr_device_class = device_class self._attr_device_class = device_class
@property @property

Some files were not shown because too many files have changed in this diff Show More