mirror of
https://github.com/home-assistant/core.git
synced 2026-05-26 10:45:11 +02:00
Compare commits
335 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78db1e3407 | |||
| 2368a3614d | |||
| 5053392cf2 | |||
| 6ec11460ed | |||
| 975e30c048 | |||
| 7655cb0fc6 | |||
| 7566839e9d | |||
| 7db5e82f58 | |||
| 7e67c53417 | |||
| 89fb856302 | |||
| a2fbd2b1ea | |||
| 231ed34133 | |||
| 6cff433b2e | |||
| eca83fb7b1 | |||
| 2c5adaec5c | |||
| 5d75f1c33b | |||
| d628d2314e | |||
| a9547ec349 | |||
| 2ec637df84 | |||
| 4f50ee5675 | |||
| 0faf96b983 | |||
| c3dacbc601 | |||
| 2659484000 | |||
| 6830ca75f5 | |||
| 38b4184dc3 | |||
| cfde7975d8 | |||
| d7ab696a4c | |||
| 7f7dad7f71 | |||
| 0ed21dbed7 | |||
| d2b37ee28b | |||
| b82c95e77f | |||
| baa61982a1 | |||
| 8ff6de788d | |||
| 640f82642a | |||
| 64ed269f9c | |||
| 2b58ef96eb | |||
| 74ca79ac28 | |||
| afb27bc165 | |||
| 0cbf27f44f | |||
| a5ceafa544 | |||
| cd4d669231 | |||
| cc411d06b5 | |||
| 1329f12d37 | |||
| 3899f5347b | |||
| cf02cfaa7c | |||
| e77c16ea1b | |||
| f1e2f94ee0 | |||
| 3516883b0a | |||
| c8b70b1a38 | |||
| 946625e281 | |||
| f4b7840d5c | |||
| 060f447e4a | |||
| d5bae0a2cf | |||
| f9bef804b1 | |||
| 6de03f4ed6 | |||
| e7f3e5637f | |||
| 80cefc74ec | |||
| 2f33b4b7f9 | |||
| cf52a7a509 | |||
| f5835f849a | |||
| ec5210dca8 | |||
| 422ea1a9b1 | |||
| b6f69f6b99 | |||
| a2a3819241 | |||
| 3ce33b0ac6 | |||
| e507a97d8b | |||
| 5801fdad14 | |||
| 2f4abd6a25 | |||
| 1c045ab715 | |||
| d4ca541a96 | |||
| a07a9dc6c8 | |||
| 6d60b3a23a | |||
| 37bb895b91 | |||
| f87dc917a6 | |||
| 71a15c188e | |||
| 003ecdb867 | |||
| ec7e5e5a75 | |||
| 7587f062e1 | |||
| 11970144e4 | |||
| 70750a6d79 | |||
| a53437315f | |||
| 5c73ad0310 | |||
| 4a04a271ec | |||
| 52c27bdea5 | |||
| 6fdc52c002 | |||
| e560bbc103 | |||
| b8c573685f | |||
| 3764b70b90 | |||
| 5d2de6f82b | |||
| 64d17f44fa | |||
| 6f67d44cfe | |||
| def3befb0e | |||
| 05716ae196 | |||
| c0a864297f | |||
| 04bb84cd03 | |||
| cb55accc3b | |||
| d21c227804 | |||
| 1ebccd9fa2 | |||
| cfbd0f3217 | |||
| 4afb7c0997 | |||
| 105caccc51 | |||
| 6419551117 | |||
| 585bd6616a | |||
| b8dd97cf21 | |||
| 68fc4aed78 | |||
| 7dbb259625 | |||
| 057eac7fb6 | |||
| 31c9cdf742 | |||
| 3147104132 | |||
| d6d0f37b52 | |||
| 75e48745a8 | |||
| 533417778c | |||
| e49fd4ebbd | |||
| 8412b029b1 | |||
| c65de7521f | |||
| 752c17917e | |||
| f643c7ddc6 | |||
| 6f5d4cf991 | |||
| b52466fed1 | |||
| 189534e32b | |||
| 684ae23b18 | |||
| f4d2f65602 | |||
| 65879ff37b | |||
| d902104bee | |||
| 7bad27c412 | |||
| 74a7102cf6 | |||
| e88fb03388 | |||
| 92ce5ed75a | |||
| 466e28eae2 | |||
| d53a8c7df9 | |||
| 8fe26bdf59 | |||
| f75d56c096 | |||
| 7e5942ae51 | |||
| f9ef9963e6 | |||
| cea718528f | |||
| 9c38868bbe | |||
| 62da1c34fb | |||
| 2d3a3bf4fc | |||
| daa60c6d55 | |||
| d45730aa02 | |||
| 05ef944766 | |||
| a51daf48c7 | |||
| 6a789d5af7 | |||
| ad4e218b69 | |||
| 55f576c784 | |||
| c7c3988b11 | |||
| ac825ca36d | |||
| 0b439a25e1 | |||
| 0385e81010 | |||
| 699fed7a3a | |||
| 0eefc8f327 | |||
| 6888d203eb | |||
| 359949adc2 | |||
| afe7d0cbbf | |||
| 2f7b3cb7d9 | |||
| c49ed549db | |||
| 3016198644 | |||
| f0c9156cdb | |||
| f091871aa5 | |||
| d25207180a | |||
| 9ce047b9be | |||
| 488c04fc5b | |||
| 7598fdb8cb | |||
| 49e22072c9 | |||
| c056242390 | |||
| 9cbb14bbde | |||
| 6634c4ce78 | |||
| ae1355666b | |||
| 2d0d202b80 | |||
| 9fd48344f8 | |||
| 7b4ed59861 | |||
| fb8f82542e | |||
| af5583ba76 | |||
| 2a943369d5 | |||
| 29425fd0ac | |||
| 271111fe75 | |||
| 37e9bdd36f | |||
| e1d1bdd377 | |||
| b3a60de487 | |||
| 0cb7ea5584 | |||
| 7bc7694e14 | |||
| c45c949080 | |||
| ec4f64172b | |||
| f88b7bcdf6 | |||
| 05009871aa | |||
| 4aa7323af2 | |||
| bcacf3a73c | |||
| 96a6babaef | |||
| e856271a5a | |||
| add023ed74 | |||
| 8d456cb24f | |||
| 5ebd95eb34 | |||
| 228d7189c3 | |||
| a8e141a48a | |||
| d42d52a0f7 | |||
| cee0fe071d | |||
| e3593c3076 | |||
| 5498de07ff | |||
| ac3f973d7d | |||
| 6b8a2a4032 | |||
| 74e40af4bb | |||
| 833e15d6f2 | |||
| ee56fd1eb0 | |||
| e6528bae8a | |||
| a17eb65498 | |||
| 912a839d66 | |||
| 4306863729 | |||
| ba2f66e751 | |||
| 94581d8ab6 | |||
| 7d6ec7fc58 | |||
| f49de3548e | |||
| 49ab42d3a2 | |||
| 383f6142f0 | |||
| 2f120cf604 | |||
| 37288849b3 | |||
| aa8659f507 | |||
| 40c0d79d1d | |||
| bef8632d78 | |||
| f00decfaa3 | |||
| 42e7add026 | |||
| 263aa3f16e | |||
| 03b364dcf0 | |||
| 3b1aaf39af | |||
| b82ba43fa4 | |||
| d81ef5593c | |||
| 5c5e50f024 | |||
| e796d9c467 | |||
| 342f23526f | |||
| 814ec697cf | |||
| 120f1446d4 | |||
| 170af75b7d | |||
| 5432d29489 | |||
| 8098f4f6bc | |||
| 6a70077687 | |||
| 5dbb0464ba | |||
| 1df165ea02 | |||
| 62542eb911 | |||
| a842cac34c | |||
| 2460f688e3 | |||
| a868ea443c | |||
| 1d8565483b | |||
| 1ef3301253 | |||
| 525952f016 | |||
| 3257275c5a | |||
| cb54fd4921 | |||
| b391fc61ea | |||
| fcd4e4939c | |||
| deb8b5da05 | |||
| c7754a6ce9 | |||
| 242724bd50 | |||
| 42454563db | |||
| bf03d0c216 | |||
| 568107e06b | |||
| 7da44428b6 | |||
| 0a27f31949 | |||
| 905b868c82 | |||
| 3187289913 | |||
| 87cecd4a44 | |||
| fed38b0e38 | |||
| 6a36d1260b | |||
| 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 |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
||||
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,11 +15,11 @@ Dockerfile.dev linguist-language=Dockerfile
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
|
||||
machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
requirements_all.txt linguist-generated=true
|
||||
requirements_test_all.txt linguist-generated=true
|
||||
requirements_test_pre_commit.txt linguist-generated=true
|
||||
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||
.github/workflows/*.lock.yml linguist-generated=true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
@@ -1421,7 +1421,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1592,7 +1592,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
@@ -1620,7 +1620,7 @@ jobs:
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
report_type: test_results
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -609,6 +609,7 @@ homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.velux.*
|
||||
homeassistant.components.victron_gx.*
|
||||
homeassistant.components.vistapool.*
|
||||
homeassistant.components.vivotek.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
|
||||
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
+8
-8
@@ -236,8 +236,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||
/homeassistant/components/blink/ @fronzbot
|
||||
/tests/components/blink/ @fronzbot
|
||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/homeassistant/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
@@ -945,8 +945,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
/tests/components/kostal_plenticore/ @stegm
|
||||
/homeassistant/components/kraken/ @eifinger
|
||||
@@ -1413,8 +1411,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 +1536,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
|
||||
@@ -1932,6 +1930,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
/tests/components/vilfo/ @ManneW
|
||||
/homeassistant/components/vistapool/ @fdebrus
|
||||
/tests/components/vistapool/ @fdebrus
|
||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||
/tests/components/vivotek/ @HarlemSquirrel
|
||||
/homeassistant/components/vizio/ @raman325
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"service": "mdi:dialpad"
|
||||
},
|
||||
"alarm_toggle_chime": {
|
||||
"service": "mdi:abc"
|
||||
"service": "mdi:bell-ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_NAME,
|
||||
UnitOfTemperature,
|
||||
@@ -373,9 +372,6 @@ def async_get_entities(
|
||||
"""Return all entities that are supported by Alexa."""
|
||||
entities: list[AlexaEntity] = []
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
continue
|
||||
|
||||
if state.domain not in ENTITY_ADAPTERS:
|
||||
continue
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Invalid authentication credentials: {error}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
|
||||
@@ -7,10 +7,11 @@ from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_HOST, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
"""Constants for the Altruist integration."""
|
||||
|
||||
DOMAIN = "altruist"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_HOST = "host"
|
||||
|
||||
@@ -10,13 +10,12 @@ import logging
|
||||
from altruistclient import AltruistClient, AltruistError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ from pyatv.interface import (
|
||||
PushListener,
|
||||
PushUpdater,
|
||||
)
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -139,7 +140,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 +189,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
|
||||
@@ -345,7 +346,10 @@ class AppleTvMediaPlayer(
|
||||
play_item = await media_source.async_resolve_media(
|
||||
self.hass, media_id, self.entity_id
|
||||
)
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
if play_item.path and self._is_feature_available(FeatureName.StreamFile):
|
||||
media_id = str(play_item.path)
|
||||
else:
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
media_type = MediaType.MUSIC
|
||||
|
||||
if self._is_feature_available(FeatureName.StreamFile) and (
|
||||
@@ -353,11 +357,16 @@ class AppleTvMediaPlayer(
|
||||
):
|
||||
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
||||
await self.atv.stream.stream_file(media_id)
|
||||
elif self._is_feature_available(FeatureName.PlayUrl):
|
||||
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
||||
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
||||
):
|
||||
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
||||
await self.atv.stream.play_url(media_id)
|
||||
else:
|
||||
_LOGGER.error("Media streaming is not possible with current configuration")
|
||||
_LOGGER.error(
|
||||
"Media streaming is not possible with current configuration for %s",
|
||||
media_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def media_image_hash(self) -> str | None:
|
||||
@@ -446,7 +455,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 +515,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",
|
||||
|
||||
@@ -193,7 +193,11 @@ async def async_setup_entry(
|
||||
Aranet4BluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_register_processor(
|
||||
processor, AranetSensorEntityDescription
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Aranet4BluetoothSensorEntity(
|
||||
|
||||
@@ -16,6 +16,13 @@ from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceIn
|
||||
|
||||
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
|
||||
|
||||
STEP_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow."""
|
||||
@@ -31,13 +38,22 @@ 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_try_connect(self, host: str, port: int) -> None:
|
||||
"""Verify the device is reachable."""
|
||||
async def _async_try_connect(self, host: str, port: int) -> dict[str, str]:
|
||||
"""Verify the device is reachable; return errors keyed by reason."""
|
||||
client = Client(host, port)
|
||||
try:
|
||||
await client.start()
|
||||
except socket.gaierror:
|
||||
return {"base": "invalid_host"}
|
||||
except TimeoutError:
|
||||
return {"base": "timeout_connect"}
|
||||
except ConnectionRefusedError:
|
||||
return {"base": "connection_refused"}
|
||||
except ConnectionFailed, OSError:
|
||||
return {"base": "cannot_connect"}
|
||||
finally:
|
||||
await client.stop()
|
||||
return {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -53,19 +69,10 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
user_input[CONF_HOST], user_input[CONF_PORT], uuid
|
||||
)
|
||||
|
||||
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:
|
||||
errors = await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})",
|
||||
data={
|
||||
@@ -74,16 +81,46 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
schema = vol.Schema(fields)
|
||||
schema = STEP_DATA_SCHEMA
|
||||
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=schema, errors=errors)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
if user_input is not None:
|
||||
uuid = await get_uniqueid_from_host(
|
||||
async_get_clientsession(self.hass), user_input[CONF_HOST]
|
||||
)
|
||||
if uuid:
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_mismatch()
|
||||
|
||||
errors = await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
},
|
||||
)
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
STEP_DATA_SCHEMA, user_input or reconfigure_entry.data
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -113,9 +150,7 @@ 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:
|
||||
if await self._async_try_connect(host, port):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
self.host = host
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -16,6 +18,13 @@
|
||||
"confirm": {
|
||||
"description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"description": "[%key:component::arcam_fmj::config::step::user::description%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,12 +9,11 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT
|
||||
from homeassistant.const import ATTR_MODEL, ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
ATTR_FIRMWARE,
|
||||
ATTR_MODEL,
|
||||
DEFAULT_ADDRESS,
|
||||
DEFAULT_INTEGRATION_TITLE,
|
||||
DOMAIN,
|
||||
|
||||
@@ -19,8 +19,4 @@ DEVICES = "devices"
|
||||
MANUFACTURER = "ABB"
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_FIRMWARE = "firmware"
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
ATTR_SERIAL_NUMBER,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
@@ -31,7 +32,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import (
|
||||
ATTR_DEVICE_NAME,
|
||||
ATTR_FIRMWARE,
|
||||
ATTR_MODEL,
|
||||
DEFAULT_DEVICE_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,10 +17,11 @@ from homeassistant.components.backup import (
|
||||
OnProgressCallback,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import S3ConfigEntry
|
||||
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -8,6 +8,7 @@ from botocore.exceptions import ClientError, ConnectionError, ParamValidationErr
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
@@ -20,7 +21,6 @@ from .const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
CONF_PREFIX,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DEFAULT_ENDPOINT_URL,
|
||||
DESCRIPTION_AWS_S3_DOCS_URL,
|
||||
|
||||
@@ -11,8 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
AWS_DOMAIN = "amazonaws.com"
|
||||
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
|
||||
|
||||
@@ -8,10 +8,11 @@ from aiobotocore.client import AioBaseClient as S3Client
|
||||
from botocore.exceptions import BotoCoreError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_BUCKET, CONF_PREFIX, DOMAIN
|
||||
from .const import CONF_BUCKET, DOMAIN
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
SCAN_INTERVAL = timedelta(hours=6)
|
||||
|
||||
@@ -5,15 +5,10 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.backup import DATA_MANAGER as BACKUP_DATA_MANAGER
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_PREFIX,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_SECRET_ACCESS_KEY, DOMAIN
|
||||
from .coordinator import S3ConfigEntry
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@@ -50,6 +49,9 @@ from .const import (
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import axis
|
||||
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
|
||||
DEFAULT_PORT = 443
|
||||
DEFAULT_PROTOCOL = "https"
|
||||
@@ -94,7 +96,8 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
serial = api.vapix.serial_number
|
||||
if (serial := self._get_serial_number(api)) is None:
|
||||
return self.async_abort(reason="no_serial_number")
|
||||
config = {
|
||||
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
@@ -139,25 +142,15 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async def _create_entry(self, serial: str) -> ConfigFlowResult:
|
||||
"""Create entry for device.
|
||||
|
||||
Generate a name to be used as a prefix for device entities.
|
||||
Use the discovered device name when available.
|
||||
"""
|
||||
model = self.config[CONF_MODEL]
|
||||
same_model = [
|
||||
entry.data[CONF_NAME]
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
|
||||
]
|
||||
|
||||
name = model
|
||||
for idx in range(len(same_model) + 1):
|
||||
name = f"{model} {idx}"
|
||||
if name not in same_model:
|
||||
break
|
||||
|
||||
if (title_placeholders := self.context.get("title_placeholders")) is not None:
|
||||
name = title_placeholders[CONF_NAME]
|
||||
else:
|
||||
name = f"{self.config[CONF_MODEL]} - {serial}"
|
||||
self.config[CONF_NAME] = name
|
||||
|
||||
title = f"{model} - {serial}"
|
||||
return self.async_create_entry(title=title, data=self.config)
|
||||
return self.async_create_entry(title=name, data=self.config)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -269,6 +262,19 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
@staticmethod
|
||||
def _get_serial_number(api: axis.AxisDevice) -> str | None:
|
||||
"""Retrieve the device serial number from the Axis API.
|
||||
|
||||
Tries basic_device_info first, then property_handler. Returns None if not found.
|
||||
"""
|
||||
vapix = api.vapix
|
||||
if vapix.basic_device_info.initialized:
|
||||
return vapix.basic_device_info["0"].serial_number
|
||||
if vapix.params.property_handler.initialized:
|
||||
return vapix.params.property_handler["0"].system_serial_number
|
||||
return None
|
||||
|
||||
|
||||
class AxisOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Axis device options."""
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import axis
|
||||
from axis.errors import Unauthorized
|
||||
from axis.interfaces.mqtt import mqtt_json_to_event
|
||||
from axis.models.mqtt import ClientState
|
||||
from axis.models.mqtt import ClientState, mqtt_json_to_event
|
||||
from axis.stream_manager import Signal, State
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==71"],
|
||||
"requirements": ["axis==72"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"link_local_address": "Link local addresses are not supported",
|
||||
"no_serial_number": "Could not retrieve a serial number from the device. Please check device connectivity and try again.",
|
||||
"not_axis_device": "Discovered device not an Axis device",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blebox_uniapi"],
|
||||
"requirements": ["blebox-uniapi==2.5.3"],
|
||||
"requirements": ["blebox-uniapi==2.5.4"],
|
||||
"zeroconf": ["_bbxsrv._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "blue_current",
|
||||
"name": "Blue Current",
|
||||
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
|
||||
"codeowners": ["@gleeuwen", "@jtodorova23"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -124,7 +124,9 @@ async def async_setup_entry(
|
||||
BlueMaestroBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
|
||||
|
||||
class BlueMaestroBluetoothSensorEntity(
|
||||
|
||||
@@ -11,6 +11,7 @@ from bluetooth_adapters import (
|
||||
ADAPTER_CONNECTION_SLOTS,
|
||||
ADAPTER_HW_VERSION,
|
||||
ADAPTER_MANUFACTURER,
|
||||
ADAPTER_PASSIVE_SCAN,
|
||||
ADAPTER_SW_VERSION,
|
||||
DEFAULT_ADDRESS,
|
||||
DEFAULT_CONNECTION_SLOTS,
|
||||
@@ -69,6 +70,7 @@ from .api import (
|
||||
async_register_callback,
|
||||
async_register_scanner,
|
||||
async_remove_scanner,
|
||||
async_request_active_scan,
|
||||
async_scanner_by_source,
|
||||
async_scanner_count,
|
||||
async_scanner_devices_by_address,
|
||||
@@ -79,7 +81,6 @@ from .const import (
|
||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||
CONF_ADAPTER,
|
||||
CONF_DETAILS,
|
||||
CONF_PASSIVE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
CONF_SOURCE_DEVICE_ID,
|
||||
CONF_SOURCE_DOMAIN,
|
||||
@@ -93,7 +94,7 @@ from .manager import HomeAssistantBluetoothManager
|
||||
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
||||
from .models import BluetoothCallback, BluetoothChange
|
||||
from .storage import BluetoothStorage
|
||||
from .util import adapter_title
|
||||
from .util import adapter_title, resolve_scanning_mode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -128,6 +129,7 @@ __all__ = [
|
||||
"async_register_callback",
|
||||
"async_register_scanner",
|
||||
"async_remove_scanner",
|
||||
"async_request_active_scan",
|
||||
"async_scanner_by_source",
|
||||
"async_scanner_count",
|
||||
"async_scanner_devices_by_address",
|
||||
@@ -387,12 +389,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Bluetooth adapter {adapter} with address {address} not found"
|
||||
)
|
||||
passive = entry.options.get(CONF_PASSIVE)
|
||||
adapters = await manager.async_get_bluetooth_adapters()
|
||||
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||
details = adapters[adapter]
|
||||
mode = resolve_scanning_mode(entry.options)
|
||||
# AUTO needs passive scanning support to flip on demand; without it
|
||||
# the scanner would start passive on hardware that can't do passive.
|
||||
if mode is BluetoothScanningMode.AUTO and not details.get(ADAPTER_PASSIVE_SCAN):
|
||||
mode = BluetoothScanningMode.ACTIVE
|
||||
scanner = HaScanner(mode, adapter, address)
|
||||
scanner.async_setup()
|
||||
details = adapters[adapter]
|
||||
if entry.title == address:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, title=adapter_title(adapter, details)
|
||||
|
||||
@@ -68,9 +68,20 @@ class ActiveBluetoothProcessorCoordinator[_DataT](
|
||||
| None = None,
|
||||
poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None,
|
||||
connectable: bool = True,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the processor."""
|
||||
super().__init__(hass, logger, address, mode, update_method, connectable)
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
address,
|
||||
mode,
|
||||
update_method,
|
||||
connectable,
|
||||
scan_interval,
|
||||
scan_duration,
|
||||
)
|
||||
|
||||
self._needs_poll_method = needs_poll_method
|
||||
self._poll_method = poll_method
|
||||
|
||||
@@ -130,17 +130,26 @@ def async_register_callback(
|
||||
callback: BluetoothCallback,
|
||||
match_dict: BluetoothCallbackMatcher | None,
|
||||
mode: BluetoothScanningMode,
|
||||
*,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Register to receive a callback on bluetooth change.
|
||||
|
||||
mode is currently not used as we only support active scanning.
|
||||
Passive scanning will be available in the future. The flag
|
||||
is required to be present to avoid a future breaking change
|
||||
when we support passive scanning.
|
||||
When ``mode`` is not PASSIVE and ``match_dict["address"]`` is set,
|
||||
the address is registered with habluetooth's active-scan scheduler
|
||||
so AUTO-mode scanners flip ACTIVE on demand for that device.
|
||||
``scan_interval`` / ``scan_duration`` default to habluetooth's
|
||||
DEFAULT_ACTIVE_SCAN_* (5 minutes / 10 seconds) when not provided;
|
||||
integrations that need a different cadence can pass explicit
|
||||
values. Without an address in the matcher the active-scan request
|
||||
is skipped; the callback itself still fires normally.
|
||||
|
||||
Returns a callback that can be used to cancel the registration.
|
||||
"""
|
||||
return _get_manager(hass).async_register_callback(callback, match_dict)
|
||||
return _get_manager(hass).async_register_callback(
|
||||
callback, match_dict, mode, scan_interval, scan_duration
|
||||
)
|
||||
|
||||
|
||||
async def async_process_advertisements(
|
||||
@@ -161,7 +170,7 @@ async def async_process_advertisements(
|
||||
done.set_result(service_info)
|
||||
|
||||
unload = _get_manager(hass).async_register_callback(
|
||||
_async_discovered_device, match_dict
|
||||
_async_discovered_device, match_dict, mode, scan_duration=timeout
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -275,3 +284,19 @@ def async_set_fallback_availability_interval(
|
||||
) -> None:
|
||||
"""Override the fallback availability timeout for a MAC address."""
|
||||
_get_manager(hass).async_set_fallback_availability_interval(address, interval)
|
||||
|
||||
|
||||
async def async_request_active_scan(
|
||||
hass: HomeAssistant, duration: float | None = None
|
||||
) -> None:
|
||||
"""Run an on-demand active sweep across every AUTO scanner.
|
||||
|
||||
Intended for config-flow discovery and other one-shot probes that
|
||||
need fresh advertisements without waiting for the periodic
|
||||
rediscovery cadence. Awaits ``duration`` seconds so the caller can
|
||||
then read newly discovered advertisements. Defaults to habluetooth's
|
||||
on-demand sweep duration when ``duration`` is not provided; the
|
||||
scheduler clamps the value to its allowed range. Concurrent callers
|
||||
dedupe to a single bus-wide window.
|
||||
"""
|
||||
await _get_manager(hass).async_request_active_scan(duration)
|
||||
|
||||
@@ -12,7 +12,7 @@ from bluetooth_adapters import (
|
||||
adapter_model,
|
||||
get_adapters,
|
||||
)
|
||||
from habluetooth import get_manager
|
||||
from habluetooth import BluetoothScanningMode, get_manager
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import onboarding
|
||||
@@ -22,33 +22,64 @@ from homeassistant.config_entries import (
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_SOURCE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_ADAPTER,
|
||||
CONF_DETAILS,
|
||||
CONF_MODE,
|
||||
CONF_PASSIVE,
|
||||
CONF_SOURCE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
CONF_SOURCE_DEVICE_ID,
|
||||
CONF_SOURCE_DOMAIN,
|
||||
CONF_SOURCE_MODEL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .util import adapter_title
|
||||
from .util import adapter_title, resolve_scanning_mode
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSIVE, default=False): bool,
|
||||
}
|
||||
_MODE_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
BluetoothScanningMode.AUTO.value,
|
||||
BluetoothScanningMode.ACTIVE.value,
|
||||
BluetoothScanningMode.PASSIVE.value,
|
||||
],
|
||||
translation_key="mode",
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Build the options schema with the saved mode as the default."""
|
||||
current = resolve_scanning_mode(handler.options).value
|
||||
return vol.Schema({vol.Required(CONF_MODE, default=current): _MODE_SELECTOR})
|
||||
|
||||
|
||||
async def _validate_options(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Mirror CONF_MODE into the legacy CONF_PASSIVE for downgrade safety."""
|
||||
user_input[CONF_PASSIVE] = (
|
||||
user_input[CONF_MODE] == BluetoothScanningMode.PASSIVE.value
|
||||
)
|
||||
return user_input
|
||||
|
||||
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
|
||||
"init": SchemaFlowFormStep(_options_schema, validate_user_input=_validate_options),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,17 +7,21 @@ from habluetooth import ( # noqa: F401
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
SCANNER_WATCHDOG_INTERVAL,
|
||||
SCANNER_WATCHDOG_TIMEOUT,
|
||||
BluetoothScanningMode,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_MODE # noqa: F401
|
||||
|
||||
DOMAIN = "bluetooth"
|
||||
|
||||
CONF_ADAPTER = "adapter"
|
||||
CONF_DETAILS = "details"
|
||||
# CONF_PASSIVE is the legacy boolean option; we keep writing it alongside
|
||||
# CONF_MODE so a downgrade to a pre-AUTO release reads a sensible value.
|
||||
CONF_PASSIVE = "passive"
|
||||
|
||||
DEFAULT_MODE = BluetoothScanningMode.AUTO.value
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_SOURCE: Final = "source"
|
||||
CONF_SOURCE_DOMAIN: Final = "source_domain"
|
||||
CONF_SOURCE_MODEL: Final = "source_model"
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID: Final = "source_config_entry_id"
|
||||
|
||||
@@ -21,7 +21,11 @@ from habluetooth import (
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_LOGGING_CHANGED
|
||||
from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_LOGGING_CHANGED,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
@@ -33,7 +37,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util.package import is_docker_env
|
||||
|
||||
from .const import (
|
||||
CONF_SOURCE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
CONF_SOURCE_DEVICE_ID,
|
||||
CONF_SOURCE_DOMAIN,
|
||||
@@ -202,6 +205,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
self,
|
||||
callback: BluetoothCallback,
|
||||
matcher: BluetoothCallbackMatcher | None,
|
||||
mode: BluetoothScanningMode = BluetoothScanningMode.ACTIVE,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a callback."""
|
||||
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
|
||||
@@ -216,15 +222,31 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
connectable = callback_matcher[CONNECTABLE]
|
||||
self._callback_index.add_callback_matcher(callback_matcher)
|
||||
|
||||
# If the matcher targets a specific address and the caller
|
||||
# didn't explicitly ask for PASSIVE, wire it into habluetooth's
|
||||
# active-scan scheduler so AUTO-mode scanners flip ACTIVE on
|
||||
# demand for this device. ``scan_interval``/``scan_duration``
|
||||
# default to habluetooth's DEFAULT_ACTIVE_SCAN_* when None.
|
||||
cancel_active_scan: Callable[[], None] | None = None
|
||||
if (
|
||||
mode is not BluetoothScanningMode.PASSIVE
|
||||
and (address := callback_matcher.get(ADDRESS)) is not None
|
||||
):
|
||||
cancel_active_scan = self.async_register_active_scan(
|
||||
address, scan_interval, scan_duration
|
||||
)
|
||||
|
||||
def _async_remove_callback() -> None:
|
||||
self._callback_index.remove_callback_matcher(callback_matcher)
|
||||
if cancel_active_scan is not None:
|
||||
cancel_active_scan()
|
||||
|
||||
# If we have history for the subscriber, we can trigger the callback
|
||||
# immediately with the last packet so the subscriber can see the
|
||||
# device.
|
||||
history = self._connectable_history if connectable else self._all_history
|
||||
service_infos: Iterable[BluetoothServiceInfoBleak] = []
|
||||
if address := callback_matcher.get(ADDRESS):
|
||||
if (address := callback_matcher.get(ADDRESS)) is not None:
|
||||
if service_info := history.get(address):
|
||||
service_infos = [service_info]
|
||||
else:
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==2.1.1",
|
||||
"bleak-retry-connector==4.6.0",
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.29.11",
|
||||
"dbus-fast==5.0.0",
|
||||
"habluetooth==6.1.0"
|
||||
"bleak==3.0.2",
|
||||
"bleak-retry-connector==4.6.1",
|
||||
"bluetooth-adapters==2.3.0",
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.14",
|
||||
"habluetooth==6.7.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -298,9 +298,13 @@ class PassiveBluetoothProcessorCoordinator[_DataT](BasePassiveBluetoothCoordinat
|
||||
mode: BluetoothScanningMode,
|
||||
update_method: Callable[[BluetoothServiceInfoBleak], _DataT],
|
||||
connectable: bool = False,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(hass, logger, address, mode, connectable)
|
||||
super().__init__(
|
||||
hass, logger, address, mode, connectable, scan_interval, scan_duration
|
||||
)
|
||||
self._processors: list[PassiveBluetoothDataProcessor[Any, _DataT]] = []
|
||||
self._update_method = update_method
|
||||
self.last_update_success = True
|
||||
|
||||
@@ -48,9 +48,21 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"passive": "Passive scanning"
|
||||
"mode": "Scanning mode"
|
||||
},
|
||||
"data_description": {
|
||||
"mode": "Auto is recommended for most setups. It saves battery on your Bluetooth devices while still catching new devices and updates quickly."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"mode": {
|
||||
"options": {
|
||||
"active": "Active (uses more device battery, fastest updates)",
|
||||
"auto": "Auto (recommended, saves device battery)",
|
||||
"passive": "Passive (lowest device battery use, some details may be missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
connectable: bool,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.hass = hass
|
||||
@@ -38,6 +40,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
self.connectable = connectable
|
||||
self._on_stop: list[CALLBACK_TYPE] = []
|
||||
self.mode = mode
|
||||
self._scan_interval = scan_interval
|
||||
self._scan_duration = scan_duration
|
||||
self._last_unavailable_time = 0.0
|
||||
self._last_name = address
|
||||
# Subclasses are responsible for setting _available to True
|
||||
@@ -92,6 +96,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
address=self.address, connectable=self.connectable
|
||||
),
|
||||
self.mode,
|
||||
scan_interval=self._scan_interval,
|
||||
scan_duration=self._scan_duration,
|
||||
)
|
||||
)
|
||||
self._on_stop.append(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""The bluetooth integration utilities."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bluetooth_adapters import (
|
||||
ADAPTER_ADDRESS,
|
||||
ADAPTER_MANUFACTURER,
|
||||
@@ -9,14 +13,32 @@ from bluetooth_adapters import (
|
||||
adapter_unique_name,
|
||||
)
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
from habluetooth import get_manager
|
||||
from habluetooth import BluetoothScanningMode, get_manager
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import CONF_MODE, CONF_PASSIVE, DEFAULT_MODE
|
||||
from .models import BluetoothServiceInfoBleak
|
||||
from .storage import BluetoothStorage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_scanning_mode(options: Mapping[str, Any]) -> BluetoothScanningMode:
|
||||
"""Resolve CONF_MODE, falling back to legacy CONF_PASSIVE or DEFAULT_MODE."""
|
||||
if (mode_value := options.get(CONF_MODE)) is not None:
|
||||
try:
|
||||
return BluetoothScanningMode(mode_value)
|
||||
except TypeError, ValueError:
|
||||
_LOGGER.warning("Unknown bluetooth scanning mode %r", mode_value)
|
||||
return BluetoothScanningMode(DEFAULT_MODE)
|
||||
if (legacy_passive := options.get(CONF_PASSIVE)) is True:
|
||||
return BluetoothScanningMode.PASSIVE
|
||||
if legacy_passive is False:
|
||||
return BluetoothScanningMode.ACTIVE
|
||||
return BluetoothScanningMode(DEFAULT_MODE)
|
||||
|
||||
|
||||
class InvalidConfigEntryID(HomeAssistantError):
|
||||
"""Invalid config entry id."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,14 @@ from pybravia import BraviaAuthError, BraviaClient, BraviaError, BraviaNotSuppor
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN
|
||||
from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.helpers import instance_id
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.service_info.ssdp import (
|
||||
@@ -23,7 +30,6 @@ from homeassistant.util.network import is_host_valid
|
||||
from .const import (
|
||||
ATTR_CID,
|
||||
ATTR_MAC,
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
|
||||
@@ -6,8 +6,6 @@ from typing import Final
|
||||
ATTR_CID: Final = "cid"
|
||||
ATTR_MAC: Final = "macAddr"
|
||||
ATTR_MANUFACTURER: Final = "Sony"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.notify import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_NAME,
|
||||
CONF_RECIPIENT,
|
||||
CONF_USERNAME,
|
||||
@@ -29,8 +30,6 @@ BASE_API_URL = "https://rest.clicksend.com/v3"
|
||||
|
||||
HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_VOICE = "voice"
|
||||
|
||||
MALE_VOICE = "male"
|
||||
|
||||
@@ -32,7 +32,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import Event, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, start
|
||||
@@ -275,9 +274,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.alexa_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
@@ -308,8 +304,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
"""If an entity should be exposed."""
|
||||
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||
if not entity_filter.empty_filter:
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
|
||||
|
||||
@@ -22,7 +22,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import (
|
||||
CoreState,
|
||||
Event,
|
||||
@@ -276,15 +275,16 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
)
|
||||
)
|
||||
|
||||
def should_expose(self, state: State) -> bool:
|
||||
"""If a state object should be exposed."""
|
||||
return self._should_expose_entity_id(state.entity_id)
|
||||
def should_expose(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||
if not entity_filter.empty_filter:
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
||||
|
||||
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||
"""If an entity ID should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
@@ -312,16 +312,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
and _supported_legacy(self.hass, entity_id)
|
||||
)
|
||||
|
||||
def _should_expose_entity_id(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||
if not entity_filter.empty_filter:
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
||||
|
||||
@property
|
||||
def agent_user_id(self) -> str:
|
||||
"""Return Agent User Id to use for query responses."""
|
||||
@@ -473,7 +463,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
entity_id = event.data["entity_id"]
|
||||
|
||||
if not self._should_expose_entity_id(entity_id):
|
||||
if not self.should_expose(entity_id):
|
||||
return
|
||||
|
||||
self.async_schedule_google_sync_all()
|
||||
@@ -496,8 +486,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
# Check if any exposed entity uses the device area
|
||||
if not any(
|
||||
entity_entry.area_id is None
|
||||
and self._should_expose_entity_id(entity_entry.entity_id)
|
||||
entity_entry.area_id is None and self.should_expose(entity_entry.entity_id)
|
||||
for entity_entry in er.async_entries_for_device(
|
||||
er.async_get(self.hass), event.data["device_id"]
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.components.homeassistant import exposed_entities
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.system_health import get_info as get_system_health_info
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -973,7 +972,7 @@ async def google_assistant_get(
|
||||
return
|
||||
|
||||
entity = google_helpers.GoogleEntity(hass, gconf, state)
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported():
|
||||
if not entity.is_supported():
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_SUPPORTED,
|
||||
@@ -1075,9 +1074,7 @@ async def alexa_get(
|
||||
"""Get data for a single alexa entity."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
|
||||
hass, entity_id
|
||||
):
|
||||
if not entity_supported_by_alexa(hass, entity_id):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_SUPPORTED,
|
||||
|
||||
@@ -17,10 +17,11 @@ from homeassistant.components.backup import (
|
||||
OnProgressCallback,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import R2ConfigEntry
|
||||
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CACHE_TTL = 300
|
||||
|
||||
@@ -13,6 +13,7 @@ from botocore.exceptions import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PREFIX
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
@@ -25,7 +26,6 @@ from .const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
CONF_PREFIX,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DEFAULT_ENDPOINT_URL,
|
||||
DESCRIPTION_R2_AUTH_DOCS_URL,
|
||||
|
||||
@@ -11,8 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
# R2 is S3-compatible. Endpoint should be like:
|
||||
# https://<accountid>.r2.cloudflarestorage.com
|
||||
|
||||
@@ -5,6 +5,3 @@ ATTR_URL = "color_extract_url"
|
||||
|
||||
DOMAIN = "color_extractor"
|
||||
DEFAULT_NAME = "Color extractor"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
SERVICE_TURN_ON = "turn_on"
|
||||
|
||||
@@ -14,11 +14,11 @@ from homeassistant.components.light import (
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
LIGHT_TURN_ON_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON
|
||||
from homeassistant.const import SERVICE_TURN_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
|
||||
from .const import ATTR_PATH, ATTR_URL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -141,7 +141,7 @@ async def async_handle_service(service_call: ServiceCall) -> None:
|
||||
service_data[ATTR_RGB_COLOR] = color
|
||||
|
||||
await service_call.hass.services.async_call(
|
||||
LIGHT_DOMAIN, LIGHT_SERVICE_TURN_ON, service_data, blocking=True
|
||||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, blocking=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -60,7 +60,9 @@ class CheckConfigView(HomeAssistantView):
|
||||
vol.Optional("location_name"): str,
|
||||
vol.Optional("longitude"): cv.longitude,
|
||||
vol.Optional("radius"): cv.positive_int,
|
||||
vol.Optional("time_zone"): cv.time_zone,
|
||||
# Validated by async_set_time_zone in the executor to avoid
|
||||
# blocking I/O loading zoneinfo data on the event loop.
|
||||
vol.Optional("time_zone"): str,
|
||||
vol.Optional("update_units"): bool,
|
||||
vol.Optional("unit_system"): unit_system.validate_unit_system,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -42,6 +42,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: CookidooConfigEntry) ->
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def _migrate_identifiers(
|
||||
hass: HomeAssistant,
|
||||
config_entry: CookidooConfigEntry,
|
||||
old_prefix: str,
|
||||
new_unique_id: str,
|
||||
) -> None:
|
||||
"""Migrate device identifiers and entity unique_ids from old to new prefix."""
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
for dev in device_entries:
|
||||
new_identifiers = {
|
||||
(DOMAIN, new_unique_id) if domain == DOMAIN else (domain, identifier)
|
||||
for domain, identifier in dev.identifiers
|
||||
}
|
||||
device_registry.async_update_device(dev.id, new_identifiers=new_identifiers)
|
||||
for ent in entity_entries:
|
||||
if ent.unique_id and ent.unique_id.startswith(f"{old_prefix}_"):
|
||||
entity_registry.async_update_entity(
|
||||
ent.entity_id,
|
||||
new_unique_id=f"{new_unique_id}{ent.unique_id[len(old_prefix) :]}",
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: CookidooConfigEntry
|
||||
) -> bool:
|
||||
@@ -49,41 +78,37 @@ async def async_migrate_entry(
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
# Add the unique uuid
|
||||
# Add the unique uuid (first migration, entities used config_entry_id as prefix)
|
||||
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
|
||||
|
||||
try:
|
||||
auth_data = await cookidoo.login()
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
except (CookidooRequestException, CookidooAuthException) as e:
|
||||
_LOGGER.error(
|
||||
"Could not migrate config config_entry: %s",
|
||||
str(e),
|
||||
)
|
||||
_LOGGER.error("Could not migrate config entry: %s", e)
|
||||
return False
|
||||
|
||||
unique_id = auth_data.sub
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
for dev in device_entries:
|
||||
device_registry.async_update_device(
|
||||
dev.id, new_identifiers={(DOMAIN, unique_id)}
|
||||
)
|
||||
for ent in entity_entries:
|
||||
assert ent.config_entry_id
|
||||
entity_registry.async_update_entity(
|
||||
ent.entity_id,
|
||||
new_unique_id=ent.unique_id.replace(ent.config_entry_id, unique_id),
|
||||
)
|
||||
|
||||
_migrate_identifiers(hass, config_entry, config_entry.entry_id, user_info.id)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=auth_data.sub, minor_version=2
|
||||
config_entry, unique_id=user_info.id, minor_version=3
|
||||
)
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 2:
|
||||
# Migrate unique_id from old CIAM sub to community profile id
|
||||
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
|
||||
|
||||
try:
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
except (CookidooRequestException, CookidooAuthException) as e:
|
||||
_LOGGER.error("Could not migrate config entry: %s", e)
|
||||
return False
|
||||
|
||||
old_unique_id = config_entry.unique_id
|
||||
if old_unique_id:
|
||||
_migrate_identifiers(hass, config_entry, old_unique_id, user_info.id)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=user_info.id, minor_version=3
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
|
||||
from cookidoo_api import CookidooAuthException, CookidooException
|
||||
from cookidoo_api import (
|
||||
CookidooAuthException,
|
||||
CookidooException,
|
||||
CookidooRequestException,
|
||||
)
|
||||
from cookidoo_api.types import CookidooCalendarDayRecipe
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
@@ -74,7 +78,13 @@ class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity):
|
||||
week_day
|
||||
)
|
||||
except CookidooAuthException:
|
||||
await self.coordinator.cookidoo.refresh_token()
|
||||
try:
|
||||
await self.coordinator.cookidoo.login()
|
||||
except (CookidooAuthException, CookidooRequestException) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="calendar_fetch_failed",
|
||||
) from exc
|
||||
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
|
||||
week_day
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Cookidoo."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
COUNTRY_DATA_SCHEMA: dict
|
||||
LANGUAGE_DATA_SCHEMA: dict
|
||||
@@ -223,8 +223,9 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
cookidoo = await cookidoo_from_config_data(self.hass, data_input)
|
||||
try:
|
||||
auth_data = await cookidoo.login()
|
||||
self.user_uuid = auth_data.sub
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
self.user_uuid = user_info.id
|
||||
if language_input:
|
||||
await cookidoo.get_additional_items()
|
||||
except CookidooRequestException:
|
||||
|
||||
@@ -87,7 +87,7 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
)
|
||||
except CookidooAuthException:
|
||||
try:
|
||||
await self.cookidoo.refresh_token()
|
||||
await self.cookidoo.login()
|
||||
except CookidooAuthException as exc:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -96,6 +96,11 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
|
||||
},
|
||||
) from exc
|
||||
except CookidooRequestException as exc:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_request_exception",
|
||||
) from exc
|
||||
_LOGGER.debug(
|
||||
"Authentication failed but re-authentication"
|
||||
" was successful, trying again later"
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
|
||||
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .coordinator import CookidooConfigEntry
|
||||
|
||||
@@ -21,7 +22,7 @@ async def cookidoo_from_config_data(
|
||||
)
|
||||
|
||||
return Cookidoo(
|
||||
async_get_clientsession(hass),
|
||||
async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)),
|
||||
CookidooConfig(
|
||||
email=data[CONF_EMAIL],
|
||||
password=data[CONF_PASSWORD],
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["cookidoo_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["cookidoo-api==0.14.0"]
|
||||
"requirements": ["cookidoo-api==0.17.2"]
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
|
||||
"""Implementing the Currencylayer sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by currencylayer.com"
|
||||
_attr_icon = "mdi:currency"
|
||||
_attr_icon = "mdi:currency-usd"
|
||||
|
||||
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DeconzConfigEntry
|
||||
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
|
||||
from .const import ATTR_OFFSET, ATTR_VALVE
|
||||
from .entity import DeconzDevice
|
||||
from .hub import DeconzHub
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
ATTR_DARK = "dark"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_LOCKED = "locked"
|
||||
ATTR_OFFSET = "offset"
|
||||
ATTR_ON = "on"
|
||||
ATTR_VALVE = "valve"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, final
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.components.zone import ATTR_PASSIVE, ATTR_RADIUS
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
@@ -207,6 +208,7 @@ class TrackerEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
|
||||
|
||||
CACHED_TRACKER_PROPERTIES_WITH_ATTR_ = {
|
||||
"in_zones",
|
||||
"latitude",
|
||||
"location_accuracy",
|
||||
"location_name",
|
||||
@@ -220,6 +222,7 @@ class TrackerEntity(
|
||||
"""Base class for a tracked device."""
|
||||
|
||||
entity_description: TrackerEntityDescription
|
||||
_attr_in_zones: list[str] | None = None
|
||||
_attr_latitude: float | None = None
|
||||
_attr_location_accuracy: float = 0
|
||||
_attr_location_name: str | None = None
|
||||
@@ -239,6 +242,16 @@ class TrackerEntity(
|
||||
"""All updates need to be written to the state machine if we're not polling."""
|
||||
return not self.should_poll
|
||||
|
||||
@cached_property
|
||||
def in_zones(self) -> list[str] | None:
|
||||
"""Return the entity_id of zones the device is currently in.
|
||||
|
||||
The list may be in any order; the base class sorts it by zone radius
|
||||
and discards zones which do not exist. Ignored if latitude and
|
||||
longitude are both set.
|
||||
"""
|
||||
return self._attr_in_zones
|
||||
|
||||
@cached_property
|
||||
def location_accuracy(self) -> float:
|
||||
"""Return the location accuracy of the device.
|
||||
@@ -269,6 +282,20 @@ class TrackerEntity(
|
||||
self.__active_zone, self.__in_zones = zone.async_in_zones(
|
||||
self.hass, self.latitude, self.longitude, self.location_accuracy
|
||||
)
|
||||
elif (zones := self.in_zones) is not None:
|
||||
zone_states = sorted(
|
||||
(
|
||||
zone_state
|
||||
for entity_id in zones
|
||||
if (zone_state := self.hass.states.get(entity_id)) is not None
|
||||
),
|
||||
key=lambda z: z.attributes[ATTR_RADIUS],
|
||||
)
|
||||
self.__active_zone = next(
|
||||
(z for z in zone_states if not z.attributes.get(ATTR_PASSIVE)),
|
||||
None,
|
||||
)
|
||||
self.__in_zones = [z.entity_id for z in zone_states]
|
||||
else:
|
||||
self.__active_zone = None
|
||||
self.__in_zones = None
|
||||
@@ -280,7 +307,9 @@ class TrackerEntity(
|
||||
if self.location_name is not None:
|
||||
return self.location_name
|
||||
|
||||
if self.latitude is not None and self.longitude is not None:
|
||||
if (
|
||||
self.latitude is not None and self.longitude is not None
|
||||
) or self.__in_zones is not None:
|
||||
zone_state = self.__active_zone
|
||||
if zone_state is None:
|
||||
state = STATE_NOT_HOME
|
||||
@@ -296,11 +325,10 @@ class TrackerEntity(
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
|
||||
attr: dict[str, Any] = {ATTR_IN_ZONES: self.__in_zones or []}
|
||||
attr.update(super().state_attributes)
|
||||
|
||||
if self.latitude is not None and self.longitude is not None:
|
||||
attr[ATTR_IN_ZONES] = self.__in_zones or []
|
||||
attr[ATTR_LATITUDE] = self.latitude
|
||||
attr[ATTR_LONGITUDE] = self.longitude
|
||||
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
|
||||
@@ -329,6 +357,23 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
"""Return true if the device is connected."""
|
||||
raise NotImplementedError
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
|
||||
attr.update(super().state_attributes)
|
||||
|
||||
if not self.is_connected:
|
||||
return attr
|
||||
|
||||
attr[ATTR_IN_ZONES] = [
|
||||
zone.ENTITY_ID_HOME,
|
||||
*zone.async_get_enclosing_zones(self.hass, zone.ENTITY_ID_HOME),
|
||||
]
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class ScannerEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""A class that describes tracker entities."""
|
||||
@@ -456,9 +501,12 @@ class ScannerEntity(
|
||||
# Do this last or else the entity registry update listener has been installed
|
||||
await super().async_internal_added_to_hass()
|
||||
|
||||
@final
|
||||
# BaseScannerEntity.state_attributes is @final to keep external subclasses
|
||||
# from tampering with it; ScannerEntity is an in-tree subclass that
|
||||
# intentionally extends it with ip/mac/hostname.
|
||||
@final # type: ignore[misc]
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, StateType]:
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
attr = super().state_attributes
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user