mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 19:25:18 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec5d1c0b7 | |||
| eaa980f466 |
@@ -18,13 +18,6 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
|
||||
4. Ensure any existing review comments have been addressed.
|
||||
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
|
||||
|
||||
## Verification:
|
||||
|
||||
- After the review, run parallel subagents for each finding to double check it.
|
||||
- Spawn up to a maximum of 10 parallel subagents at a time.
|
||||
- Gather the results from the subagents and summarize them in the final review comments.
|
||||
|
||||
|
||||
## IMPORTANT:
|
||||
- Just review. DO NOT make any changes
|
||||
- Be constructive and specific in your comments
|
||||
|
||||
+1
-1
@@ -14,6 +14,7 @@ Dockerfile.dev linguist-language=Dockerfile
|
||||
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
Dockerfile linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
@@ -22,4 +23,3 @@ requirements_all.txt linguist-generated=true
|
||||
requirements_test_all.txt linguist-generated=true
|
||||
requirements_test_pre_commit.txt linguist-generated=true
|
||||
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||
.github/workflows/*.lock.yml linguist-generated=true
|
||||
|
||||
@@ -11,6 +11,3 @@ updates:
|
||||
- github_actions
|
||||
cooldown:
|
||||
default-days: 7
|
||||
ignore:
|
||||
# Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump.
|
||||
- dependency-name: "github/gh-aw-actions/**"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"pep621",
|
||||
"pip_requirements",
|
||||
"pre-commit",
|
||||
"dockerfile",
|
||||
"custom.regex",
|
||||
"homeassistant-manifest"
|
||||
],
|
||||
@@ -22,10 +21,6 @@
|
||||
]
|
||||
},
|
||||
|
||||
"dockerfile": {
|
||||
"managerFilePatterns": ["/^Dockerfile$/"]
|
||||
},
|
||||
|
||||
"homeassistant-manifest": {
|
||||
"managerFilePatterns": [
|
||||
"/^homeassistant/components/[^/]+/manifest\\.json$/"
|
||||
@@ -40,14 +35,6 @@
|
||||
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
|
||||
"depNameTemplate": "ruff",
|
||||
"datasourceTemplate": "pypi"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update go2rtc RECOMMENDED_VERSION in const.py alongside the Dockerfile pin",
|
||||
"managerFilePatterns": ["/^homeassistant/components/go2rtc/const\\.py$/"],
|
||||
"matchStrings": ["RECOMMENDED_VERSION = \"(?<currentValue>[\\d.]+)\""],
|
||||
"depNameTemplate": "ghcr.io/alexxit/go2rtc",
|
||||
"datasourceTemplate": "docker"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -128,7 +115,6 @@
|
||||
"standard-aifc",
|
||||
"standard-telnetlib",
|
||||
"ulid-transform",
|
||||
"unidiff",
|
||||
"url-normalize",
|
||||
"xmltodict"
|
||||
],
|
||||
@@ -198,13 +184,6 @@
|
||||
"enabled": true,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Docker allowlist (ghcr.io exposes no release timestamps so the global cooldown needs to be bypassed)",
|
||||
"matchPackageNames": ["ghcr.io/alexxit/go2rtc"],
|
||||
"enabled": true,
|
||||
"minimumReleaseAge": null,
|
||||
"labels": ["dependency"]
|
||||
},
|
||||
{
|
||||
"description": "Group ruff pre-commit hook with its PyPI twin into one PR",
|
||||
"matchPackageNames": ["astral-sh/ruff-pre-commit", "ruff"],
|
||||
@@ -234,12 +213,6 @@
|
||||
"matchPackageNames": ["pylint", "astroid"],
|
||||
"groupName": "pylint",
|
||||
"groupSlug": "pylint"
|
||||
},
|
||||
{
|
||||
"description": "Group go2rtc Dockerfile pin with const.py RECOMMENDED_VERSION into one PR",
|
||||
"matchPackageNames": ["ghcr.io/alexxit/go2rtc"],
|
||||
"groupName": "go2rtc",
|
||||
"groupSlug": "go2rtc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.05.0"
|
||||
BASE_IMAGE_VERSION: "2026.04.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Check requirements (deterministic)
|
||||
|
||||
# Stage 1 of the Check requirements pipeline.
|
||||
#
|
||||
# Runs the deterministic Python checks and uploads the structured
|
||||
# results as an artifact. Stage 2 (the agentic workflow defined in
|
||||
# `check-requirements.md`) consumes the artifact on completion.
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
# Auto-trigger on PRs that touch tracked requirement files is disabled
|
||||
# for now while we iterate — testing the workflow_run handoff to the
|
||||
# agentic stage is hard with an auto-trigger. Re-enable once the chain
|
||||
# has been validated end-to-end.
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
# paths:
|
||||
# - "**/requirements*.txt"
|
||||
# - "homeassistant/package_constraints.txt"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pull_request_number:
|
||||
description: "Pull request number to (re-)check"
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deterministic:
|
||||
name: Run deterministic requirement checks
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # To fetch the PR diff via gh CLI
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Install script dependencies
|
||||
run: pip install -r script/check_requirements/requirements.txt
|
||||
- name: Collect PR diff
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
mkdir -p deterministic
|
||||
gh pr diff "${PR_NUMBER}" > deterministic/pr.diff
|
||||
- name: Run deterministic checks
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
python -m script.check_requirements \
|
||||
--pr-number "${PR_NUMBER}" \
|
||||
--diff deterministic/pr.diff \
|
||||
--output deterministic/results.json
|
||||
- name: Upload deterministic-results artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: deterministic/results.json
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,253 +0,0 @@
|
||||
---
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Check requirements (deterministic)"]
|
||||
types: [completed]
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
network:
|
||||
allowed:
|
||||
- python
|
||||
tools:
|
||||
web-fetch: {}
|
||||
github:
|
||||
toolsets: [default, actions]
|
||||
min-integrity: unapproved
|
||||
safe-outputs:
|
||||
add-comment:
|
||||
max: 1
|
||||
target: "${{ env.PR_NUMBER }}"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download deterministic-results artifact
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/gh-aw/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract PR number from artifact
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
run: |
|
||||
PR=$(python3 -c 'import json,sys;print(json.load(open("/tmp/gh-aw/deterministic/results.json"))["pr_number"])')
|
||||
echo "PR_NUMBER=${PR}" >> "${GITHUB_ENV}"
|
||||
post-steps:
|
||||
- name: Verify agent produced an add_comment safe-output
|
||||
if: always() && github.event.workflow_run.conclusion == 'success'
|
||||
run: |
|
||||
OUTPUT=/tmp/gh-aw/agent_output.json
|
||||
if [ ! -f "${OUTPUT}" ]; then
|
||||
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '"add_comment"' "${OUTPUT}"; then
|
||||
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
|
||||
echo "Agent output:"
|
||||
cat "${OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
description: >
|
||||
Resolves the deterministic-stage artifact's NEEDS_AGENT checks for changed
|
||||
Python package requirements on PRs targeting the core repo, then posts the
|
||||
final review comment. Triggered by completion of the deterministic workflow.
|
||||
Reads the uploaded artifact from disk, replaces placeholders for any check
|
||||
whose status is `needs_agent`, and posts the merged comment using the PR
|
||||
number recorded inside the artifact itself. Each check kind has a dedicated
|
||||
instruction section below; if the artifact contains a check kind that does
|
||||
not have a section here, the agent fails hard rather than guess.
|
||||
---
|
||||
|
||||
# Check requirements (AW)
|
||||
|
||||
You are a code review assistant for the Home Assistant project. The
|
||||
deterministic stage has already evaluated every check it can on its own
|
||||
and produced an artifact containing the PR number, per-package check
|
||||
results, and a pre-rendered comment with placeholders. **Your only job is
|
||||
to read that artifact, resolve any `needs_agent` checks, and post the
|
||||
final comment.**
|
||||
|
||||
## Step 1 — Read the deterministic-stage artifact
|
||||
|
||||
The deterministic stage uploaded its results to the runner at
|
||||
`/tmp/gh-aw/deterministic/results.json`.
|
||||
|
||||
The JSON has this shape:
|
||||
|
||||
- `pr_number` — the PR being checked. The `add_comment` safe-output is
|
||||
already targeted at this PR (the workflow extracted `pr_number` from
|
||||
the artifact and wired it into the safe-output config), so **you do
|
||||
not need to set `item_number` yourself** — just emit `add_comment`
|
||||
with the rendered body.
|
||||
- `needs_agent` — `true` iff any package's check needs resolution.
|
||||
- `packages[]` — one entry per changed package. Each entry has:
|
||||
- `name`, `old_version` (`null` for a newly added package; otherwise the
|
||||
previous pin), `new_version`, `repo_url`, `publisher_kind`.
|
||||
- `checks` — a dict keyed by **check kind** (string). Each value has a
|
||||
`status` (`pass`, `warn`, `fail`, or `needs_agent`) and `details`.
|
||||
- `rendered_comment` — the final PR comment body, already rendered. For
|
||||
every check whose status is `needs_agent` it contains two placeholders
|
||||
you must replace:
|
||||
- `{{CHECK_CELL:<pkg-name>:<check-kind>}}` — one cell of the summary
|
||||
table. Replace with exactly one of `✅`, `⚠️`, `❌`.
|
||||
- `{{CHECK_DETAIL:<pkg-name>:<check-kind>}}` — the body of one bullet
|
||||
in the package's `<details>` block. Replace with
|
||||
`<icon> <one-line explanation>` (the bullet's leading
|
||||
`- **<label>**:` is already rendered — replace only the placeholder).
|
||||
|
||||
You **must not** modify any other content in `rendered_comment`. Do not
|
||||
re-evaluate checks that already have a deterministic status. Do not add
|
||||
or remove packages.
|
||||
|
||||
## Step 2 — Resolve each `needs_agent` check
|
||||
|
||||
For each `package` in `packages`:
|
||||
|
||||
For each `(check_kind, result)` in `package.checks` where
|
||||
`result.status == "needs_agent"`:
|
||||
|
||||
1. Look up `## Check kind: <check_kind>` in the **Check instructions**
|
||||
section below.
|
||||
2. **If no matching section exists**: emit a single `add_comment` whose
|
||||
body is:
|
||||
|
||||
```
|
||||
<!-- requirements-check -->
|
||||
## Check requirements
|
||||
|
||||
❌ Internal error: the deterministic artifact contains a check kind
|
||||
(`<check_kind>` on package `<pkg-name>`) that this workflow has no
|
||||
instructions for. Update `.github/workflows/check-requirements.md`
|
||||
to add a matching `## Check kind: <check_kind>` section, or remove
|
||||
the kind from the deterministic stage.
|
||||
```
|
||||
|
||||
Then stop. **Do not improvise** a verdict for an unknown check kind.
|
||||
3. Otherwise, follow the instructions in that section. They tell you
|
||||
which icon (✅/⚠️/❌) and one-line explanation to produce.
|
||||
|
||||
## Step 3 — Post the comment
|
||||
|
||||
1. Replace every `{{CHECK_CELL:…}}` and `{{CHECK_DETAIL:…}}` placeholder
|
||||
in `rendered_comment` with the resolved value.
|
||||
2. Emit the resulting markdown using `add_comment` — set `body` to the
|
||||
merged `rendered_comment` verbatim (the leading
|
||||
`<!-- requirements-check -->` marker must be preserved). The PR
|
||||
target is already set by the workflow; do not pass `item_number`.
|
||||
|
||||
If the artifact's top-level `needs_agent` is `false` (no checks need
|
||||
you), emit `rendered_comment` unchanged.
|
||||
|
||||
## Check instructions
|
||||
|
||||
### Check kind: `repo_public`
|
||||
|
||||
Verify that the package's source repository is publicly reachable.
|
||||
|
||||
1. Read `package.repo_url`.
|
||||
2. Use the `web-fetch` tool to GET that URL.
|
||||
3. Decide the verdict:
|
||||
- HTTP 200, returns a public repository page → ✅
|
||||
`<repo_url> is publicly accessible.`
|
||||
- HTTP 4xx/5xx, or the response redirects to a login / sign-in page →
|
||||
❌ `Source repository at <repo_url> is not publicly accessible.
|
||||
Home Assistant requires all dependencies to have publicly available
|
||||
source code.`
|
||||
- Any other inconclusive result → ⚠️ with a one-line description.
|
||||
|
||||
If `repo_public` resolves to ❌ for a package, **also** mark that
|
||||
package's `release_pipeline` cell/detail as `—` (em dash) and explain
|
||||
`Skipped because the source repository is not publicly accessible.` —
|
||||
because the release pipeline cannot be inspected without a public repo.
|
||||
|
||||
### Check kind: `pr_link`
|
||||
|
||||
Verify the PR description contains the right link for the change.
|
||||
|
||||
1. Fetch the PR body via the GitHub MCP tool, using the `pr_number`
|
||||
field from the artifact.
|
||||
2. Extract all URLs from the body.
|
||||
3. For a **new package** (`package.old_version` is `null`):
|
||||
- The PR body must contain a URL that points at `package.repo_url`
|
||||
(any sub-path of the same `owner/repo` on the same host is
|
||||
acceptable). A PyPI link is **not** sufficient.
|
||||
- ✅ if such a URL is present.
|
||||
- ❌ otherwise:
|
||||
`PR description must link to the source repository at <repo_url>.
|
||||
A PyPI page link is not sufficient.`
|
||||
4. For a **version bump** (`package.old_version` is not `null`):
|
||||
- The PR body must contain a URL on the same host as
|
||||
`package.repo_url` that references **both** `package.old_version`
|
||||
and `package.new_version` (e.g. a GitHub compare URL
|
||||
`compare/vX...vY`, a release / changelog URL containing both
|
||||
versions, etc.).
|
||||
- ✅ if such a URL is present and the versions match the actual bump.
|
||||
- ❌ otherwise:
|
||||
`PR description should link to a changelog or compare URL on
|
||||
<repo_url> that mentions both <old_version> and <new_version>.`
|
||||
|
||||
### Check kind: `release_pipeline`
|
||||
|
||||
Inspect the upstream project's release / publish CI pipeline.
|
||||
|
||||
For each package needing inspection, determine the source repository
|
||||
host from `package.repo_url`, then apply the corresponding checklist.
|
||||
|
||||
#### GitHub repositories (`github.com`)
|
||||
|
||||
1. List workflows: `GET /repos/{owner}/{repo}/actions/workflows`.
|
||||
2. Identify any workflow whose name or filename suggests publishing to
|
||||
PyPI (`release`, `publish`, `pypi`, or `deploy`).
|
||||
3. Fetch the workflow file and check:
|
||||
- **Trigger sanity**: triggered by `push` to tags,
|
||||
`release: published`, or `workflow_run` on a release job —
|
||||
**not** solely `workflow_dispatch` with no environment-protection
|
||||
guard.
|
||||
- **OIDC / Trusted Publisher**: look for `id-token: write` and one of
|
||||
`pypa/gh-action-pypi-publish`, `actions/attest-build-provenance`,
|
||||
or `TWINE_PASSWORD` from a static `secrets.PYPI_TOKEN`.
|
||||
- **No manual upload bypass**: no ungated `twine upload` or
|
||||
`pip upload`.
|
||||
4. Verdict:
|
||||
- ✅ if OIDC + sane triggers + no bypass.
|
||||
- ⚠️ if static token but version bump, or details unclear.
|
||||
- ❌ if static token on a new package, or only-manual triggers with
|
||||
no environment protection.
|
||||
|
||||
#### GitLab repositories (`gitlab.com` or self-hosted GitLab)
|
||||
|
||||
1. Resolve the project ID via
|
||||
`GET https://gitlab.com/api/v4/projects/{url-encoded-namespace-and-name}`.
|
||||
2. Fetch `.gitlab-ci.yml` via
|
||||
`GET https://gitlab.com/api/v4/projects/{id}/repository/files/.gitlab-ci.yml/raw?ref=HEAD`.
|
||||
3. Apply the same conceptual checks: tag-only / protected-branch
|
||||
triggers, GitLab OIDC `id_tokens` or CI/CD protected `PYPI_TOKEN`, no
|
||||
ungated `twine upload`. Same verdict rules as GitHub.
|
||||
|
||||
#### Other code hosting providers (Bitbucket, Codeberg, Gitea, Sourcehut, …)
|
||||
|
||||
1. Use `web-fetch` to retrieve any visible CI configuration
|
||||
(`.circleci/config.yml`, `Jenkinsfile`, `azure-pipelines.yml`,
|
||||
`bitbucket-pipelines.yml`, `.builds/*.yml`).
|
||||
2. Apply the conceptual checks: automated triggers, CI-injected
|
||||
credentials, no manual `twine upload`.
|
||||
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
|
||||
inspected; hosting provider is not GitHub or GitLab.`
|
||||
|
||||
## Notes
|
||||
|
||||
- Be constructive and helpful. Reference the inspected workflow / CI
|
||||
file by URL where useful so the contributor can fix the issue.
|
||||
- The dedup of the requirements-check comment is handled by gh-aw's
|
||||
`add_comment` safe-output via the `<!-- requirements-check -->`
|
||||
marker on the first line of `rendered_comment`.
|
||||
- If the deterministic workflow concluded with a non-success status,
|
||||
this workflow's `if:` guard on `Download deterministic-results
|
||||
artifact` skipped the download. If you find no file at
|
||||
`/tmp/gh-aw/deterministic/results.json`, emit nothing — the post-step
|
||||
verification is also gated and will not complain.
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.13
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
@@ -23,7 +23,6 @@ repos:
|
||||
- id: zizmor
|
||||
args:
|
||||
- --pedantic
|
||||
exclude: ^\.github/workflows/.*\.lock\.yml$
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
@@ -47,7 +46,6 @@ repos:
|
||||
additional_dependencies:
|
||||
- prettier@3.6.2
|
||||
- prettier-plugin-sort-json@4.2.0
|
||||
exclude: ^\.github/workflows/.*\.lock\.yml$
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.14.5
|
||||
3.14.4
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
ignore: |
|
||||
tests/fixtures/core/config/yaml_errors/
|
||||
.github/workflows/*.lock.yml
|
||||
rules:
|
||||
braces:
|
||||
level: error
|
||||
|
||||
Generated
-2
@@ -68,8 +68,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/components/ai_task/ @home-assistant/core
|
||||
/tests/components/ai_task/ @home-assistant/core
|
||||
/homeassistant/components/aidot/ @s1eedz @HongBryan
|
||||
/tests/components/aidot/ @s1eedz @HongBryan
|
||||
/homeassistant/components/air_quality/ @home-assistant/core
|
||||
/tests/components/air_quality/ @home-assistant/core
|
||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||
|
||||
Generated
+2
-2
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
|
||||
# Partly generated by hassfest.
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
ARG BUILD_FROM
|
||||
@@ -26,7 +26,7 @@ WORKDIR /usr/src
|
||||
COPY rootfs /
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:1.9.14@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
|
||||
@@ -19,7 +19,6 @@ from .hub import AdsHub
|
||||
|
||||
DEFAULT_NAME = "ADS select"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_OPTIONS = "options"
|
||||
|
||||
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""The aidot integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AidotConfigEntry, AidotDeviceManagerCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AidotConfigEntry) -> bool:
|
||||
"""Set up aidot from a config entry."""
|
||||
|
||||
coordinator = AidotDeviceManagerCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(coordinator.async_add_listener(lambda: None))
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AidotConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.async_cleanup()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Config flow for Aidot integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aidot.client import AidotClient
|
||||
from aidot.const import CONF_ID, DEFAULT_COUNTRY_CODE, SUPPORTED_COUNTRY_CODES
|
||||
from aidot.exceptions import AidotUserOrPassIncorrect
|
||||
from aiohttp import ClientError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY_CODE,
|
||||
default=DEFAULT_COUNTRY_CODE,
|
||||
): selector.CountrySelector(
|
||||
selector.CountrySelectorConfig(
|
||||
countries=SUPPORTED_COUNTRY_CODES,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AidotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle aidot config flow."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
client = AidotClient(
|
||||
session=async_get_clientsession(self.hass),
|
||||
country_code=user_input[CONF_COUNTRY_CODE],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
login_info = await client.async_post_login()
|
||||
except AidotUserOrPassIncorrect:
|
||||
errors["base"] = "invalid_auth"
|
||||
except TimeoutError, ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(login_info[CONF_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_USERNAME]} {user_input[CONF_COUNTRY_CODE]}",
|
||||
data=login_info,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Constants for the aidot integration."""
|
||||
|
||||
DOMAIN = "aidot"
|
||||
@@ -1,163 +0,0 @@
|
||||
"""Coordinator for Aidot."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aidot.client import AidotClient
|
||||
from aidot.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_AES_KEY,
|
||||
CONF_DEVICE_LIST,
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from aidot.device_client import DeviceClient, DeviceStatusData
|
||||
from aidot.exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type AidotConfigEntry = ConfigEntry[AidotDeviceManagerCoordinator]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_DEVICE_LIST_INTERVAL = timedelta(hours=6)
|
||||
|
||||
|
||||
class AidotDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceStatusData]):
|
||||
"""Class to manage Aidot data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AidotConfigEntry,
|
||||
device_client: DeviceClient,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=None,
|
||||
)
|
||||
self.device_client = device_client
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self.device_client.on_status_update = self._handle_status_update
|
||||
|
||||
def _handle_status_update(self, status: DeviceStatusData) -> None:
|
||||
"""Handle status callback."""
|
||||
self.async_set_updated_data(status)
|
||||
|
||||
async def _async_update_data(self) -> DeviceStatusData:
|
||||
"""Return current status."""
|
||||
return self.device_client.status
|
||||
|
||||
|
||||
class AidotDeviceManagerCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class to manage fetching Aidot data."""
|
||||
|
||||
config_entry: AidotConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AidotConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_DEVICE_LIST_INTERVAL,
|
||||
)
|
||||
self.client = AidotClient(
|
||||
session=async_get_clientsession(hass),
|
||||
token=config_entry.data,
|
||||
)
|
||||
self.client.set_token_fresh_cb(self.token_fresh_cb)
|
||||
self.device_coordinators: dict[str, AidotDeviceUpdateCoordinator] = {}
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
try:
|
||||
await self.async_auto_login()
|
||||
except AidotUserOrPassIncorrect as error:
|
||||
raise ConfigEntryError from error
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data async."""
|
||||
try:
|
||||
data = await self.client.async_get_all_device()
|
||||
except AidotAuthFailed as error:
|
||||
raise ConfigEntryError from error
|
||||
current_devices = {
|
||||
device[CONF_ID]: device
|
||||
for device in data[CONF_DEVICE_LIST]
|
||||
if (
|
||||
device[CONF_TYPE] == "light"
|
||||
and CONF_AES_KEY in device
|
||||
and device[CONF_AES_KEY][0] is not None
|
||||
)
|
||||
}
|
||||
|
||||
removed_ids = set(self.device_coordinators) - set(current_devices)
|
||||
for dev_id in removed_ids:
|
||||
coordinator = self.device_coordinators.pop(dev_id)
|
||||
coordinator.device_client.on_status_update = None
|
||||
if removed_ids:
|
||||
self._purge_deleted_lists()
|
||||
|
||||
for dev_id, device in current_devices.items():
|
||||
if dev_id not in self.device_coordinators:
|
||||
device_client = self.client.get_device_client(device)
|
||||
device_coordinator = AidotDeviceUpdateCoordinator(
|
||||
self.hass, self.config_entry, device_client
|
||||
)
|
||||
await device_coordinator.async_config_entry_first_refresh()
|
||||
self.device_coordinators[dev_id] = device_coordinator
|
||||
|
||||
async def async_cleanup(self) -> None:
|
||||
"""Perform cleanup actions."""
|
||||
for coordinator in self.device_coordinators.values():
|
||||
coordinator.device_client.on_status_update = None
|
||||
await self.client.async_cleanup()
|
||||
|
||||
def token_fresh_cb(self) -> None:
|
||||
"""Update token."""
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=self.client.login_info.copy()
|
||||
)
|
||||
|
||||
async def async_auto_login(self) -> None:
|
||||
"""Async auto login."""
|
||||
if self.client.login_info.get(CONF_ACCESS_TOKEN) is None:
|
||||
await self.client.async_post_login()
|
||||
|
||||
def _purge_deleted_lists(self) -> None:
|
||||
"""Purge device entries of deleted lists."""
|
||||
|
||||
device_reg = dr.async_get(self.hass)
|
||||
identifiers = {
|
||||
(
|
||||
DOMAIN,
|
||||
device_coordinator.device_client.info.dev_id,
|
||||
)
|
||||
for device_coordinator in self.device_coordinators.values()
|
||||
}
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_reg, self.config_entry.entry_id
|
||||
):
|
||||
if not set(device.identifiers) & identifiers:
|
||||
_LOGGER.debug("Removing obsolete device entry %s", device.name)
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
@@ -1,122 +0,0 @@
|
||||
"""Support for Aidot lights."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGBW_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AidotConfigEntry, AidotDeviceUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AidotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Light."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
AidotLight(device_coordinator)
|
||||
for device_coordinator in coordinator.device_coordinators.values()
|
||||
)
|
||||
|
||||
|
||||
class AidotLight(CoordinatorEntity[AidotDeviceUpdateCoordinator], LightEntity):
|
||||
"""Representation of a Aidot Wi-Fi Light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: AidotDeviceUpdateCoordinator) -> None:
|
||||
"""Initialize the light."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.device_client.info.dev_id
|
||||
if hasattr(coordinator.device_client.info, "cct_max"):
|
||||
self._attr_max_color_temp_kelvin = coordinator.device_client.info.cct_max
|
||||
if hasattr(coordinator.device_client.info, "cct_min"):
|
||||
self._attr_min_color_temp_kelvin = coordinator.device_client.info.cct_min
|
||||
|
||||
model_id = coordinator.device_client.info.model_id
|
||||
manufacturer = model_id.split(".")[0]
|
||||
model = model_id[len(manufacturer) + 1 :]
|
||||
mac = coordinator.device_client.info.mac
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
name=coordinator.device_client.info.name,
|
||||
hw_version=coordinator.device_client.info.hw_version,
|
||||
)
|
||||
if coordinator.device_client.info.enable_rgbw:
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
self._attr_supported_color_modes = {ColorMode.RGBW, ColorMode.COLOR_TEMP}
|
||||
elif coordinator.device_client.info.enable_cct:
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
|
||||
else:
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
self._update_status()
|
||||
|
||||
def _update_status(self) -> None:
|
||||
"""Update light status from coordinator data."""
|
||||
self._attr_is_on = self.coordinator.data.on
|
||||
self._attr_brightness = self.coordinator.data.dimming
|
||||
self._attr_color_temp_kelvin = self.coordinator.data.cct
|
||||
self._attr_rgbw_color = self.coordinator.data.rgbw
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.data.online
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update."""
|
||||
self._update_status()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on, applying brightness, color temperature, RGBW, or plain on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||
await self.coordinator.device_client.async_set_brightness(brightness)
|
||||
self.coordinator.data.dimming = brightness
|
||||
self._attr_brightness = brightness
|
||||
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
await self.coordinator.device_client.async_set_cct(color_temp_kelvin)
|
||||
self.coordinator.data.cct = color_temp_kelvin
|
||||
self._attr_color_temp_kelvin = color_temp_kelvin
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif ATTR_RGBW_COLOR in kwargs:
|
||||
rgbw_color = kwargs.get(ATTR_RGBW_COLOR)
|
||||
await self.coordinator.device_client.async_set_rgbw(rgbw_color)
|
||||
self.coordinator.data.rgbw = rgbw_color
|
||||
self._attr_rgbw_color = rgbw_color
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
else:
|
||||
await self.coordinator.device_client.async_turn_on()
|
||||
|
||||
self.coordinator.data.on = True
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self.coordinator.device_client.async_turn_off()
|
||||
self.coordinator.data.on = False
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "aidot",
|
||||
"name": "AiDot",
|
||||
"codeowners": ["@s1eedz", "@HongBryan"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aidot",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-aidot==0.3.53"]
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: This integration does not register any events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: This integration has no option flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
entity-disabled-by-default: todo
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country_code": "Country",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country_code": "The country selected by AiDot app when logging in",
|
||||
"password": "Password for logging in through AiDot app",
|
||||
"username": "Account logged in through AiDot app"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except AirOSKeyDataMissingError as err:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryError("key_data_missing") from err
|
||||
except Exception as err:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryError("unknown") from err
|
||||
|
||||
airos_class: type[AirOS8 | AirOS6] = (
|
||||
|
||||
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
DOMAIN = "altruist"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_HOST = "host"
|
||||
|
||||
@@ -16,8 +16,6 @@ from .entity import AnthropicBaseLLMEntity
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LLM_HASS_API,
|
||||
CONF_NAME,
|
||||
CONF_PROMPT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, llm
|
||||
from homeassistant.helpers import llm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
@@ -44,13 +44,12 @@ from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_CODE_EXECUTION,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_PROMPT,
|
||||
CONF_PROMPT_CACHING,
|
||||
CONF_RECOMMENDED,
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_TOOL_SEARCH,
|
||||
CONF_WEB_FETCH,
|
||||
CONF_WEB_FETCH_MAX_USES,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
@@ -453,19 +452,11 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_MAX_USES,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
|
||||
): cv.positive_int,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_FETCH,
|
||||
default=DEFAULT[CONF_WEB_FETCH],
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_FETCH_MAX_USES,
|
||||
default=DEFAULT[CONF_WEB_FETCH_MAX_USES],
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ DEFAULT_CONVERSATION_NAME = "Claude conversation"
|
||||
DEFAULT_AI_TASK_NAME = "Claude AI Task"
|
||||
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
CONF_CODE_EXECUTION = "code_execution"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
@@ -17,8 +18,6 @@ CONF_PROMPT_CACHING = "prompt_caching"
|
||||
CONF_THINKING_BUDGET = "thinking_budget"
|
||||
CONF_THINKING_EFFORT = "thinking_effort"
|
||||
CONF_TOOL_SEARCH = "tool_search"
|
||||
CONF_WEB_FETCH = "web_fetch"
|
||||
CONF_WEB_FETCH_MAX_USES = "web_fetch_max_uses"
|
||||
CONF_WEB_SEARCH = "web_search"
|
||||
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
|
||||
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
|
||||
@@ -46,8 +45,6 @@ DEFAULT = {
|
||||
CONF_THINKING_BUDGET: MIN_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT: "low",
|
||||
CONF_TOOL_SEARCH: False,
|
||||
CONF_WEB_FETCH: False,
|
||||
CONF_WEB_FETCH_MAX_USES: 5,
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
|
||||
@@ -4,16 +4,14 @@ from typing import Literal
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT, MATCH_ALL
|
||||
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_PROMPT, DOMAIN
|
||||
from .entity import AnthropicBaseLLMEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -5,10 +5,11 @@ from typing import TYPE_CHECKING, Any
|
||||
from anthropic import __title__, __version__
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PROMPT
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import (
|
||||
CONF_PROMPT,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
|
||||
@@ -17,6 +17,8 @@ from anthropic.types import (
|
||||
Base64PDFSourceParam,
|
||||
BashCodeExecutionToolResultBlock,
|
||||
CitationsDelta,
|
||||
CitationsWebSearchResultLocation,
|
||||
CitationWebSearchResultLocationParam,
|
||||
CodeExecutionTool20250825Param,
|
||||
CodeExecutionToolResultBlock,
|
||||
CodeExecutionToolResultBlockContent,
|
||||
@@ -68,9 +70,6 @@ from anthropic.types import (
|
||||
ToolUseBlock,
|
||||
ToolUseBlockParam,
|
||||
Usage,
|
||||
WebFetchTool20250910Param,
|
||||
WebFetchTool20260209Param,
|
||||
WebFetchToolResultBlock,
|
||||
WebSearchTool20250305Param,
|
||||
WebSearchTool20260209Param,
|
||||
WebSearchToolResultBlock,
|
||||
@@ -98,12 +97,6 @@ from anthropic.types.tool_search_tool_result_block_param import (
|
||||
Content as ToolSearchToolResultBlockParamContentParam,
|
||||
)
|
||||
from anthropic.types.tool_use_block import Caller
|
||||
from anthropic.types.web_fetch_tool_result_block import (
|
||||
Content as WebFetchToolResultBlockContent,
|
||||
)
|
||||
from anthropic.types.web_fetch_tool_result_block_param import (
|
||||
Content as WebFetchToolResultBlockParamContentParam,
|
||||
)
|
||||
import voluptuous as vol
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -125,8 +118,6 @@ from .const import (
|
||||
CONF_THINKING_BUDGET,
|
||||
CONF_THINKING_EFFORT,
|
||||
CONF_TOOL_SEARCH,
|
||||
CONF_WEB_FETCH,
|
||||
CONF_WEB_FETCH_MAX_USES,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
@@ -217,9 +208,17 @@ class ContentDetails:
|
||||
"""Add a citation to the current detail."""
|
||||
if not self.citation_details:
|
||||
self.citation_details.append(CitationDetails())
|
||||
self.citation_details[-1].citations.append(
|
||||
cast(TextCitationParam, citation.to_dict())
|
||||
)
|
||||
citation_param: TextCitationParam | None = None
|
||||
if isinstance(citation, CitationsWebSearchResultLocation):
|
||||
citation_param = CitationWebSearchResultLocationParam(
|
||||
type="web_search_result_location",
|
||||
title=citation.title,
|
||||
url=citation.url,
|
||||
cited_text=citation.cited_text,
|
||||
encrypted_index=citation.encrypted_index,
|
||||
)
|
||||
if citation_param:
|
||||
self.citation_details[-1].citations.append(citation_param)
|
||||
|
||||
def delete_empty(self) -> None:
|
||||
"""Delete empty citation details."""
|
||||
@@ -290,15 +289,6 @@ def _convert_content( # noqa: C901
|
||||
content.tool_result,
|
||||
),
|
||||
}
|
||||
elif content.tool_name == "web_fetch":
|
||||
tool_result_block = {
|
||||
"type": "web_fetch_tool_result",
|
||||
"tool_use_id": content.tool_call_id,
|
||||
"content": cast(
|
||||
WebFetchToolResultBlockParamContentParam,
|
||||
content.tool_result,
|
||||
),
|
||||
}
|
||||
else:
|
||||
tool_result_block = {
|
||||
"type": "tool_result",
|
||||
@@ -425,7 +415,6 @@ def _convert_content( # noqa: C901
|
||||
id=tool_call.id,
|
||||
name=cast(
|
||||
Literal[
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"code_execution",
|
||||
"bash_code_execution",
|
||||
@@ -439,7 +428,6 @@ def _convert_content( # noqa: C901
|
||||
if tool_call.external
|
||||
and tool_call.tool_name
|
||||
in [
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"code_execution",
|
||||
"bash_code_execution",
|
||||
@@ -621,7 +609,6 @@ class AnthropicDeltaStream:
|
||||
if isinstance(
|
||||
content_block,
|
||||
(
|
||||
WebFetchToolResultBlock,
|
||||
WebSearchToolResultBlock,
|
||||
CodeExecutionToolResultBlock,
|
||||
BashCodeExecutionToolResultBlock,
|
||||
@@ -737,15 +724,13 @@ class AnthropicDeltaStream:
|
||||
self,
|
||||
tool_use_id: str,
|
||||
tool_name: Literal[
|
||||
"web_fetch_tool_result",
|
||||
"web_search_tool_result",
|
||||
"code_execution_tool_result",
|
||||
"bash_code_execution_tool_result",
|
||||
"text_editor_code_execution_tool_result",
|
||||
"tool_search_tool_result",
|
||||
],
|
||||
content: WebFetchToolResultBlockContent
|
||||
| WebSearchToolResultBlockContent
|
||||
content: WebSearchToolResultBlockContent
|
||||
| CodeExecutionToolResultBlockContent
|
||||
| BashCodeExecutionToolResultBlockContent
|
||||
| TextEditorCodeExecutionToolResultBlockContent
|
||||
@@ -922,7 +907,6 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
"GetLiveContext",
|
||||
"code_execution",
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
]
|
||||
|
||||
system = chat_log.content[0]
|
||||
@@ -996,12 +980,12 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
]
|
||||
|
||||
if options[CONF_CODE_EXECUTION]:
|
||||
# The `web_search_20260209` and `web_fetch_20260209` tools
|
||||
# automatically enable `code_execution_20260120` tool
|
||||
# The `web_search_20260209` tool automatically enables
|
||||
# `code_execution_20260120` tool
|
||||
if (
|
||||
not self.model_info.capabilities
|
||||
or not self.model_info.capabilities.code_execution.supported
|
||||
or (not options[CONF_WEB_SEARCH] and not options[CONF_WEB_FETCH])
|
||||
or not options[CONF_WEB_SEARCH]
|
||||
):
|
||||
tools.append(
|
||||
CodeExecutionTool20250825Param(
|
||||
@@ -1039,28 +1023,6 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
}
|
||||
tools.append(web_search)
|
||||
|
||||
if options[CONF_WEB_FETCH]:
|
||||
if (
|
||||
not self.model_info.capabilities
|
||||
or not self.model_info.capabilities.code_execution.supported
|
||||
or not options[CONF_CODE_EXECUTION]
|
||||
):
|
||||
tools.append(
|
||||
WebFetchTool20250910Param(
|
||||
name="web_fetch",
|
||||
type="web_fetch_20250910",
|
||||
max_uses=options[CONF_WEB_FETCH_MAX_USES],
|
||||
)
|
||||
)
|
||||
else:
|
||||
tools.append(
|
||||
WebFetchTool20260209Param(
|
||||
name="web_fetch",
|
||||
type="web_fetch_20260209",
|
||||
max_uses=options[CONF_WEB_FETCH_MAX_USES],
|
||||
)
|
||||
)
|
||||
|
||||
# Handle attachments by adding them to the last user message
|
||||
last_content = chat_log.content[-1]
|
||||
if last_content.role == "user" and last_content.attachments:
|
||||
|
||||
@@ -38,7 +38,10 @@ rules:
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: |
|
||||
The API does not limit parallel updates.
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
# Gold
|
||||
|
||||
@@ -40,11 +40,9 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
self._current_subentry_id = None
|
||||
self._model_list_cache = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None
|
||||
) -> RepairsFlowResult:
|
||||
async def async_step_init(self, user_input: dict[str, str]) -> RepairsFlowResult:
|
||||
"""Handle the steps of a fix flow."""
|
||||
if user_input and user_input.get(CONF_CHAT_MODEL):
|
||||
if user_input.get(CONF_CHAT_MODEL):
|
||||
self._async_update_current_subentry(user_input)
|
||||
|
||||
target = await self._async_next_target()
|
||||
|
||||
@@ -51,11 +51,13 @@
|
||||
"advanced": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]"
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::temperature%]"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]"
|
||||
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]",
|
||||
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::temperature%]"
|
||||
},
|
||||
"title": "[%key:component::anthropic::config_subentries::conversation::step::advanced::title%]"
|
||||
},
|
||||
@@ -78,8 +80,6 @@
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_effort%]",
|
||||
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data::tool_search%]",
|
||||
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data::user_location%]",
|
||||
"web_fetch": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_fetch%]",
|
||||
"web_fetch_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_fetch_max_uses%]",
|
||||
"web_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_search%]",
|
||||
"web_search_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data::web_search_max_uses%]"
|
||||
},
|
||||
@@ -90,8 +90,6 @@
|
||||
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_effort%]",
|
||||
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::tool_search%]",
|
||||
"user_location": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::user_location%]",
|
||||
"web_fetch": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_fetch%]",
|
||||
"web_fetch_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_fetch_max_uses%]",
|
||||
"web_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_search%]",
|
||||
"web_search_max_uses": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::web_search_max_uses%]"
|
||||
},
|
||||
@@ -118,11 +116,13 @@
|
||||
"advanced": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"prompt_caching": "Caching strategy"
|
||||
"prompt_caching": "Caching strategy",
|
||||
"temperature": "Temperature"
|
||||
},
|
||||
"data_description": {
|
||||
"chat_model": "The model to serve the responses.",
|
||||
"prompt_caching": "Optimize your API cost and response times based on your usage."
|
||||
"prompt_caching": "Optimize your API cost and response times based on your usage.",
|
||||
"temperature": "Control the randomness of the response, trading off between creativity and coherence."
|
||||
},
|
||||
"title": "Advanced settings"
|
||||
},
|
||||
@@ -149,8 +149,6 @@
|
||||
"thinking_effort": "Thinking effort",
|
||||
"tool_search": "Enable tool search tool",
|
||||
"user_location": "Include home location",
|
||||
"web_fetch": "Enable web fetch",
|
||||
"web_fetch_max_uses": "Maximum web fetches",
|
||||
"web_search": "Enable web search",
|
||||
"web_search_max_uses": "Maximum web searches"
|
||||
},
|
||||
@@ -161,8 +159,6 @@
|
||||
"thinking_effort": "Control how many tokens Claude uses when responding, trading off between response thoroughness and token efficiency",
|
||||
"tool_search": "Enable dynamic tool discovery instead of preloading all tools into the context",
|
||||
"user_location": "Localize search results based on home location",
|
||||
"web_fetch": "The web fetch tool allows Claude to retrieve full content from specified web pages and PDF documents to augment Claude's context with live web content",
|
||||
"web_fetch_max_uses": "Limit the number of web fetches performed per response",
|
||||
"web_search": "The web search tool gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff",
|
||||
"web_search_max_uses": "Limit the number of searches performed per response"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ from pyatv.interface import AppleTV, KeyboardListener
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -21,33 +21,23 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Load Apple TV binary sensor based on a config entry."""
|
||||
# apple_tv config entries always have a unique id
|
||||
manager = config_entry.runtime_data
|
||||
added = False
|
||||
cb: CALLBACK_TYPE
|
||||
|
||||
@callback
|
||||
def setup_entities(atv: AppleTV) -> None:
|
||||
nonlocal added
|
||||
if added:
|
||||
return
|
||||
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
|
||||
assert config_entry.unique_id is not None
|
||||
name: str = config_entry.data[CONF_NAME]
|
||||
async_add_entities(
|
||||
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
|
||||
)
|
||||
added = True
|
||||
cb()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
|
||||
)
|
||||
cb = async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
|
||||
)
|
||||
|
||||
# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
|
||||
# before this platform was forwarded, in which case the signal above was
|
||||
# missed; handle that case directly.
|
||||
if manager.atv is not None:
|
||||
setup_entities(manager.atv)
|
||||
config_entry.async_on_unload(cb)
|
||||
|
||||
|
||||
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
|
||||
|
||||
@@ -16,9 +16,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import ArcamFmjConfigEntry
|
||||
from .entity import ArcamFmjEntity
|
||||
|
||||
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
|
||||
@@ -53,19 +53,18 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
||||
self.state = State(client, zone)
|
||||
self.update_in_progress = False
|
||||
|
||||
device_name = config_entry.title
|
||||
name = config_entry.title
|
||||
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||
unique_id_device = unique_id
|
||||
if zone != 1:
|
||||
unique_id_device += f"-{zone}"
|
||||
device_name += f" Zone {zone}"
|
||||
name += f" Zone {zone}"
|
||||
|
||||
self.device_name = device_name
|
||||
self.device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id_device)},
|
||||
manufacturer="Arcam",
|
||||
model="Arcam FMJ AVR",
|
||||
name=device_name,
|
||||
name=name,
|
||||
)
|
||||
self.zone_unique_id = f"{unique_id}-{zone}"
|
||||
|
||||
|
||||
@@ -1,36 +1,11 @@
|
||||
"""Base entity for Arcam FMJ integration."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import functools
|
||||
from typing import Any
|
||||
|
||||
from arcam.fmj import ConnectionFailed
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ArcamFmjCoordinator
|
||||
|
||||
|
||||
def convert_exception[**_P, _R](
|
||||
func: Callable[_P, Coroutine[Any, Any, _R]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
||||
"""Convert a connection failure into a translated HomeAssistantError."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def _convert_exception(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionFailed as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="connection_failed"
|
||||
) from exception
|
||||
|
||||
return _convert_exception
|
||||
|
||||
|
||||
class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
|
||||
"""Base entity for Arcam FMJ."""
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Arcam media player."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from arcam.fmj import SourceCodes
|
||||
from arcam.fmj import ConnectionFailed, SourceCodes
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
@@ -16,19 +18,15 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, EVENT_TURN_ON
|
||||
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator
|
||||
from .entity import ArcamFmjEntity, convert_exception
|
||||
from .entity import ArcamFmjEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# arcam-fmj serializes commands on a single TCP writer at the library
|
||||
# layer; serialize at HA's layer to match the device's contract.
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -43,6 +41,23 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
def convert_exception[**_P, _R](
|
||||
func: Callable[_P, Coroutine[Any, Any, _R]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
||||
"""Return decorator to convert a connection error into a home assistant error."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def _convert_exception(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionFailed as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="connection_failed"
|
||||
) from exception
|
||||
|
||||
return _convert_exception
|
||||
|
||||
|
||||
class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
"""Representation of a media device."""
|
||||
|
||||
@@ -64,17 +79,11 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device.
|
||||
|
||||
``None`` is returned (surfaced as ``unknown``) when the device has
|
||||
not yet reported a power state; this is distinct from a real
|
||||
powered-off state and must not be collapsed to ``OFF``.
|
||||
"""
|
||||
power = self._state.get_power()
|
||||
if power is None:
|
||||
return None
|
||||
return MediaPlayerState.ON if power else MediaPlayerState.OFF
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the state of the device."""
|
||||
if self._state.get_power():
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@convert_exception
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
@@ -170,7 +179,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
]
|
||||
|
||||
return BrowseMedia(
|
||||
title=self.coordinator.device_name,
|
||||
title="Arcam FMJ Receiver",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="root",
|
||||
media_content_type="library",
|
||||
|
||||
@@ -22,9 +22,6 @@ from .entity import ArcamFmjEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _enum_options(value: type[IntOrTypeEnum]) -> list[str]:
|
||||
return [
|
||||
|
||||
@@ -19,8 +19,6 @@ DEVICES = "devices"
|
||||
MANUFACTURER = "ABB"
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_FIRMWARE = "firmware"
|
||||
|
||||
@@ -82,7 +82,7 @@ rules:
|
||||
comment: |
|
||||
This integration does not have any entities that should disabled by default.
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from autoskope_client.constants import MANUFACTURER
|
||||
from autoskope_client.models import Vehicle
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.components.device_tracker import SourceType, TrackerEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -113,6 +113,11 @@ class AutoskopeDeviceTracker(
|
||||
return float(vehicle.position.longitude)
|
||||
return None
|
||||
|
||||
@property
|
||||
def source_type(self) -> SourceType:
|
||||
"""Return the source type of the device."""
|
||||
return SourceType.GPS
|
||||
|
||||
@property
|
||||
def location_accuracy(self) -> float:
|
||||
"""Return the location accuracy of the device in meters."""
|
||||
|
||||
@@ -67,7 +67,7 @@ rules:
|
||||
comment: |
|
||||
Only one entity type (device_tracker) is created, making this not applicable.
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
|
||||
@@ -32,7 +32,6 @@ from .const import DOMAIN, INTEGRATION_TITLE, UNKNOWN_NAME
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
UPDATE_EXCEPTIONS = (BleakError, OSError, RuntimeError)
|
||||
BREAKS_IN_HA_VERSION = "2026.12.0"
|
||||
AVEA_MAX_BRIGHTNESS = 4095
|
||||
|
||||
|
||||
def _normalize_name(name: str | None) -> str | None:
|
||||
@@ -42,16 +41,6 @@ def _normalize_name(name: str | None) -> str | None:
|
||||
return name
|
||||
|
||||
|
||||
def _ha_brightness_to_avea(brightness: int) -> int:
|
||||
"""Convert Home Assistant brightness to Avea brightness."""
|
||||
return round((brightness / 255) * AVEA_MAX_BRIGHTNESS)
|
||||
|
||||
|
||||
def _avea_brightness_to_ha(brightness: int) -> int:
|
||||
"""Convert Avea brightness to Home Assistant brightness."""
|
||||
return round(255 * (brightness / AVEA_MAX_BRIGHTNESS))
|
||||
|
||||
|
||||
def _create_deprecated_yaml_issue(hass: HomeAssistant) -> None:
|
||||
"""Create the deprecated YAML issue for Avea."""
|
||||
ir.async_create_issue(
|
||||
@@ -95,9 +84,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Avea light platform."""
|
||||
async_add_entities(
|
||||
[AveaLight(entry.runtime_data, entry.title)], update_before_add=True
|
||||
)
|
||||
async_add_entities([AveaLight(entry.runtime_data)], update_before_add=True)
|
||||
|
||||
|
||||
def _discover_bulbs_for_import() -> list[dict[str, str]]:
|
||||
@@ -182,23 +169,20 @@ class AveaLight(LightEntity):
|
||||
_attr_color_mode = ColorMode.HS
|
||||
_attr_supported_color_modes = {ColorMode.HS}
|
||||
|
||||
def __init__(self, light: avea.Bulb, entry_title: str) -> None:
|
||||
def __init__(self, light: avea.Bulb) -> None:
|
||||
"""Initialize an AveaLight."""
|
||||
self._light = light
|
||||
self._attr_name = entry_title
|
||||
self._attr_name = light.name
|
||||
self._attr_brightness = light.brightness
|
||||
self._last_brightness = 255
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
if not kwargs:
|
||||
self._light.set_brightness(_ha_brightness_to_avea(self._last_brightness))
|
||||
self._light.set_brightness(4095)
|
||||
else:
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
if brightness:
|
||||
self._last_brightness = brightness
|
||||
self._light.set_brightness(_ha_brightness_to_avea(brightness))
|
||||
bright = round((kwargs[ATTR_BRIGHTNESS] / 255) * 4095)
|
||||
self._light.set_brightness(bright)
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
|
||||
self._light.set_rgb(rgb[0], rgb[1], rgb[2])
|
||||
@@ -206,23 +190,9 @@ class AveaLight(LightEntity):
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
self._light.set_brightness(0)
|
||||
self._attr_is_on = False
|
||||
self._attr_brightness = 0
|
||||
|
||||
def update(self) -> None:
|
||||
"""Fetch new state data for this light."""
|
||||
connected = self._light.connect()
|
||||
|
||||
try:
|
||||
brightness = self._light.get_brightness()
|
||||
rgb_color = self._light.get_rgb()
|
||||
finally:
|
||||
if connected:
|
||||
self._light.disconnect()
|
||||
|
||||
if brightness is not None:
|
||||
if (brightness := self._light.get_brightness()) is not None:
|
||||
self._attr_is_on = brightness != 0
|
||||
self._attr_brightness = _avea_brightness_to_ha(brightness)
|
||||
if self._attr_brightness:
|
||||
self._last_brightness = self._attr_brightness
|
||||
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb_color)
|
||||
self._attr_brightness = round(255 * (brightness / 4095))
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["avea"],
|
||||
"requirements": ["avea==1.8.0"]
|
||||
"requirements": ["avea==1.6.1"]
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except ValueError as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_endpoint_url",
|
||||
|
||||
@@ -11,7 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
AWS_DOMAIN = "amazonaws.com"
|
||||
|
||||
@@ -72,7 +72,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: This integration does not use icons.
|
||||
|
||||
@@ -75,13 +75,11 @@ def handle_backup_errors[_R, **P](
|
||||
err.message,
|
||||
exc_info=True,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupAgentError(
|
||||
f"Error during backup operation in {func.__name__}:"
|
||||
f" Status {err.status_code}, message: {err.message}"
|
||||
) from err
|
||||
except ServiceRequestError as err:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupAgentError(
|
||||
f"Timeout during backup operation in {func.__name__}"
|
||||
) from err
|
||||
@@ -92,7 +90,6 @@ def handle_backup_errors[_R, **P](
|
||||
err,
|
||||
exc_info=True,
|
||||
)
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupAgentError(
|
||||
f"Error during backup operation in {func.__name__}: {err}"
|
||||
) from err
|
||||
@@ -121,7 +118,6 @@ class AzureStorageBackupAgent(BackupAgent):
|
||||
"""Download a backup file."""
|
||||
blob = await self._find_blob_by_backup_id(backup_id)
|
||||
if blob is None:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
download_stream = await self._client.download_blob(blob.name)
|
||||
return download_stream.chunks()
|
||||
@@ -159,7 +155,6 @@ class AzureStorageBackupAgent(BackupAgent):
|
||||
"""Delete a backup file."""
|
||||
blob = await self._find_blob_by_backup_id(backup_id)
|
||||
if blob is None:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
await self._client.delete_blob(blob.name)
|
||||
|
||||
@@ -186,7 +181,6 @@ class AzureStorageBackupAgent(BackupAgent):
|
||||
"""Return a backup."""
|
||||
blob = await self._find_blob_by_backup_id(backup_id)
|
||||
if blob is None:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
return AgentBackup.from_dict(json.loads(blob.metadata["backup_metadata"]))
|
||||
|
||||
@@ -89,7 +89,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except exception.MissingAccountData as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -20,12 +20,12 @@ from homeassistant.components.backup import (
|
||||
OnProgressCallback,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.async_iterator import AsyncIteratorReader
|
||||
|
||||
from . import BackblazeConfigEntry
|
||||
from .const import (
|
||||
CONF_PREFIX,
|
||||
DATA_BACKUP_AGENT_LISTENERS,
|
||||
DOMAIN,
|
||||
METADATA_FILE_SUFFIX,
|
||||
|
||||
@@ -8,7 +8,6 @@ from b2sdk.v2 import exception
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
@@ -23,6 +22,7 @@ from .const import (
|
||||
CONF_APPLICATION_KEY,
|
||||
CONF_BUCKET,
|
||||
CONF_KEY_ID,
|
||||
CONF_PREFIX,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ DOMAIN: Final = "backblaze_b2"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_APPLICATION_KEY = "application_key"
|
||||
CONF_BUCKET = "bucket"
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
|
||||
@@ -96,7 +96,7 @@ rules:
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: This integration does not use icons.
|
||||
|
||||
@@ -67,9 +67,6 @@
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to endpoint"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Authentication failed using the provided key ID and application key."
|
||||
},
|
||||
"invalid_bucket_name": {
|
||||
"message": "Bucket does not exist or is not writable by the provided credentials."
|
||||
},
|
||||
|
||||
@@ -80,8 +80,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
def _color_temp_to_native_scale(self, x: int) -> int:
|
||||
"""Convert color temperature from Kelvin to native BleBox scale (0-255).
|
||||
|
||||
BleBox native scale is inverted:
|
||||
0=warm (2700K), 255=cold (6500K).
|
||||
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
|
||||
"""
|
||||
scaled = (
|
||||
(self._attr_max_color_temp_kelvin - x)
|
||||
@@ -99,8 +98,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
def _color_temp_from_native_scale(self, x: int) -> int:
|
||||
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.
|
||||
|
||||
BleBox native scale is inverted:
|
||||
0=warm (2700K), 255=cold (6500K).
|
||||
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
|
||||
"""
|
||||
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
|
||||
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
|
||||
@@ -203,10 +201,6 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
else:
|
||||
value = feature.apply_brightness(value, brightness)
|
||||
|
||||
if isinstance(value, (list, tuple)) and not any(value):
|
||||
await self._feature.async_off()
|
||||
return
|
||||
|
||||
try:
|
||||
await self._feature.async_on(value)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -169,7 +169,6 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
try:
|
||||
await self._camera.save_recent_clips(output_dir=file_path)
|
||||
except OSError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
@@ -191,7 +190,6 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
try:
|
||||
await self._camera.video_to_file(filename)
|
||||
except OSError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -16,7 +16,6 @@ CONF_DETAILS = "details"
|
||||
CONF_PASSIVE = "passive"
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_SOURCE: Final = "source"
|
||||
CONF_SOURCE_DOMAIN: Final = "source_domain"
|
||||
CONF_SOURCE_MODEL: Final = "source_model"
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"bleak-retry-connector==4.6.0",
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.29.11",
|
||||
"dbus-fast==5.0.0",
|
||||
"bluetooth-data-tools==1.28.4",
|
||||
"dbus-fast==4.0.4",
|
||||
"habluetooth==6.1.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -89,9 +89,7 @@ class PassiveBluetoothDataUpdateCoordinator(
|
||||
|
||||
|
||||
class PassiveBluetoothCoordinatorEntity[ # pylint: disable=home-assistant-enforce-class-module
|
||||
_PassiveBluetoothDataUpdateCoordinatorT: (
|
||||
PassiveBluetoothDataUpdateCoordinator
|
||||
) = PassiveBluetoothDataUpdateCoordinator
|
||||
_PassiveBluetoothDataUpdateCoordinatorT: PassiveBluetoothDataUpdateCoordinator = PassiveBluetoothDataUpdateCoordinator
|
||||
](BaseCoordinatorEntity[_PassiveBluetoothDataUpdateCoordinatorT]):
|
||||
"""A class for entities using DataUpdateCoordinator."""
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@ def serialize_service_info(
|
||||
"address": service_info.address,
|
||||
"rssi": service_info.rssi,
|
||||
"manufacturer_data": {
|
||||
str(manufacturer_id): data.hex()
|
||||
for manufacturer_id, data in service_info.manufacturer_data.items()
|
||||
str(manufacturer_id): manufacturer_data.hex()
|
||||
for manufacturer_id, manufacturer_data in service_info.manufacturer_data.items()
|
||||
},
|
||||
"service_data": {
|
||||
service_uuid: service_data.hex()
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Final
|
||||
ATTR_CID: Final = "cid"
|
||||
ATTR_MAC: Final = "macAddr"
|
||||
ATTR_MANUFACTURER: Final = "Sony"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
|
||||
@@ -183,7 +183,6 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
|
||||
try:
|
||||
await self.coordinator.client.thermostat(**data, circuit=self._circuit)
|
||||
except BSBLANError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise HomeAssistantError(
|
||||
"An error occurred while updating the BSBLAN device",
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==6.0.1"],
|
||||
"requirements": ["python-bsblan==5.2.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -8,7 +8,6 @@ from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
@@ -21,6 +20,7 @@ if TYPE_CHECKING:
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_MONDAY_SLOTS = "monday_slots"
|
||||
ATTR_TUESDAY_SLOTS = "tuesday_slots"
|
||||
ATTR_WEDNESDAY_SLOTS = "wednesday_slots"
|
||||
|
||||
@@ -151,9 +151,7 @@ def sensor_update_to_bluetooth_data_update(
|
||||
device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[
|
||||
description.device_class
|
||||
]
|
||||
for device_key, description in (
|
||||
sensor_update.binary_entity_descriptions.items()
|
||||
)
|
||||
for device_key, description in sensor_update.binary_entity_descriptions.items()
|
||||
if description.device_class
|
||||
},
|
||||
entity_data={
|
||||
|
||||
@@ -17,8 +17,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import TIMEOUT
|
||||
|
||||
type CalDavConfigEntry = ConfigEntry[caldav.DAVClient]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
ssl_verify_cert=entry.data[CONF_VERIFY_SSL],
|
||||
timeout=TIMEOUT,
|
||||
timeout=30,
|
||||
)
|
||||
try:
|
||||
await hass.async_add_executor_job(client.principal)
|
||||
|
||||
@@ -52,16 +52,14 @@ async def async_get_calendars(
|
||||
warned_calendars.add((url, comp))
|
||||
if comp in ASSUMED_COMPONENTS:
|
||||
_LOGGER.warning(
|
||||
"CalDAV server does not report supported"
|
||||
" components for calendar %s, "
|
||||
"CalDAV server does not report supported components for calendar %s, "
|
||||
"assuming it supports the requested component '%s'",
|
||||
name or url,
|
||||
comp,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"CalDAV server does not report supported"
|
||||
" components for calendar %s. "
|
||||
"CalDAV server does not report supported components for calendar %s. "
|
||||
"Not assuming support for requested component '%s'",
|
||||
name or url,
|
||||
comp,
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN, TIMEOUT
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,7 +65,6 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
ssl_verify_cert=user_input[CONF_VERIFY_SSL],
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.principal)
|
||||
@@ -76,9 +75,6 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# AuthorizationError can be raised if the url is incorrect or
|
||||
# on some other unexpected server response.
|
||||
return "cannot_connect"
|
||||
except requests.Timeout as err:
|
||||
_LOGGER.warning("Timeout connecting to CalDAV server: %s", err)
|
||||
return "cannot_connect"
|
||||
except requests.ConnectionError as err:
|
||||
_LOGGER.warning("Connection Error connecting to CalDAV server: %s", err)
|
||||
return "cannot_connect"
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "caldav"
|
||||
TIMEOUT: Final = 30
|
||||
|
||||
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
|
||||
DOMAIN = "calendar"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[CalendarEntity]] = HassKey(DOMAIN)
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_EVENT = "event"
|
||||
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@ async def async_setup_entry(
|
||||
) -> bool:
|
||||
"""Set up Cambridge Audio integration from a config entry."""
|
||||
|
||||
client = StreamMagicClient(
|
||||
entry.data[CONF_HOST], async_get_clientsession(hass), should_close_session=False
|
||||
)
|
||||
client = StreamMagicClient(entry.data[CONF_HOST], async_get_clientsession(hass))
|
||||
|
||||
async def _connection_update_callback(
|
||||
_client: StreamMagicClient, _callback_type: CallbackType
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
@@ -25,9 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"address": address},
|
||||
f"Could not find Casper Glow device with address {address}"
|
||||
)
|
||||
|
||||
glow = CasperGlow(ble_device)
|
||||
|
||||
@@ -54,9 +54,6 @@
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Could not find Casper Glow device with address {address}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components import onboarding
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_UUID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import SectionConfig, section
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
@@ -17,7 +17,7 @@ from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, DOMAIN
|
||||
if TYPE_CHECKING:
|
||||
from . import CastConfigEntry
|
||||
|
||||
CONF_MORE_OPTIONS = "more_options"
|
||||
IGNORE_CEC_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
|
||||
KNOWN_HOSTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
@@ -27,19 +27,7 @@ KNOWN_HOSTS_SCHEMA = vol.Schema(
|
||||
)
|
||||
}
|
||||
)
|
||||
OPTIONS_SCHEMA = KNOWN_HOSTS_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MORE_OPTIONS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UUID): str,
|
||||
vol.Optional(CONF_IGNORE_CEC): str,
|
||||
}
|
||||
),
|
||||
SectionConfig(collapsed=True),
|
||||
)
|
||||
}
|
||||
)
|
||||
WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -104,55 +92,100 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class CastOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Google Cast options."""
|
||||
|
||||
async def async_step_init(
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Google Cast options flow."""
|
||||
self.updated_config: dict[str, Any] = {}
|
||||
|
||||
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
|
||||
"""Manage the Google Cast options."""
|
||||
return await self.async_step_basic_options()
|
||||
|
||||
async def async_step_basic_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Google Cast options."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
ignore_cec = _string_to_list(
|
||||
user_input[CONF_MORE_OPTIONS].get(CONF_IGNORE_CEC, "")
|
||||
)
|
||||
known_hosts = _trim_items(user_input.get(CONF_KNOWN_HOSTS, []))
|
||||
wanted_uuid = _string_to_list(
|
||||
user_input[CONF_MORE_OPTIONS].get(CONF_UUID, "")
|
||||
)
|
||||
updated_config = dict(self.config_entry.data)
|
||||
updated_config[CONF_IGNORE_CEC] = ignore_cec
|
||||
updated_config[CONF_KNOWN_HOSTS] = known_hosts
|
||||
updated_config[CONF_UUID] = wanted_uuid
|
||||
self.updated_config = dict(self.config_entry.data)
|
||||
self.updated_config[CONF_KNOWN_HOSTS] = known_hosts
|
||||
|
||||
if self.show_advanced_options:
|
||||
return await self.async_step_advanced_options()
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=updated_config
|
||||
self.config_entry, data=self.updated_config
|
||||
)
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
suggested: dict[str, Any] = {CONF_MORE_OPTIONS: {}}
|
||||
if CONF_KNOWN_HOSTS in self.config_entry.data:
|
||||
suggested[CONF_KNOWN_HOSTS] = self.config_entry.data[CONF_KNOWN_HOSTS]
|
||||
for key in (CONF_UUID, CONF_IGNORE_CEC):
|
||||
if key not in self.config_entry.data:
|
||||
continue
|
||||
suggested[CONF_MORE_OPTIONS][key] = _list_to_string(
|
||||
self.config_entry.data[key]
|
||||
return self.async_show_form(
|
||||
step_id="basic_options",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
KNOWN_HOSTS_SCHEMA, self.config_entry.data
|
||||
),
|
||||
errors=errors,
|
||||
last_step=not self.show_advanced_options,
|
||||
)
|
||||
|
||||
async def async_step_advanced_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Google Cast options."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
bad_cec, ignore_cec = _string_to_list(
|
||||
user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA
|
||||
)
|
||||
bad_uuid, wanted_uuid = _string_to_list(
|
||||
user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA
|
||||
)
|
||||
|
||||
if not bad_cec and not bad_uuid:
|
||||
self.updated_config[CONF_IGNORE_CEC] = ignore_cec
|
||||
self.updated_config[CONF_UUID] = wanted_uuid
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=self.updated_config
|
||||
)
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
fields: dict[vol.Marker, type[str]] = {}
|
||||
current_config = self.config_entry.data
|
||||
suggested_value = _list_to_string(current_config.get(CONF_UUID))
|
||||
_add_with_suggestion(fields, CONF_UUID, suggested_value)
|
||||
suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC))
|
||||
_add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(OPTIONS_SCHEMA, suggested),
|
||||
step_id="advanced_options",
|
||||
data_schema=vol.Schema(fields),
|
||||
errors=errors,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
|
||||
def _list_to_string(items: list[str]) -> str:
|
||||
def _list_to_string(items):
|
||||
comma_separated_string = ""
|
||||
if items:
|
||||
comma_separated_string = ",".join(items)
|
||||
return comma_separated_string
|
||||
|
||||
|
||||
def _string_to_list(string: str) -> list[str]:
|
||||
return [x.strip() for x in string.split(",") if x.strip()]
|
||||
def _string_to_list(string, schema):
|
||||
invalid = False
|
||||
items = [x.strip() for x in string.split(",") if x.strip()]
|
||||
try:
|
||||
items = schema(items)
|
||||
except vol.Invalid:
|
||||
invalid = True
|
||||
|
||||
return invalid, items
|
||||
|
||||
|
||||
def _trim_items(items: list[str]) -> list[str]:
|
||||
return [x.strip() for x in items if x.strip()]
|
||||
|
||||
|
||||
def _add_with_suggestion(
|
||||
fields: dict[vol.Marker, type[str]], key: str, suggested_value: str
|
||||
) -> None:
|
||||
fields[vol.Optional(key, description={"suggested_value": suggested_value})] = str
|
||||
|
||||
@@ -718,12 +718,9 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
await self.hass.async_add_executor_job(
|
||||
self._quick_play, app_name, app_data
|
||||
)
|
||||
except NotImplementedError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="app_not_supported",
|
||||
translation_placeholders={"app_name": app_name},
|
||||
) from err
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except NotImplementedError:
|
||||
_LOGGER.error("App %s not supported", app_name)
|
||||
return
|
||||
|
||||
# Try the cast platforms
|
||||
@@ -772,8 +769,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
media_id,
|
||||
err,
|
||||
)
|
||||
# Fallback: if playlist parsing fails, forward the raw URL to the device
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except PlaylistError as err:
|
||||
_LOGGER.warning(
|
||||
"[%s %s] Failed to parse playlist %s: %s",
|
||||
|
||||
@@ -18,33 +18,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"app_not_supported": {
|
||||
"message": "App {app_name} is not supported"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"invalid_known_hosts": "[%key:component::cast::config::error::invalid_known_hosts%]"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"advanced_options": {
|
||||
"data": {
|
||||
"ignore_cec": "Ignore CEC",
|
||||
"uuid": "Allowed UUIDs"
|
||||
},
|
||||
"description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.",
|
||||
"title": "Advanced Google Cast configuration"
|
||||
},
|
||||
"basic_options": {
|
||||
"data": {
|
||||
"known_hosts": "[%key:component::cast::config::step::config::data::known_hosts%]"
|
||||
},
|
||||
"data_description": {
|
||||
"known_hosts": "[%key:component::cast::config::step::config::data_description::known_hosts%]"
|
||||
},
|
||||
"sections": {
|
||||
"more_options": {
|
||||
"data": {
|
||||
"ignore_cec": "Ignore CEC",
|
||||
"uuid": "Allowed UUIDs"
|
||||
},
|
||||
"data_description": {
|
||||
"ignore_cec": "A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.",
|
||||
"uuid": "A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices."
|
||||
},
|
||||
"name": "More options"
|
||||
}
|
||||
},
|
||||
"title": "[%key:component::cast::config::step::config::title%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,42 +95,3 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self,
|
||||
user_input: Mapping[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry."""
|
||||
self._errors = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input.get(CONF_PORT, DEFAULT_PORT)
|
||||
|
||||
if (
|
||||
host != reconfigure_entry.data[CONF_HOST]
|
||||
or port != reconfigure_entry.data[CONF_PORT]
|
||||
):
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
|
||||
if await self._test_connection(user_input):
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={CONF_HOST: host, CONF_PORT: port},
|
||||
unique_id=f"{host}:{port}",
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
),
|
||||
user_input or reconfigure_entry.data,
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"import_failed": "Import from config failed",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"import_failed": "Import from config failed"
|
||||
},
|
||||
"error": {
|
||||
"connection_refused": "Connection refused when connecting to host",
|
||||
@@ -12,13 +11,6 @@
|
||||
"resolve_failed": "This host cannot be resolved"
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"title": "Reconfigure the certificate to test"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
|
||||
@@ -17,8 +17,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
APPLICATION_NAME,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
@@ -47,6 +45,8 @@ HA_USER_AGENT = (
|
||||
)
|
||||
|
||||
ATTR_UID = "uid"
|
||||
ATTR_LATITUDE = "latitude"
|
||||
ATTR_LONGITUDE = "longitude"
|
||||
ATTR_EMPTY_SLOTS = "empty_slots"
|
||||
ATTR_FREE_EBIKES = "free_ebikes"
|
||||
ATTR_TIMESTAMP = "timestamp"
|
||||
|
||||
@@ -29,7 +29,6 @@ BASE_API_URL = "https://rest.clicksend.com/v3"
|
||||
|
||||
HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_VOICE = "voice"
|
||||
|
||||
|
||||
@@ -32,28 +32,28 @@ set_temperature:
|
||||
max: 250
|
||||
step: 0.1
|
||||
mode: box
|
||||
temperature_range:
|
||||
fields:
|
||||
target_temp_high:
|
||||
filter:
|
||||
supported_features:
|
||||
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 250
|
||||
step: 0.1
|
||||
mode: box
|
||||
target_temp_low:
|
||||
filter:
|
||||
supported_features:
|
||||
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 250
|
||||
step: 0.1
|
||||
mode: box
|
||||
target_temp_high:
|
||||
filter:
|
||||
supported_features:
|
||||
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 250
|
||||
step: 0.1
|
||||
mode: box
|
||||
target_temp_low:
|
||||
filter:
|
||||
supported_features:
|
||||
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 250
|
||||
step: 0.1
|
||||
mode: box
|
||||
hvac_mode:
|
||||
selector:
|
||||
state:
|
||||
|
||||
@@ -373,12 +373,7 @@
|
||||
"name": "Target temperature"
|
||||
}
|
||||
},
|
||||
"name": "Set thermostat target temperature",
|
||||
"sections": {
|
||||
"temperature_range": {
|
||||
"name": "Temperature range"
|
||||
}
|
||||
}
|
||||
"name": "Set thermostat target temperature"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a thermostat on/off.",
|
||||
|
||||
@@ -220,11 +220,8 @@ class CloudClient(Interface):
|
||||
)
|
||||
if is_cloud_ice_servers_enabled:
|
||||
if self._cloud_ice_servers_listener is None:
|
||||
ice_servers = self.cloud.ice_servers
|
||||
self._cloud_ice_servers_listener = (
|
||||
await ice_servers.async_register_ice_servers_listener(
|
||||
register_cloud_ice_server
|
||||
)
|
||||
self._cloud_ice_servers_listener = await self.cloud.ice_servers.async_register_ice_servers_listener(
|
||||
register_cloud_ice_server
|
||||
)
|
||||
elif self._cloud_ice_servers_listener:
|
||||
self._cloud_ice_servers_listener()
|
||||
|
||||
@@ -11,7 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
# R2 is S3-compatible. Endpoint should be like:
|
||||
|
||||
@@ -94,7 +94,7 @@ rules:
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: This integration does not use icons.
|
||||
|
||||
@@ -6,5 +6,4 @@ ATTR_URL = "color_extract_url"
|
||||
DOMAIN = "color_extractor"
|
||||
DEFAULT_NAME = "Color extractor"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
SERVICE_TURN_ON = "turn_on"
|
||||
|
||||
@@ -65,7 +65,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
|
||||
@@ -94,12 +94,13 @@ class ComfoConnectFan(FanEntity):
|
||||
self._handle_mode_update,
|
||||
)
|
||||
)
|
||||
|
||||
def _register_sensors() -> None:
|
||||
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)
|
||||
self._ccb.comfoconnect.register_sensor(SENSOR_OPERATING_MODE_BIS)
|
||||
|
||||
await self.hass.async_add_executor_job(_register_sensors)
|
||||
await self.hass.async_add_executor_job(
|
||||
self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
await self.hass.async_add_executor_job(
|
||||
self._ccb.comfoconnect.register_sensor, SENSOR_OPERATING_MODE_BIS
|
||||
)
|
||||
|
||||
def _handle_speed_update(self, value: float) -> None:
|
||||
"""Handle update callbacks."""
|
||||
|
||||
@@ -10,11 +10,10 @@ from homeassistant.components.notify import (
|
||||
)
|
||||
from homeassistant.const import CONF_COMMAND
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.process import kill_subprocess
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, DOMAIN, LOGGER
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .utils import create_platform_yaml_not_supported_issue, render_template_args
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -67,18 +66,9 @@ class CommandLineNotificationService(BaseNotificationService):
|
||||
proc.returncode,
|
||||
command,
|
||||
)
|
||||
except subprocess.TimeoutExpired as err:
|
||||
_LOGGER.debug("Timeout for command: %s", command)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
kill_subprocess(proc)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_error",
|
||||
translation_placeholders={"command": command},
|
||||
) from err
|
||||
except subprocess.SubprocessError as err:
|
||||
_LOGGER.debug("Error trying to exec command: %s", command)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_error",
|
||||
translation_placeholders={"command": command, "error": str(err)},
|
||||
) from err
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"command_error": {
|
||||
"message": "Error trying to execute command: {command}. Error: {error}"
|
||||
},
|
||||
"timeout_error": {
|
||||
"message": "Timeout trying to execute command: {command}"
|
||||
"issues": {
|
||||
"platform_yaml_not_supported": {
|
||||
"description": "Platform YAML setup is not supported.\nChange from configuring it using the `{platform}:` key to using the `command_line:` key directly in configuration.yaml and restart Home Assistant to resolve the issue.\nTo see the detailed documentation, select Learn more.",
|
||||
"title": "Platform YAML is not supported in Command Line"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -4,9 +4,7 @@ import asyncio
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
async_create_platform_config_not_supported_issue,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
@@ -100,11 +98,13 @@ def create_platform_yaml_not_supported_issue(
|
||||
hass: HomeAssistant, platform_domain: str
|
||||
) -> None:
|
||||
"""Create an issue when platform yaml is used."""
|
||||
async_create_platform_config_not_supported_issue(
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
platform_domain,
|
||||
yaml_config_under_integration_supported=True,
|
||||
f"{platform_domain}_platform_yaml_not_supported",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="platform_yaml_not_supported",
|
||||
translation_placeholders={"platform": platform_domain},
|
||||
learn_more_url="https://www.home-assistant.io/integrations/command_line/",
|
||||
logger=LOGGER,
|
||||
)
|
||||
|
||||
@@ -91,11 +91,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Compensation sensor."""
|
||||
hass.data[DATA_COMPENSATION] = {}
|
||||
|
||||
# Exit early if no compensations are configured using the compensation: key in configuration.yaml.
|
||||
# This allows us to create an issue if platform: compensation is present in the sensor: section.
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
for compensation, conf in config[DOMAIN].items():
|
||||
_LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import numpy as np
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -32,10 +31,7 @@ from homeassistant.core import (
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_create_platform_config_not_supported_issue,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
@@ -45,7 +41,6 @@ from .const import (
|
||||
CONF_PRECISION,
|
||||
DATA_COMPENSATION,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,14 +58,6 @@ async def async_setup_platform(
|
||||
) -> None:
|
||||
"""Set up the Compensation sensor."""
|
||||
if discovery_info is None:
|
||||
async_create_platform_config_not_supported_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SENSOR_DOMAIN,
|
||||
yaml_config_under_integration_supported=True,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/compensation/",
|
||||
logger=_LOGGER,
|
||||
)
|
||||
return
|
||||
|
||||
compensation: str = discovery_info[CONF_COMPENSATION]
|
||||
|
||||
@@ -16,7 +16,6 @@ PLATFORMS = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WATER_HEATER,
|
||||
]
|
||||
|
||||
|
||||
@@ -279,20 +279,6 @@
|
||||
"no_alarm": "mdi:check-circle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"device_on_off": {
|
||||
"default": "mdi:power",
|
||||
"state": {
|
||||
"off": "mdi:power-off"
|
||||
}
|
||||
},
|
||||
"force_dhw": {
|
||||
"default": "mdi:water-boiler",
|
||||
"state": {
|
||||
"off": "mdi:water-boiler-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,14 +421,6 @@
|
||||
"weather_curve": {
|
||||
"name": "Weather curve"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"device_on_off": {
|
||||
"name": "Device on/off"
|
||||
},
|
||||
"force_dhw": {
|
||||
"name": "Force domestic hot water"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user