Compare commits

..

4 Commits

Author SHA1 Message Date
Stefan Agner ba625e5098 Drop unused ATTR_LOCATION import 2026-05-20 11:44:11 +02:00
copilot-swe-agent[bot] cbfc323f04 Drop unused KEY_TO_UPDATE_TYPES and ATTR_STATE in hassio const
Agent-Logs-Url: https://github.com/home-assistant/core/sessions/cce88fc6-5dd6-48c2-a981-10954ad09082

Co-authored-by: agners <34061+agners@users.noreply.github.com>
2026-05-20 09:20:03 +00:00
copilot-swe-agent[bot] f083852511 Use shared ATTR_LOCATION and ATTR_STATE constants in hassio
Agent-Logs-Url: https://github.com/home-assistant/core/sessions/42eee317-d7ee-43a7-862c-a92f7a796969

Co-authored-by: agners <34061+agners@users.noreply.github.com>
2026-05-19 13:59:59 +00:00
copilot-swe-agent[bot] dc97d8c096 Initial plan 2026-05-19 13:52:08 +00:00
597 changed files with 11575 additions and 20462 deletions
@@ -18,13 +18,6 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
+1 -1
View File
@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.05.0"
BASE_IMAGE_VERSION: "2026.04.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
@@ -1,31 +0,0 @@
name: Check requirements (changes detection)
# Stage 1 of the agentic Check requirements workflow.
# Just kicks off Stage 2 (`check-requirements-dispatcher.yml`) which starts the agentic workflow
# yamllint disable-line rule:truthy
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
- "pyproject.toml"
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
changes:
name: Requirements files changed
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Record PR number
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |-
echo "Requirements files changed in PR #${PR_NUMBER}"
@@ -1,73 +0,0 @@
name: Check requirements (dispatcher)
# Stage 2 of the agentic Check requirements workflow. Runs on completion of
# stage 1 (`check-requirements-changes.yml`) and dispatches stage 3
# (`check-requirements.lock.yml`)
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
# yamllint disable-line rule:truthy
on: # zizmor: ignore[dangerous-triggers]
# workflow_run is safe here: this workflow does not check out PR code or run
# any code from the triggering PR. It only resolves the PR number from the
# head SHA and dispatches `check-requirements.lock.yml` with that number as
# a sanitized string input. The PR code is analysed downstream in the
# agentic workflow (`check-requirements.lock.yml`)
workflow_run:
workflows: ["Check requirements (changes detection)"]
types: [completed]
permissions: {}
jobs:
dispatch:
name: Dispatch agentic requirements check
if: >
github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write # For triggering the downstream workflow
pull-requests: read # For querying PRs by commit SHA
steps:
- name: Resolve PR number from head SHA and trigger agentic requirements check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
const headBranch = context.payload.workflow_run.head_branch;
const headRepository = context.payload.workflow_run.head_repository;
const headRepo = headRepository.full_name;
// Query the head repository (which may be a fork). When the PR comes
// from a fork, the upstream's listPullRequestsAssociatedWithCommit
// returns no results for the fork's commit SHA.
const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: headRepository.owner.login,
repo: headRepository.name,
commit_sha: headSha,
});
const matches = pulls.filter(p =>
p.state === 'open'
&& p.head.ref === headBranch
&& p.head.repo?.full_name === headRepo
);
if (matches.length === 0) {
core.info(`No open PR found for head SHA ${headSha} on ${headRepo}:${headBranch}; nothing to dispatch.`);
return;
}
const defaultBranch = context.payload.workflow_run.repository.default_branch;
for (const pr of matches) {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'check-requirements.lock.yml',
ref: defaultBranch,
inputs: {
pull_request_number: String(pr.number),
},
});
core.info(`Dispatched check-requirements.lock.yml for PR #${pr.number}.`);
}
+72 -66
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"62eb6e3d38092bd041a0c1ddfdaef94cf4b9c694b2d2bcac6cbbecd6810230ca","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"70aa938926d4aac250b6d7aca7251f663476fc2da39a29d1ffd569dc725c133a","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"}]}
# ___ _ _
# / _ \ | | (_)
@@ -34,6 +34,7 @@
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
@@ -46,8 +47,20 @@
# - ghcr.io/github/github-mcp-server:v1.0.4
# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
name: "Check requirements"
name: "Requirements License and Availability Check"
on:
pull_request:
# forks: # Fork filtering applied via job conditions
# - "*" # Fork filtering applied via job conditions
paths:
- requirements*.txt
- homeassistant/package_constraints.txt
- pyproject.toml
types:
- opened
- synchronize
- reopened
# roles: all # Roles processed as role check in pre-activation job
workflow_dispatch:
inputs:
aw_context:
@@ -63,10 +76,10 @@ on:
permissions: {}
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}"
cancel-in-progress: true
group: ${{ github.workflow }}-${{ inputs.pull_request_number }}
run-name: "Check requirements"
run-name: "Requirements License and Availability Check"
jobs:
activation:
@@ -75,6 +88,7 @@ jobs:
actions: read
contents: read
outputs:
body: ${{ steps.sanitized.outputs.body }}
comment_id: ""
comment_repo: ""
engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
@@ -85,6 +99,8 @@ jobs:
setup-span-id: ${{ steps.setup.outputs.span-id }}
setup-trace-id: ${{ steps.setup.outputs.trace-id }}
stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
text: ${{ steps.sanitized.outputs.text }}
title: ${{ steps.sanitized.outputs.title }}
steps:
- name: Setup Scripts
id: setup
@@ -93,7 +109,7 @@ jobs:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -106,7 +122,7 @@ jobs:
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_AGENT_VERSION: "1.0.48"
GH_AW_INFO_CLI_VERSION: "v0.74.4"
GH_AW_INFO_WORKFLOW_NAME: "Check requirements"
GH_AW_INFO_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
GH_AW_INFO_STAGED: "false"
@@ -171,6 +187,17 @@ jobs:
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs');
await main();
- name: Compute current body text
id: sanitized
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
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"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs');
await main();
- name: Create prompt with built-in context
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
@@ -183,25 +210,24 @@ jobs:
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
# poutine:ignore untrusted_checkout_exec
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
<system>
GH_AW_PROMPT_2df1318dbe2d4011_EOF
GH_AW_PROMPT_f9148b37e60758ba_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_2df1318dbe2d4011_EOF'
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
<safe-output-tools>
Tools: add_comment, missing_tool, missing_data, noop
</safe-output-tools>
GH_AW_PROMPT_2df1318dbe2d4011_EOF
GH_AW_PROMPT_f9148b37e60758ba_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -230,19 +256,18 @@ jobs:
{{/if}}
</github-context>
GH_AW_PROMPT_2df1318dbe2d4011_EOF
GH_AW_PROMPT_f9148b37e60758ba_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
</system>
{{#runtime-import .github/workflows/check-requirements.md}}
GH_AW_PROMPT_2df1318dbe2d4011_EOF
GH_AW_PROMPT_f9148b37e60758ba_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_ENGINE_ID: "copilot"
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@@ -261,7 +286,6 @@ jobs:
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools'
with:
script: |
@@ -282,7 +306,6 @@ jobs:
GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
GH_AW_INPUTS_PULL_REQUEST_NUMBER: process.env.GH_AW_INPUTS_PULL_REQUEST_NUMBER,
GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST
}
});
@@ -352,7 +375,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -407,13 +430,16 @@ jobs:
GH_HOST: github.com
- name: Install AWF binary
run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
- name: Parse integrity filter lists
id: parse-guard-vars
- name: Determine automatic lockdown mode for GitHub MCP Server
id: determine-automatic-lockdown
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
env:
GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }}
GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }}
GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }}
run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh"
GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
with:
script: |
const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');
await determineAutomaticLockdown(github, context, core);
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -433,21 +459,19 @@ jobs:
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46 ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 ghcr.io/github/github-mcp-server:v1.0.4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
- name: Generate Safe Outputs Config
env:
GH_AW_INPUT_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
run: |
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_c7878b8b9775118a_EOF
{"add_comment":{"max":1,"target":"${GH_AW_INPUT_PULL_REQUEST_NUMBER}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_c7878b8b9775118a_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e3fcd372e2542806_EOF'
{"add_comment":{"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_e3fcd372e2542806_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: ${{ inputs.pull_request_number }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -603,6 +627,8 @@ jobs:
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }}
GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }}
GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -eo pipefail
@@ -633,7 +659,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_103328ae7b98b0c7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
cat << GH_AW_MCP_CONFIG_710f47825b96ccb9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
"github": {
@@ -647,11 +673,8 @@ jobs:
},
"guard-policies": {
"allow-only": {
"approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }},
"blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }},
"min-integrity": "unapproved",
"repos": "all",
"trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }}
"min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
"repos": "$GITHUB_MCP_GUARD_REPOS"
}
}
},
@@ -677,7 +700,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
GH_AW_MCP_CONFIG_103328ae7b98b0c7_EOF
GH_AW_MCP_CONFIG_710f47825b96ccb9_EOF
- name: Mount MCP servers as CLIs
id: mount-mcp-clis
continue-on-error: true
@@ -876,21 +899,6 @@ jobs:
if [ ! -f /tmp/gh-aw/agent_output.json ]; then
echo '{"items":[]}' > /tmp/gh-aw/agent_output.json
fi
- if: always()
name: Verify agent produced an add_comment safe-output
run: |-
OUTPUT=/tmp/gh-aw/agent_output.json
if [ ! -f "${OUTPUT}" ]; then
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
exit 1
fi
if ! grep -q '"add_comment"' "${OUTPUT}"; then
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
echo "Agent output:"
cat "${OUTPUT}"
exit 1
fi
- name: Upload agent artifacts
if: always()
continue-on-error: true
@@ -902,8 +910,6 @@ jobs:
/tmp/gh-aw/sandbox/agent/logs/
/tmp/gh-aw/redacted-urls.log
/tmp/gh-aw/mcp-logs/
/tmp/gh-aw/proxy-logs/
!/tmp/gh-aw/proxy-logs/proxy-tls/
/tmp/gh-aw/agent_usage.json
/tmp/gh-aw/agent-stdio.log
/tmp/gh-aw/pre-agent-audit.txt
@@ -953,7 +959,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -977,7 +983,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: "1"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
@@ -993,7 +999,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
@@ -1010,7 +1016,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_MISSING_TOOL_CREATE_ISSUE: "true"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
@@ -1024,7 +1030,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
@@ -1038,7 +1044,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
GH_AW_WORKFLOW_ID: "check-requirements"
@@ -1092,7 +1098,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -1160,7 +1166,7 @@ jobs:
if: always() && steps.detection_guard.outputs.run_detection == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
WORKFLOW_NAME: "Check requirements"
WORKFLOW_NAME: "Requirements License and Availability Check"
WORKFLOW_DESCRIPTION: "Checks changed Python package requirements on PRs targeting the core repo (including PRs opened from forks) and verifies licenses match PyPI metadata, source repositories are publicly accessible, PyPI releases were uploaded via automated CI (Trusted Publisher attestation), the package's release pipeline uses OIDC or equivalent automated credentials (not static tokens), and the PR description contains the required links."
HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
with:
@@ -1290,7 +1296,7 @@ jobs:
GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }}
GH_AW_ENGINE_VERSION: "1.0.48"
GH_AW_WORKFLOW_ID: "check-requirements"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
outputs:
code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
@@ -1310,7 +1316,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -1345,7 +1351,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\":\"${{ inputs.pull_request_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},\"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: |
+63 -74
View File
@@ -1,11 +1,19 @@
---
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
- "pyproject.toml"
forks: ["*"]
workflow_dispatch:
inputs:
pull_request_number:
description: "Pull request number to (re-)check"
required: true
type: number
roles: all
permissions:
contents: read
pull-requests: read
@@ -17,29 +25,9 @@ tools:
web-fetch: {}
github:
toolsets: [default]
min-integrity: unapproved
safe-outputs:
add-comment:
max: 1
target: ${{ inputs.pull_request_number }}
concurrency:
group: ${{ github.workflow }}-${{ inputs.pull_request_number }}
cancel-in-progress: true
post-steps:
- name: Verify agent produced an add_comment safe-output
if: always()
run: |
OUTPUT=/tmp/gh-aw/agent_output.json
if [ ! -f "${OUTPUT}" ]; then
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
exit 1
fi
if ! grep -q '"add_comment"' "${OUTPUT}"; then
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
echo "Agent output:"
cat "${OUTPUT}"
exit 1
fi
description: >
Checks changed Python package requirements on PRs targeting the core repo
(including PRs opened from forks) and verifies licenses match PyPI metadata, source
@@ -49,7 +37,7 @@ description: >
description contains the required links.
---
# Check requirements
# Requirements License and Availability Check
You are a code review assistant for the Home Assistant project. Your job is to
review changes to Python package requirements and verify they meet the project's
@@ -69,14 +57,8 @@ standards.
## Step 1 — Identify Changed Packages
This workflow is triggered via `workflow_dispatch`. The PR number to check is
**#${{ inputs.pull_request_number }}**. Use that PR number for **every** GitHub
API call in the steps below (fetching the diff, the PR body, etc.). Do **not**
rely on `github.event.pull_request` — it is not populated for
`workflow_dispatch` runs.
Use the GitHub tool to fetch the PR diff for that PR number. Look for
lines that were added (`+`) or removed (`-`) in **any** of these files:
Use the GitHub tool to fetch the PR diff. Look for lines that were added (`+`)
or removed (`-`) in **any** of these files:
- `requirements.txt`
- `requirements_all.txt`
- `requirements_test.txt`
@@ -134,7 +116,7 @@ manually.
repository.
- If at least one distribution file has a valid Trusted Publisher attestation,
mark ✅ CI-uploaded.
- If no attestation is found for any file (404 for all), mark ⚠️ — "Release
- If no attestation is found for any file (404 for all), mark — "Release
has no provenance attestation; it may have been uploaded manually".
- If an attestation exists but the `publisher` does not identify a recognized
CI system or Trusted Publisher, mark ⚠️ — "Attestation present but
@@ -156,8 +138,10 @@ For each new or bumped package:
## Step 4 — Check PR Description
Read the PR body from the GitHub API for PR
#${{ inputs.pull_request_number }}. Extract all URLs present in the PR body.
Read the PR body from the GitHub API using the PR number from the workflow
context (`pull-request-number`). If that value is absent, use the
`workflow_dispatch` input `pull_request_number`.
Extract all URLs present in the PR body.
### 4a — New packages: repository link required
@@ -177,29 +161,39 @@ must point directly to the source repository (e.g. a GitHub or GitLab URL).
"PR description must link to the source repository at `<repo_url>` (found
via PyPI). A PyPI page link is not sufficient."
### 4b — Version bumps: changelog or diff link matching the bump
### 4b — Version bumps: changelog or diff link required
For **version bumps**: the PR description must contain a link to a changelog,
release notes page, or a diff/comparison URL that references the **exact
versions** being bumped (old → new) as recorded in the diff from Step 1.
release notes page, or a diff/comparison URL that references the **correct
versions** being bumped (old → new).
Checks to perform for each bumped package (old version = X, new version = Y):
1. Extract all URLs from the PR body that contain the repository's domain or
path (as identified in Step 3).
2. Verify that at least one such URL includes both the old version (X) and the
new version (Y) in some form — e.g. a GitHub compare URL like
2. Verify that at least one such URL includes both the old version string and
new version string in some form — e.g. a GitHub compare URL like
`compare/vX...vY`, a releases URL mentioning version Y, or a
`CHANGELOG.md` anchor referencing Y.
3. Confirm the link's version range matches the actual bump in the diff. If
the link references versions different from X → Y (for example, the PR
bumps `1.2.3 → 1.3.0` but the link points to `compare/v1.2.0...v1.2.4`),
the link does not match the bump.
3. If no URL matches, check if the PR body contains any changelog/diff link at
all for this package.
Outcome:
- ✅ — a URL pointing to the correct repo with version references that match
the exact bump (X → Y).
- no changelog/diff link is found, or the link does not match the actual
bump (X → Y). Explain what was found and what is expected.
- ✅ — a URL pointing to the correct repo with version references covering the
exact bump (X → Y).
- ⚠️a changelog/diff link exists but does not clearly reference the correct
versions or the correct repository; explain what was found and what is
expected.
- ❌ — no changelog or diff link found at all in the PR description for this
package.
### 4c — Diff consistency check
For each **version bump**, verify that the version change recorded in the diff
(Step 1) is internally consistent:
- The `-` line must contain the old version and the `+` line must contain the
new version for the same package name.
- Flag ❌ if the diff shows a downgrade (new version < old version) without an
explanation, or if the version strings cannot be parsed.
## Step 5 — Verify Source Repository is Publicly Accessible
@@ -244,12 +238,9 @@ workflow is sane. The checks differ by hosting provider.
- `pypa/gh-action-pypi-publish` action
- `actions/attest-build-provenance` action
- Any step that sets `TWINE_PASSWORD` from `secrets.PYPI_TOKEN` directly
(treat this as a static long-lived API token rather than OIDC).
Mark ✅ if OIDC is used, ⚠️ if the publish method cannot be determined.
If a static secret token is the only credential, mark ⚠️ for version
bumps (the package was already accepted at a previous version; suggest
the upstream maintainer switch to OIDC / Trusted Publisher for better
security) and ❌ for new packages.
(flag ❌ if a long-lived API token is used instead of OIDC).
Mark ✅ if OIDC is used, ⚠️ if the publish method cannot be determined,
❌ if a static secret token is the only credential.
c. **No manual upload bypass**: Verify there is no step that calls
`twine upload` or `pip upload` outside of a properly gated job (e.g., one
that requires an environment approval). Flag ⚠️ if such steps exist.
@@ -276,11 +267,9 @@ workflow is sane. The checks differ by hosting provider.
b. **Automated credentials**: The job should use GitLab's OIDC ID token
(`id_tokens:` block) and `pypa/gh-action-pypi-publish` equivalent, or
reference `secrets.PYPI_TOKEN` / `$PYPI_TOKEN` injected from GitLab CI/CD
protected variables. Flag ❌ if the token is hard-coded or unprotected.
Mark ✅ if OIDC is used, ⚠️ if the method cannot be determined. If a
protected static token is the only credential, mark ⚠️ for version bumps
(suggest the upstream maintainer switch to OIDC / Trusted Publisher for
better security) and ❌ for new packages.
protected variables (flag ❌ if the token is hard-coded or unprotected).
Mark ✅ if OIDC or protected CI variables are used, ⚠️ if the method
cannot be determined, ❌ if credentials appear to be insecure.
c. **No manual upload bypass**: Flag ⚠️ if any job calls `twine upload`
without being behind a protected-variable or environment guard.
5. If no publish job is found, mark ⚠️ — "No publish job found in .gitlab-ci.yml;
@@ -336,13 +325,13 @@ when the repository is not publicly accessible).
```
<!-- requirements-check -->
## Check requirements
## Requirements Check
| Package | Type | Old→New | License | Repo Public | CI Upload | Release Pipeline | PR Link |
|---------|------|---------|---------|-------------|-----------|------------------|---------|
| PackageA | bump | 1.2.3→1.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ |
| PackageB | new | —→4.5.6 | ❌ | ✅ | ⚠️ | ⚠️ | ❌ |
| PackageC | bump | 2.0.0→2.1.0 | ✅ | ❌ | — | — | |
| Package | Type | Old→New | License | Repo Public | CI Upload | Release Pipeline | PR Link | Diff Consistent |
|---------|------|---------|---------|-------------|-----------|------------------|---------|-----------------|
| PackageA | bump | 1.2.3→1.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| PackageB | new | —→4.5.6 | ❌ | ✅ | | ⚠️ | ❌ | ✅ |
| PackageC | bump | 2.0.0→2.1.0 | ✅ | ❌ | — | — | ⚠️ | ✅ |
```
### 7c — Per-package detail sections
@@ -356,8 +345,8 @@ After the table, add one collapsible `<details>` block per package.
Each block must include the full detail for every check: the license found, the
repository URL, whether a provenance attestation was found, the release
pipeline findings, and the PR link found (or missing, or mismatched with the
actual bump). For failed or warned checks, explain exactly what the contributor
pipeline findings, the PR link found (or missing), and whether the diff is
consistent. For failed or warned checks, explain exactly what the contributor
must fix, including the expected source repository URL, expected version range,
etc.
@@ -369,9 +358,10 @@ Template (repeat for each package):
- **License**: ❌ License is `UNKNOWN` — not in the approved list. Check PyPI metadata and `script/licenses.py`.
- **Repository Public**: ✅ https://github.com/example/packageb is publicly accessible.
- **CI Upload**: ⚠️ No provenance attestation found for any distribution file. The release may have been uploaded manually.
- **CI Upload**: No provenance attestation found for any distribution file. The release may have been uploaded manually.
- **Release Pipeline**: ⚠️ No publish workflow found in the repository; it is unclear how this package is released to PyPI.
- **PR Link**: ❌ PR description must link to the source repository at https://github.com/example/packageb (a PyPI page link is not sufficient).
- **Diff Consistent**: ✅
</details>
```
@@ -387,6 +377,7 @@ Collapsed example (all checks passed):
- **CI Upload**: ✅ Trusted Publisher attestation found (GitHub Actions).
- **Release Pipeline**: ✅ OIDC via `pypa/gh-action-pypi-publish`; triggered on `release: published`; `environment: release` gate.
- **PR Link**: ✅ https://github.com/example/packagea/compare/v1.2.3...v1.3.0
- **Diff Consistent**: ✅
</details>
```
@@ -405,12 +396,10 @@ Collapsed example (all checks passed):
description checks as for production dependencies.
- A package that appears in both a production file and a test file should only
be reported once; use the production file entry as the canonical one.
- This workflow is invoked exclusively via `workflow_dispatch`. The stage-1
workflow `Check requirements (changes detection)` runs on `pull_request` with
a paths filter on the tracked requirements files, and its completion triggers
the dispatcher (`Check requirements (dispatcher)`) which calls this workflow
with the PR number. Members can also dispatch this workflow manually with the
PR number to re-run the check after updating the PR description or fixing
issues without changing any requirements files. On a retrigger the existing
comment is updated in place so there is always exactly one requirements-check
comment in the PR.
- This workflow is only triggered when a commit actually changes one of the
tracked requirements files (for `synchronize` events GitHub compares the
before/after SHAs of the push, not the entire PR diff). Members can manually
retrigger the workflow via `workflow_dispatch` with the PR number to re-run
the check after updating the PR description or fixing issues without changing
any requirements files. On a retrigger the existing comment is updated in
place so there is always exactly one requirements-check comment in the PR.
@@ -236,7 +236,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with:
model: openai/gpt-4o
system-prompt: |
@@ -62,7 +62,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with:
model: openai/gpt-4o-mini
system-prompt: |
+1 -1
View File
@@ -1 +1 @@
3.14.5
3.14.4
@@ -16,8 +16,6 @@ from .entity import AnthropicBaseLLMEntity
if TYPE_CHECKING:
from . import AnthropicConfigEntry
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +24,6 @@ from homeassistant.const import (
CONF_API_KEY,
CONF_LLM_HASS_API,
CONF_NAME,
CONF_PROMPT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, llm
@@ -44,6 +43,7 @@ from .const import (
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_PROMPT_CACHING,
CONF_RECOMMENDED,
CONF_THINKING_BUDGET,
@@ -10,6 +10,8 @@ DEFAULT_CONVERSATION_NAME = "Claude conversation"
DEFAULT_AI_TASK_NAME = "Claude AI Task"
CONF_RECOMMENDED = "recommended"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
CONF_CODE_EXECUTION = "code_execution"
CONF_MAX_TOKENS = "max_tokens"
@@ -4,16 +4,14 @@ from typing import Literal
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT, MATCH_ALL
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AnthropicConfigEntry
from .const import DOMAIN
from .const import CONF_PROMPT, DOMAIN
from .entity import AnthropicBaseLLMEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
@@ -5,10 +5,11 @@ from typing import TYPE_CHECKING, Any
from anthropic import __title__, __version__
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY, CONF_PROMPT
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import entity_registry as er
from .const import (
CONF_PROMPT,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_REGION,
@@ -38,7 +38,10 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
parallel-updates:
status: exempt
comment: |
The API does not limit parallel updates.
reauthentication-flow: done
test-coverage: done
# Gold
@@ -16,9 +16,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
@@ -25,10 +25,6 @@ from .entity import ArcamFmjEntity, convert_exception
_LOGGER = logging.getLogger(__name__)
# arcam-fmj serializes commands on a single TCP writer at the library
# layer; serialize at HA's layer to match the device's contract.
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
@@ -22,9 +22,6 @@ from .entity import ArcamFmjEntity
_LOGGER = logging.getLogger(__name__)
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
PARALLEL_UPDATES = 0
def _enum_options(value: type[IntOrTypeEnum]) -> list[str]:
return [
+2 -2
View File
@@ -205,9 +205,9 @@ class AveaLight(LightEntity):
def turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
if self._attr_brightness:
self._last_brightness = self._attr_brightness
self._light.set_brightness(0)
self._attr_is_on = False
self._attr_brightness = 0
def update(self) -> None:
"""Fetch new state data for this light."""
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["avea"],
"requirements": ["avea==1.8.0"]
"requirements": ["avea==1.6.1"]
}
@@ -20,12 +20,12 @@ from homeassistant.components.backup import (
OnProgressCallback,
suggested_filename,
)
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.async_iterator import AsyncIteratorReader
from . import BackblazeConfigEntry
from .const import (
CONF_PREFIX,
DATA_BACKUP_AGENT_LISTENERS,
DOMAIN,
METADATA_FILE_SUFFIX,
@@ -8,7 +8,6 @@ from b2sdk.v2 import exception
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PREFIX
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
@@ -23,6 +22,7 @@ from .const import (
CONF_APPLICATION_KEY,
CONF_BUCKET,
CONF_KEY_ID,
CONF_PREFIX,
DOMAIN,
)
@@ -10,6 +10,8 @@ DOMAIN: Final = "backblaze_b2"
CONF_KEY_ID = "key_id"
CONF_APPLICATION_KEY = "application_key"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
f"{DOMAIN}.backup_agent_listeners"
@@ -20,7 +20,7 @@
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==5.0.0",
"dbus-fast==4.0.4",
"habluetooth==6.1.0"
]
}
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==6.0.1"],
"requirements": ["python-bsblan==5.2.1"],
"zeroconf": [
{
"name": "bsb-lan*",
+2 -1
View File
@@ -8,7 +8,6 @@ from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -21,6 +20,8 @@ if TYPE_CHECKING:
LOGGER = logging.getLogger(__name__)
# pylint: disable-next=home-assistant-duplicate-const
ATTR_DEVICE_ID = "device_id"
ATTR_MONDAY_SLOTS = "monday_slots"
ATTR_TUESDAY_SLOTS = "tuesday_slots"
ATTR_WEDNESDAY_SLOTS = "wednesday_slots"
+1 -3
View File
@@ -17,8 +17,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import TIMEOUT
type CalDavConfigEntry = ConfigEntry[caldav.DAVClient]
_LOGGER = logging.getLogger(__name__)
@@ -34,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
ssl_verify_cert=entry.data[CONF_VERIFY_SSL],
timeout=TIMEOUT,
timeout=30,
)
try:
await hass.async_add_executor_job(client.principal)
@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, TIMEOUT
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +65,6 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
ssl_verify_cert=user_input[CONF_VERIFY_SSL],
timeout=TIMEOUT,
)
try:
await self.hass.async_add_executor_job(client.principal)
@@ -76,9 +75,6 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
# AuthorizationError can be raised if the url is incorrect or
# on some other unexpected server response.
return "cannot_connect"
except requests.Timeout as err:
_LOGGER.warning("Timeout connecting to CalDAV server: %s", err)
return "cannot_connect"
except requests.ConnectionError as err:
_LOGGER.warning("Connection Error connecting to CalDAV server: %s", err)
return "cannot_connect"
-1
View File
@@ -3,4 +3,3 @@
from typing import Final
DOMAIN: Final = "caldav"
TIMEOUT: Final = 30
@@ -7,7 +7,6 @@ from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [
@@ -25,9 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
if not ble_device:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"address": address},
f"Could not find Casper Glow device with address {address}"
)
glow = CasperGlow(ble_device)
@@ -54,9 +54,6 @@
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Casper Glow: {error}"
},
"device_not_found": {
"message": "Could not find Casper Glow device with address {address}"
}
}
}
@@ -95,42 +95,3 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=self._errors,
)
async def async_step_reconfigure(
self,
user_input: Mapping[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
self._errors = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
host = user_input[CONF_HOST]
port = user_input.get(CONF_PORT, DEFAULT_PORT)
if (
host != reconfigure_entry.data[CONF_HOST]
or port != reconfigure_entry.data[CONF_PORT]
):
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
if await self._test_connection(user_input):
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={CONF_HOST: host, CONF_PORT: port},
unique_id=f"{host}:{port}",
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
),
user_input or reconfigure_entry.data,
),
errors=self._errors,
)
@@ -2,8 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"import_failed": "Import from config failed"
},
"error": {
"connection_refused": "Connection refused when connecting to host",
@@ -12,13 +11,6 @@
"resolve_failed": "This host cannot be resolved"
},
"step": {
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"title": "Reconfigure the certificate to test"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
+4 -2
View File
@@ -17,8 +17,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
APPLICATION_NAME,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
@@ -47,6 +45,10 @@ HA_USER_AGENT = (
)
ATTR_UID = "uid"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LATITUDE = "latitude"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LONGITUDE = "longitude"
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_FREE_EBIKES = "free_ebikes"
ATTR_TIMESTAMP = "timestamp"
+22 -22
View File
@@ -32,28 +32,28 @@ set_temperature:
max: 250
step: 0.1
mode: box
temperature_range:
fields:
target_temp_high:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
target_temp_low:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
target_temp_high:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
target_temp_low:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
hvac_mode:
selector:
state:
@@ -373,12 +373,7 @@
"name": "Target temperature"
}
},
"name": "Set thermostat target temperature",
"sections": {
"temperature_range": {
"name": "Temperature range"
}
}
"name": "Set thermostat target temperature"
},
"toggle": {
"description": "Toggles a thermostat on/off.",
@@ -7,6 +7,12 @@
"message": "Timeout trying to execute command: {command}"
}
},
"issues": {
"platform_yaml_not_supported": {
"description": "Platform YAML setup is not supported.\nChange from configuring it using the `{platform}:` key to using the `command_line:` key directly in configuration.yaml and restart Home Assistant to resolve the issue.\nTo see the detailed documentation, select Learn more.",
"title": "Platform YAML is not supported in Command Line"
}
},
"services": {
"reload": {
"description": "Reloads command line configuration from the YAML-configuration.",
@@ -4,9 +4,7 @@ import asyncio
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_platform import (
async_create_platform_config_not_supported_issue,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from .const import DOMAIN, LOGGER
@@ -100,11 +98,13 @@ def create_platform_yaml_not_supported_issue(
hass: HomeAssistant, platform_domain: str
) -> None:
"""Create an issue when platform yaml is used."""
async_create_platform_config_not_supported_issue(
async_create_issue(
hass,
DOMAIN,
platform_domain,
yaml_config_under_integration_supported=True,
f"{platform_domain}_platform_yaml_not_supported",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="platform_yaml_not_supported",
translation_placeholders={"platform": platform_domain},
learn_more_url="https://www.home-assistant.io/integrations/command_line/",
logger=LOGGER,
)
@@ -91,11 +91,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Compensation sensor."""
hass.data[DATA_COMPENSATION] = {}
# Exit early if no compensations are configured using the compensation: key in configuration.yaml.
# This allows us to create an issue if platform: compensation is present in the sensor: section.
if DOMAIN not in config:
return True
for compensation, conf in config[DOMAIN].items():
_LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
@@ -8,7 +8,6 @@ import numpy as np
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
SensorEntity,
)
from homeassistant.const import (
@@ -32,10 +31,7 @@ from homeassistant.core import (
State,
callback,
)
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_create_platform_config_not_supported_issue,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -45,7 +41,6 @@ from .const import (
CONF_PRECISION,
DATA_COMPENSATION,
DEFAULT_NAME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@@ -63,14 +58,6 @@ async def async_setup_platform(
) -> None:
"""Set up the Compensation sensor."""
if discovery_info is None:
async_create_platform_config_not_supported_issue(
hass,
DOMAIN,
SENSOR_DOMAIN,
yaml_config_under_integration_supported=True,
learn_more_url="https://www.home-assistant.io/integrations/compensation/",
logger=_LOGGER,
)
return
compensation: str = discovery_info[CONF_COMPENSATION]
+20 -34
View File
@@ -12,9 +12,9 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
@@ -37,71 +37,49 @@ async def async_setup_entry(
async_add_entities(
[
DemoSensor(
"sensor_1",
"sensor_1",
"Outside Temperature",
15.6,
SensorDeviceClass.TEMPERATURE,
SensorStateClass.MEASUREMENT,
UnitOfTemperature.CELSIUS,
),
DemoSensor(
"battery_1",
"sensor_1",
"Outside Temperature",
12,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
DemoSensor(
"sensor_2",
"sensor_2",
"Outside Humidity",
54,
SensorDeviceClass.HUMIDITY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
None,
),
DemoSensor(
"sensor_3",
"sensor_3",
"Carbon monoxide",
54,
SensorDeviceClass.CO,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
None,
),
DemoSensor(
"sensor_4",
"sensor_4",
"Carbon dioxide",
54,
SensorDeviceClass.CO2,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
14,
),
DemoSensor(
"battery_4",
"sensor_4",
"Carbon dioxide",
99,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
DemoSensor(
"sensor_5",
"sensor_5",
"Power consumption",
100,
SensorDeviceClass.POWER,
SensorStateClass.MEASUREMENT,
UnitOfPower.WATT,
None,
),
DemoSumSensor(
"sensor_6",
@@ -110,6 +88,7 @@ async def async_setup_entry(
SensorDeviceClass.ENERGY,
SensorStateClass.TOTAL,
UnitOfEnergy.KILO_WATT_HOUR,
None,
"total_energy_kwh",
),
DemoSumSensor(
@@ -119,6 +98,7 @@ async def async_setup_entry(
SensorDeviceClass.ENERGY,
SensorStateClass.TOTAL,
UnitOfEnergy.MEGA_WATT_HOUR,
None,
"total_energy_mwh",
),
DemoSumSensor(
@@ -128,6 +108,7 @@ async def async_setup_entry(
SensorDeviceClass.GAS,
SensorStateClass.TOTAL,
UnitOfVolume.CUBIC_METERS,
None,
"total_gas_m3",
),
DemoSumSensor(
@@ -137,16 +118,17 @@ async def async_setup_entry(
SensorDeviceClass.GAS,
SensorStateClass.TOTAL,
UnitOfVolume.CUBIC_FEET,
None,
"total_gas_ft3",
),
DemoSensor(
unique_id="sensor_10",
device_id="sensor_10",
device_name="Thermostat",
state="eco",
device_class=SensorDeviceClass.ENUM,
state_class=None,
unit_of_measurement=None,
battery=None,
options=["away", "comfort", "eco", "sleep"],
translation_key="thermostat_mode",
),
@@ -158,21 +140,20 @@ class DemoSensor(SensorEntity):
"""Representation of a Demo sensor."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
device_id: str,
device_name: str | None,
state: float | str | None,
device_class: SensorDeviceClass,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
battery: int | None,
options: list[str] | None = None,
translation_key: str | None = None,
entity_category: EntityCategory | None = None,
entity_name: str | None = None,
) -> None:
"""Initialize the sensor."""
self._attr_device_class = device_class
@@ -182,14 +163,15 @@ class DemoSensor(SensorEntity):
self._attr_unique_id = unique_id
self._attr_options = options
self._attr_translation_key = translation_key
self._attr_entity_category = entity_category
self._attr_name = entity_name
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
if battery:
self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery}
class DemoSumSensor(RestoreSensor):
"""Representation of a Demo sensor."""
@@ -205,6 +187,7 @@ class DemoSumSensor(RestoreSensor):
device_class: SensorDeviceClass,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
battery: int | None,
suggested_entity_id: str,
) -> None:
"""Initialize the sensor."""
@@ -221,6 +204,9 @@ class DemoSumSensor(RestoreSensor):
name=device_name,
)
if battery:
self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery}
@callback
def _async_bump_sum(self, now: datetime) -> None:
"""Bump the sum."""
@@ -5,7 +5,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .config_entry import ( # noqa: F401
BaseScannerEntity,
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -166,11 +166,7 @@ def _async_register_mac(
class BaseTrackerEntity(Entity):
"""Represent a tracked device.
Not intended to be directly inherited by integrations. Integrations should
inherit TrackerEntity, BaseScannerEntity or ScannerEntity instead.
"""
"""Represent a tracked device."""
_attr_device_info: None = None
_attr_entity_category = EntityCategory.DIAGNOSTIC
@@ -308,28 +304,6 @@ class TrackerEntity(
return attr
class BaseScannerEntity(BaseTrackerEntity):
"""Base class for a tracked device that can be connected or disconnected.
Unlike ScannerEntity, this entity does not make assumptions about MAC
addresses being used to identify the device.
"""
@property
def state(self) -> str | None:
"""Return the state of the device."""
if self.is_connected is None:
return None
if self.is_connected:
return STATE_HOME
return STATE_NOT_HOME
@property
def is_connected(self) -> bool | None:
"""Return true if the device is connected."""
raise NotImplementedError
class ScannerEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes tracker entities."""
@@ -342,7 +316,7 @@ CACHED_SCANNER_PROPERTIES_WITH_ATTR_ = {
class ScannerEntity(
BaseScannerEntity, cached_properties=CACHED_SCANNER_PROPERTIES_WITH_ATTR_
BaseTrackerEntity, cached_properties=CACHED_SCANNER_PROPERTIES_WITH_ATTR_
):
"""Base class for a tracked device that is on a scanned network."""
@@ -367,6 +341,18 @@ class ScannerEntity(
"""Return hostname of the device."""
return self._attr_hostname
@property
def state(self) -> str:
"""Return the state of the device."""
if self.is_connected:
return STATE_HOME
return STATE_NOT_HOME
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
raise NotImplementedError
@property
def unique_id(self) -> str | None:
"""Return unique ID of the entity."""
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==3.2.0",
"aiodiscover==2.7.1",
"cached-ipaddress==1.0.1"
]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.4"]
"requirements": ["aiodns==4.0.3"]
}
+1 -5
View File
@@ -60,11 +60,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
translation_placeholders={"error": repr(err)},
) from err
except DucoError as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"error": repr(err)},
) from err
raise ConfigEntryError(f"Duco API error: {err}") from err
async def _async_update_data(self) -> DucoData:
"""Fetch node data from the Duco box."""
@@ -16,7 +16,6 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import SERVICE_ALARM_ARM_VACATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -44,6 +43,8 @@ DISPLAY_MESSAGE_SERVICE_SCHEMA: VolDictType = {
}
SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation"
SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant"
SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant"
SERVICE_ALARM_BYPASS = "alarm_bypass"
@@ -10,7 +10,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE,
CONF_HOST,
CONF_PASSWORD,
CONF_PREFIX,
@@ -33,6 +32,9 @@ from .discovery import (
async_update_entry_from_discovery,
)
# pylint: disable-next=home-assistant-duplicate-const
CONF_DEVICE = "device"
NON_SECURE_PORT = 2101
SECURE_PORT = 2601
STANDARD_PORTS = {NON_SECURE_PORT, SECURE_PORT}
+16 -20
View File
@@ -99,16 +99,14 @@ increase_speed:
supported_features:
- fan.FanEntityFeature.SET_SPEED
fields:
additional_fields:
collapsed: true
fields:
percentage_step:
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
percentage_step:
advanced: true
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
decrease_speed:
target:
@@ -117,13 +115,11 @@ decrease_speed:
supported_features:
- fan.FanEntityFeature.SET_SPEED
fields:
additional_fields:
collapsed: true
fields:
percentage_step:
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
percentage_step:
advanced: true
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
+2 -13
View File
@@ -2,7 +2,6 @@
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"section_additional_fields_name": "Additional options",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -110,12 +109,7 @@
"name": "Decrement"
}
},
"name": "Decrease fan speed",
"sections": {
"additional_fields": {
"name": "[%key:component::fan::common::section_additional_fields_name%]"
}
}
"name": "Decrease fan speed"
},
"increase_speed": {
"description": "Increases the speed of a fan.",
@@ -125,12 +119,7 @@
"name": "Increment"
}
},
"name": "Increase fan speed",
"sections": {
"additional_fields": {
"name": "[%key:component::fan::common::section_additional_fields_name%]"
}
}
"name": "Increase fan speed"
},
"oscillate": {
"description": "Controls the oscillation of a fan.",
+2 -65
View File
@@ -1,82 +1,19 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
from datetime import timedelta
import logging
from freebox_api.exceptions import HttpRequestError
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.event import async_track_time_interval
from .const import DOMAIN, PLATFORMS
from .const import PLATFORMS
from .router import FreeboxConfigEntry, FreeboxRouter, get_api
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
# Old entity name suffixes that need rewriting to the entity description key.
# Format: (platform, old name suffix, new key)
_STATIC_UNIQUE_ID_MIGRATIONS: tuple[tuple[Platform, str, str], ...] = (
(Platform.SENSOR, "Freebox download speed", "rate_down"),
(Platform.SENSOR, "Freebox upload speed", "rate_up"),
(Platform.SENSOR, "Freebox missed calls", "missed"),
(Platform.BUTTON, "Reboot Freebox", "reboot"),
(Platform.BUTTON, "Mark calls as read", "mark_calls_as_read"),
(Platform.SWITCH, "Freebox WiFi", "wifi"),
)
async def async_migrate_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool:
"""Migrate old config entries."""
if entry.version < 2:
api = await get_api(hass, entry.data[CONF_HOST])
try:
await api.open(entry.data[CONF_HOST], entry.data[CONF_PORT])
freebox_config = await api.system.get_config()
except HttpRequestError:
_LOGGER.warning(
"Unable to migrate Freebox entry to version 2: cannot reach the router"
)
return False
finally:
await api.close()
mac: str = freebox_config["mac"]
entity_registry = er.async_get(hass)
migrations: list[tuple[Platform, str, str]] = [
(platform, f"{mac} {old_suffix}", f"{mac} {new_key}")
for platform, old_suffix, new_key in _STATIC_UNIQUE_ID_MIGRATIONS
]
migrations.extend(
(
Platform.SENSOR,
f"{mac} Freebox {sensor['name']}",
f"{mac} {sensor['id']}",
)
for sensor in freebox_config.get("sensors", [])
)
for platform, old_uid, new_uid in migrations:
if entity_id := entity_registry.async_get_entity_id(
platform, DOMAIN, old_uid
):
entity_registry.async_update_entity(entity_id, new_unique_id=new_uid)
_LOGGER.debug(
"Migrated %s unique_id from %s to %s",
entity_id,
old_uid,
new_uid,
)
hass.config_entries.async_update_entry(entry, version=2)
return True
async def async_setup_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool:
"""Set up Freebox entry."""
@@ -48,7 +48,6 @@ class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity):
"""Representation of a Freebox alarm."""
_attr_code_arm_required = False
_attr_name = None
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
"""Initialize an alarm."""
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
RAID_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="raid_degraded",
translation_key="raid_degraded",
name="degraded",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -68,7 +68,7 @@ async def async_setup_entry(
class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
"""Representation of a Freebox binary sensor."""
_endpoint_name = "trigger"
_sensor_name = "trigger"
def __init__(
self,
@@ -79,11 +79,9 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
"""Initialize a Freebox binary sensor."""
super().__init__(router, node, sub_node)
self._command_id = self.get_command_id(
node["type"]["endpoints"], "signal", self._endpoint_name
)
self._attr_is_on = self._edit_state(
self.get_value("signal", self._endpoint_name)
node["type"]["endpoints"], "signal", self._sensor_name
)
self._attr_is_on = self._edit_state(self.get_value("signal", self._sensor_name))
async def async_update_signal(self) -> None:
"""Update name & state."""
@@ -93,10 +91,10 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
await FreeboxHomeEntity.async_update_signal(self)
def _edit_state(self, state: bool | None) -> bool | None:
"""Edit state depending on endpoint name."""
"""Edit state depending on sensor name."""
if state is None:
return None
if self._endpoint_name == "trigger":
if self._sensor_name == "trigger":
return not state
return state
@@ -105,14 +103,12 @@ class FreeboxPirSensor(FreeboxHomeBinarySensor):
"""Representation of a Freebox motion binary sensor."""
_attr_device_class = BinarySensorDeviceClass.MOTION
_attr_name = None
class FreeboxDwsSensor(FreeboxHomeBinarySensor):
"""Representation of a Freebox door opener binary sensor."""
_attr_device_class = BinarySensorDeviceClass.DOOR
_attr_name = None
class FreeboxCoverSensor(FreeboxHomeBinarySensor):
@@ -125,15 +121,14 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor):
_attr_device_class = BinarySensorDeviceClass.SAFETY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
_attr_translation_key = "cover"
_endpoint_name = "cover"
_sensor_name = "cover"
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
"""Initialize a cover for another device."""
cover_node = next(
filter(
lambda x: x["name"] == self._endpoint_name and x["ep_type"] == "signal",
lambda x: x["name"] == self._sensor_name and x["ep_type"] == "signal",
node["type"]["endpoints"],
),
None,
@@ -158,7 +153,7 @@ class FreeboxRaidDegradedSensor(BinarySensorEntity):
self._router = router
self._attr_device_info = router.device_info
self._raid = raid
self._attr_translation_placeholders = {"id": str(raid["id"])}
self._attr_name = f"Raid array {raid['id']} {description.name}"
self._attr_unique_id = (
f"{router.mac} {description.key} {raid['name']} {raid['id']}"
)
+3 -3
View File
@@ -25,13 +25,14 @@ class FreeboxButtonEntityDescription(ButtonEntityDescription):
BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = (
FreeboxButtonEntityDescription(
key="reboot",
name="Reboot Freebox",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
async_press=lambda router: router.reboot(),
),
FreeboxButtonEntityDescription(
key="mark_calls_as_read",
translation_key="mark_calls_as_read",
name="Mark calls as read",
entity_category=EntityCategory.DIAGNOSTIC,
async_press=lambda router: router.call.mark_calls_log_as_read(),
),
@@ -54,7 +55,6 @@ async def async_setup_entry(
class FreeboxButton(ButtonEntity):
"""Representation of a Freebox button."""
_attr_has_entity_name = True
entity_description: FreeboxButtonEntityDescription
def __init__(
@@ -64,7 +64,7 @@ class FreeboxButton(ButtonEntity):
self.entity_description = description
self._router = router
self._attr_device_info = router.device_info
self._attr_unique_id = f"{router.mac} {description.key}"
self._attr_unique_id = f"{router.mac} {description.name}"
async def async_press(self) -> None:
"""Press the button."""
+31 -51
View File
@@ -1,16 +1,17 @@
"""Support for Freebox cameras."""
import logging
from typing import Any
from aiohttp import web
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager, async_get_image
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=home-assistant-component-root-import
DEFAULT_ARGUMENTS,
FFmpegCamera,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -18,7 +19,7 @@ from .const import ATTR_DETECTION, FreeboxHomeCategory
from .entity import FreeboxHomeEntity
from .router import FreeboxConfigEntry, FreeboxRouter
_FFMPEG_ARGUMENTS = "-pred 1"
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
@@ -62,21 +63,25 @@ def add_entities(
async_add_entities(new_tracked, True)
class FreeboxCamera(FreeboxHomeEntity, Camera):
class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera):
"""Representation of a Freebox camera."""
_attr_name = None
_attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
def __init__(
self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any]
) -> None:
"""Initialize a camera."""
super().__init__(router, node)
Camera.__init__(self)
self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG]
self._input: str = node["props"]["Stream"]
super().__init__(router, node)
device_info = {
CONF_NAME: node["label"].strip(),
CONF_INPUT: node["props"]["Stream"],
CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS,
}
FFmpegCamera.__init__(self, hass, device_info)
self._supported_features = (
CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
)
self._command_motion_detection = self.get_command_id(
node["type"]["endpoints"], "slot", ATTR_DETECTION
@@ -84,39 +89,6 @@ class FreeboxCamera(FreeboxHomeEntity, Camera):
self._attr_extra_state_attributes = {}
self.update_node(node)
async def stream_source(self) -> str:
"""Return the stream source."""
return self._input.split(" ")[-1]
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
return await async_get_image(
self.hass,
self._input,
output_format=IMAGE_JPEG,
extra_cmd=_FFMPEG_ARGUMENTS,
)
async def handle_async_mjpeg_stream(
self, request: web.Request
) -> web.StreamResponse:
"""Generate an HTTP MJPEG stream from the camera."""
stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(self._input, extra_cmd=_FFMPEG_ARGUMENTS)
try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self.hass,
request,
stream_reader,
self._ffmpeg.ffmpeg_stream_content_type,
)
finally:
await stream.close()
async def async_enable_motion_detection(self) -> None:
"""Enable motion detection in the camera."""
if await self.set_home_endpoint_value(self._command_motion_detection, True):
@@ -130,17 +102,25 @@ class FreeboxCamera(FreeboxHomeEntity, Camera):
async def async_update_signal(self) -> None:
"""Update the camera node."""
self.update_node(self._router.home_devices[self._id])
await super().async_update_signal()
self.async_write_ha_state()
def update_node(self, node: dict[str, Any]) -> None:
"""Update params."""
self._attr_is_streaming = node["status"] == "active"
self._name = node["label"].strip()
# Get status
if self._node["status"] == "active":
self._attr_is_streaming = True
else:
self._attr_is_streaming = False
# Parse all endpoints values
for endpoint in filter(
lambda x: x["ep_type"] == "signal", node["show_endpoints"]
):
self._attr_extra_state_attributes[endpoint["name"]] = endpoint["value"]
# Get motion detection status
self._attr_motion_detection_enabled = self._attr_extra_state_attributes[
ATTR_DETECTION
]
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
class FreeboxFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 2
VERSION = 1
def __init__(self) -> None:
"""Initialize config flow."""
@@ -55,7 +55,6 @@ def add_entities(
class FreeboxDevice(ScannerEntity):
"""Representation of a Freebox device."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None:
+11 -11
View File
@@ -3,7 +3,6 @@
import logging
from typing import Any
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -17,8 +16,6 @@ _LOGGER = logging.getLogger(__name__)
class FreeboxHomeEntity(Entity):
"""Representation of a Freebox base entity."""
_attr_has_entity_name = True
def __init__(
self,
router: FreeboxRouter,
@@ -30,9 +27,12 @@ class FreeboxHomeEntity(Entity):
self._node = node
self._sub_node = sub_node
self._id = node["id"]
self._attr_name = node["label"].strip()
self._device_name = self._attr_name
self._attr_unique_id = f"{self._router.mac}-node_{self._id}"
if sub_node is not None:
self._attr_name += " " + sub_node["label"].strip()
self._attr_unique_id += "-" + sub_node["name"].strip()
self._available = True
@@ -52,7 +52,7 @@ class FreeboxHomeEntity(Entity):
identifiers={(DOMAIN, self._id)},
manufacturer=self._manufacturer,
model=self._model,
name=node["label"].strip(),
name=self._device_name,
sw_version=self._firmware,
via_device=(DOMAIN, router.mac),
)
@@ -60,13 +60,13 @@ class FreeboxHomeEntity(Entity):
async def async_update_signal(self) -> None:
"""Update signal."""
self._node = self._router.home_devices[self._id]
# Propagate Freebox device label changes to the device registry so
# the entity stays in sync when users rename it on the Freebox app.
device_registry = dr.async_get(self.hass)
if device := device_registry.async_get_device(identifiers={(DOMAIN, self._id)}):
new_name = self._node["label"].strip()
if device.name != new_name:
device_registry.async_update_device(device.id, name=new_name)
# Update name
if self._sub_node is None:
self._attr_name = self._node["label"].strip()
else:
self._attr_name = (
self._node["label"].strip() + " " + self._sub_node["label"].strip()
)
self.async_write_ha_state()
async def set_home_endpoint_value(
+3 -22
View File
@@ -1,26 +1,7 @@
{
"entity": {
"sensor": {
"missed": {
"default": "mdi:phone-missed"
},
"partition_free_space": {
"default": "mdi:harddisk"
},
"rate_down": {
"default": "mdi:download-network"
},
"rate_up": {
"default": "mdi:upload-network"
}
},
"switch": {
"wifi": {
"default": "mdi:wifi",
"state": {
"off": "mdi:wifi-off"
}
}
"services": {
"reboot": {
"service": "mdi:restart"
}
}
}
+1 -4
View File
@@ -125,7 +125,6 @@ class FreeboxRouter:
self.supports_raid = True
self.raids: dict[int, dict[str, Any]] = {}
self.sensors_temperature: dict[str, int] = {}
self.sensors_temperature_names: dict[str, str] = {}
self.sensors_connection: dict[str, float] = {}
self.call_list: list[dict[str, Any]] = []
self.home_granted = True
@@ -186,9 +185,7 @@ class FreeboxRouter:
# temperature sensors in celsius degree.
# Name and id of sensors may vary under Freebox devices.
for sensor in syst_datas["sensors"]:
sensor_id = sensor["id"]
self.sensors_temperature[sensor_id] = sensor.get("value")
self.sensors_temperature_names[sensor_id] = sensor["name"]
self.sensors_temperature[sensor["name"]] = sensor.get("value")
# Connection sensors
connection_datas: dict[str, Any] = await self._api.connection.get_status()
+13 -12
View File
@@ -25,34 +25,36 @@ _LOGGER = logging.getLogger(__name__)
CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="rate_down",
translation_key="rate_down",
name="Freebox download speed",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:download-network",
),
SensorEntityDescription(
key="rate_up",
translation_key="rate_up",
name="Freebox upload speed",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:upload-network",
),
)
CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="missed",
translation_key="missed",
native_unit_of_measurement="calls",
state_class=SensorStateClass.MEASUREMENT,
name="Freebox missed calls",
icon="mdi:phone-missed",
),
)
DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="partition_free_space",
translation_key="partition_free_space",
name="free space",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk",
),
)
@@ -75,14 +77,14 @@ async def async_setup_entry(
FreeboxSensor(
router,
SensorEntityDescription(
key=sensor_id,
name=sensor_name,
key=sensor_name,
name=f"Freebox {sensor_name}",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
)
for sensor_id, sensor_name in router.sensors_temperature_names.items()
for sensor_name in router.sensors_temperature
]
entities.extend(
@@ -119,7 +121,6 @@ class FreeboxSensor(SensorEntity):
"""Representation of a Freebox sensor."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self, router: FreeboxRouter, description: SensorEntityDescription
@@ -127,7 +128,7 @@ class FreeboxSensor(SensorEntity):
"""Initialize a Freebox sensor."""
self.entity_description = description
self._router = router
self._attr_unique_id = f"{router.mac} {description.key}"
self._attr_unique_id = f"{router.mac} {description.name}"
self._attr_device_info = router.device_info
@callback
@@ -203,7 +204,7 @@ class FreeboxDiskSensor(FreeboxSensor):
super().__init__(router, description)
self._disk_id = disk["id"]
self._partition_id = partition["id"]
self._attr_translation_placeholders = {"partition": partition["label"]}
self._attr_name = f"{partition['label']} {description.name}"
self._attr_unique_id = (
f"{router.mac} {description.key} {disk['id']} {partition['id']}"
)
@@ -0,0 +1,3 @@
# Freebox service entries description.
reboot:
+4 -32
View File
@@ -25,38 +25,10 @@
}
}
},
"entity": {
"binary_sensor": {
"cover": {
"name": "Cover"
},
"raid_degraded": {
"name": "RAID array {id} degraded"
}
},
"button": {
"mark_calls_as_read": {
"name": "Mark calls as read"
}
},
"sensor": {
"missed": {
"name": "Missed calls"
},
"partition_free_space": {
"name": "{partition} free space"
},
"rate_down": {
"name": "Download speed"
},
"rate_up": {
"name": "Upload speed"
}
},
"switch": {
"wifi": {
"name": "Wi-Fi"
}
"services": {
"reboot": {
"description": "Reboots the Freebox.",
"name": "Reboot"
}
}
}
+2 -4
View File
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
SWITCH_DESCRIPTIONS = [
SwitchEntityDescription(
key="wifi",
translation_key="wifi",
name="Freebox WiFi",
entity_category=EntityCategory.CONFIG,
)
]
@@ -41,8 +41,6 @@ async def async_setup_entry(
class FreeboxSwitch(SwitchEntity):
"""Representation of a freebox switch."""
_attr_has_entity_name = True
def __init__(
self, router: FreeboxRouter, entity_description: SwitchEntityDescription
) -> None:
@@ -50,7 +48,7 @@ class FreeboxSwitch(SwitchEntity):
self.entity_description = entity_description
self._router = router
self._attr_device_info = router.device_info
self._attr_unique_id = f"{router.mac} {entity_description.key}"
self._attr_unique_id = f"{router.mac} {entity_description.name}"
async def _async_set_state(self, enabled: bool) -> None:
"""Turn the switch on or off."""
@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
@@ -28,6 +28,8 @@ CONF_SENSOR = "target_sensor"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
# pylint: disable-next=home-assistant-duplicate-const
CONF_DEVICE_CLASS = "device_class"
CONF_MIN_DUR = "min_cycle_duration"
CONF_DRY_TOLERANCE = "dry_tolerance"
CONF_WET_TOLERANCE = "wet_tolerance"
@@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components import fan, switch
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, PERCENTAGE
from homeassistant.const import CONF_NAME, PERCENTAGE
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
@@ -16,6 +16,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
)
from . import (
CONF_DEVICE_CLASS,
CONF_DRY_TOLERANCE,
CONF_HUMIDIFIER,
CONF_MIN_DUR,
@@ -20,7 +20,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_START,
@@ -57,6 +56,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_AWAY_FIXED,
CONF_AWAY_HUMIDITY,
CONF_DEVICE_CLASS,
CONF_DRY_TOLERANCE,
CONF_HUMIDIFIER,
CONF_INITIAL_STATE,
@@ -5,11 +5,7 @@ from typing import Any
from google_air_quality_api.api import GoogleAirQualityApi
from google_air_quality_api.auth import Auth
from google_air_quality_api.exceptions import (
GoogleAirQualityApiError,
InvalidCustomLAQIConfigurationError,
)
from google_air_quality_api.mapping import AQICategoryMapping
from google_air_quality_api.exceptions import GoogleAirQualityApiError
import voluptuous as vol
from homeassistant.config_entries import (
@@ -22,7 +18,6 @@ from homeassistant.config_entries import (
)
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
@@ -31,28 +26,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
CountrySelector,
LocationSelector,
LocationSelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CONF_REFERRER,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
SECTION_API_KEY_OPTIONS,
)
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
_LOGGER = logging.getLogger(__name__)
AIR_QUALITY_COVERAGE_URL = (
"https://developers.google.com/maps/documentation/air-quality/coverage"
)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
@@ -72,31 +50,10 @@ async def _validate_input(
description_placeholders: dict[str, str],
) -> bool:
try:
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS) or {}
enable_custom_laqi = custom_options.get(CONF_ENABLE_CUSTOM_LAQI)
if enable_custom_laqi:
country = custom_options.get(CONF_COUNTRY)
custom_laqi = custom_options.get(CUSTOM_LAQI)
# When custom LAQI is enabled, both country and custom_laqi must be provided
if not country or not custom_laqi:
errors[CUSTOM_LOCAL_AQI_OPTIONS] = "missing_custom_laqi_options"
return False
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
region_code=country,
custom_local_aqi=custom_laqi,
)
else:
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except InvalidCustomLAQIConfigurationError:
errors["base"] = "mismatch_country_and_laqi"
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except GoogleAirQualityApiError as err:
errors["base"] = "cannot_connect"
description_placeholders["error_message"] = str(err)
@@ -122,25 +79,6 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
CONF_LONGITUDE: hass.config.longitude,
},
): LocationSelector(LocationSelectorConfig(radius=False)),
vol.Optional(CUSTOM_LOCAL_AQI_OPTIONS): section(
vol.Schema(
{
vol.Required(CONF_ENABLE_CUSTOM_LAQI, default=False): bool,
vol.Optional(
CONF_COUNTRY, default=hass.config.country
): CountrySelector(),
vol.Optional(CUSTOM_LAQI): SelectSelector(
SelectSelectorConfig(
options=sorted(
AQICategoryMapping.get_all_laq_indices()
),
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
SectionConfig(collapsed=True),
),
}
)
@@ -185,7 +123,6 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"api_key_url": "https://developers.google.com/maps/documentation/air-quality/get-api-key",
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL,
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
}
if user_input is not None:
@@ -195,13 +132,10 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
return self.async_abort(reason="already_configured")
session = async_get_clientsession(self.hass)
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
auth = Auth(session, user_input[CONF_API_KEY], referrer=referrer)
api = GoogleAirQualityApi(auth)
if await _validate_input(user_input, api, errors, description_placeholders):
subentry_data = dict(user_input[CONF_LOCATION])
custom_opts = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_opts and custom_opts.get(CONF_ENABLE_CUSTOM_LAQI):
subentry_data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_opts
return self.async_create_entry(
title="Google Air Quality",
data={
@@ -211,7 +145,7 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
subentries=[
{
"subentry_type": "location",
"data": subentry_data,
"data": user_input[CONF_LOCATION],
"title": user_input[CONF_NAME],
"unique_id": None,
},
@@ -251,9 +185,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL
}
description_placeholders: dict[str, str] = {}
if user_input is not None:
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
errors["base"] = "location_already_configured"
@@ -270,13 +202,9 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
description_placeholders=description_placeholders,
)
if await _validate_input(user_input, api, errors, description_placeholders):
data = dict(user_input[CONF_LOCATION])
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_options and custom_options.get(CONF_ENABLE_CUSTOM_LAQI):
data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_options
return self.async_create_entry(
title=user_input[CONF_NAME],
data=data,
data=user_input[CONF_LOCATION],
)
else:
user_input = {}
@@ -2,9 +2,6 @@
from typing import Final
CONF_ENABLE_CUSTOM_LAQI: Final = "enable_custom_laqi"
CONF_REFERRER: Final = "referrer"
CUSTOM_LAQI: Final = "custom_laqi"
CUSTOM_LOCAL_AQI_OPTIONS: Final = "custom_local_aqi_options"
DOMAIN: Final = "google_air_quality"
DOMAIN = "google_air_quality"
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
CONF_REFERRER: Final = "referrer"
@@ -10,16 +10,11 @@ from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.model import AirQualityCurrentConditionsData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -54,27 +49,11 @@ class GoogleAirQualityUpdateCoordinator(
subentry = config_entry.subentries[subentry_id]
self.lat = subentry.data[CONF_LATITUDE]
self.long = subentry.data[CONF_LONGITUDE]
self.custom_local_aqi: str | None = None
self.region_code: str | None = None
options = subentry.data.get(CUSTOM_LOCAL_AQI_OPTIONS)
if isinstance(options, dict) and options.get(CONF_ENABLE_CUSTOM_LAQI):
custom_laqi = options.get(CUSTOM_LAQI)
region_code = options.get(CONF_COUNTRY)
if custom_laqi is not None and region_code is not None:
self.custom_local_aqi = custom_laqi
self.region_code = region_code
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
"""Fetch air quality data for this coordinate."""
try:
return await self.client.async_get_current_conditions(
lat=self.lat,
lon=self.long,
region_code=self.region_code,
custom_local_aqi=self.custom_local_aqi,
)
return await self.client.async_get_current_conditions(self.lat, self.long)
except GoogleAirQualityApiError as ex:
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
raise UpdateFailed(
@@ -204,6 +204,7 @@ async def async_setup_entry(
for subentry_id, subentry in entry.subentries.items():
coordinator = coordinators[subentry_id]
_LOGGER.debug("subentry.data: %s", subentry.data)
async_add_entities(
(
AirQualitySensorEntity(coordinator, description, subentry_id, subentry)
@@ -15,8 +15,6 @@
},
"error": {
"cannot_connect": "Unable to connect to the Google Air Quality API:\n\n{error_message}",
"mismatch_country_and_laqi": "This local AQI is not available for the selected country. Please select an available combination.",
"missing_custom_laqi_options": "Please provide both country and custom local AQI when custom local AQI is enabled.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
@@ -41,20 +39,6 @@
"referrer": "Specify this only if the API key has a [website application restriction]({restricting_api_keys_url})."
},
"name": "Optional API key options"
},
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "Custom local AQI",
"enable_custom_laqi": "Enable custom local AQI"
},
"data_description": {
"country": "Country of the location",
"custom_laqi": "The target air quality index",
"enable_custom_laqi": "Select to enable a custom local air quality index"
},
"description": "Country and custom local AQI must match. You can find the available combinations here: {air_quality_coverage_url}",
"name": "Custom local AQI options"
}
}
}
@@ -67,11 +51,8 @@
},
"entry_type": "Air quality location",
"error": {
"cannot_connect": "[%key:component::google_air_quality::config::error::cannot_connect%]",
"location_already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"location_name_already_configured": "Location name already configured.",
"mismatch_country_and_laqi": "[%key:component::google_air_quality::config::error::mismatch_country_and_laqi%]",
"missing_custom_laqi_options": "[%key:component::google_air_quality::config::error::missing_custom_laqi_options%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
@@ -88,22 +69,6 @@
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"sections": {
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::enable_custom_laqi%]"
},
"data_description": {
"country": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::enable_custom_laqi%]"
},
"description": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::description%]",
"name": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::name%]"
}
},
"title": "Air quality data location"
}
}
+2 -1
View File
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
ATTR_ELEVATION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_MODE,
@@ -35,6 +34,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_CLIMB = "climb"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ELEVATION = "elevation"
ATTR_SPEED = "speed"
ATTR_TOTAL_SATELLITES = "total_satellites"
ATTR_USED_SATELLITES = "used_satellites"
@@ -69,8 +69,8 @@ GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="domestic_hot_water_0_temperature",
translation_key="domestic_hot_water_0_temperature",
key="domestic_home_water_temperature",
translation_key="domestic_home_water_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -54,8 +54,8 @@
"dhw": "Domestic hot water",
"dhw_boost": "Domestic hot water boost",
"heat": "Heat",
"hibernate": "Setback mode",
"hibernate_to": "Away mode",
"hibernate": "Hibernate",
"hibernate_to": "Hibernate to",
"off": "[%key:common::state::off%]",
"timer": "Timer"
}
-17
View File
@@ -131,12 +131,8 @@ ATTR_AUTO_UPDATE = "auto_update"
ATTR_VERSION = "version"
ATTR_VERSION_LATEST = "version_latest"
ATTR_CPU_PERCENT = "cpu_percent"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCATION = "location"
ATTR_MEMORY_PERCENT = "memory_percent"
ATTR_SLUG = "slug"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_STATE = "state"
ATTR_STARTED = "started"
ATTR_URL = "url"
ATTR_REPOSITORY = "repository"
@@ -177,19 +173,6 @@ CORE_CONTAINER = "homeassistant"
SUPERVISOR_CONTAINER = "hassio_supervisor"
CONTAINER_STATS = "stats"
CONTAINER_INFO = "info"
# This is a mapping of which endpoint the key in the addon data
# is obtained from so we know which endpoint to update when the
# coordinator polls for updates.
KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
ATTR_VERSION_LATEST: {CONTAINER_INFO},
ATTR_MEMORY_PERCENT: {CONTAINER_STATS},
ATTR_CPU_PERCENT: {CONTAINER_STATS},
ATTR_VERSION: {CONTAINER_INFO},
ATTR_STATE: {CONTAINER_INFO},
}
REQUEST_REFRESH_DELAY = 10
HELP_URLS = {
+1 -2
View File
@@ -15,7 +15,7 @@ from aiohasupervisor.models import (
)
import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME
from homeassistant.const import ATTR_DEVICE_ID, ATTR_LOCATION, ATTR_NAME
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -43,7 +43,6 @@ from .const import (
ATTR_HOMEASSISTANT,
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
ATTR_INPUT,
ATTR_LOCATION,
ATTR_PASSWORD,
ATTR_SLUG,
DOMAIN,
+2 -1
View File
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_MODEL,
CONF_DISKS,
CONF_HOST,
CONF_NAME,
@@ -29,6 +28,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 7634
@@ -458,7 +458,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
hass,
DOMAIN,
"unsupported_local_deps",
breaks_in_ha_version="2026.11.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
@@ -167,6 +167,10 @@
"description": "Please do the following steps:\n- Adopt your configuration to support template rendering to native python types.\n- Remove the `legacy_templates` key from the `homeassistant` configuration in your configuration.yaml file.\n- Restart Home Assistant to fix this issue.",
"title": "The support for legacy templates is being removed"
},
"no_platform_setup": {
"description": "It's not possible to configure {platform} {domain} by adding `{platform_key}` to the {domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n1. Remove `{platform_key}` occurrences from the `{domain}:` configuration in your YAML configuration file.\n2. Restart Home Assistant.\n\nExample that should be removed:\n{yaml_example}",
"title": "Unused YAML configuration for the {platform} integration"
},
"orphaned_ignored_config_entry": {
"fix_flow": {
"abort": {
@@ -185,18 +189,10 @@
},
"title": "Orphaned ignored config entry for {domain}"
},
"platform_config_not_supported": {
"description": "Configuring the {integration_domain} integration by adding `{platform_key}` under the `{platform_domain}:` key is not supported. The {integration_domain} integration must be configured under its own `{integration_domain}:` key instead.\n\nTo resolve this:\n\n1. Remove the following from your YAML configuration file:\n\n{yaml_example}\n\n2. Move the configuration under the `{integration_domain}:` key instead.\n\n3. Restart Home Assistant.\n\nTo see the detailed documentation, select Learn more.",
"title": "Unsupported YAML configuration for the {integration_domain} integration"
},
"platform_only": {
"description": "The {domain} integration does not support configuration under its own key, it must be configured under its supported platforms.\n\nTo resolve this:\n\n1. Remove `{domain}:` from your YAML configuration file.\n\n2. Restart Home Assistant.",
"title": "The {domain} integration does not support YAML configuration under its own key"
},
"platform_setup_not_supported": {
"description": "It's not possible to configure {integration_domain} {platform_domain} by adding `{platform_key}` to the {platform_domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n\n1. Remove `{platform_key}` occurrences from the `{platform_domain}:` configuration in your YAML configuration file.\n\n2. Restart Home Assistant.",
"title": "Unused YAML configuration for the {integration_domain} integration"
},
"storage_corruption": {
"fix_flow": {
"step": {
@@ -7,13 +7,7 @@ import logging
import voluptuous as vol
from homeassistant import exceptions
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_ENTITY_ID,
CONF_FOR,
CONF_PLATFORM,
MATCH_ALL,
)
from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL
from homeassistant.core import (
CALLBACK_TYPE,
Event,
@@ -38,6 +32,8 @@ from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
# pylint: disable-next=home-assistant-duplicate-const
CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from"
CONF_TO = "to"
CONF_NOT_FROM = "not_from"
@@ -14,7 +14,6 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_OFFSET,
CONF_PLATFORM,
CONF_WEEKDAY,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
WEEKDAYS,
@@ -39,6 +38,9 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
# pylint: disable-next=home-assistant-duplicate-const
CONF_WEEKDAY = "weekday"
_TIME_TRIGGER_ENTITY = vol.All(str, cv.entity_domain(["input_datetime", "sensor"]))
_TIME_AT_SCHEMA = vol.Any(cv.time, _TIME_TRIGGER_ENTITY)
@@ -384,17 +384,18 @@ class OptionsFlowHandler(OptionsFlow):
return self.async_show_form(step_id="yaml")
async def async_step_bridged_device_triggers(
async def async_step_advanced(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Choose bridged device triggers options."""
"""Choose advanced options."""
hk_options = self.hk_options
show_advanced_options = self.show_advanced_options
bridge_mode = hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
if user_input is not None or not bridge_mode:
if not show_advanced_options or user_input is not None or not bridge_mode:
if user_input:
hk_options.update(user_input)
if bridge_mode:
if show_advanced_options and bridge_mode:
hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
hk_options.pop(CONF_DOMAINS, None)
@@ -410,7 +411,7 @@ class OptionsFlowHandler(OptionsFlow):
if device_id in all_supported_devices
]
return self.async_show_form(
step_id="bridged_device_triggers",
step_id="advanced",
data_schema=vol.Schema(
{
vol.Optional(CONF_DEVICES, default=devices): cv.multi_select(
@@ -445,7 +446,7 @@ class OptionsFlowHandler(OptionsFlow):
if not entity_config:
all_entity_config.pop(entity_id)
return await self.async_step_bridged_device_triggers()
return await self.async_step_advanced()
cameras_with_audio = []
cameras_with_copy = []
@@ -494,7 +495,7 @@ class OptionsFlowHandler(OptionsFlow):
hk_options[CONF_FILTER] = entity_filter
if self.included_cameras:
return await self.async_step_cameras()
return await self.async_step_bridged_device_triggers()
return await self.async_step_advanced()
entity_filter = hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
@@ -538,7 +539,7 @@ class OptionsFlowHandler(OptionsFlow):
hk_options[CONF_FILTER] = _async_build_entities_filter(domains, entities)
if self.included_cameras:
return await self.async_step_cameras()
return await self.async_step_bridged_device_triggers()
return await self.async_step_advanced()
entity_filter: EntityFilterDict = hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
@@ -593,7 +594,7 @@ class OptionsFlowHandler(OptionsFlow):
)
if self.included_cameras:
return await self.async_step_cameras()
return await self.async_step_bridged_device_triggers()
return await self.async_step_advanced()
entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
@@ -25,12 +25,12 @@
},
"title": "Select the entity for the accessory"
},
"bridged_device_triggers": {
"advanced": {
"data": {
"devices": "Devices (Triggers)"
},
"description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.",
"title": "Bridged device triggers"
"title": "Advanced configuration"
},
"cameras": {
"data": {
+2
View File
@@ -15,6 +15,8 @@ ATTR_ACTIONS = "actions"
ATTR_BADGE = "badge"
ATTR_DATA = "data"
ATTR_DIR = "dir"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ICON = "icon"
ATTR_IMAGE = "image"
ATTR_LANG = "lang"
ATTR_RENOTIFY = "renotify"
+1 -1
View File
@@ -9,7 +9,6 @@ from homeassistant.components.notify import (
ATTR_TITLE_DEFAULT,
DOMAIN as NOTIFY_DOMAIN,
)
from homeassistant.const import ATTR_ICON
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
@@ -18,6 +17,7 @@ from .const import (
ATTR_ACTIONS,
ATTR_BADGE,
ATTR_DIR,
ATTR_ICON,
ATTR_IMAGE,
ATTR_LANG,
ATTR_RENOTIFY,
@@ -4,14 +4,7 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.const import (
ATTR_MODE,
CONF_MODE,
CONF_OPTIONS,
PERCENTAGE,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -34,6 +27,9 @@ from .const import (
HumidifierEntityFeature,
)
# pylint: disable-next=home-assistant-duplicate-const
CONF_MODE = "mode"
IS_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_OPTIONS): {
@@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant.const import ATTR_MODE, CONF_MODE, CONF_OPTIONS, STATE_OFF, STATE_ON
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -18,6 +18,9 @@ from homeassistant.helpers.trigger import (
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
# pylint: disable-next=home-assistant-duplicate-const
CONF_MODE = "mode"
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
@@ -133,11 +133,6 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
"""
return self._is_hard_wired
@property
def available(self) -> bool:
"""Return True if shade position data is available."""
return super().available and self.positions.primary is not None
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
@@ -156,10 +156,6 @@ class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN):
assert self.address
if device is None:
LOGGER.debug("Could not find device with address '%s'", self.address)
return None
try:
(manufacturer, device_type, _model) = await Mower(
channel_id, self.address
@@ -2,7 +2,9 @@
from ibeacon_ble import iBeaconAdvertisement
from homeassistant.components.device_tracker import BaseScannerEntity, SourceType
from homeassistant.components.device_tracker import SourceType
from homeassistant.components.device_tracker.config_entry import BaseTrackerEntity
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -44,11 +46,10 @@ async def async_setup_entry(
)
class IBeaconTrackerEntity(IBeaconEntity, BaseScannerEntity):
class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity):
"""An iBeacon Tracker entity."""
_attr_name = None
_attr_source_type: SourceType = SourceType.BLUETOOTH_LE
_attr_translation_key = "device_tracker"
def __init__(
@@ -66,9 +67,14 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseScannerEntity):
self._active = True
@property
def is_connected(self) -> bool:
"""Return true if the device is connected."""
return self._active
def state(self) -> str:
"""Return the state of the device."""
return STATE_HOME if self._active else STATE_NOT_HOME
@property
def source_type(self) -> SourceType:
"""Return tracker source type."""
return SourceType.BLUETOOTH_LE
@callback
def _async_seen(
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["indevolt-api==1.8.1"]
"requirements": ["indevolt-api==1.8.0"]
}
@@ -1,11 +1,15 @@
"""Jewish Calendar constants."""
from __future__ import annotations
from enum import StrEnum
from typing import TYPE_CHECKING, Self
DOMAIN = "jewish_calendar"
ATTR_AFTER_SUNSET = "after_sunset"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_DATE = "date"
ATTR_NUSACH = "nusach"
CONF_ALTITUDE = "altitude" # The name used by the hdate library for elevation
@@ -9,7 +9,7 @@ from hdate.omer import Nusach, Omer
from hdate.translator import Language, set_language
import voluptuous as vol
from homeassistant.const import ATTR_DATE, CONF_LANGUAGE, SUN_EVENT_SUNSET
from homeassistant.const import CONF_LANGUAGE, SUN_EVENT_SUNSET
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -23,7 +23,7 @@ from homeassistant.helpers.selector import LanguageSelector, LanguageSelectorCon
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import dt as dt_util
from .const import ATTR_AFTER_SUNSET, ATTR_NUSACH, DOMAIN, SERVICE_COUNT_OMER
from .const import ATTR_AFTER_SUNSET, ATTR_DATE, ATTR_NUSACH, DOMAIN, SERVICE_COUNT_OMER
_LOGGER = logging.getLogger(__name__)
OMER_SCHEMA = vol.Schema(
@@ -11,7 +11,7 @@
"services": {
"test_service_1": {
"sections": {
"additional_fields": "mdi:test-tube"
"advanced_fields": "mdi:test-tube"
},
"service": "mdi:flask"
}
@@ -15,7 +15,7 @@ test_service_1:
- "off"
- "auto"
- "cool"
additional_fields:
advanced_fields:
collapsed: true
fields:
field_3:
@@ -159,9 +159,9 @@
},
"name": "Test action {meep_1}",
"sections": {
"additional_fields": {
"description": "Some additional things",
"name": "Additional options"
"advanced_fields": {
"description": "Some very advanced things",
"name": "Advanced options"
}
}
}
+3 -14
View File
@@ -1,18 +1,12 @@
"""Remote control support for LG Netcast TV."""
from collections.abc import Iterable
import time
from typing import TYPE_CHECKING, Any
from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError
from requests import RequestException
from homeassistant.components.remote import (
ATTR_DELAY_SECS,
ATTR_NUM_REPEATS,
DEFAULT_DELAY_SECS,
RemoteEntity,
)
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -60,21 +54,16 @@ class LgNetCastRemote(RemoteEntity):
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send commands to the TV."""
num_repeats = kwargs[ATTR_NUM_REPEATS]
delay_secs = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
commands: list[int] = []
for cmd in command:
if cmd not in VALID_COMMANDS:
raise ServiceValidationError(f"Unknown command: {cmd!r}")
commands.append(getattr(LG_COMMAND, cmd))
for i in range(num_repeats):
for _ in range(num_repeats):
try:
with self._client as client:
if i > 0:
time.sleep(delay_secs)
for j, lg_command in enumerate(commands):
if j > 0:
time.sleep(delay_secs)
for lg_command in commands:
client.send_command(lg_command)
except LgNetCastError, RequestException:
self._attr_is_on = False
+3 -3
View File
@@ -227,7 +227,7 @@ turn_on:
selector:
state:
attribute: effect
additional_fields:
advanced_fields:
collapsed: true
fields:
rgbw_color: &rgbw_color
@@ -298,7 +298,7 @@ turn_off:
domain: light
fields:
transition: *transition
additional_fields:
advanced_fields:
collapsed: true
fields:
flash: *flash
@@ -313,7 +313,7 @@ toggle:
color_temp_kelvin: *color_temp_kelvin
brightness_pct: *brightness_pct
effect: *effect
additional_fields:
advanced_fields:
collapsed: true
fields:
rgbw_color: *rgbw_color
+7 -7
View File
@@ -35,7 +35,7 @@
"field_white_name": "White",
"field_xy_color_description": "Color in XY-format. A list of two decimal numbers between 0 and 1.",
"field_xy_color_name": "XY-color",
"section_additional_fields_name": "Additional options",
"section_advanced_fields_name": "Advanced options",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
@@ -396,8 +396,8 @@
},
"name": "Toggle light",
"sections": {
"additional_fields": {
"name": "[%key:component::light::common::section_additional_fields_name%]"
"advanced_fields": {
"name": "[%key:component::light::common::section_advanced_fields_name%]"
}
}
},
@@ -415,8 +415,8 @@
},
"name": "Turn off light",
"sections": {
"additional_fields": {
"name": "[%key:component::light::common::section_additional_fields_name%]"
"advanced_fields": {
"name": "[%key:component::light::common::section_advanced_fields_name%]"
}
}
},
@@ -490,8 +490,8 @@
},
"name": "Turn on light",
"sections": {
"additional_fields": {
"name": "[%key:component::light::common::section_additional_fields_name%]"
"advanced_fields": {
"name": "[%key:component::light::common::section_advanced_fields_name%]"
}
}
}

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