mirror of
https://github.com/home-assistant/core.git
synced 2026-05-22 08:45:16 +02:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b99ee250d | |||
| e4d7812883 | |||
| 43834ec64e | |||
| 49fc1b413d | |||
| bffb0417cc | |||
| 8b8c687fc3 | |||
| e3dd6b5fc5 | |||
| 94d620438b | |||
| 8867b792dc | |||
| 97967abfeb | |||
| af8fea272d | |||
| 2db0eed570 | |||
| ded1628c20 | |||
| a02e54f332 | |||
| 1858649bc7 | |||
| 109e09c3ec | |||
| ad139b259b | |||
| a1a76874fd | |||
| e7bd56325b | |||
| ef2ef0c8ba | |||
| a8381e923a | |||
| b7adba559b | |||
| 5cf6dceb04 | |||
| 975bcc5431 | |||
| f24a44e81f | |||
| 43c91843cd | |||
| dbce1d328a | |||
| d294b04b79 | |||
| 8b0e9060b3 | |||
| 39066b6e3a | |||
| a23a9b350b | |||
| fdaa807ca8 | |||
| f290dcc03f | |||
| 654408cc76 | |||
| 1f814faad8 | |||
| 6e00eecfcd | |||
| 8c8620c511 | |||
| cca8825ca5 | |||
| 92fbcc29a5 | |||
| 1c28833f39 | |||
| cfdef77222 | |||
| 49720475da | |||
| 7967b84cc6 | |||
| c715557813 | |||
| 79e5330782 | |||
| 5210ca64b1 | |||
| 65283e3d77 | |||
| 427cb9f8db | |||
| a09e042d42 | |||
| 072e9b51a2 | |||
| b96342c4f3 | |||
| 56eae8c808 | |||
| 9fbdf86104 | |||
| 8ff5da59c4 | |||
| 298f4f8ed0 | |||
| 6fdc0bb90b | |||
| 94c3ad2cb2 | |||
| d83d44648c | |||
| 279b614b7c | |||
| 244dfe014a | |||
| 6b379e50cf | |||
| 1368cd15da | |||
| 8c8cc3acb9 | |||
| b0634bea35 | |||
| 5ae31cad6f | |||
| b45aaaa177 | |||
| 6560496440 | |||
| 489dda8efb | |||
| 30c942d139 | |||
| c735e47e23 | |||
| 3856405c72 | |||
| 323479ca44 | |||
| c8bfe56975 | |||
| ab214b64f2 | |||
| fea673d93a | |||
| 5405151112 | |||
| b3c210ef24 | |||
| 5f5d74cfbd | |||
| c188fdcc8b | |||
| a3b43fc19b | |||
| 894a68acb6 | |||
| 30bc3fc412 |
@@ -19,7 +19,6 @@ machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
requirements_all.txt linguist-generated=true
|
||||
requirements_test_all.txt linguist-generated=true
|
||||
requirements_test_pre_commit.txt linguist-generated=true
|
||||
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||
.github/workflows/*.lock.yml linguist-generated=true
|
||||
|
||||
@@ -25,6 +25,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
|
||||
+55
-23
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1ad29b8fb97f5df4466be54051779a3188f094d7efb041a8ed55211eab33c5f5","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"75b8b624ba0c144fb4b28cba143d16a47c30de8afae568fa3256c6febe01a68a","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d3abfe96a194bce3a523ed2093ddedd5704cdf62","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
|
||||
# ___ _ _
|
||||
# / _ \ | | (_)
|
||||
@@ -65,7 +65,9 @@ run-name: "Check requirements (AW)"
|
||||
|
||||
jobs:
|
||||
activation:
|
||||
needs: pre_activation
|
||||
needs:
|
||||
- extract_pr_number
|
||||
- pre_activation
|
||||
# zizmor: ignore[dangerous-triggers] - workflow_run trigger is secured with role and fork validation
|
||||
if: >
|
||||
(needs.pre_activation.outputs.activated == 'true') && (github.event_name != 'workflow_run' || github.event.workflow_run.repository.id == github.repository_id &&
|
||||
@@ -189,20 +191,20 @@ jobs:
|
||||
run: |
|
||||
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
|
||||
{
|
||||
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
|
||||
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
|
||||
<system>
|
||||
GH_AW_PROMPT_bb296919e461941b_EOF
|
||||
GH_AW_PROMPT_198418d99edc7d5b_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
|
||||
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
|
||||
<safe-output-tools>
|
||||
Tools: add_comment, missing_tool, missing_data, noop
|
||||
</safe-output-tools>
|
||||
GH_AW_PROMPT_bb296919e461941b_EOF
|
||||
GH_AW_PROMPT_198418d99edc7d5b_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
|
||||
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
|
||||
<github-context>
|
||||
The following GitHub context information is available for this workflow:
|
||||
{{#if github.actor}}
|
||||
@@ -231,12 +233,12 @@ jobs:
|
||||
{{/if}}
|
||||
</github-context>
|
||||
|
||||
GH_AW_PROMPT_bb296919e461941b_EOF
|
||||
GH_AW_PROMPT_198418d99edc7d5b_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
|
||||
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
|
||||
</system>
|
||||
{{#runtime-import .github/workflows/check-requirements.md}}
|
||||
GH_AW_PROMPT_bb296919e461941b_EOF
|
||||
GH_AW_PROMPT_198418d99edc7d5b_EOF
|
||||
} > "$GH_AW_PROMPT"
|
||||
- name: Interpolate variables and render templates
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -314,7 +316,9 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
agent:
|
||||
needs: activation
|
||||
needs:
|
||||
- activation
|
||||
- extract_pr_number
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -385,11 +389,6 @@ jobs:
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/gh-aw/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- if: github.event.workflow_run.conclusion == 'success'
|
||||
name: Extract PR number from artifact
|
||||
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}"
|
||||
|
||||
- name: Configure Git credentials
|
||||
env:
|
||||
@@ -454,15 +453,15 @@ jobs:
|
||||
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
|
||||
mkdir -p /tmp/gh-aw/safeoutputs
|
||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF'
|
||||
{"add_comment":{"max":1,"target":"${{ env.PR_NUMBER }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF'
|
||||
{"add_comment":{"max":1,"target":"${{ needs.extract_pr_number.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF
|
||||
- name: Generate Safe Outputs Tools
|
||||
env:
|
||||
GH_AW_TOOLS_META_JSON: |
|
||||
{
|
||||
"description_suffixes": {
|
||||
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ env.PR_NUMBER }}. Supports reply_to_id for discussion threading."
|
||||
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
|
||||
},
|
||||
"repo_params": {},
|
||||
"dynamic_tools": []
|
||||
@@ -648,7 +647,7 @@ jobs:
|
||||
|
||||
mkdir -p /home/runner/.copilot
|
||||
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
|
||||
cat << GH_AW_MCP_CONFIG_d12799b4d7ffe5c2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
cat << GH_AW_MCP_CONFIG_175174907e5a28b4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
@@ -692,7 +691,7 @@ jobs:
|
||||
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
|
||||
}
|
||||
}
|
||||
GH_AW_MCP_CONFIG_d12799b4d7ffe5c2_EOF
|
||||
GH_AW_MCP_CONFIG_175174907e5a28b4_EOF
|
||||
- name: Mount MCP servers as CLIs
|
||||
id: mount-mcp-clis
|
||||
continue-on-error: true
|
||||
@@ -939,6 +938,7 @@ jobs:
|
||||
- activation
|
||||
- agent
|
||||
- detection
|
||||
- extract_pr_number
|
||||
- safe_outputs
|
||||
if: >
|
||||
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
|
||||
@@ -1283,6 +1283,37 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
extract_pr_number:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
outputs:
|
||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||
steps:
|
||||
- name: Configure GH_HOST for enterprise compatibility
|
||||
id: ghes-host-config
|
||||
shell: bash
|
||||
run: |
|
||||
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
|
||||
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
|
||||
GH_HOST="${GITHUB_SERVER_URL#https://}"
|
||||
GH_HOST="${GH_HOST#http://}"
|
||||
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
|
||||
- name: Download deterministic-results artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- name: Extract PR number from artifact
|
||||
id: extract
|
||||
run: |
|
||||
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
|
||||
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
pre_activation:
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
@@ -1321,6 +1352,7 @@ jobs:
|
||||
- activation
|
||||
- agent
|
||||
- detection
|
||||
- extract_pr_number
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
@@ -1393,7 +1425,7 @@ jobs:
|
||||
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,files.pythonhosted.org,github.com,host.docker.internal,pip.pypa.io,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,telemetry.enterprise.githubcopilot.com"
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_API_URL: ${{ github.api_url }}
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ env.PR_NUMBER }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.extract_pr_number.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
|
||||
with:
|
||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -19,7 +19,30 @@ tools:
|
||||
safe-outputs:
|
||||
add-comment:
|
||||
max: 1
|
||||
target: "${{ env.PR_NUMBER }}"
|
||||
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
|
||||
needs:
|
||||
- extract_pr_number
|
||||
jobs:
|
||||
extract_pr_number:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||
steps:
|
||||
- name: Download deterministic-results artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: check-requirements-deterministic
|
||||
path: /tmp/deterministic
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract PR number from artifact
|
||||
id: extract
|
||||
run: |
|
||||
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
|
||||
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
@@ -32,11 +55,6 @@ steps:
|
||||
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'
|
||||
@@ -80,10 +98,11 @@ The deterministic stage uploaded its results to the runner at
|
||||
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.
|
||||
already targeted at this PR (a pre-job extracts `pr_number` from the
|
||||
artifact and the workflow wires it into the safe-output config via
|
||||
`needs.extract_pr_number.outputs.pr_number`), so **you do not need to
|
||||
set `item_number` yourself** — just emit `add_comment` with the
|
||||
rendered body.
|
||||
- `needs_agent` — `true` iff any package's check needs resolution.
|
||||
- `packages[]` — one entry per changed package. Each entry has:
|
||||
- `name`, `old_version` (`null` for a newly added package; otherwise the
|
||||
@@ -161,9 +180,10 @@ Verify that the package's source repository is publicly reachable.
|
||||
- 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.
|
||||
package's `release_pipeline` and `async_blocking` cells/details as `—`
|
||||
(em dash) and explain `Skipped because the source repository is not
|
||||
publicly accessible.` — neither check can be performed without a public
|
||||
repo.
|
||||
|
||||
### Check kind: `pr_link`
|
||||
|
||||
@@ -239,6 +259,111 @@ host from `package.repo_url`, then apply the corresponding checklist.
|
||||
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
|
||||
inspected; hosting provider is not GitHub or GitLab.`
|
||||
|
||||
### Check kind: `async_blocking`
|
||||
|
||||
Verify whether the dependency performs blocking I/O inside async code
|
||||
paths. Home Assistant runs on a single asyncio event loop, so a library
|
||||
that exposes an `async` surface must not call blocking APIs from inside
|
||||
its `async def` functions — that stalls the whole loop. A purely sync
|
||||
library is fine: Home Assistant integrations are expected to wrap such
|
||||
calls in an executor.
|
||||
|
||||
**Two modes — pick by inspecting `package.old_version`:**
|
||||
|
||||
- `old_version` is `null` → **new package**: review the *entire current
|
||||
source tree*. Nothing about this dependency has been vetted before.
|
||||
- `old_version` is a string → **version bump**: review only the *diff
|
||||
between `old_version` and `new_version`*. The previous version was
|
||||
already accepted, so blocking calls that were present in
|
||||
`old_version` are not regressions; report only what `new_version`
|
||||
introduces.
|
||||
|
||||
#### Step 1 — Decide whether the library exposes an async surface
|
||||
|
||||
Use the `github` MCP tool (for `github.com` repos) or `web-fetch`
|
||||
(other hosts) on `package.repo_url`. Always inspect the tag /
|
||||
ref matching `new_version` (e.g. `v{new_version}` or `{new_version}`).
|
||||
|
||||
- Locate the top-level package directory (usually named after the
|
||||
import name, often equal or close to `package.name`).
|
||||
- Check `pyproject.toml` / `setup.py` / `setup.cfg` / `README*` for
|
||||
async indicators (`Framework :: AsyncIO` trove classifier, `asyncio`
|
||||
/ `aiohttp` / `httpx` / `anyio` in dependencies, an async usage
|
||||
example in the README).
|
||||
- Grep the package source for `async def`. A handful of `async def`
|
||||
entries in the public modules is enough to treat the library as
|
||||
having an async surface.
|
||||
|
||||
If the library is **sync-only** (no `async def` in its public modules
|
||||
and no async framework dependency) → ✅
|
||||
`Sync-only library; Home Assistant integrations must wrap calls in an
|
||||
executor.` *This verdict is the same in both modes.*
|
||||
|
||||
#### Step 2a — Mode: new package (`old_version` is `null`)
|
||||
|
||||
Inspect **every `async def` in the public modules** for blocking
|
||||
patterns. Walk transitively into helpers the async functions call.
|
||||
|
||||
#### Step 2b — Mode: version bump (`old_version` is a string)
|
||||
|
||||
Fetch the diff between the two tags and review **only changed lines**:
|
||||
|
||||
- GitHub: `GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag}` via
|
||||
the `github` MCP tool, or
|
||||
`https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff`
|
||||
via `web-fetch`. Try the common tag formats in order until one
|
||||
resolves: `v{version}`, `{version}`, `release-{version}`.
|
||||
- GitLab: `https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff`.
|
||||
- Other hosts: use the project's equivalent compare URL via
|
||||
`web-fetch`.
|
||||
|
||||
If neither tag format resolves on the host, fall back to a full review
|
||||
(Step 2a) and mention in the detail that the diff was unavailable.
|
||||
|
||||
When reviewing the diff, only flag blocking patterns that appear in
|
||||
**added lines** *inside or reachable from* an `async def`. A blocking
|
||||
call that existed in `old_version` and is unchanged is not a regression
|
||||
for this bump.
|
||||
|
||||
#### Step 3 — Blocking patterns to look for
|
||||
|
||||
In both modes, the patterns to flag inside `async def` bodies are:
|
||||
|
||||
- Sync HTTP: `requests.`, `urllib.request`, `urllib3.` direct use,
|
||||
`http.client.`, sync `httpx.Client(` / `httpx.get(` (NOT the
|
||||
`AsyncClient`), `pycurl`.
|
||||
- `time.sleep(` (must be `await asyncio.sleep(`).
|
||||
- Sync sockets: bare `socket.socket` reads/writes, `ssl.wrap_socket`,
|
||||
blocking `select.select`.
|
||||
- File I/O: `open(` / `pathlib.Path.read_*` / `.write_*` for
|
||||
non-trivial sizes (small one-shot reads during import are
|
||||
acceptable; reads/writes on the request path are not — prefer
|
||||
`aiofiles` / executor).
|
||||
- Sync DB drivers used directly: `sqlite3`, `psycopg2`, `pymysql`,
|
||||
`pymongo` (sync client), `redis.Redis` (sync client).
|
||||
- `subprocess.run` / `subprocess.call` / `os.system` (must be
|
||||
`asyncio.create_subprocess_*`).
|
||||
|
||||
A call that is clearly dispatched to an executor
|
||||
(`run_in_executor`, `asyncio.to_thread`, `anyio.to_thread.run_sync`)
|
||||
does NOT count as blocking.
|
||||
|
||||
#### Step 4 — Verdict
|
||||
|
||||
- ✅ — no offending blocking pattern in the surface being reviewed
|
||||
(whole tree for a new package, added lines for a bump). For a bump,
|
||||
phrase the detail as `No new blocking calls introduced in
|
||||
{old_version} → {new_version}.`.
|
||||
- ⚠️ — blocking calls exist only in sync helpers that the async API
|
||||
does not call, or only on a clearly non-hot path (e.g. one-shot
|
||||
setup before the event loop is running). Cite at least one
|
||||
`<file>:<line>` and explain why it is not on the hot path.
|
||||
- ❌ — a blocking call is reachable from an `async def` that is part
|
||||
of the public API on the request / polling path (for a bump: the
|
||||
call was introduced or moved onto the hot path by this version).
|
||||
Cite the offending `<file>:<line>` as a clickable link on the repo
|
||||
host so the contributor can jump to it.
|
||||
|
||||
## Notes
|
||||
|
||||
- Be constructive and helpful. Reference the inspected workflow / CI
|
||||
|
||||
Vendored
+3
-3
@@ -132,7 +132,7 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"label": "Install all production Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_all.txt",
|
||||
"group": {
|
||||
@@ -146,9 +146,9 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all Test Requirements",
|
||||
"label": "Install all (test & production) Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
|
||||
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
@@ -15,6 +15,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -1413,8 +1413,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pushover/ @engrbm87
|
||||
/homeassistant/components/pvoutput/ @frenck
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/homeassistant/components/pyload/ @tr4nt0r
|
||||
/tests/components/pyload/ @tr4nt0r
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
@@ -1538,8 +1538,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
/homeassistant/components/samsung_infrared/ @lmaertin
|
||||
/tests/components/samsung_infrared/ @lmaertin
|
||||
/homeassistant/components/samsungtv/ @chemelli74 @epenet
|
||||
/tests/components/samsungtv/ @chemelli74 @epenet
|
||||
/homeassistant/components/samsungtv/ @chemelli74
|
||||
/tests/components/samsungtv/ @chemelli74
|
||||
/homeassistant/components/sanix/ @tomaszsluszniak
|
||||
/tests/components/sanix/ @tomaszsluszniak
|
||||
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||
|
||||
@@ -134,7 +134,7 @@ class AuthManagerFlowManager(
|
||||
"""
|
||||
flow = cast(LoginFlow, flow)
|
||||
|
||||
if result["type"] != FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not FlowResultType.CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
|
||||
@@ -5,8 +5,12 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.components.hassio import HassioNotReadyError
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -49,6 +53,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
_DATA_SNAPSHOTS_URL: HassKey[str | None] = HassKey(f"{DOMAIN}_snapshots_url")
|
||||
|
||||
LABS_SNAPSHOT_FEATURE = "snapshots"
|
||||
|
||||
@@ -57,18 +62,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
analytics_config = config.get(DOMAIN, {})
|
||||
|
||||
snapshots_url: str | None = None
|
||||
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||
await labs.async_update_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
|
||||
)
|
||||
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||
else:
|
||||
snapshots_url = None
|
||||
|
||||
hass.data[_DATA_SNAPSHOTS_URL] = snapshots_url
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Analytics from a config entry."""
|
||||
snapshots_url = hass.data.get(_DATA_SNAPSHOTS_URL)
|
||||
analytics = Analytics(hass, snapshots_url)
|
||||
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
try:
|
||||
await analytics.load()
|
||||
except HassioNotReadyError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="supervisor_not_ready",
|
||||
) from err
|
||||
|
||||
started = False
|
||||
|
||||
@@ -80,26 +106,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
async def start_schedule(hass: HomeAssistant) -> None:
|
||||
"""Start the send schedule once Home Assistant has started."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
entry.async_on_unload(
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
)
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
entry.async_on_unload(async_at_started(hass, start_schedule))
|
||||
|
||||
hass.data[DATA_COMPONENT] = analytics
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload an Analytics config entry."""
|
||||
analytics = hass.data.pop(DATA_COMPONENT)
|
||||
analytics.cancel_scheduled()
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "analytics"})
|
||||
@@ -109,7 +139,9 @@ def websocket_analytics(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return analytics preferences."""
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
||||
@@ -130,8 +162,10 @@ async def websocket_analytics_preferences(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update analytics preferences."""
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
preferences = msg[ATTR_PREFERENCES]
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
|
||||
await analytics.save_preferences(preferences)
|
||||
await analytics.async_schedule()
|
||||
|
||||
@@ -299,12 +299,8 @@ class Analytics:
|
||||
self._data = AnalyticsData.from_dict(stored)
|
||||
|
||||
if self.supervisor and not self.onboarded:
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable
|
||||
# during setup of the Supervisor integration. That will fail setup
|
||||
# of this integration. However there is no better option at this time
|
||||
# since we need to get the diagnostic setting from Supervisor to correctly
|
||||
# setup this integration and we can't raise ConfigEntryNotReady to
|
||||
# trigger a retry from async_setup.
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable.
|
||||
# The caller is responsible for handling this and triggering a retry.
|
||||
supervisor_info = hassio.get_supervisor_info(self._hass)
|
||||
|
||||
# User have not configured analytics, get this setting from the supervisor
|
||||
@@ -349,8 +345,7 @@ class Analytics:
|
||||
await self._save()
|
||||
|
||||
if self.supervisor:
|
||||
# get_supervisor_info was called during setup so we can't get here
|
||||
# if it raised. The others may raise HassioNotReadyError if only some
|
||||
# The others may raise HassioNotReadyError if only some
|
||||
# data was successfully fetched from Supervisor
|
||||
supervisor_info = hassio.get_supervisor_info(hass)
|
||||
with contextlib.suppress(hassio.HassioNotReadyError):
|
||||
@@ -630,6 +625,16 @@ class Analytics:
|
||||
err,
|
||||
)
|
||||
|
||||
@callback
|
||||
def cancel_scheduled(self) -> None:
|
||||
"""Cancel all scheduled analytics tasks."""
|
||||
if self._basic_scheduled is not None:
|
||||
self._basic_scheduled()
|
||||
self._basic_scheduled = None
|
||||
if self._snapshot_scheduled is not None:
|
||||
self._snapshot_scheduled()
|
||||
self._snapshot_scheduled = None
|
||||
|
||||
async def async_schedule(self) -> None:
|
||||
"""Schedule analytics."""
|
||||
if not self.onboarded:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Config flow for Analytics integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Analytics."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_system(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
return self.async_create_entry(title="Analytics", data={})
|
||||
@@ -3,6 +3,8 @@
|
||||
"name": "Analytics",
|
||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": true,
|
||||
"single_config_entry": true,
|
||||
"dependencies": ["api", "websocket_api", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
"integration_type": "system",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"supervisor_not_ready": {
|
||||
"message": "Supervisor was not ready during setup, will retry"
|
||||
}
|
||||
},
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) – never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
|
||||
|
||||
@@ -226,7 +226,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Set initial options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.17"]
|
||||
"requirements": ["py-aosmith==1.0.18"]
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
supports_vacation_mode = any(
|
||||
supported_mode.mode == AOSmithOperationMode.VACATION
|
||||
supported_mode.mode is AOSmithOperationMode.VACATION
|
||||
for supported_mode in self.device.supported_modes
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return True if away mode is on."""
|
||||
return self.device.status.current_mode == AOSmithOperationMode.VACATION
|
||||
return self.device.status.current_mode is AOSmithOperationMode.VACATION
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new target operation mode."""
|
||||
|
||||
@@ -369,7 +369,7 @@ class AppleTVManager(DeviceListener):
|
||||
|
||||
attrs[ATTR_MODEL] = (
|
||||
dev_info.raw_model
|
||||
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
|
||||
else model_str(dev_info.model)
|
||||
)
|
||||
attrs[ATTR_SW_VERSION] = dev_info.version
|
||||
|
||||
@@ -63,7 +63,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
|
||||
# Listen to keyboard updates
|
||||
atv.keyboard.listener = self
|
||||
# Set initial state based on current focus state
|
||||
self._update_state(atv.keyboard.text_focus_state == KeyboardFocusState.Focused)
|
||||
self._update_state(atv.keyboard.text_focus_state is KeyboardFocusState.Focused)
|
||||
|
||||
@callback
|
||||
def async_device_disconnected(self) -> None:
|
||||
@@ -78,7 +78,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
|
||||
|
||||
This is a callback function from pyatv.interface.KeyboardListener.
|
||||
"""
|
||||
self._update_state(new_state == KeyboardFocusState.Focused)
|
||||
self._update_state(new_state is KeyboardFocusState.Focused)
|
||||
|
||||
def _update_state(self, new_state: bool) -> None:
|
||||
"""Update and report."""
|
||||
|
||||
@@ -354,7 +354,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"name": self.atv.name,
|
||||
"type": (
|
||||
dev_info.raw_model
|
||||
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
|
||||
else model_str(dev_info.model)
|
||||
),
|
||||
}
|
||||
@@ -441,12 +441,12 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_password()
|
||||
|
||||
# Figure out, depending on protocol, what kind of pairing is needed
|
||||
if service.pairing == PairingRequirement.Unsupported:
|
||||
if service.pairing is PairingRequirement.Unsupported:
|
||||
_LOGGER.debug("%s does not support pairing", self.protocol)
|
||||
return await self.async_pair_next_protocol()
|
||||
if service.pairing == PairingRequirement.Disabled:
|
||||
if service.pairing is PairingRequirement.Disabled:
|
||||
return await self.async_step_protocol_disabled()
|
||||
if service.pairing == PairingRequirement.NotNeeded:
|
||||
if service.pairing is PairingRequirement.NotNeeded:
|
||||
_LOGGER.debug("%s does not require pairing", self.protocol)
|
||||
self.credentials[self.protocol.value] = None
|
||||
return await self.async_pair_next_protocol()
|
||||
@@ -457,7 +457,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
pair_args: dict[str, Any] = {}
|
||||
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
|
||||
pair_args["name"] = "Home Assistant"
|
||||
if self.protocol == Protocol.DMAP:
|
||||
if self.protocol is Protocol.DMAP:
|
||||
pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
|
||||
|
||||
# Initiate the pairing process
|
||||
|
||||
@@ -139,7 +139,7 @@ class AppleTvMediaPlayer(
|
||||
all_features = atv.features.all_features()
|
||||
for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items():
|
||||
feature_info = all_features.get(feature_name)
|
||||
if feature_info and feature_info.state != FeatureState.Unsupported:
|
||||
if feature_info and feature_info.state is not FeatureState.Unsupported:
|
||||
self._attr_supported_features |= support_flag
|
||||
|
||||
# No need to schedule state update here as that will happen when the first
|
||||
@@ -188,14 +188,14 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.OFF
|
||||
if (
|
||||
self._is_feature_available(FeatureName.PowerState)
|
||||
and self.atv.power.power_state == PowerState.Off
|
||||
and self.atv.power.power_state is PowerState.Off
|
||||
):
|
||||
return MediaPlayerState.OFF
|
||||
if self._playing:
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
return MediaPlayerState.IDLE
|
||||
if state == DeviceState.Playing:
|
||||
if state is DeviceState.Playing:
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
@@ -446,7 +446,7 @@ class AppleTvMediaPlayer(
|
||||
def shuffle(self) -> bool | None:
|
||||
"""Boolean if shuffle is enabled."""
|
||||
if self._playing and self._is_feature_available(FeatureName.Shuffle):
|
||||
return self._playing.shuffle != ShuffleState.Off
|
||||
return self._playing.shuffle is not ShuffleState.Off
|
||||
return None
|
||||
|
||||
def _is_feature_available(self, feature: FeatureName) -> bool:
|
||||
@@ -506,7 +506,7 @@ class AppleTvMediaPlayer(
|
||||
and (self._is_feature_available(FeatureName.TurnOff))
|
||||
and (
|
||||
not self._is_feature_available(FeatureName.PowerState)
|
||||
or self.atv.power.power_state == PowerState.On
|
||||
or self.atv.power.power_state is PowerState.On
|
||||
)
|
||||
):
|
||||
await self.atv.power.turn_off()
|
||||
|
||||
@@ -59,7 +59,7 @@ def _check_keyboard_focus(atv: AppleTVInterface) -> None:
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_available",
|
||||
) from err
|
||||
if focus_state != KeyboardFocusState.Focused:
|
||||
if focus_state is not KeyboardFocusState.Focused:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_focused",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Config flow to configure the Arcam FMJ component."""
|
||||
|
||||
import socket
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from arcam.fmj.client import Client, ConnectionFailed
|
||||
from arcam.fmj import ConnectionFailed
|
||||
from arcam.fmj.client import Client
|
||||
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -29,26 +31,19 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
|
||||
|
||||
async def _async_check_and_create(self, host: str, port: int) -> ConfigFlowResult:
|
||||
async def _async_try_connect(self, host: str, port: int) -> None:
|
||||
"""Verify the device is reachable."""
|
||||
client = Client(host, port)
|
||||
try:
|
||||
await client.start()
|
||||
except ConnectionFailed:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"{DEFAULT_NAME} ({host})",
|
||||
data={CONF_HOST: host, CONF_PORT: port},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
uuid = await get_uniqueid_from_host(
|
||||
async_get_clientsession(self.hass), user_input[CONF_HOST]
|
||||
@@ -58,18 +53,36 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
user_input[CONF_HOST], user_input[CONF_PORT], uuid
|
||||
)
|
||||
|
||||
return await self._async_check_and_create(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
try:
|
||||
await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
except socket.gaierror:
|
||||
errors["base"] = "invalid_host"
|
||||
except TimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except ConnectionRefusedError:
|
||||
errors["base"] = "connection_refused"
|
||||
except ConnectionFailed, OSError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})",
|
||||
data={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
},
|
||||
)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
schema = vol.Schema(fields)
|
||||
if user_input is not None:
|
||||
schema = self.add_suggested_values_to_schema(schema, user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -79,7 +92,10 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.context["title_placeholders"] = placeholders
|
||||
|
||||
if user_input is not None:
|
||||
return await self._async_check_and_create(self.host, self.port)
|
||||
return self.async_create_entry(
|
||||
title=f"{DEFAULT_NAME} ({self.host})",
|
||||
data={CONF_HOST: self.host, CONF_PORT: self.port},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm", description_placeholders=placeholders
|
||||
@@ -97,6 +113,11 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
await self._async_set_unique_id_and_update(host, port, uuid)
|
||||
|
||||
try:
|
||||
await self._async_try_connect(host, port)
|
||||
except ConnectionFailed, OSError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
self.host = host
|
||||
self.port = DEFAULT_PORT
|
||||
self.port = port
|
||||
return await self.async_step_confirm()
|
||||
|
||||
@@ -263,9 +263,9 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
source = self._state.get_source()
|
||||
if source == SourceCodes.DAB:
|
||||
if source is SourceCodes.DAB:
|
||||
value = self._state.get_dab_station()
|
||||
elif source == SourceCodes.FM:
|
||||
elif source is SourceCodes.FM:
|
||||
value = self._state.get_rds_information()
|
||||
else:
|
||||
value = None
|
||||
@@ -274,7 +274,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media, music track only."""
|
||||
if self._state.get_source() == SourceCodes.DAB:
|
||||
if self._state.get_source() is SourceCodes.DAB:
|
||||
value = self._state.get_dls_pdt()
|
||||
else:
|
||||
value = None
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"connection_refused": "Host refused connection",
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
},
|
||||
"flow_title": "{host}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
|
||||
@@ -1355,7 +1355,7 @@ class PipelineRun:
|
||||
) -> bool:
|
||||
"""Return true if all targeted entities were in the same area as the device."""
|
||||
if (
|
||||
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
|
||||
intent_response.response_type is not intent.IntentResponseType.ACTION_DONE
|
||||
or not intent_response.matched_states
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -251,12 +251,12 @@ class AuthProvidersView(HomeAssistantView):
|
||||
|
||||
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return {
|
||||
key: val for key, val in result.items() if key not in ("result", "data")
|
||||
}
|
||||
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = dict(result)
|
||||
@@ -289,11 +289,11 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
result: AuthFlowResult,
|
||||
) -> web.Response:
|
||||
"""Convert the flow result to a response."""
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200.
|
||||
# We need to manually log failed login attempts.
|
||||
if (
|
||||
result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
and (errors := result.get("errors"))
|
||||
and errors.get("base")
|
||||
in (
|
||||
|
||||
@@ -142,9 +142,9 @@ def websocket_depose_mfa(
|
||||
|
||||
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return dict(result)
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = dict(result)
|
||||
|
||||
@@ -273,7 +273,7 @@ async def ws_subscribe_scanner_details(
|
||||
|
||||
def _async_registration_changed(registration: HaScannerRegistration) -> None:
|
||||
added_event = HaScannerRegistrationEvent.ADDED
|
||||
event_type = "add" if registration.event == added_event else "remove"
|
||||
event_type = "add" if registration.event is added_event else "remove"
|
||||
_async_event_message({event_type: [registration.scanner.details]})
|
||||
|
||||
manager = _get_manager(hass)
|
||||
|
||||
@@ -185,7 +185,6 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
|
||||
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,
|
||||
translation_key="set_data_error",
|
||||
) from err
|
||||
|
||||
@@ -158,7 +158,10 @@ def process_service_info(
|
||||
)
|
||||
|
||||
# If payload is encrypted and the bindkey is not verified then we need to reauth
|
||||
if data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified:
|
||||
if (
|
||||
data.encryption_scheme is not EncryptionScheme.NONE
|
||||
and not data.bindkey_verified
|
||||
):
|
||||
entry.async_start_reauth(hass, data={"device": data})
|
||||
|
||||
return update
|
||||
|
||||
@@ -59,7 +59,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info = discovery_info
|
||||
self._discovered_device = device
|
||||
|
||||
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
@@ -125,7 +125,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info = discovery.discovery_info
|
||||
self._discovered_device = discovery.device
|
||||
|
||||
if discovery.device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if discovery.device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
|
||||
return self._async_get_or_create_entry()
|
||||
@@ -164,7 +164,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._discovery_info = device.last_service_info
|
||||
|
||||
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
|
||||
# Otherwise there wasn't actually encryption so abort
|
||||
|
||||
@@ -45,6 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
|
||||
# on some other unexpected server response.
|
||||
_LOGGER.warning("Unexpected CalDAV server response: %s", err)
|
||||
return False
|
||||
except requests.Timeout as err:
|
||||
raise ConfigEntryNotReady("Timeout connecting to CalDAV server") from err
|
||||
except requests.ConnectionError as err:
|
||||
raise ConfigEntryNotReady("Connection error from CalDAV server") from err
|
||||
except DAVError as err:
|
||||
|
||||
@@ -38,6 +38,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import CalDavConfigEntry
|
||||
from .api import async_get_calendars
|
||||
from .const import TIMEOUT
|
||||
from .coordinator import CalDavUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -91,7 +92,12 @@ async def async_setup_platform(
|
||||
days = config[CONF_DAYS]
|
||||
|
||||
client = caldav.DAVClient(
|
||||
url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL]
|
||||
url,
|
||||
None,
|
||||
username,
|
||||
password,
|
||||
ssl_verify_cert=config[CONF_VERIFY_SSL],
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
|
||||
calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT)
|
||||
@@ -231,7 +237,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.coordinator.calendar.add_event, **item_data),
|
||||
)
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
@callback
|
||||
|
||||
@@ -138,7 +138,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
)
|
||||
# refreshing async otherwise it would take too much time
|
||||
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
@@ -150,7 +150,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
)
|
||||
except NotFoundError as err:
|
||||
raise HomeAssistantError(f"Could not find To-do item {uid}") from err
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||
vtodo["SUMMARY"] = item.summary or ""
|
||||
@@ -174,7 +174,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
)
|
||||
# refreshing async otherwise it would take too much time
|
||||
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
@@ -188,14 +188,14 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
items = await asyncio.gather(*tasks)
|
||||
except NotFoundError as err:
|
||||
raise HomeAssistantError("Could not find To-do item") from err
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||
|
||||
# Run serially as some CalDAV servers do not support concurrent modifications
|
||||
for item in items:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(item.delete)
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV delete error: {err}") from err
|
||||
# refreshing async otherwise it would take too much time
|
||||
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Config flow for the Cert Expiry platform."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
@@ -19,8 +18,6 @@ from .errors import (
|
||||
)
|
||||
from .helper import get_cert_expiry_timestamp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
@@ -75,9 +72,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=title,
|
||||
data={CONF_HOST: host, CONF_PORT: port},
|
||||
)
|
||||
if self.source == SOURCE_IMPORT:
|
||||
_LOGGER.error("Config import failed for %s", user_input[CONF_HOST])
|
||||
return self.async_abort(reason="import_failed")
|
||||
else:
|
||||
user_input = {}
|
||||
user_input[CONF_HOST] = ""
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"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%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -110,7 +110,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
if self._area.human_status is AlarmAreaState.UNKNOWN:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -124,7 +124,7 @@ class ComelitAlarmEntity(
|
||||
self._area.human_status,
|
||||
self._area.armed,
|
||||
)
|
||||
if self._area.human_status == AlarmAreaState.ARMED:
|
||||
if self._area.human_status is AlarmAreaState.ARMED:
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
|
||||
return AlarmControlPanelState.ARMED_AWAY
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
|
||||
|
||||
@@ -43,7 +43,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: cast(ComelitVedoAreaObject, obj).anomaly,
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoAreaObject, obj).human_status != AlarmAreaState.UNKNOWN
|
||||
cast(ComelitVedoAreaObject, obj).human_status is not AlarmAreaState.UNKNOWN
|
||||
),
|
||||
),
|
||||
ComelitBinarySensorEntityDescription(
|
||||
@@ -67,7 +67,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
object_type=ALARM_ZONE,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status == AlarmZoneState.FAULTY
|
||||
cast(ComelitVedoZoneObject, obj).human_status is AlarmZoneState.FAULTY
|
||||
),
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status
|
||||
|
||||
@@ -166,12 +166,12 @@ class ComelitVedoSensorEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Sensor availability."""
|
||||
return self._zone_object.human_status != AlarmZoneState.UNAVAILABLE
|
||||
return self._zone_object.human_status is not AlarmZoneState.UNAVAILABLE
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Sensor value."""
|
||||
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
|
||||
if (status := self._zone_object.human_status) is AlarmZoneState.UNKNOWN:
|
||||
return None
|
||||
|
||||
return cast(str, status.value)
|
||||
|
||||
@@ -148,7 +148,7 @@ def _prepare_config_flow_result_json(
|
||||
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return prepare_result_json(result)
|
||||
|
||||
data = {key: val for key, val in result.items() if key not in ("data", "context")}
|
||||
|
||||
@@ -646,7 +646,7 @@ class DefaultAgent(ConversationEntity):
|
||||
cache_value = self._intent_cache.get(cache_key)
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
cache_value.stage is IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
):
|
||||
_LOGGER.debug("Got cached result for exposed entities")
|
||||
return cache_value.result
|
||||
@@ -686,7 +686,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unexposed_entities_match = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
cache_value.stage is IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for all entities")
|
||||
return cache_value.result
|
||||
@@ -731,7 +731,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unknown_names = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES
|
||||
cache_value.stage is IntentMatchingStage.UNKNOWN_NAMES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for unknown names")
|
||||
return cache_value.result
|
||||
@@ -1447,7 +1447,7 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
response = await self._async_process_intent_result(result, user_input, chat_log)
|
||||
if (
|
||||
response.response_type == intent.IntentResponseType.ERROR
|
||||
response.response_type is intent.IntentResponseType.ERROR
|
||||
and response.error_code
|
||||
not in (
|
||||
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
|
||||
@@ -1546,7 +1546,7 @@ def _get_match_error_response(
|
||||
# device_class only
|
||||
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
|
||||
|
||||
if (reason == intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
if (reason is intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
domain = next(iter(constraints.domains)) # first domain
|
||||
if constraints.area_name:
|
||||
# domain in area
|
||||
@@ -1565,7 +1565,7 @@ def _get_match_error_response(
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
if reason == intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if reason is intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if constraints.floor_name:
|
||||
# duplicate on floor
|
||||
return ErrorKey.DUPLICATE_ENTITIES_IN_FLOOR, {
|
||||
@@ -1582,26 +1582,26 @@ def _get_match_error_response(
|
||||
|
||||
return ErrorKey.DUPLICATE_ENTITIES, {"entity": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.INVALID_AREA:
|
||||
if reason is intent.MatchFailedReason.INVALID_AREA:
|
||||
# Invalid area name
|
||||
return ErrorKey.NO_AREA, {"area": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.INVALID_FLOOR:
|
||||
if reason is intent.MatchFailedReason.INVALID_FLOOR:
|
||||
# Invalid floor name
|
||||
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.FEATURE:
|
||||
if reason is intent.MatchFailedReason.FEATURE:
|
||||
# Feature not supported by entity
|
||||
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
|
||||
|
||||
if reason == intent.MatchFailedReason.STATE:
|
||||
if reason is intent.MatchFailedReason.STATE:
|
||||
# Entity is not in correct state
|
||||
assert constraints.states
|
||||
state = next(iter(constraints.states))
|
||||
|
||||
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
|
||||
|
||||
if reason == intent.MatchFailedReason.ASSISTANT:
|
||||
if reason is intent.MatchFailedReason.ASSISTANT:
|
||||
# Not exposed
|
||||
if constraints.name:
|
||||
if constraints.area_name:
|
||||
|
||||
@@ -80,7 +80,7 @@ async def async_validate_device_automation_config(
|
||||
# the checks below which look for a config entry matching the device automation
|
||||
# domain
|
||||
if (
|
||||
automation_type == DeviceAutomationType.ACTION
|
||||
automation_type is DeviceAutomationType.ACTION
|
||||
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
|
||||
):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config_entry import ( # noqa: F401
|
||||
DATA_COMPONENT,
|
||||
BaseScannerEntity,
|
||||
BaseTrackerEntity,
|
||||
ScannerEntity,
|
||||
ScannerEntityDescription,
|
||||
TrackerEntity,
|
||||
@@ -33,6 +40,8 @@ from .const import ( # noqa: F401
|
||||
DEFAULT_TRACK_NEW,
|
||||
DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
LOGGER,
|
||||
PLATFORM_TYPE_LEGACY,
|
||||
SCAN_INTERVAL,
|
||||
SourceType,
|
||||
)
|
||||
@@ -44,7 +53,9 @@ from .legacy import ( # noqa: F401
|
||||
SOURCE_TYPES,
|
||||
AsyncSeeCallback,
|
||||
DeviceScanner,
|
||||
DeviceTracker,
|
||||
SeeCallback,
|
||||
async_create_platform_type,
|
||||
async_setup_integration as async_setup_legacy_integration,
|
||||
see,
|
||||
)
|
||||
@@ -57,5 +68,43 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the device tracker."""
|
||||
async_setup_legacy_integration(hass, config)
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
|
||||
LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
component.config = {}
|
||||
component.register_shutdown()
|
||||
|
||||
# The tracker is loaded in the async_setup_legacy_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
if platform.type != PLATFORM_TYPE_LEGACY:
|
||||
await component.async_setup_platform(p_type, {}, info)
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
async_setup_legacy_integration(hass, config, tracker_future),
|
||||
eager_start=True,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -37,11 +37,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_time_interval,
|
||||
async_track_utc_time_change,
|
||||
@@ -204,40 +200,7 @@ def see(
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Set up the legacy integration."""
|
||||
# The tracker is loaded in the _async_setup_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
_async_setup_integration(hass, config, tracker_future), eager_start=True
|
||||
)
|
||||
|
||||
|
||||
async def _async_setup_integration(
|
||||
async def async_setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
tracker_future: asyncio.Future[DeviceTracker],
|
||||
|
||||
@@ -261,7 +261,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -281,7 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
await self._device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -290,7 +290,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
@@ -718,7 +718,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
# If already playing, or don't want to autoplay, no need to call Play
|
||||
autoplay = extra.get("autoplay", True)
|
||||
if self._device.transport_state == TransportState.PLAYING or not autoplay:
|
||||
if self._device.transport_state is TransportState.PLAYING or not autoplay:
|
||||
return
|
||||
|
||||
# Play it
|
||||
@@ -748,7 +748,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
|
||||
@@ -782,10 +782,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.REPEAT_ONE:
|
||||
if play_mode is PlayMode.REPEAT_ONE:
|
||||
return RepeatMode.ONE
|
||||
|
||||
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
|
||||
|
||||
@@ -236,7 +236,7 @@ class DmsDeviceSource:
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -258,7 +258,7 @@ class DmsDeviceSource:
|
||||
await self.device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -267,7 +267,7 @@ class DmsDeviceSource:
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
from pycares import AresError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
@@ -78,11 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
errors = [
|
||||
result
|
||||
for result in results
|
||||
if isinstance(
|
||||
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
|
||||
)
|
||||
result for result in results if isinstance(result, (TimeoutError, DNSError))
|
||||
]
|
||||
if errors and len(errors) == len(results):
|
||||
await _close_resolvers()
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"download_dir": "Download directory"
|
||||
},
|
||||
"data_description": {
|
||||
"download_dir": "The directory where downloaded files will be stored. This can be an absolute path or a path relative to the Home Assistant configuration directory."
|
||||
},
|
||||
"description": "Select a location to get to store downloads. The setup will check if the directory exists."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class EcobeeBaseEntity(Entity):
|
||||
"""Base methods for Ecobee entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
|
||||
"""Initiate base methods for Ecobee entities."""
|
||||
self.data = data
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"entity": {
|
||||
"number": {
|
||||
"fan_min_on_time": {
|
||||
"default": "mdi:fan-clock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"create_vacation": {
|
||||
"service": "mdi:umbrella-beach"
|
||||
|
||||
@@ -24,7 +24,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
|
||||
"""Implement the notification entity for the Ecobee thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
@@ -74,6 +74,10 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
EcobeeFanMinOnTime(data, index) for index in range(len(data.ecobee.thermostats))
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -86,7 +90,6 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
|
||||
_attr_native_max_value = 60
|
||||
_attr_native_step = 5
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -130,7 +133,6 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
|
||||
"""
|
||||
|
||||
_attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:thermometer-off"
|
||||
_attr_mode = NumberMode.BOX
|
||||
_attr_native_min_value = -25
|
||||
@@ -165,3 +167,39 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
|
||||
"""Set new compressor minimum temperature."""
|
||||
self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value)
|
||||
self.update_without_throttle = True
|
||||
|
||||
|
||||
class EcobeeFanMinOnTime(EcobeeBaseEntity, NumberEntity):
|
||||
"""Minimum minutes per hour that the fan must run on an ecobee thermostat."""
|
||||
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 60
|
||||
_attr_native_step = 5
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
_attr_translation_key = "fan_min_on_time"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: EcobeeData,
|
||||
thermostat_index: int,
|
||||
) -> None:
|
||||
"""Initialize ecobee fan minimum on time."""
|
||||
super().__init__(data, thermostat_index)
|
||||
self._attr_unique_id = f"{self.base_unique_id}_fan_min_on_time"
|
||||
self.update_without_throttle = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self.update_without_throttle:
|
||||
await self.data.update(no_throttle=True)
|
||||
self.update_without_throttle = False
|
||||
else:
|
||||
await self.data.update()
|
||||
self._attr_native_value = self.thermostat["settings"]["fanMinOnTime"]
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Set new fan minimum on time value."""
|
||||
step = self._attr_native_step
|
||||
aligned_value = int(round(value / step) * step)
|
||||
self.data.ecobee.set_fan_min_on_time(self.thermostat_index, aligned_value)
|
||||
self.update_without_throttle = True
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"compressor_protection_min_temp": {
|
||||
"name": "Compressor minimum temperature"
|
||||
},
|
||||
"fan_min_on_time": {
|
||||
"name": "Fan minimum on time"
|
||||
},
|
||||
"ventilator_min_type_away": {
|
||||
"name": "Ventilator minimum time away"
|
||||
},
|
||||
|
||||
@@ -53,7 +53,6 @@ async def async_setup_entry(
|
||||
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
"""Represent 20 min timer for an ecobee thermostat with ventilator."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Ventilator 20m Timer"
|
||||
|
||||
def __init__(
|
||||
@@ -104,7 +103,6 @@ class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
class EcobeeSwitchAuxHeatOnly(EcobeeBaseEntity, SwitchEntity):
|
||||
"""Representation of a aux_heat_only ecobee switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "aux_heat_only"
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -31,6 +31,7 @@ class EkeyEvent(EventEntity):
|
||||
|
||||
_attr_device_class = EventDeviceClass.BUTTON
|
||||
_attr_event_types = ["event happened"]
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -150,9 +150,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
|
||||
|
||||
raw_pcm_compatible = (
|
||||
metadata.codec == AudioCodecs.PCM
|
||||
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel == AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate == AudioBitRates.BITRATE_16
|
||||
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel is AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate is AudioBitRates.BITRATE_16
|
||||
)
|
||||
if raw_pcm_compatible:
|
||||
file_format = "pcm_s16le_16"
|
||||
|
||||
@@ -50,5 +50,5 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
# Zone in NORMAL state is OFF; any other state is ON
|
||||
self._attr_is_on = bool(
|
||||
self._element.logical_status != ZoneLogicalStatus.NORMAL
|
||||
self._element.logical_status is not ZoneLogicalStatus.NORMAL
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
ThermostatMode.EMERGENCY_HEAT,
|
||||
):
|
||||
return self._element.heat_setpoint
|
||||
if self._element.mode == ThermostatMode.COOL:
|
||||
if self._element.mode is ThermostatMode.COOL:
|
||||
return self._element.cool_setpoint
|
||||
return None
|
||||
|
||||
@@ -162,6 +162,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode]
|
||||
if (
|
||||
self._attr_hvac_mode == HVACMode.OFF
|
||||
and self._element.fan == ThermostatFan.ON
|
||||
and self._element.fan is ThermostatFan.ON
|
||||
):
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
|
||||
@@ -56,7 +56,7 @@ class ElkNumberSetting(ElkAttachedEntity, NumberEntity):
|
||||
def __init__(self, element: Setting, elk: Any, elk_data: ELKM1Data) -> None:
|
||||
"""Initialize the number setting."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
if element.value_format == SettingFormat.TIMER:
|
||||
if element.value_format is SettingFormat.TIMER:
|
||||
self._attr_device_class = NumberDeviceClass.DURATION
|
||||
self._attr_native_unit_of_measurement = UnitOfTime.SECONDS
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
for setting in elk.settings:
|
||||
setting = cast(Setting, setting)
|
||||
domain = (
|
||||
"time" if setting.value_format == SettingFormat.TIME_OF_DAY else "number"
|
||||
"time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
|
||||
)
|
||||
|
||||
orig_unique_id = generate_unique_id(elk_data.prefix, setting)
|
||||
@@ -288,7 +288,7 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def temperature_unit(self) -> str | None:
|
||||
"""Return the temperature unit."""
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
return None
|
||||
|
||||
@@ -305,18 +305,18 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
if self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
if self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
return UnitOfElectricPotential.VOLT
|
||||
return None
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
self._attr_native_value = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPERATURE
|
||||
)
|
||||
elif self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
elif self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
self._attr_native_value = f"{self._element.voltage}"
|
||||
else:
|
||||
self._attr_native_value = pretty_const(self._element.logical_status.name)
|
||||
|
||||
@@ -66,7 +66,7 @@ class ElkThermostatEMHeat(ElkEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get the current emergency heat status."""
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
|
||||
return self._element.mode is ThermostatMode.EMERGENCY_HEAT
|
||||
|
||||
def _elk_set(self, mode: ThermostatMode) -> None:
|
||||
"""Set the thermostat mode."""
|
||||
|
||||
@@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
time_settings = [
|
||||
setting
|
||||
for setting in cast(list[Setting], elk.settings)
|
||||
if setting.value_format == SettingFormat.TIME_OF_DAY
|
||||
if setting.value_format is SettingFormat.TIME_OF_DAY
|
||||
]
|
||||
|
||||
create_elk_entities(
|
||||
|
||||
@@ -96,7 +96,7 @@ def __get_coordinator(call: ServiceCall) -> EnergyZeroDataUpdateCoordinator:
|
||||
"config_entry": entry_id,
|
||||
},
|
||||
)
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unloaded_config_entry",
|
||||
@@ -125,7 +125,7 @@ async def __get_prices(
|
||||
|
||||
data: Electricity | Gas
|
||||
|
||||
if price_type == PriceType.GAS:
|
||||
if price_type is PriceType.GAS:
|
||||
data = await coordinator.energyzero.get_gas_prices_legacy(
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
if entry.state == ConfigEntryState.LOADED:
|
||||
if entry.state is ConfigEntryState.LOADED:
|
||||
await coordinator.async_refresh()
|
||||
else:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -284,7 +284,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage initial options."""
|
||||
entry = self._get_entry()
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
self.client = entry.runtime_data
|
||||
|
||||
@@ -509,14 +509,12 @@ FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
|
||||
icon="mdi:battery",
|
||||
scope=FitbitScope.DEVICE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
has_entity_name=True,
|
||||
)
|
||||
FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
|
||||
key="devices/battery_level",
|
||||
translation_key="battery_level",
|
||||
scope=FitbitScope.DEVICE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
has_entity_name=True,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
)
|
||||
@@ -654,6 +652,7 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
|
||||
|
||||
entity_description: FitbitSensorEntityDescription
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -713,6 +712,7 @@ class FitbitBatteryLevelSensor(
|
||||
"""Implementation of a Fitbit battery level sensor."""
|
||||
|
||||
entity_description: FitbitSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_attr_attribution = ATTRIBUTION
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -109,7 +109,7 @@ def setup_service(hass: HomeAssistant) -> None:
|
||||
entry: FlumeConfigEntry | None = hass.config_entries.async_get_entry(entry_id)
|
||||
if not entry:
|
||||
raise ValueError(f"Invalid config entry: {entry_id}")
|
||||
if not entry.state == ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Config entry not loaded: {entry_id}")
|
||||
return {
|
||||
"notifications": entry.runtime_data.notifications_coordinator.notifications # type: ignore[dict-item]
|
||||
|
||||
@@ -136,7 +136,7 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
ConfigEntryState.SETUP_IN_PROGRESS,
|
||||
ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
) or entry.state == ConfigEntryState.SETUP_RETRY:
|
||||
) or entry.state is ConfigEntryState.SETUP_RETRY:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
|
||||
@@ -51,7 +51,7 @@ async def async_setup_entry(
|
||||
entry.data.get(CONF_NAME, entry.title)
|
||||
base_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
if device.device_type == DeviceType.Switch:
|
||||
if device.device_type is DeviceType.Switch:
|
||||
entities.append(FluxPowerStateSelect(coordinator.device, entry))
|
||||
if device.operating_modes:
|
||||
entities.append(
|
||||
|
||||
@@ -32,7 +32,7 @@ async def async_setup_entry(
|
||||
entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = []
|
||||
base_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
if coordinator.device.device_type == DeviceType.Switch:
|
||||
if coordinator.device.device_type is DeviceType.Switch:
|
||||
entities.append(FluxSwitch(coordinator, base_unique_id, None))
|
||||
|
||||
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
|
||||
|
||||
@@ -9,7 +9,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfDataRate,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -45,6 +50,7 @@ CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="missed",
|
||||
native_unit_of_measurement="calls",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -53,6 +59,7 @@ DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
key="partition_free_space",
|
||||
translation_key="partition_free_space",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -80,6 +87,7 @@ async def async_setup_entry(
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
for sensor_id, sensor_name in router.sensors_temperature_names.items()
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
from freebox_api.exceptions import InsufficientPermissionsError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -19,7 +18,6 @@ SWITCH_DESCRIPTIONS = [
|
||||
SwitchEntityDescription(
|
||||
key="wifi",
|
||||
translation_key="wifi",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -438,6 +438,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity):
|
||||
self._attr_is_on = turn_on
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-missing-has-entity-name
|
||||
class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
"""Defines a FRITZ!Box Tools PortForward switch."""
|
||||
|
||||
@@ -604,6 +605,7 @@ class FritzBoxProfileSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-missing-has-entity-name
|
||||
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
"""Defines a FRITZ!Box Tools Wifi switch."""
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ class FroniusSolarNet:
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
else:
|
||||
await _coordinator.async_config_entry_first_refresh()
|
||||
@@ -220,7 +220,7 @@ class FroniusSolarNet:
|
||||
|
||||
# Only for re-scans. Initial setup adds entities
|
||||
# through sensor.async_setup_entry
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -235,7 +235,7 @@ class FroniusSolarNet:
|
||||
try:
|
||||
_inverter_info = await self.fronius.inverter_info()
|
||||
except FroniusError as err:
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
# During a re-scan we will attempt again as per schedule.
|
||||
_LOGGER.debug("Re-scan failed for %s", self.host)
|
||||
return inverter_infos
|
||||
|
||||
@@ -42,7 +42,7 @@ async def _collect_coordinators(
|
||||
raise HomeAssistantError(f"Device '{target}' not found in device registry")
|
||||
coordinators = list[FullyKioskDataUpdateCoordinator]()
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
if config_entry.state is not ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
coordinators.append(config_entry.runtime_data)
|
||||
return coordinators
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
|
||||
mfg_data = await async_get_manufacturer_data({address})
|
||||
product_type = mfg_data[address].product_type
|
||||
if product_type == ProductType.UNKNOWN:
|
||||
if product_type is ProductType.UNKNOWN:
|
||||
raise ConfigEntryNotReady("Unable to find product type")
|
||||
|
||||
client = Client(get_connection(hass, address), product_type)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioghost"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["aioghost==0.4.0"]
|
||||
"requirements": ["aioghost==0.4.16"]
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ def _get_entity_descriptions(
|
||||
local_sync = True
|
||||
if (
|
||||
search := data.get(CONF_SEARCH)
|
||||
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
|
||||
) or calendar_item.access_role is AccessRole.FREE_BUSY_READER:
|
||||
read_only = True
|
||||
local_sync = False
|
||||
entity_description = GoogleCalendarEntityDescription(
|
||||
@@ -386,14 +386,14 @@ class GoogleCalendarEntity(
|
||||
"""Return True if the event is visible and not declined."""
|
||||
|
||||
if any(
|
||||
attendee.is_self and attendee.response_status == ResponseStatus.DECLINED
|
||||
attendee.is_self and attendee.response_status is ResponseStatus.DECLINED
|
||||
for attendee in event.attendees
|
||||
):
|
||||
return False
|
||||
# Calendar enttiy may be limited to a specific event type
|
||||
if (
|
||||
self.entity_description.event_type is not None
|
||||
and self.entity_description.event_type != event.event_type
|
||||
and self.entity_description.event_type is not event.event_type
|
||||
):
|
||||
return False
|
||||
# Default calendar entity omits the special types but includes all the others
|
||||
|
||||
@@ -247,7 +247,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the location step."""
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -225,7 +225,7 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Set conversation options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -754,7 +754,7 @@ async def async_prepare_files_for_prompt(
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
if uploaded_file.state is FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing"
|
||||
" failed, reason:"
|
||||
@@ -766,7 +766,7 @@ async def async_prepare_files_for_prompt(
|
||||
tasks = [
|
||||
asyncio.create_task(wait_for_file_processing(part))
|
||||
for part in prompt_parts
|
||||
if part.state != FileState.ACTIVE
|
||||
if part.state is not FileState.ACTIVE
|
||||
]
|
||||
async with asyncio.timeout(TIMEOUT_MILLIS / 1000):
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@@ -237,7 +237,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the location step."""
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -26,7 +26,7 @@ def _get_coordinators(
|
||||
coordinators: dict[str, GrowattCoordinator] = {}
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
continue
|
||||
|
||||
for coord in entry.runtime_data.devices.values():
|
||||
|
||||
@@ -806,10 +806,10 @@ async def _create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa:
|
||||
data["daysOfMonth"] = [start_date.day]
|
||||
data["weeksOfMonth"] = []
|
||||
|
||||
if interval := call.data.get(ATTR_INTERVAL):
|
||||
if (interval := call.data.get(ATTR_INTERVAL)) is not None:
|
||||
data["everyX"] = interval
|
||||
|
||||
if streak := call.data.get(ATTR_STREAK):
|
||||
if (streak := call.data.get(ATTR_STREAK)) is not None:
|
||||
data["streak"] = streak
|
||||
|
||||
try:
|
||||
|
||||
@@ -247,7 +247,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
return (
|
||||
f"https://github.com/home-assistant/operating-system/releases/tag/{version}"
|
||||
@@ -304,7 +304,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/supervisor/commits/main"
|
||||
return f"https://github.com/home-assistant/supervisor/releases/tag/{version}"
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ def _get_controller(hass: HomeAssistant) -> Heos:
|
||||
hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, DOMAIN)
|
||||
)
|
||||
|
||||
if not entry or not entry.state == ConfigEntryState.LOADED:
|
||||
if not entry or entry.state is not ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="integration_not_loaded"
|
||||
)
|
||||
|
||||
@@ -266,14 +266,12 @@ class HomeConnectAirConditioningEntity(HomeConnectEntity, ClimateEntity):
|
||||
value=BSH_POWER_ON,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="power_on",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"appliance_name": self.appliance.info.name,
|
||||
"value": BSH_POWER_ON,
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
@@ -418,10 +418,10 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(otbr_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_otbr_addon()
|
||||
|
||||
if addon_info.state == AddonState.RUNNING:
|
||||
if addon_info.state is AddonState.RUNNING:
|
||||
await otbr_manager.async_stop_addon()
|
||||
|
||||
return await self.async_step_start_otbr_addon()
|
||||
|
||||
@@ -111,7 +111,7 @@ class WaitingAddonManager(AddonManager):
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
||||
if info is not None and info.state is AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
@@ -383,7 +383,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
multipan_manager = await get_multiprotocol_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(multipan_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_addon_not_installed()
|
||||
return await self.async_step_addon_installed()
|
||||
|
||||
@@ -691,10 +691,10 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_flasher_addon()
|
||||
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
if addon_info.state is AddonState.NOT_RUNNING:
|
||||
return await self.async_step_configure_flasher_addon()
|
||||
|
||||
# If the addon is already installed and running, fail
|
||||
@@ -907,7 +907,7 @@ async def check_multi_pan_addon(hass: HomeAssistant) -> None:
|
||||
# Request the addon to start if it's not started
|
||||
# `async_start_addon` returns as soon as the start request has been sent
|
||||
# and does not wait for the addon to be started, so we raise below
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
if addon_info.state is AddonState.NOT_RUNNING:
|
||||
await multipan_manager.async_start_addon()
|
||||
|
||||
if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING):
|
||||
@@ -927,7 +927,7 @@ async def multi_pan_addon_using_device(hass: HomeAssistant, device_path: str) ->
|
||||
multipan_manager = await get_multiprotocol_addon_manager(hass)
|
||||
addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
|
||||
|
||||
if addon_info.state != AddonState.RUNNING:
|
||||
if addon_info.state is not AddonState.RUNNING:
|
||||
return False
|
||||
|
||||
if addon_info.options["device"] != device_path:
|
||||
|
||||
@@ -124,7 +124,7 @@ class OwningAddon:
|
||||
except AddonError:
|
||||
return False
|
||||
else:
|
||||
return addon_info.state == AddonState.RUNNING
|
||||
return addon_info.state is AddonState.RUNNING
|
||||
|
||||
@asynccontextmanager
|
||||
async def temporarily_stop(self, hass: HomeAssistant) -> AsyncGenerator[None]:
|
||||
@@ -137,7 +137,7 @@ class OwningAddon:
|
||||
yield
|
||||
return
|
||||
|
||||
if addon_info.state != AddonState.RUNNING:
|
||||
if addon_info.state is not AddonState.RUNNING:
|
||||
yield
|
||||
return
|
||||
|
||||
@@ -173,7 +173,7 @@ class OwningIntegration:
|
||||
yield
|
||||
return
|
||||
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
yield
|
||||
return
|
||||
|
||||
@@ -213,7 +213,7 @@ async def get_otbr_addon_firmware_info(
|
||||
except AddonError:
|
||||
return None
|
||||
|
||||
if otbr_addon_info.state == AddonState.NOT_INSTALLED:
|
||||
if otbr_addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return None
|
||||
|
||||
if (otbr_path := otbr_addon_info.options.get("device")) is None:
|
||||
@@ -238,7 +238,7 @@ async def get_z2m_addon_firmware_info(
|
||||
except AddonError:
|
||||
return None
|
||||
|
||||
if z2m_addon_info.state == AddonState.NOT_INSTALLED:
|
||||
if z2m_addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return None
|
||||
|
||||
serial = z2m_addon_info.options.get("serial")
|
||||
@@ -286,7 +286,7 @@ async def guess_hardware_owners(
|
||||
except AddonError:
|
||||
pass
|
||||
else:
|
||||
if multipan_addon_info.state != AddonState.NOT_INSTALLED:
|
||||
if multipan_addon_info.state is not AddonState.NOT_INSTALLED:
|
||||
multipan_path = multipan_addon_info.options.get("device")
|
||||
|
||||
if multipan_path is not None:
|
||||
|
||||
@@ -122,7 +122,7 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
existing_entry = await self.async_set_unique_id(self._name)
|
||||
if (
|
||||
existing_entry
|
||||
and existing_entry.state == ConfigEntryState.LOADED
|
||||
and existing_entry.state is ConfigEntryState.LOADED
|
||||
and existing_entry.runtime_data.connected
|
||||
and existing_entry.data[CONF_HOST] != self._host
|
||||
):
|
||||
|
||||
@@ -298,7 +298,7 @@ class HKDevice:
|
||||
# yet.
|
||||
attempts = None if self.hass.state is CoreState.running else 1
|
||||
if (
|
||||
transport == Transport.BLE
|
||||
transport is Transport.BLE
|
||||
and pairing.accessories
|
||||
and pairing.accessories.has_aid(1)
|
||||
):
|
||||
@@ -328,7 +328,7 @@ class HKDevice:
|
||||
)
|
||||
entry.async_on_unload(self._async_cancel_subscription_timer)
|
||||
|
||||
if transport != Transport.BLE:
|
||||
if transport is not Transport.BLE:
|
||||
# Although async_populate_accessories_state fetched the accessory database,
|
||||
# the /accessories endpoint may return cached values from the accessory's
|
||||
# perspective. For example, Ecobee thermostats may report stale temperature
|
||||
@@ -349,7 +349,7 @@ class HKDevice:
|
||||
|
||||
await self.async_process_entity_map()
|
||||
|
||||
if transport != Transport.BLE:
|
||||
if transport is not Transport.BLE:
|
||||
# Start regular polling after entity map is processed
|
||||
self._async_start_polling()
|
||||
|
||||
@@ -359,7 +359,7 @@ class HKDevice:
|
||||
|
||||
self.async_set_available_state(self.pairing.is_available)
|
||||
|
||||
if transport == Transport.BLE:
|
||||
if transport is Transport.BLE:
|
||||
# If we are using BLE, we need to periodically check of the
|
||||
# BLE device is available since we won't get callbacks
|
||||
# when it goes away since we HomeKit supports disconnected
|
||||
@@ -420,7 +420,7 @@ class HKDevice:
|
||||
identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number))
|
||||
|
||||
connections: set[tuple[str, str]] = set()
|
||||
if self.pairing.transport == Transport.BLE and (
|
||||
if self.pairing.transport is Transport.BLE and (
|
||||
discovery := self.pairing.controller.discoveries.get(
|
||||
normalize_hkid(self.unique_id)
|
||||
)
|
||||
@@ -622,7 +622,7 @@ class HKDevice:
|
||||
current_unique_id.add((accessory.aid, service.iid, None))
|
||||
|
||||
for char in service.characteristics:
|
||||
if self.pairing.transport != Transport.BLE:
|
||||
if self.pairing.transport is not Transport.BLE:
|
||||
if char.type == CharacteristicsTypes.THREAD_CONTROL_POINT:
|
||||
continue
|
||||
|
||||
@@ -1057,7 +1057,7 @@ class HKDevice:
|
||||
@property
|
||||
def is_unprovisioned_thread_device(self) -> bool:
|
||||
"""Is this a thread capable device not connected by CoAP."""
|
||||
if self.pairing.controller.transport_type != TransportType.BLE:
|
||||
if self.pairing.controller.transport_type is not TransportType.BLE:
|
||||
return False
|
||||
|
||||
if not self.entity_map.aid(1).services.first(
|
||||
@@ -1069,7 +1069,7 @@ class HKDevice:
|
||||
|
||||
async def async_thread_provision(self) -> None:
|
||||
"""Migrate a HomeKit pairing to CoAP (Thread)."""
|
||||
if self.pairing.controller.transport_type == TransportType.COAP:
|
||||
if self.pairing.controller.transport_type is TransportType.COAP:
|
||||
raise HomeAssistantError("Already connected to a thread network")
|
||||
|
||||
if not (dataset := await async_get_preferred_dataset(self.hass)):
|
||||
|
||||
@@ -700,7 +700,7 @@ async def async_setup_entry(
|
||||
|
||||
@callback
|
||||
def async_add_accessory(accessory: Accessory) -> bool:
|
||||
if conn.pairing.transport != Transport.BLE:
|
||||
if conn.pairing.transport is not Transport.BLE:
|
||||
return False
|
||||
|
||||
accessory_info = accessory.services.first(
|
||||
|
||||
@@ -180,27 +180,24 @@ async def async_setup_entry(
|
||||
class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
"""Representation of the HomematicIP cloud connection sensor."""
|
||||
|
||||
_attr_translation_key = "cloud_connection"
|
||||
|
||||
def __init__(self, hap: HomematicipHAP) -> None:
|
||||
"""Initialize the cloud connection sensor."""
|
||||
super().__init__(hap, hap.home, feature_id="cloud_connection")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name cloud connection entity."""
|
||||
|
||||
name = "Cloud Connection"
|
||||
# Add a prefix to the name if the homematic ip home has a name.
|
||||
return name if not self._home.name else f"{self._home.name} {name}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device specific attributes."""
|
||||
# Adds a sensor to the existing HAP device
|
||||
# Merges into the existing HAP device registered in __init__.py.
|
||||
# Name must match __init__.py logic for has_entity_name to work.
|
||||
label = self._home.label or ""
|
||||
return DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers of Homematic IP device
|
||||
(DOMAIN, self._home.id)
|
||||
}
|
||||
},
|
||||
name=label,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -579,6 +576,7 @@ class HomematicipPluggableMainsFailureSurveillanceSensor(
|
||||
class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorEntity):
|
||||
"""Representation of the HomematicIP security zone sensor group."""
|
||||
|
||||
_attr_has_entity_name = False
|
||||
_attr_device_class = BinarySensorDeviceClass.SAFETY
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -74,6 +74,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
|
||||
basically enabled in the hmip app.
|
||||
"""
|
||||
|
||||
_attr_has_entity_name = False
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
@@ -320,6 +320,7 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
|
||||
class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
|
||||
"""Representation of the HomematicIP cover shutter group."""
|
||||
|
||||
_attr_has_entity_name = False
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None:
|
||||
|
||||
@@ -74,6 +74,7 @@ GROUP_ATTRIBUTES = {
|
||||
class HomematicipGenericEntity(Entity):
|
||||
"""Representation of the HomematicIP generic entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
@@ -112,6 +113,14 @@ class HomematicipGenericEntity(Entity):
|
||||
# Marker showing that the HmIP device hase been removed.
|
||||
self.hmip_device_removed = False
|
||||
|
||||
# Compute entity name based on has_entity_name mode.
|
||||
if not self._attr_has_entity_name:
|
||||
# Legacy mode (groups, special entities): compose the full name
|
||||
# including device/group label and home prefix.
|
||||
self._attr_name = self._compute_legacy_name()
|
||||
else:
|
||||
self._setup_entity_name()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo | None:
|
||||
"""Return device specific attributes."""
|
||||
@@ -120,6 +129,14 @@ class HomematicipGenericEntity(Entity):
|
||||
device_id = str(self._device.id)
|
||||
home_id = str(self._device.homeId)
|
||||
|
||||
# Include the home name in the device name so that the
|
||||
# previous "{home} {device}" naming is preserved after
|
||||
# switching to has_entity_name=True.
|
||||
device_name = self._device.label
|
||||
home_name = getattr(self._home, "name", None)
|
||||
if device_name and home_name:
|
||||
device_name = f"{home_name} {device_name}"
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers of Homematic IP device
|
||||
@@ -127,7 +144,7 @@ class HomematicipGenericEntity(Entity):
|
||||
},
|
||||
manufacturer=self._device.oem,
|
||||
model=self._device.modelType,
|
||||
name=self._device.label,
|
||||
name=device_name,
|
||||
sw_version=self._device.firmwareVersion,
|
||||
# Link to the homematic ip access point.
|
||||
via_device=(DOMAIN, home_id),
|
||||
@@ -200,38 +217,93 @@ class HomematicipGenericEntity(Entity):
|
||||
self.async_remove(force_remove=True), eager_start=False
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the generic entity."""
|
||||
def _compute_legacy_name(self) -> str:
|
||||
"""Compute the full legacy name for entities without has_entity_name.
|
||||
|
||||
name = ""
|
||||
# Try to get a label from a channel.
|
||||
functional_channels = getattr(self._device, "functionalChannels", None)
|
||||
if functional_channels and self.functional_channel:
|
||||
if self._is_multi_channel:
|
||||
label = getattr(self.functional_channel, "label", None)
|
||||
if label:
|
||||
name = str(label)
|
||||
elif len(functional_channels) > 1:
|
||||
label = getattr(functional_channels[1], "label", None)
|
||||
if label:
|
||||
name = str(label)
|
||||
|
||||
# Use device label, if name is not defined by channel label.
|
||||
if not name:
|
||||
name = self._device.label or ""
|
||||
if self._post:
|
||||
name = f"{name} {self._post}"
|
||||
elif self._is_multi_channel:
|
||||
name = f"{name} Channel{self.get_channel_index()}"
|
||||
|
||||
# Add a prefix to the name if the homematic ip home has a name.
|
||||
Used by group entities and other special cases where has_entity_name
|
||||
is False. Includes device/group label, post suffix, and home prefix.
|
||||
"""
|
||||
name = self._device.label or ""
|
||||
if self._post:
|
||||
name = f"{name} {self._post}" if name else self._post
|
||||
home_name = getattr(self._home, "name", None)
|
||||
if name and home_name:
|
||||
name = f"{home_name} {name}"
|
||||
|
||||
return name
|
||||
|
||||
def _setup_entity_name(self) -> None:
|
||||
"""Set up entity naming for has_entity_name mode.
|
||||
|
||||
With has_entity_name=True, HA composes the full friendly name as
|
||||
"{device_name} {entity_name}". This method sets the appropriate
|
||||
naming attributes.
|
||||
|
||||
For multi-channel entities, channel labels provide _attr_name (dynamic).
|
||||
For entities with _post, _attr_name is derived from the post suffix,
|
||||
with the first letter capitalized for display consistency.
|
||||
For primary entities, HA uses device_class as the name.
|
||||
"""
|
||||
# Multi-channel entities: use channel label as entity name.
|
||||
if self._is_multi_channel and self.functional_channel:
|
||||
label = getattr(self.functional_channel, "label", None)
|
||||
if label:
|
||||
label_str = str(label)
|
||||
device_label = self._device.label or ""
|
||||
# Strip device name prefix from channel label to avoid
|
||||
# duplication when HA composes "{device_name} {entity_name}".
|
||||
# E.g., device "Licht Flur" + channel "Licht Flur 5" -> "5".
|
||||
if device_label and label_str.startswith(device_label):
|
||||
stripped = label_str[len(device_label) :].strip()
|
||||
if stripped:
|
||||
self._attr_name = stripped
|
||||
# Otherwise channel label equals device label (modulo
|
||||
# whitespace); leave _attr_name unset so HA composes just
|
||||
# the device name without duplicating it.
|
||||
return
|
||||
self._attr_name = label_str
|
||||
return
|
||||
# Fallback: use post suffix or generic channel name.
|
||||
if self._post:
|
||||
self._attr_name = self._post[0].upper() + self._post[1:]
|
||||
else:
|
||||
self._attr_name = f"Channel{self.get_channel_index()}"
|
||||
return
|
||||
|
||||
# Entities with a post suffix: use it as the entity name,
|
||||
# capitalizing the first letter for display consistency.
|
||||
if self._post:
|
||||
self._attr_name = self._post[0].upper() + self._post[1:]
|
||||
return
|
||||
|
||||
# Non-multi-channel entities on devices with multiple channels:
|
||||
# use the first functional channel's label as name context.
|
||||
# This preserves names like "Treppe CH" for single-function entities
|
||||
# on multi-channel devices (e.g., HmIP-BSL switch channel).
|
||||
functional_channels = getattr(self._device, "functionalChannels", None)
|
||||
if functional_channels and len(functional_channels) > 1:
|
||||
ch1 = (
|
||||
functional_channels.get(1)
|
||||
if isinstance(functional_channels, dict)
|
||||
else functional_channels[1]
|
||||
)
|
||||
label = getattr(ch1, "label", None) if ch1 else None
|
||||
if label:
|
||||
label_str = str(label)
|
||||
device_label = self._device.label or ""
|
||||
# Strip device name prefix to avoid duplication.
|
||||
if device_label and label_str.startswith(device_label):
|
||||
stripped = label_str[len(device_label) :].strip()
|
||||
if stripped:
|
||||
self._attr_name = stripped
|
||||
# Otherwise channel label equals device label (modulo
|
||||
# whitespace); leave _attr_name unset.
|
||||
return
|
||||
self._attr_name = label_str
|
||||
return
|
||||
|
||||
# Primary entity on device: leave unset so HA derives name from
|
||||
# device_class or translation_key.
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
|
||||
@@ -82,7 +82,6 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post=description.key,
|
||||
channel=channel,
|
||||
is_multi_channel=False,
|
||||
feature_id="doorbell",
|
||||
|
||||
@@ -1070,9 +1070,7 @@ class HmipSmokeDetectorSensor(HomematicipGenericEntity, SensorEntity):
|
||||
description: HmipSmokeDetectorSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the smoke detector sensor."""
|
||||
super().__init__(
|
||||
hap, device, post=description.key, feature_id="smoke_detector_sensor"
|
||||
)
|
||||
super().__init__(hap, device, feature_id="smoke_detector_sensor")
|
||||
self.entity_description = description
|
||||
self._sensor_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"cloud_connection": {
|
||||
"name": "Cloud connection"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"optical_signal_light": {
|
||||
"state_attributes": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user