Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston a6bbf2f78f Bump cryptography to 49.0.0 and pyOpenSSL to 26.3.0 2026-06-25 14:53:39 +02:00
64 changed files with 680 additions and 1321 deletions
-1
View File
@@ -95,7 +95,6 @@ components: &components
- homeassistant/components/input_select/**
- homeassistant/components/input_text/**
- homeassistant/components/labs/**
- homeassistant/components/llm/**
- homeassistant/components/logbook/**
- homeassistant/components/logger/**
- homeassistant/components/lovelace/**
@@ -12,7 +12,6 @@ on:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "**/requirements*.txt"
- "homeassistant/package_constraints.txt"
workflow_dispatch:
inputs:
@@ -59,7 +58,6 @@ jobs:
echo "head_sha=${HEAD_SHA}" >> "${GITHUB_OUTPUT}"
- name: Run deterministic checks
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
+129 -58
View File
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"36a7fc263a2ce868d74a266f23eb7772d82fd397806464384fe087479ddd4a70","body_hash":"bba8c011f2b82bb4d9847a359f43f0e7d91245b280678c20e5112b3c9e77d5cd","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"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":"5c2fe865bb4dc46e1450f6ee0d0541d759aea73a","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7b142e96e0f8b454cdcc9c0c25070cf9a52c44d83a6b1fbc3ad6725b6567337c","body_hash":"3894ded07d5934ac5f29d160ffb1f9115cf72b6da8a7e453a4d4f69e8641a48e","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# 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":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"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":"v0.79.6","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -31,12 +31,12 @@
# - GITHUB_TOKEN
#
# Custom actions used:
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
# - github/gh-aw-actions/setup@v0.79.6
#
# Container images used:
# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -155,7 +155,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- name: Checkout .github and .agents folders
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
@@ -344,8 +344,9 @@ jobs:
agent:
needs:
- activation
- prepare
if: (needs.prepare.outputs.skip != 'true') && (needs.activation.outputs.daily_effective_workflow_exceeded != 'true')
- extract_pr_number
- gate
if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true'
runs-on: ubuntu-latest
permissions:
actions: read
@@ -382,7 +383,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -403,7 +404,7 @@ jobs:
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
} >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Create gh-aw temp directory
@@ -488,15 +489,15 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_823c5547a5e52957_EOF'
{"add_comment":{"max":1,"target":"${{ needs.prepare.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_823c5547a5e52957_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f496a449c5dccca1_EOF'
{"add_comment":{"max":1,"target":"${{ needs.extract_pr_number.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_f496a449c5dccca1_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: ${{ needs.prepare.outputs.pr_number }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -993,7 +994,8 @@ jobs:
- activation
- agent
- detection
- prepare
- extract_pr_number
- gate
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1016,7 +1018,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1206,7 +1208,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1234,7 +1236,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
- name: Checkout repository for patch context
if: needs.agent.outputs.has_patch == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# --- Threat Detection ---
@@ -1427,6 +1429,111 @@ jobs:
}
}
extract_pr_number:
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
gate:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
- name: Decide whether requirements changed since the last comment
id: gate
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
pre_activation:
runs-on: ubuntu-slim
outputs:
@@ -1438,7 +1545,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1461,48 +1568,12 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs');
await main();
prepare:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
pr_number: ${{ steps.prepare.outputs.pr_number }}
skip: ${{ steps.prepare.outputs.skip }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Resolve skip and PR number from the artifact
id: prepare
run: |
echo "skip=$(jq -r '.skip_aw' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
echo "pr_number=$(jq -r '.pr_number' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
safe_outputs:
needs:
- activation
- agent
- detection
- prepare
- extract_pr_number
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
@@ -1538,7 +1609,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1583,7 +1654,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\":\"${{ needs.prepare.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.extract_pr_number.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+68 -15
View File
@@ -15,41 +15,94 @@ tools:
github:
toolsets: [repos, pull_requests]
min-integrity: unapproved
if: needs.prepare.outputs.skip != 'true'
safe-outputs:
add-comment:
max: 1
target: "${{ needs.prepare.outputs.pr_number }}"
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
needs:
- prepare
- extract_pr_number
jobs:
prepare:
# The deterministic stage always uploads an artifact; its `skip_aw` flag is
# true when no tracked requirement file changed since the last comment,
# which is our cue to skip the (token-spending) agent. Recover the PR number
# to comment on either way.
gate:
# Skip the (token-spending) agent when no tracked requirement file changed
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.prepare.outputs.skip }}
pr_number: ${{ steps.prepare.outputs.pr_number }}
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Decide whether requirements changed since the last comment
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
extract_pr_number:
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Download deterministic-results artifact
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve skip and PR number from the artifact
id: prepare
- name: Extract PR number from artifact
id: extract
run: |
echo "skip=$(jq -r '.skip_aw' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
echo "pr_number=$(jq -r '.pr_number' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.id }}
cancel-in-progress: true
Generated
-2
View File
@@ -1027,8 +1027,6 @@ CLAUDE.md @home-assistant/core
/tests/components/litterrobot/ @natekspencer @tkdrob
/homeassistant/components/livisi/ @StefanIacobLivisi @planbnet
/tests/components/livisi/ @StefanIacobLivisi @planbnet
/homeassistant/components/llm/ @home-assistant/core
/tests/components/llm/ @home-assistant/core
/homeassistant/components/local_calendar/ @allenporter
/tests/components/local_calendar/ @allenporter
/homeassistant/components/local_ip/ @issacg
+10 -10
View File
@@ -11,10 +11,10 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
UnitOfDensity,
PERCENTAGE,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -76,14 +76,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM1,
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -94,7 +94,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM10,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -105,7 +105,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
@@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_CO,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -137,7 +137,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_NO2,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -148,7 +148,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_SO2,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -159,7 +159,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_O3,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.1.6"]
"requirements": ["aioamazondevices==14.1.3"]
}
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==45.5.2",
"aioesphomeapi==45.3.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.9.4"
],
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260624.1"]
"requirements": ["home-assistant-frontend==20260624.0"]
}
-79
View File
@@ -1,79 +0,0 @@
"""The LLM integration.
Owns the LLM tools platform: integrations contribute tools to the LLM APIs
through an ``<integration>/llm.py`` platform with an ``async_get_tools`` hook.
The platforms are loaded lazily and queried per request. The framework
(``Tool``, the APIs) lives in ``homeassistant.helpers.llm``.
"""
from dataclasses import dataclass
import logging
from typing import Protocol
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.integration_platform import LazyIntegrationPlatforms
from homeassistant.helpers.llm import LLMContext, Tool
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
DATA_PLATFORMS: HassKey[LazyIntegrationPlatforms[LLMToolsPlatformProtocol]] = HassKey(
"llm_platforms"
)
@dataclass(slots=True)
class LLMTools:
"""Tools and an optional prompt fragment contributed by a platform."""
tools: list[Tool]
prompt: str | None = None
class LLMToolsPlatformProtocol(Protocol):
"""Define the format that LLM tools platforms can have."""
@callback
def async_get_tools(self, hass: HomeAssistant, llm_context: LLMContext) -> LLMTools:
"""Return the integration's LLM tools for the given context."""
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LLM integration."""
hass.data[DATA_PLATFORMS] = LazyIntegrationPlatforms(
hass, DOMAIN, _process_llm_tools_platform
)
return True
@callback
def _process_llm_tools_platform(
hass: HomeAssistant, domain: str, platform: LLMToolsPlatformProtocol
) -> LLMToolsPlatformProtocol:
"""Process an integration's LLM tools platform."""
return platform
async def async_get_tools(hass: HomeAssistant, llm_context: LLMContext) -> LLMTools:
"""Return the tools and merged prompt from all integration platforms."""
platforms = await hass.data[DATA_PLATFORMS].async_get_platforms()
tools: list[Tool] = []
prompts: list[str] = []
# Sort by domain so the tool and prompt order is independent of load order.
for domain, platform in sorted(platforms.items()):
try:
result = platform.async_get_tools(hass, llm_context)
except Exception:
_LOGGER.exception("Error getting tools from LLM platform %s", domain)
continue
tools.extend(result.tools)
if result.prompt:
prompts.append(result.prompt)
return LLMTools(tools=tools, prompt="\n".join(prompts) if prompts else None)
-3
View File
@@ -1,3 +0,0 @@
"""Constants for the LLM integration."""
DOMAIN = "llm"
@@ -1,9 +0,0 @@
{
"domain": "llm",
"name": "LLM",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/llm",
"integration_type": "system",
"iot_class": "calculated",
"quality_scale": "internal"
}
+6 -4
View File
@@ -12,9 +12,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -30,7 +32,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.AIR_HUMIDITY: SensorEntityDescription(
key="air_humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.AIR_PRESSURE: SensorEntityDescription(
@@ -47,7 +49,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.ECO2: SensorEntityDescription(
key="eco2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.LIGHT: SensorEntityDescription(
@@ -65,7 +67,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.VOC: SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
),
}
+21 -20
View File
@@ -22,19 +22,20 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
Platform,
UnitOfApparentPower,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfReactivePower,
UnitOfTemperature,
UnitOfTime,
@@ -443,7 +444,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="HumiditySensor",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
device_to_ha=lambda x: x / HUMIDITY_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
@@ -458,7 +459,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SoilMoistureSensor",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.MOISTURE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -483,7 +484,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PowerSource",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
# value has double precision
@@ -624,7 +625,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="EveThermoValvePosition",
translation_key="valve_position",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
),
entity_class=MatterSensor,
required_attributes=(EveCluster.Attributes.ValvePosition,),
@@ -657,7 +658,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="CarbonDioxideSensor",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -670,7 +671,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="TotalVolatileOrganicCompoundsSensor",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -698,7 +699,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM1Sensor",
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -711,7 +712,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM25Sensor",
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -724,7 +725,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM10Sensor",
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -749,7 +750,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="CarbonMonoxideSensor",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -762,7 +763,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="NitrogenDioxideSensor",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -775,7 +776,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="OzoneConcentrationSensor",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.OZONE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -801,7 +802,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="HepaFilterCondition",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="hepa_filter_condition",
),
@@ -812,7 +813,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ActivatedCarbonFilterCondition",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="activated_carbon_filter_condition",
),
@@ -1296,7 +1297,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="ThermostatPIHeatingDemand",
translation_key="pi_heating_demand",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterSensor,
@@ -1378,7 +1379,7 @@ DISCOVERY_SCHEMAS = [
entity_registry_enabled_default=False,
translation_key="window_covering_target_position",
device_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
),
entity_class=MatterSensor,
required_attributes=(
@@ -1463,7 +1464,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="EnergyEvseStateOfCharge",
translation_key="evse_soc",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -1487,7 +1488,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="WaterHeaterManagementTankPercentage",
translation_key="tank_percentage",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
@@ -0,0 +1,21 @@
"""Support for Mycroft AI."""
import voluptuous as vol
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
DOMAIN = "mycroft"
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA
)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Mycroft component."""
hass.data[DOMAIN] = config[DOMAIN][CONF_HOST]
discovery.load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config)
return True
@@ -0,0 +1,11 @@
{
"domain": "mycroft",
"name": "Mycroft",
"codeowners": [],
"disabled": "Dependencies not compatible with the new pip resolver",
"documentation": "https://www.home-assistant.io/integrations/mycroft",
"iot_class": "local_push",
"loggers": ["mycroftapi"],
"quality_scale": "legacy",
"requirements": ["mycroftapi==2.0"]
}
@@ -0,0 +1,42 @@
"""Mycroft AI notification platform."""
import logging
from typing import Any, override
from mycroftapi import MycroftAPI
from homeassistant.components.notify import BaseNotificationService
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
def get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> MycroftNotificationService:
"""Get the Mycroft notification service."""
return MycroftNotificationService(hass.data[DOMAIN])
class MycroftNotificationService(BaseNotificationService):
"""The Mycroft Notification Service."""
def __init__(self, mycroft_ip: str) -> None:
"""Initialize the service."""
self.mycroft_ip = mycroft_ip
@override
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message mycroft to speak on instance."""
text = message
mycroft = MycroftAPI(self.mycroft_ip)
if mycroft is not None:
mycroft.speak_text(text)
else:
_LOGGER.warning("Could not reach this instance of mycroft")
+1 -3
View File
@@ -365,7 +365,5 @@ async def create_rexel_client(
gateway_id=entry.data[CONF_GATEWAY_ID],
),
session=async_create_clientsession(hass),
settings=OverkizClientSettings(
action_queue=ActionQueueSettings(), default_rts_command_duration=0
),
settings=OverkizClientSettings(action_queue=ActionQueueSettings()),
)
+7 -6
View File
@@ -15,12 +15,13 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
@@ -54,7 +55,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
OverkizSensorDescription(
key=OverkizState.CORE_BATTERY_LEVEL,
name="Battery level",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -328,7 +329,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
native_value=lambda value: round(cast(float, value), 2),
device_class=SensorDeviceClass.HUMIDITY,
# core:MeasuredValueType = core:RelativeValueInPercentage
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
# TemperatureSensor/TemperatureSensor
@@ -368,7 +369,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
key=OverkizState.CORE_CO_CONCENTRATION,
name="CO concentration",
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
# AirSensor/CO2Sensor
@@ -376,7 +377,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
key=OverkizState.CORE_CO2_CONCENTRATION,
name="CO2 concentration",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
# SunSensor/SunEnergySensor
@@ -488,7 +489,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
OverkizSensorDescription(
key=OverkizState.CORE_TARGET_CLOSURE,
name="Target closure",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
),
# ThreeWayWindowHandle/WindowHandle
@@ -30,7 +30,6 @@ from homeassistant.util.event_type import EventType
# startup
from . import (
backup, # noqa: F401
entity_options,
entity_registry,
websocket_api,
)
@@ -43,7 +42,6 @@ from .const import ( # noqa: F401
SupportedDialect,
)
from .core import Recorder
from .entity_options import is_entity_recorded # noqa: F401
from .services import async_setup_services
from .tasks import AddRecorderPlatformTask
from .util import get_instance
@@ -127,6 +125,15 @@ CONFIG_SCHEMA = vol.Schema(
)
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
"""Check if an entity is being recorded.
Async friendly.
"""
instance = get_instance(hass)
return instance.entity_filter is None or instance.entity_filter(entity_id)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the recorder."""
conf = config[DOMAIN]
@@ -160,7 +167,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
get_instance.cache_clear()
entity_registry.async_setup(hass)
entity_options.async_setup(hass)
instance.async_initialize()
instance.async_register()
instance.start()
@@ -1,68 +0,0 @@
"""Control recorder entity options."""
import dataclasses
from enum import StrEnum
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from .util import get_instance
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
"""Check if an entity is being recorded.
Async friendly.
"""
instance = get_instance(hass)
return instance.entity_filter is None or instance.entity_filter(entity_id)
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the recorder entity options."""
websocket_api.async_register_command(hass, ws_get_entity_options)
class EntityRecordingDisabler(StrEnum):
"""What disabled recording of an entity."""
USER = "user"
@dataclasses.dataclass(frozen=True)
class RecorderEntityOptions:
"""Recorder options for an entity."""
recording_disabled_by: EntityRecordingDisabler | None = None
def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage."""
return {
"recording_disabled_by": self.recording_disabled_by,
}
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/entity_options/get",
vol.Required("entity_id"): cv.strict_entity_id,
}
)
@callback
def ws_get_entity_options(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Get recorder settings for a single entity."""
entity_id: str = msg["entity_id"]
recording_disabled = (
None if is_entity_recorded(hass, entity_id) else EntityRecordingDisabler.USER
)
options = RecorderEntityOptions(recording_disabled_by=recording_disabled)
connection.send_result(msg["id"], options.to_json())
@@ -1,8 +1,7 @@
"""The Steam integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.core import HomeAssistant
from .coordinator import SteamConfigEntry, SteamDataUpdateCoordinator
@@ -22,22 +21,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> boo
async def async_unload_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version < 2:
# Migrate entity unique id
@callback
def migrate_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
if entity_entry.unique_id.startswith("sensor.steam_"):
new = entity_entry.unique_id.removeprefix("sensor.steam_") + "_account"
return {"new_unique_id": new}
return None
await er.async_migrate_entries(hass, entry.entry_id, migrate_unique_id)
hass.config_entries.async_update_entry(entry, version=2)
return True
@@ -41,8 +41,6 @@ def validate_input(user_input: dict[str, str]) -> dict[str, str | int]:
class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Steam."""
VERSION = 2
@staticmethod
@callback
@override
@@ -152,7 +150,7 @@ class SteamOptionsFlowHandler(OptionsFlowWithReload):
for _id in self.options[CONF_ACCOUNTS]:
if _id not in user_input[CONF_ACCOUNTS] and (
entity_id := er.async_get(self.hass).async_get_entity_id(
Platform.SENSOR, DOMAIN, f"{_id}_account"
Platform.SENSOR, DOMAIN, f"sensor.steam_{_id}"
)
):
er.async_get(self.hass).async_remove(entity_id)
@@ -1,6 +1,5 @@
"""Entity classes for the Steam integration."""
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -13,17 +12,9 @@ class SteamEntity(CoordinatorEntity[SteamDataUpdateCoordinator]):
_attr_has_entity_name = True
def __init__(
self,
coordinator: SteamDataUpdateCoordinator,
steamid: str,
description: SensorEntityDescription,
) -> None:
def __init__(self, coordinator: SteamDataUpdateCoordinator) -> None:
"""Initialize a Steam entity."""
super().__init__(coordinator)
self._steamid = steamid
self.entity_description = description
self._attr_unique_id = f"{steamid}_{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url="https://store.steampowered.com",
entry_type=DeviceEntryType.SERVICE,
@@ -80,7 +80,10 @@ class SteamSensorEntity(SteamEntity, SensorEntity):
description: SteamSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, steamid, description)
super().__init__(coordinator)
self._steamid = steamid
self.entity_description = description
self._attr_unique_id = f"sensor.steam_{steamid}"
self._attr_name = self.entity_description.name_fn(coordinator.data[steamid])
@property
+20 -14
View File
@@ -85,18 +85,34 @@ def sun(
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
after_sunrise = sunrise is not None and today > dt_util.as_local(sunrise).date()
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
if after_sunrise and has_sunrise_condition:
tomorrow = today + timedelta(days=1)
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
after_sunset = sunset is not None and today > dt_util.as_local(sunset).date()
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
if after_sunset and has_sunset_condition:
tomorrow = today + timedelta(days=1)
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
# A missing sunrise/sunset means the sun doesn't rise/set on this day, which
# happens in polar regions.
# Special case: before sunrise OR after sunset
# This will handle the very rare case in the polar region when the sun rises/sets
# but does not set/rise.
# However this entire condition does not handle those full days of darkness
# or light, the following should be used instead:
#
# condition:
# condition: state
# entity_id: sun.sun
# state: 'above_horizon' (or 'below_horizon')
#
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
return utcnow < wanted_time_before or utcnow > wanted_time_after
if sunrise is None and has_sunrise_condition:
# There is no sunrise today
condition_trace_set_result(False, message="no sunrise today")
@@ -107,16 +123,6 @@ def sun(
condition_trace_set_result(False, message="no sunset today")
return False
# "before: sunrise" combined with "after: sunset" describes the dark period
# around midnight, so it is evaluated as an OR (true before sunrise or after
# sunset) rather than the usual AND of the two bounds.
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
return utcnow < wanted_time_before or utcnow > wanted_time_after
if before == SUN_EVENT_SUNRISE:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
+8 -3
View File
@@ -13,7 +13,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import Platform, UnitOfDensity, UnitOfRatio, UnitOfTime
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
PERCENTAGE,
Platform,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -57,7 +62,7 @@ SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = (
key="battery_level",
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
value=lambda device: cast(int, device.device_info.battery_level),
),
)
@@ -68,7 +73,7 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = (
key="aqi",
translation_key="aqi",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
value=_get_air_quality,
),
TradfriSensorEntityDescription(
+10 -7
View File
@@ -6,18 +6,21 @@ import logging
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
Platform,
UnitOfConductivity,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
UnitOfVolume,
UnitOfVolumetricFlux,
@@ -1020,7 +1023,7 @@ UNITS = (
},
),
UnitOfMeasurement(
unit=UnitOfRatio.PERCENTAGE,
unit=PERCENTAGE,
aliases={"pct", "percent", "% RH"},
device_classes={
SensorDeviceClass.BATTERY,
@@ -1029,14 +1032,14 @@ UNITS = (
},
),
UnitOfMeasurement(
unit=UnitOfRatio.PARTS_PER_MILLION,
unit=CONCENTRATION_PARTS_PER_MILLION,
device_classes={
SensorDeviceClass.CO,
SensorDeviceClass.CO2,
},
),
UnitOfMeasurement(
unit=UnitOfRatio.PARTS_PER_BILLION,
unit=CONCENTRATION_PARTS_PER_BILLION,
device_classes={
SensorDeviceClass.CO,
SensorDeviceClass.CO2,
@@ -1083,7 +1086,7 @@ UNITS = (
device_classes={SensorDeviceClass.ILLUMINANCE},
),
UnitOfMeasurement(
unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
# The μ-char has 2 unicode variants \u00b5 and \u03bc
# The \u03bc variant (Greek Mu char) is recommended
aliases={"ug/m3", "\u03bcg/m3", "\u00b5g/m3", "ug/m³"},
@@ -1100,7 +1103,7 @@ UNITS = (
},
),
UnitOfMeasurement(
unit=UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
unit=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
aliases={"mg/m3"},
device_classes={
SensorDeviceClass.NITROGEN_DIOXIDE,
+2 -2
View File
@@ -44,7 +44,7 @@
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": [
"tuya-device-handlers==0.0.24",
"tuya-device-sharing-sdk==0.2.10"
"tuya-device-handlers==0.0.22",
"tuya-device-sharing-sdk==0.2.8"
]
}
+25 -24
View File
@@ -31,13 +31,14 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfRatio,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
@@ -74,7 +75,7 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = (
TuyaSensorEntityDescription(
key=DPCode.BATTERY_PERCENTAGE,
translation_key="battery",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -82,7 +83,7 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = (
TuyaSensorEntityDescription(
key=DPCode.BATTERY, # Used by non-standard contact sensor implementations
translation_key="battery",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -180,7 +181,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.CH2O_VALUE,
@@ -198,7 +199,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.PM10,
@@ -214,7 +215,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_monoxide",
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
*BATTERY_SENSORS,
),
@@ -301,21 +302,21 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.CO_VALUE,
translation_key="carbon_monoxide",
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.CO2_VALUE,
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.EC_CURRENT,
@@ -534,7 +535,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.CH2O_VALUE,
@@ -552,14 +553,14 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.PM10,
translation_key="pm10",
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
*BATTERY_SENSORS,
),
@@ -569,7 +570,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.VOC_VALUE,
@@ -582,7 +583,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.VA_HUMIDITY,
@@ -683,7 +684,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.TEMP,
@@ -708,7 +709,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="concentration_carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.TOTAL_TIME,
@@ -755,7 +756,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
*BATTERY_SENSORS,
),
@@ -807,7 +808,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.CH2O_VALUE,
@@ -831,7 +832,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.HUMIDITY_VALUE,
@@ -844,14 +845,14 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="pm1",
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.PM10,
translation_key="pm10",
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
*BATTERY_SENSORS,
),
@@ -1229,14 +1230,14 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="carbon_dioxide",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
TuyaSensorEntityDescription(
key=DPCode.PM25_VALUE,
translation_key="pm25",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
TuyaSensorEntityDescription(
key=DPCode.CH2O_VALUE,
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["uiprotect"],
"quality_scale": "platinum",
"requirements": ["uiprotect==15.1.0"]
"requirements": ["uiprotect==15.0.0"]
}
@@ -149,6 +149,7 @@ SELECTOR_TYPES = (
XiaomiMiioSelectDescription(
key=ATTR_DISPLAY_ORIENTATION,
attr_name=ATTR_DISPLAY_ORIENTATION,
name="Display Orientation",
options_map={
"Portrait": "Forward",
"LandscapeLeft": "Left",
@@ -164,6 +165,7 @@ SELECTOR_TYPES = (
XiaomiMiioSelectDescription(
key=ATTR_MODE,
attr_name=ATTR_MODE,
name="Mode",
set_method="set_mode",
set_method_error_message="Setting the mode of the fan failed.",
icon="mdi:fan",
@@ -174,6 +176,7 @@ SELECTOR_TYPES = (
XiaomiMiioSelectDescription(
key=ATTR_LED_BRIGHTNESS,
attr_name=ATTR_LED_BRIGHTNESS,
name="Led Brightness",
set_method="set_led_brightness",
set_method_error_message="Setting the led brightness failed.",
icon="mdi:brightness-6",
@@ -184,6 +187,7 @@ SELECTOR_TYPES = (
XiaomiMiioSelectDescription(
key=ATTR_PTC_LEVEL,
attr_name=ATTR_PTC_LEVEL,
name="Auxiliary Heat Level",
set_method="set_ptc_level",
set_method_error_message="Setting the ptc level failed.",
icon="mdi:fire-circle",
@@ -138,7 +138,6 @@
},
"select": {
"airpurifier_mode": {
"name": "Mode",
"state": {
"auto": "[%key:common::state::auto%]",
"favorite": "Favorite",
@@ -146,7 +145,6 @@
}
},
"display_orientation": {
"name": "Display orientation",
"state": {
"forward": "Forward",
"left": "Left",
@@ -154,7 +152,6 @@
}
},
"led_brightness": {
"name": "LED brightness",
"state": {
"bright": "Bright",
"dim": "Dim",
@@ -162,7 +159,6 @@
}
},
"ptc_level": {
"name": "Auxiliary heat level",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
@@ -4587,6 +4587,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"mycroft": {
"name": "Mycroft",
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push"
},
"myneomitis": {
"name": "MyNeomitis",
"integration_type": "hub",
+1 -1
View File
@@ -740,7 +740,7 @@ def _get_exposed_entities(
if include_state and (
attributes := {
str(attr_name): (
attr_name: (
str(attr_value)
if isinstance(attr_value, (Enum, Decimal, int))
else attr_value
+3 -3
View File
@@ -29,7 +29,7 @@ cached-ipaddress==1.1.2
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.1
cryptography==49.0.0
dbus-fast==5.0.22
file-read-backwards==2.0.0
fnv-hash-fast==2.0.3
@@ -39,7 +39,7 @@ habluetooth==6.25.1
hass-nabucasa==2.2.0
hassil==3.8.0
home-assistant-bluetooth==2.0.0
home-assistant-frontend==20260624.1
home-assistant-frontend==20260624.0
home-assistant-intents==2026.6.24
httpx==0.28.1
ifaddr==0.2.0
@@ -56,7 +56,7 @@ psutil-home-assistant==0.0.1
PyJWT==2.12.1
pymicro-vad==1.0.1
PyNaCl==1.6.2
pyOpenSSL==26.2.0
pyOpenSSL==26.3.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.3
+1 -1
View File
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
from typing import Final
FRONTEND_VERSION: Final[str] = "20260624.1"
FRONTEND_VERSION: Final[str] = "20260624.0"
MDI_ICONS: Final[set[str]] = {
"ab-testing",
+2 -2
View File
@@ -57,10 +57,10 @@ dependencies = [
"lru-dict==1.4.1",
"PyJWT==2.12.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==48.0.1",
"cryptography==49.0.0",
"Pillow==12.2.0",
"propcache==0.5.2",
"pyOpenSSL==26.2.0",
"pyOpenSSL==26.3.0",
"orjson==3.11.9",
"packaging>=23.1",
"psutil-home-assistant==0.0.1",
+2 -2
View File
@@ -21,7 +21,7 @@ bcrypt==5.0.0
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.1
cryptography==49.0.0
fnv-hash-fast==2.0.3
ha-ffmpeg==3.2.2
hass-nabucasa==2.2.0
@@ -41,7 +41,7 @@ propcache==0.5.2
psutil-home-assistant==0.0.1
PyJWT==2.12.1
pymicro-vad==1.0.1
pyOpenSSL==26.2.0
pyOpenSSL==26.3.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.3
+6 -6
View File
@@ -193,7 +193,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==14.1.6
aioamazondevices==14.1.3
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -260,7 +260,7 @@ aioelectricitymaps==1.1.1
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==45.5.2
aioesphomeapi==45.3.1
# homeassistant.components.matrix
# homeassistant.components.slack
@@ -1272,7 +1272,7 @@ hole==0.9.2
holidays==0.99
# homeassistant.components.frontend
home-assistant-frontend==20260624.1
home-assistant-frontend==20260624.0
# homeassistant.components.conversation
home-assistant-intents==2026.6.24
@@ -3221,10 +3221,10 @@ ttls==1.8.3
ttn_client==1.3.0
# homeassistant.components.tuya
tuya-device-handlers==0.0.24
tuya-device-handlers==0.0.22
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.10
tuya-device-sharing-sdk==0.2.8
# homeassistant.components.twentemilieu
twentemilieu==3.0.0
@@ -3245,7 +3245,7 @@ uasiren==0.0.1
uhooapi==1.2.8
# homeassistant.components.unifiprotect
uiprotect==15.1.0
uiprotect==15.0.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.6.1
-1
View File
@@ -19,7 +19,6 @@ mock-open==1.4.0
mypy==2.1.0
prek==0.2.28
pydantic==2.13.4
PyGithub==2.9.1
pylint==4.0.6
pylint-per-file-ignores==3.2.1
pipdeptree==2.26.1
+14 -38
View File
@@ -2,28 +2,12 @@
import argparse
import json
import os
from pathlib import Path
import sys
from .gate import GateDecision, decide_skip
from .models import CheckRunResult
from .runner import run_checks
def _resolve_skip(pr_number: int, head_sha: str | None) -> GateDecision:
"""Decide whether this run can skip re-checking the PR.
Needs the repo and a token (from the Actions environment) to read prior
comments; without them it falls open and runs the checks.
"""
repo = os.environ.get("GITHUB_REPOSITORY")
token = os.environ.get("GITHUB_TOKEN")
if not head_sha or not repo or not token:
return GateDecision(False, "Gate inputs unavailable; running checks.")
return decide_skip(pr_number, head_sha, repo, token)
def main(argv: list[str] | None = None) -> int:
"""Run the deterministic check_requirements stage and write its artifact."""
parser = argparse.ArgumentParser(prog="python -m script.check_requirements")
@@ -47,32 +31,24 @@ def main(argv: list[str] | None = None) -> int:
)
args = parser.parse_args(argv)
decision = _resolve_skip(args.pr_number, args.head_sha)
print(decision.reason, file=sys.stderr)
if decision.skip:
result = CheckRunResult(
pr_number=args.pr_number, head_sha=args.head_sha, skip_aw=True
)
else:
try:
diff_text = args.diff.read_text(encoding="utf-8")
except FileNotFoundError:
parser.error(f"input file {args.diff} not found")
result = run_checks(
pr_number=args.pr_number,
diff_text=diff_text,
head_sha=args.head_sha,
)
print(
f"check_requirements: {len(result.packages)} package change(s); "
f"needs_agent={result.needs_agent}",
file=sys.stderr,
)
try:
diff_text = args.diff.read_text(encoding="utf-8")
except FileNotFoundError:
parser.error(f"input file {args.diff} not found")
result = run_checks(
pr_number=args.pr_number,
diff_text=diff_text,
head_sha=args.head_sha,
)
args.output.write_text(
json.dumps(result.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
print(
f"check_requirements: {len(result.packages)} package change(s); "
f"needs_agent={result.needs_agent}",
file=sys.stderr,
)
return 0
+2 -4
View File
@@ -17,13 +17,11 @@ from .models import PackageChange
# of truth for pinned package changes.
TRACKED_PATTERNS = (
"requirements*.txt",
"**/requirements*.txt",
"homeassistant/package_constraints.txt",
)
def is_tracked(path: str) -> bool:
"""Return True if `path` is a requirement file the checks care about."""
def _is_tracked(path: str) -> bool:
return any(fnmatchcase(path, pattern) for pattern in TRACKED_PATTERNS)
@@ -63,7 +61,7 @@ def parse_diff(diff_text: str) -> list[PackageChange]:
added: dict[str, _Pin] = {}
removed: dict[str, _Pin] = {}
for patched_file in PatchSet(diff_text):
if not is_tracked(patched_file.path):
if not _is_tracked(patched_file.path):
continue
for hunk in patched_file:
for line in hunk:
-107
View File
@@ -1,107 +0,0 @@
"""Decide whether the deterministic stage can skip re-checking a PR.
The deterministic stage re-runs on every `synchronize` where the PR touches a
tracked requirement file, even when the latest push changed only unrelated
files. This module answers "did a tracked requirement file actually change
since we last commented?" so the stage can skip the PyPI work and flag the
uploaded artifact as skipped, telling the agentic stage to no-op.
"""
from dataclasses import dataclass
import logging
import os
import re
from github import Auth, Github, GithubException
from github.IssueComment import IssueComment
from .diff import is_tracked
from .render import COMMIT_PATH
_LOGGER = logging.getLogger(__name__)
# The "Checked at commit [`abc1234`](...COMMIT_PATH<40-hex>)." link rendered by
# render._intro is the only place the head SHA is recorded in the comment.
_COMMIT_SHA_RE = re.compile(re.escape(COMMIT_PATH) + r"([0-9a-f]{40})", re.IGNORECASE)
_TRUSTED_AUTHOR = "github-actions[bot]"
def _is_trusted_author(comment: IssueComment) -> bool:
"""True only for the github-actions bot that posts the check comment."""
return comment.user is not None and comment.user.login == _TRUSTED_AUTHOR
@dataclass(slots=True, frozen=True)
class GateDecision:
"""Whether to skip the deterministic checks, with a human-readable reason."""
skip: bool
reason: str
def _client(token: str) -> Github:
"""A lazy GitHub client on the configured (possibly GHES) API base."""
base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/")
return Github(auth=Auth.Token(token), base_url=base_url, lazy=True)
def fetch_marker_comment_bodies(pr_number: int, repo: str, token: str) -> list[str]:
"""Return the trusted requirements-check comment bodies, oldest-first."""
try:
comments = _client(token).get_repo(repo).get_issue(pr_number).get_comments()
return [comment.body for comment in comments if _is_trusted_author(comment)]
except GithubException as err:
_LOGGER.warning("Could not read comments for PR #%s: %s", pr_number, err)
return []
def extract_prior_sha(bodies: list[str]) -> str | None:
"""Return the head SHA recorded in the most recent marker comment."""
shas = [
match.group(1).lower()
for body in bodies
for match in _COMMIT_SHA_RE.finditer(body)
]
return shas[-1] if shas else None
def compare_changed_files(
base: str, head: str, repo: str, token: str
) -> list[str] | None:
"""Return filenames changed between two commits, or None if unavailable."""
try:
comparison = _client(token).get_repo(repo).compare(base, head)
return [changed.filename for changed in comparison.files]
except GithubException as err:
_LOGGER.warning("Could not compare %s...%s: %s", base, head, err)
return None
def decide_skip(pr_number: int, head_sha: str, repo: str, token: str) -> GateDecision:
"""Decide whether requirements changed since the last comment."""
if not head_sha:
return GateDecision(False, "No head SHA available; running checks.")
prior = extract_prior_sha(fetch_marker_comment_bodies(pr_number, repo, token))
if prior is None:
return GateDecision(
False, "No previous requirements-check comment; running checks."
)
if prior == head_sha.lower():
return GateDecision(
True, f"Head {head_sha} unchanged since the last comment; skipping."
)
changed = compare_changed_files(prior, head_sha, repo, token)
if changed is None:
return GateDecision(
False, f"Could not compare {prior}...{head_sha}; running checks."
)
tracked = [path for path in changed if is_tracked(path)]
if tracked:
return GateDecision(
False,
f"Tracked requirement files changed since {prior}; running checks: "
+ ", ".join(tracked),
)
return GateDecision(
True, f"No tracked requirement files changed since {prior}; skipping."
)
-2
View File
@@ -90,7 +90,6 @@ class CheckRunResult:
head_sha: str | None = None
packages: list[PackageChange] = field(default_factory=list)
rendered_comment: str = ""
skip_aw: bool = False
@property
def needs_agent(self) -> bool:
@@ -102,7 +101,6 @@ class CheckRunResult:
return {
"version": 1,
"pr_number": self.pr_number,
"skip_aw": self.skip_aw,
"head_sha": self.head_sha,
"needs_agent": self.needs_agent,
"packages": [p.to_dict() for p in self.packages],
+1 -2
View File
@@ -14,7 +14,6 @@ from .models import CheckKind, CheckRunResult, CheckStatus, PackageChange
MARKER = "<!-- requirements-check -->"
HEADER = "## Check requirements"
REPO_URL = "https://github.com/home-assistant/core"
COMMIT_PATH = "/commit/"
# Column / bullet labels per check kind, in display order.
_CHECK_DISPLAY: tuple[tuple[CheckKind, str], ...] = (
@@ -128,7 +127,7 @@ def _intro(result: CheckRunResult) -> str:
"""Marker, header, and the optional commit line the gate reads back."""
parts: list[str] = []
if result.head_sha:
commit = f"[`{result.head_sha[:7]}`]({REPO_URL}{COMMIT_PATH}{result.head_sha})"
commit = f"[`{result.head_sha[:7]}`]({REPO_URL}/commit/{result.head_sha})"
parts.append(f"Checked at commit {commit}.")
return "\n\n".join([f"{MARKER}\n{HEADER}", *parts])
-1
View File
@@ -1,3 +1,2 @@
PyGithub==2.9.1
requests==2.34.2
unidiff==0.7.5
+2 -1
View File
@@ -622,6 +622,7 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"mullvad",
"mutesync",
"mvglive",
"mycroft",
"myq",
"mysensors",
"mystrom",
@@ -1588,6 +1589,7 @@ INTEGRATIONS_WITHOUT_SCALE = [
"mullvad",
"mutesync",
"mvglive",
"mycroft",
"myq",
"mysensors",
"mystrom",
@@ -2082,7 +2084,6 @@ NO_QUALITY_SCALE = [
"intent_script",
"intent",
"labs",
"llm",
"logbook",
"logger",
"lovelace",
@@ -38,7 +38,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-co',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_carbon_monoxide-state]
@@ -50,7 +50,7 @@
'limit': 4000,
'percent': 4,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_carbon_monoxide',
@@ -157,7 +157,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-humidity',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[sensor.home_humidity-state]
@@ -167,7 +167,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.home_humidity',
@@ -216,7 +216,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-no2',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_nitrogen_dioxide-state]
@@ -228,7 +228,7 @@
'limit': 25,
'percent': 64,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_dioxide',
@@ -277,7 +277,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-o3',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_ozone-state]
@@ -289,7 +289,7 @@
'limit': 100,
'percent': 42,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_ozone',
@@ -338,7 +338,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm1',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_pm1-state]
@@ -348,7 +348,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm1',
@@ -397,7 +397,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm10',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_pm10-state]
@@ -409,7 +409,7 @@
'limit': 45,
'percent': 14,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm10',
@@ -458,7 +458,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm25',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_pm2_5-state]
@@ -470,7 +470,7 @@
'limit': 15,
'percent': 29,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm2_5',
@@ -578,7 +578,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-so2',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor[sensor.home_sulphur_dioxide-state]
@@ -590,7 +590,7 @@
'limit': 40,
'percent': 35,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_sulphur_dioxide',
-1
View File
@@ -1 +0,0 @@
"""Tests for the LLM integration."""
-121
View File
@@ -1,121 +0,0 @@
"""Tests for the LLM integration."""
from unittest.mock import Mock
import pytest
from homeassistant.components.llm import DATA_PLATFORMS, LLMTools, async_get_tools
from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm
from homeassistant.setup import async_setup_component
from homeassistant.util.json import JsonObjectType
from tests.common import mock_platform
class _StubTool(llm.Tool):
"""Minimal tool for registry tests."""
def __init__(self, name: str) -> None:
"""Initialize the stub tool."""
self.name = name
self.description = f"{name} description"
async def async_call(
self,
hass: HomeAssistant,
tool_input: llm.ToolInput,
llm_context: llm.LLMContext,
) -> JsonObjectType:
"""Return an empty result."""
return {}
@pytest.fixture
def llm_context() -> llm.LLMContext:
"""Return an LLM context."""
return llm.LLMContext(
platform="test",
context=None,
language="*",
assistant="conversation",
device_id=None,
)
def _mock_tools_platform(
hass: HomeAssistant, domain: str, tools: LLMTools | Exception
) -> None:
"""Register a mock <integration>/llm.py platform returning the given tools."""
if isinstance(tools, Exception):
async_get_tools = Mock(side_effect=tools)
else:
async_get_tools = Mock(return_value=tools)
hass.config.components.add(domain)
mock_platform(hass, f"{domain}.llm", Mock(async_get_tools=async_get_tools))
async def test_setup(hass: HomeAssistant) -> None:
"""Test the integration sets up."""
assert await async_setup_component(hass, "llm", {})
assert DATA_PLATFORMS in hass.data
async def test_get_tools(hass: HomeAssistant, llm_context: llm.LLMContext) -> None:
"""Test that tools from an integration platform are returned."""
tool = _StubTool("my_tool")
_mock_tools_platform(
hass, "test", LLMTools(tools=[tool], prompt="use my_tool wisely")
)
assert await async_setup_component(hass, "llm", {})
result = await async_get_tools(hass, llm_context)
assert result.tools == [tool]
assert result.prompt == "use my_tool wisely"
async def test_get_tools_empty(
hass: HomeAssistant, llm_context: llm.LLMContext
) -> None:
"""Test that no platforms yields no tools."""
assert await async_setup_component(hass, "llm", {})
result = await async_get_tools(hass, llm_context)
assert result.tools == []
assert result.prompt is None
async def test_get_tools_merges_sorted(
hass: HomeAssistant, llm_context: llm.LLMContext
) -> None:
"""Test that tools and prompts are merged in a load-order-independent order."""
tool_a = _StubTool("tool_a")
tool_b = _StubTool("tool_b")
# Register "test_b" before "test_a" to prove the result is sorted by domain.
_mock_tools_platform(hass, "test_b", LLMTools(tools=[tool_b], prompt="prompt b"))
_mock_tools_platform(hass, "test_a", LLMTools(tools=[tool_a], prompt="prompt a"))
assert await async_setup_component(hass, "llm", {})
result = await async_get_tools(hass, llm_context)
assert result.tools == [tool_a, tool_b]
assert result.prompt == "prompt a\nprompt b"
async def test_get_tools_isolates_failing_platform(
hass: HomeAssistant,
llm_context: llm.LLMContext,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that one failing platform does not drop the others' tools."""
tool = _StubTool("good_tool")
_mock_tools_platform(hass, "test_bad", ValueError("boom"))
_mock_tools_platform(hass, "test_good", LLMTools(tools=[tool], prompt="prompt"))
assert await async_setup_component(hass, "llm", {})
result = await async_get_tools(hass, llm_context)
assert result.tools == [tool]
assert result.prompt == "prompt"
assert "Error getting tools from LLM platform test_bad" in caplog.text
@@ -151,7 +151,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'be37ca9c47c24498a38bc62c7c711840-sensor2-air_humidity',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_setup[sensor.test_sensor_2-state]
@@ -160,7 +160,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Sensor 2',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_sensor_2',
+124 -124
View File
@@ -103,7 +103,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-CarbonDioxideSensor-1037-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_carbon_dioxide-state]
@@ -112,7 +112,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_carbon_dioxide',
@@ -158,7 +158,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_humidity-state]
@@ -167,7 +167,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_humidity',
@@ -213,7 +213,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-NitrogenDioxideSensor-1043-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_nitrogen_dioxide-state]
@@ -222,7 +222,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'nitrogen_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor Nitrogen dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_nitrogen_dioxide',
@@ -268,7 +268,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM1Sensor-1068-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm1-state]
@@ -277,7 +277,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm1',
@@ -323,7 +323,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM10Sensor-1069-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm10-state]
@@ -332,7 +332,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm10',
@@ -378,7 +378,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM25Sensor-1066-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm2_5-state]
@@ -387,7 +387,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5',
@@ -598,7 +598,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TotalVolatileOrganicCompoundsSensor-1070-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts-state]
@@ -607,7 +607,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'volatile_organic_compounds_parts',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'lightfi-aq1-air-quality-sensor Volatile organic compounds parts',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts',
@@ -653,7 +653,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000005B-MatterNodeDevice-2-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_door_window_p2][sensor.aqara_door_and_window_sensor_p2_battery-state]
@@ -662,7 +662,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aqara Door and Window Sensor P2 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.aqara_door_and_window_sensor_p2_battery',
@@ -993,7 +993,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-3-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_motion_p2][sensor.aqara_motion_and_light_sensor_p2_battery-state]
@@ -1002,7 +1002,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aqara Motion and Light Sensor P2 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.aqara_motion_and_light_sensor_p2_battery',
@@ -1388,7 +1388,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000016C-MatterNodeDevice-2-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_multi_state_p100][sensor.multi_state_sensor_p100_battery-state]
@@ -1397,7 +1397,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Multi-State Sensor P100 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.multi_state_sensor_p100_battery',
@@ -1900,7 +1900,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000CD-MatterNodeDevice-5-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_presence_fp300][sensor.presence_multi_sensor_fp300_1_battery-state]
@@ -1909,7 +1909,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Presence Multi-Sensor FP300 1 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.presence_multi_sensor_fp300_1_battery',
@@ -2136,7 +2136,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000CD-MatterNodeDevice-4-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_presence_fp300][sensor.presence_multi_sensor_fp300_1_humidity-state]
@@ -2145,7 +2145,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Presence Multi-Sensor FP300 1 Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.presence_multi_sensor_fp300_1_humidity',
@@ -2580,7 +2580,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-6-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_sensor_w100][sensor.climate_sensor_w100_battery-state]
@@ -2589,7 +2589,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Climate Sensor W100 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.climate_sensor_w100_battery',
@@ -2975,7 +2975,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-2-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_sensor_w100][sensor.climate_sensor_w100_humidity-state]
@@ -2984,7 +2984,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Climate Sensor W100 Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.climate_sensor_w100_humidity',
@@ -3556,7 +3556,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000064-MatterNodeDevice-2-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_thermostat_w500][sensor.floor_heating_thermostat_humidity-state]
@@ -3565,7 +3565,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Floor Heating Thermostat Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.floor_heating_thermostat_humidity',
@@ -3945,7 +3945,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000014-MatterNodeDevice-2-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[aqara_u200][sensor.aqara_smart_lock_u200_battery-state]
@@ -3954,7 +3954,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aqara Smart Lock U200 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.aqara_smart_lock_u200_battery',
@@ -4633,14 +4633,14 @@
'supported_features': 0,
'translation_key': 'pi_heating_demand',
'unique_id': '00000000000004D2-0000000000000007-MatterNodeDevice-1-ThermostatPIHeatingDemand-513-8',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eberle_ute3000][sensor.connected_thermostat_ute_3000_heating_demand-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Connected Thermostat UTE 3000 Heating demand',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.connected_thermostat_ute_3000_heating_demand',
@@ -4852,7 +4852,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000002F-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[ecovacs_deebot][sensor.ecodeebot_battery-state]
@@ -4861,7 +4861,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ecodeebot Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.ecodeebot_battery',
@@ -5539,7 +5539,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_battery-state]
@@ -5548,7 +5548,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '2BAVS-AB6031X-44PE Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_battery',
@@ -5994,7 +5994,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000009-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-state]
@@ -6003,7 +6003,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Door Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_door_battery',
@@ -7973,14 +7973,14 @@
'supported_features': 0,
'translation_key': 'window_covering_target_position',
'unique_id': '00000000000004D2-0000000000000094-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_shutter][sensor.eve_shutter_switch_20eci1701_target_opening_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Shutter Switch 20ECI1701 Target opening position',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_shutter_switch_20eci1701_target_opening_position',
@@ -8249,7 +8249,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_battery-state]
@@ -8258,7 +8258,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20EBP1701 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_thermo_20ebp1701_battery',
@@ -8697,14 +8697,14 @@
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveThermoValvePosition-319486977-319422488',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_thermo_v4][sensor.eve_thermo_20ebp1701_valve_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20EBP1701 Valve position',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_thermo_20ebp1701_valve_position',
@@ -8750,7 +8750,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_battery-state]
@@ -8759,7 +8759,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20ECD1701 Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_thermo_20ecd1701_battery',
@@ -9137,14 +9137,14 @@
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-EveThermoValvePosition-319486977-319422488',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_valve_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20ECD1701 Valve position',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_thermo_20ecd1701_valve_position',
@@ -9190,7 +9190,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery-state]
@@ -9199,7 +9199,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Weather Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_weather_battery',
@@ -9306,7 +9306,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eve_weather_sensor][sensor.eve_weather_humidity-state]
@@ -9315,7 +9315,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Weather Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.eve_weather_humidity',
@@ -9991,7 +9991,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[haojai_switch][sensor.hjmt_6b_battery-state]
@@ -10000,7 +10000,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'HJMT-6B Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.hjmt_6b_battery',
@@ -10771,7 +10771,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000006-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[heiman_co_sensor][sensor.smart_co_sensor_battery-state]
@@ -10780,7 +10780,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smart CO sensor Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.smart_co_sensor_battery',
@@ -10937,7 +10937,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000006-MatterNodeDevice-1-CarbonMonoxideSensor-1036-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[heiman_co_sensor][sensor.smart_co_sensor_carbon_monoxide-state]
@@ -10946,7 +10946,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_monoxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smart CO sensor Carbon monoxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.smart_co_sensor_carbon_monoxide',
@@ -11329,7 +11329,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000058-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[heiman_motion_sensor_m1][sensor.smart_motion_sensor_battery-state]
@@ -11338,7 +11338,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smart motion sensor Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.smart_motion_sensor_battery',
@@ -11887,7 +11887,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000B-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[heiman_smoke_detector][sensor.smoke_sensor_battery-state]
@@ -11896,7 +11896,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smoke sensor Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.smoke_sensor_battery',
@@ -12365,7 +12365,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000025-MatterNodeDevice-1-CarbonDioxideSensor-1037-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[ikea_air_quality_monitor][sensor.alpstuga_air_quality_monitor_carbon_dioxide-state]
@@ -12374,7 +12374,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ALPSTUGA air quality monitor Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.alpstuga_air_quality_monitor_carbon_dioxide',
@@ -12420,7 +12420,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000025-MatterNodeDevice-1-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[ikea_air_quality_monitor][sensor.alpstuga_air_quality_monitor_humidity-state]
@@ -12429,7 +12429,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ALPSTUGA air quality monitor Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.alpstuga_air_quality_monitor_humidity',
@@ -12475,7 +12475,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000025-MatterNodeDevice-1-PM25Sensor-1066-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[ikea_air_quality_monitor][sensor.alpstuga_air_quality_monitor_pm2_5-state]
@@ -12484,7 +12484,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ALPSTUGA air quality monitor PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.alpstuga_air_quality_monitor_pm2_5',
@@ -12864,7 +12864,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000089-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[ikea_bilresa_dual_button][sensor.bilresa_dual_button_battery-state]
@@ -12873,7 +12873,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'BILRESA dual button Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.bilresa_dual_button_battery',
@@ -13412,7 +13412,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-0-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[ikea_scroll_wheel][sensor.bilresa_scroll_wheel_battery-state]
@@ -13421,7 +13421,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'BILRESA scroll wheel Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.bilresa_scroll_wheel_battery',
@@ -14682,7 +14682,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000100-MatterNodeDevice-8-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[inovelli_vtm30][sensor.white_series_onoff_switch_humidity-state]
@@ -14691,7 +14691,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'White Series OnOff Switch Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.white_series_onoff_switch_humidity',
@@ -16043,7 +16043,7 @@
'supported_features': 0,
'translation_key': 'activated_carbon_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-ActivatedCarbonFilterCondition-114-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_activated_carbon_filter_condition-state]
@@ -16051,7 +16051,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Activated carbon filter condition',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_activated_carbon_filter_condition',
@@ -16235,7 +16235,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-CarbonDioxideSensor-1037-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_carbon_dioxide-state]
@@ -16244,7 +16244,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_carbon_dioxide',
@@ -16290,7 +16290,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-CarbonMonoxideSensor-1036-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_carbon_monoxide-state]
@@ -16299,7 +16299,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_monoxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Carbon monoxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_carbon_monoxide',
@@ -16345,7 +16345,7 @@
'supported_features': 0,
'translation_key': 'hepa_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-HepaFilterCondition-113-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_hepa_filter_condition-state]
@@ -16353,7 +16353,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier HEPA filter condition',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_hepa_filter_condition',
@@ -16399,7 +16399,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-4-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_humidity-state]
@@ -16408,7 +16408,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_humidity',
@@ -16454,7 +16454,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-NitrogenDioxideSensor-1043-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_nitrogen_dioxide-state]
@@ -16463,7 +16463,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'nitrogen_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Nitrogen dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_nitrogen_dioxide',
@@ -16509,7 +16509,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-OzoneConcentrationSensor-1045-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_ozone-state]
@@ -16518,7 +16518,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'ozone',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Ozone',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_ozone',
@@ -16564,7 +16564,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM1Sensor-1068-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_pm1-state]
@@ -16573,7 +16573,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_pm1',
@@ -16619,7 +16619,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM10Sensor-1069-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_pm10-state]
@@ -16628,7 +16628,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_pm10',
@@ -16674,7 +16674,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM25Sensor-1066-0',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_pm2_5-state]
@@ -16683,7 +16683,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_pm2_5',
@@ -17067,7 +17067,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-TotalVolatileOrganicCompoundsSensor-1070-0',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_volatile_organic_compounds_parts-state]
@@ -17076,7 +17076,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'volatile_organic_compounds_parts',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Air Purifier Volatile organic compounds parts',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_volatile_organic_compounds_parts',
@@ -17249,7 +17249,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_battery_storage][sensor.mock_battery_storage_battery-state]
@@ -17258,7 +17258,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Battery Storage Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_battery_storage_battery',
@@ -18815,7 +18815,7 @@
'supported_features': 0,
'translation_key': 'activated_carbon_filter_condition',
'unique_id': '00000000000004D2-0000000000000049-MatterNodeDevice-1-ActivatedCarbonFilterCondition-114-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_extractor_hood][sensor.mock_extractor_hood_activated_carbon_filter_condition-state]
@@ -18823,7 +18823,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Extractor hood Activated carbon filter condition',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_extractor_hood_activated_carbon_filter_condition',
@@ -18939,7 +18939,7 @@
'supported_features': 0,
'translation_key': 'hepa_filter_condition',
'unique_id': '00000000000004D2-0000000000000049-MatterNodeDevice-1-HepaFilterCondition-113-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_extractor_hood][sensor.mock_extractor_hood_hepa_filter_condition-state]
@@ -18947,7 +18947,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Extractor hood HEPA filter condition',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_extractor_hood_hepa_filter_condition',
@@ -19656,7 +19656,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000015-MatterNodeDevice-1-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_humidity_sensor][sensor.mock_humidity_sensor_humidity-state]
@@ -19665,7 +19665,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Humidity Sensor Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_humidity_sensor_humidity',
@@ -22707,7 +22707,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000065-MatterNodeDevice-1-SoilMoistureSensor-1072-1',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_soil_sensor][sensor.mock_soil_sensor_moisture-state]
@@ -22716,7 +22716,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'moisture',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Soil Sensor Moisture',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_soil_sensor_moisture',
@@ -23410,14 +23410,14 @@
'supported_features': 0,
'translation_key': 'pi_heating_demand',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatPIHeatingDemand-513-8',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_heating_demand-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Thermostat Heating demand',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_heating_demand',
@@ -24348,14 +24348,14 @@
'supported_features': 0,
'translation_key': 'window_covering_target_position',
'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_window_covering_full][sensor.mock_full_window_covering_target_opening_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Full Window Covering Target opening position',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_full_window_covering_target_opening_position',
@@ -24685,14 +24685,14 @@
'supported_features': 0,
'translation_key': 'window_covering_target_position',
'unique_id': '00000000000004D2-0000000000000027-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_window_covering_pa_lift][sensor.longan_link_wncv_da01_target_opening_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Longan link WNCV DA01 Target opening position',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.longan_link_wncv_da01_target_opening_position',
@@ -25451,7 +25451,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_battery-state]
@@ -25460,7 +25460,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Robotic Vacuum Cleaner Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.robotic_vacuum_cleaner_battery',
@@ -27905,7 +27905,7 @@
'supported_features': 0,
'translation_key': 'evse_soc',
'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseStateOfCharge-153-48',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[silabs_evse_charging][sensor.evse_state_of_charge-state]
@@ -27914,7 +27914,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'evse State of charge',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.evse_state_of_charge',
@@ -30846,7 +30846,7 @@
'supported_features': 0,
'translation_key': 'tank_percentage',
'unique_id': '00000000000004D2-0000000000000039-MatterNodeDevice-2-WaterHeaterManagementTankPercentage-148-4',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[silabs_water_heater][sensor.water_heater_hot_water_level-state]
@@ -30854,7 +30854,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Water Heater Hot water level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.water_heater_hot_water_level',
@@ -31775,7 +31775,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000031-MatterNodeDevice-1-HumiditySensor-1029-0',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_humidity-state]
@@ -31784,7 +31784,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smart Radiator Thermostat X Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.smart_radiator_thermostat_x_humidity',
@@ -32395,7 +32395,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000007A-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[zemismart_mt25b][sensor.zemismart_mt25b_roller_motor_battery-state]
@@ -32404,7 +32404,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Zemismart MT25B Roller Motor Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.zemismart_mt25b_roller_motor_battery',
@@ -1987,7 +1987,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/15702199#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_garden_radiator_battery_level-state]
@@ -1996,7 +1996,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garden Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_garden_radiator_battery_level',
@@ -2447,14 +2447,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/4080031-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_hallway_shutter_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hallway Shutter Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_hallway_shutter_target_closure',
@@ -3016,7 +3016,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/9253412#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_living_room_radiator_battery_level-state]
@@ -3025,7 +3025,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Room Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_living_room_radiator_battery_level',
@@ -4087,14 +4087,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/141613-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_nursery_shutter_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nursery Shutter Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_nursery_shutter_target_closure',
@@ -4361,14 +4361,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/9740264-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_office_shutter_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Office Shutter Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_office_shutter_target_closure',
@@ -4637,7 +4637,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/9187218#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.maple_residence_study_radiator_battery_level-state]
@@ -4646,7 +4646,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.maple_residence_study_radiator_battery_level',
@@ -5518,7 +5518,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/6360455#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.attic_heater_battery_level-state]
@@ -5527,7 +5527,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Attic Heater Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.attic_heater_battery_level',
@@ -6117,14 +6117,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/3868695-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.conservatory_screen_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Conservatory Screen Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.conservatory_screen_target_closure',
@@ -6170,7 +6170,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/10832611#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.dining_room_thermostat_battery_level-state]
@@ -6179,7 +6179,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Dining Room Thermostat Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dining_room_thermostat_battery_level',
@@ -6771,7 +6771,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/3000744#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.garage_radiator_battery_level-state]
@@ -6780,7 +6780,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.garage_radiator_battery_level',
@@ -7252,7 +7252,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/10074960#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.garage_thermostat_battery_level-state]
@@ -7261,7 +7261,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Thermostat Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.garage_thermostat_battery_level',
@@ -7491,7 +7491,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/13876191#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.garden_radiator_battery_level-state]
@@ -7500,7 +7500,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garden Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.garden_radiator_battery_level',
@@ -8576,7 +8576,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/13659989#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.hallway_radiator_battery_level-state]
@@ -8585,7 +8585,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hallway Radiator Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.hallway_radiator_battery_level',
@@ -9537,14 +9537,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/3904805-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.library_screen_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Library Screen Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.library_screen_target_closure',
@@ -9710,7 +9710,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/9749990#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.living_room_thermostat_battery_level-state]
@@ -9719,7 +9719,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Room Thermostat Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_thermostat_battery_level',
@@ -9949,7 +9949,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/1292684#1-core:BatteryLevelState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.patio_thermostat_battery_level-state]
@@ -9958,7 +9958,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Patio Thermostat Battery level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.patio_thermostat_battery_level',
@@ -10546,14 +10546,14 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678--9373/5632438-core:TargetClosureState',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_entities_snapshot[local_somfy_tahoma_switch_europe_3.json][sensor.terrace_awning_target_closure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Terrace Awning Target closure',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.terrace_awning_target_closure',
@@ -4765,46 +4765,3 @@ async def test_import_statistics_with_last_reset(
},
]
}
async def test_entity_options_ws(
hass: HomeAssistant,
async_setup_recorder_instance: RecorderInstanceGenerator,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test recorder entity options WS commands."""
client = await hass_ws_client()
await async_setup_recorder_instance(hass, {"exclude": {"domains": "test2"}})
# Test getting a single entity's settings
await client.send_json_auto_id(
{
"type": "recorder/entity_options/get",
"entity_id": "test.recorder",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"recording_disabled_by": None}
await client.send_json_auto_id(
{
"type": "recorder/entity_options/get",
"entity_id": "test2.recorder",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"recording_disabled_by": "user"}
# Test getting settings for an unknown entity
await client.send_json_auto_id(
{
"type": "recorder/entity_options/get",
"entity_id": "unknown.entity",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"recording_disabled_by": None}
@@ -20,7 +20,6 @@ def mock_config_entry() -> MockConfigEntry:
data=CONF_DATA,
options=CONF_OPTIONS,
unique_id=ACCOUNT_1,
version=2,
)
@@ -32,7 +32,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <SteamSensor.ACCOUNT: 'account'>,
'unique_id': '12345678901234567_account',
'unique_id': 'sensor.steam_12345678901234567',
'unit_of_measurement': None,
})
# ---
+1 -41
View File
@@ -7,11 +7,8 @@ import steam.api
from homeassistant.components.steam_online.const import DEFAULT_NAME, DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import ACCOUNT_1, ACCOUNT_NAME_1, CONF_DATA, CONF_OPTIONS
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@@ -98,40 +95,3 @@ async def test_device_info(
assert device.identifiers == {(DOMAIN, config_entry.entry_id)}
assert device.manufacturer == DEFAULT_NAME
assert device.name == DEFAULT_NAME
@pytest.mark.usefixtures("steam_api")
async def test_migrate_entry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test entry migration."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONF_DATA,
options=CONF_OPTIONS,
unique_id=ACCOUNT_1,
version=1,
)
config_entry.add_to_hass(hass)
assert config_entry.version == 1
sensor = entity_registry.async_get_or_create(
domain=Platform.SENSOR,
platform=DOMAIN,
unique_id=f"sensor.steam_{ACCOUNT_1}",
config_entry=config_entry,
original_name=ACCOUNT_NAME_1,
)
assert sensor.unique_id == f"sensor.steam_{ACCOUNT_1}"
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.version == 2
assert (sensor := entity_registry.async_get(sensor.entity_id))
assert sensor.unique_id == f"{ACCOUNT_1}_account"
+16 -5
View File
@@ -85,6 +85,16 @@ async def assert_automation_condition_trace(hass_ws_client, automation_id, expec
assert condition_trace["result"] == expected
async def assert_automation_condition_trace_error(
hass_ws_client, automation_id, expected
):
"""Test the error of automation condition."""
condition_trace = await _get_automation_condition_trace(
hass_ws_client, automation_id
)
assert condition_trace["error"] == expected
async def test_if_action_before_sunrise_no_offset(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
@@ -1289,9 +1299,10 @@ async def test_if_action_no_sun_event_in_polar_regions(
"""Test a sun condition where the requested event never occurs.
During midnight sun and polar night the sun neither rises nor sets, so
``get_astral_event_date`` returns None for the requested event. The
condition cannot be satisfied and reports "no sunrise today" / "no sunset
today" instead of raising.
``get_astral_event_date`` returns None for both events. This documents the
legacy condition crashing on the missing event (it passes None to
``dt_util.as_local``); the crash fix and the matching "no sunrise/sunset
today" results are a follow-up.
"""
latitude, longitude, time_zone = location
await hass.config.async_set_time_zone(time_zone)
@@ -1317,10 +1328,10 @@ async def test_if_action_no_sun_event_in_polar_regions(
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_automation_condition_trace(
await assert_automation_condition_trace_error(
hass_ws_client,
"sun",
{"result": False, "message": f"no {event} today"},
"'NoneType' object has no attribute 'tzinfo'",
)
@@ -1044,7 +1044,7 @@
'object_id_base': 'Carbon dioxide',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'suggested_unit_of_measurement': 'ppm',
}),
}),
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
@@ -1056,7 +1056,7 @@
'supported_features': 0,
'translation_key': 'carbon_dioxide',
'unique_id': 'tuya.iks13mcaiyie3rryjb2occo2_value',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_platform_setup_and_discovery[sensor.aqi_carbon_dioxide-state]
@@ -1220,7 +1220,7 @@
'supported_features': 0,
'translation_key': 'pm10',
'unique_id': 'tuya.iks13mcaiyie3rryjb2ocpm10',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_platform_setup_and_discovery[sensor.aqi_pm10-state]
@@ -1229,7 +1229,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AQI PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.aqi_pm10',
@@ -1266,7 +1266,7 @@
'object_id_base': 'PM2.5',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'suggested_unit_of_measurement': 'μg/m³',
}),
}),
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
@@ -1278,7 +1278,7 @@
'supported_features': 0,
'translation_key': 'pm25',
'unique_id': 'tuya.iks13mcaiyie3rryjb2ocpm25_value',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_platform_setup_and_discovery[sensor.aqi_pm2_5-state]
@@ -1287,7 +1287,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AQI PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.aqi_pm2_5',
@@ -1391,7 +1391,7 @@
'supported_features': 0,
'translation_key': 'voc',
'unique_id': 'tuya.iks13mcaiyie3rryjb2ocvoc_value',
'unit_of_measurement': <UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER: 'mg/m³'>,
'unit_of_measurement': 'mg/m³',
})
# ---
# name: test_platform_setup_and_discovery[sensor.aqi_volatile_organic_compounds-state]
@@ -1400,7 +1400,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'volatile_organic_compounds',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AQI Volatile organic compounds',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER: 'mg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'mg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.aqi_volatile_organic_compounds',
@@ -5985,7 +5985,7 @@
'supported_features': 0,
'translation_key': 'battery',
'unique_id': 'tuya.bFFsO8HimyAJGIj7scmbattery',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_platform_setup_and_discovery[sensor.door_garage_battery-state]
@@ -5994,7 +5994,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Door Garage Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.door_garage_battery',
@@ -12304,7 +12304,7 @@
'object_id_base': 'PM2.5',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'suggested_unit_of_measurement': 'μg/m³',
}),
}),
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
@@ -12316,7 +12316,7 @@
'supported_features': 0,
'translation_key': 'pm25',
'unique_id': 'tuya.yo2karkjuhzztxsfjkpm25',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_pm2_5-state]
@@ -12325,7 +12325,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Kalado Air Purifier PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.kalado_air_purifier_pm2_5',
@@ -15899,7 +15899,7 @@
'supported_features': 0,
'translation_key': 'battery',
'unique_id': 'tuya.s3zzjdcfripbattery_percentage',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_platform_setup_and_discovery[sensor.motion_sensor_lidl_zigbee_battery-state]
@@ -15908,7 +15908,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Motion sensor lidl zigbee Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery',
@@ -16062,7 +16062,7 @@
'object_id_base': 'Carbon dioxide',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'suggested_unit_of_measurement': 'ppm',
}),
}),
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
@@ -16074,7 +16074,7 @@
'supported_features': 0,
'translation_key': 'carbon_dioxide',
'unique_id': 'tuya.rzt2knqamsxjp8f9ycjjhco2_value',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_platform_setup_and_discovery[sensor.mt15_mt29_carbon_dioxide-state]
@@ -18843,7 +18843,7 @@
'object_id_base': 'Carbon dioxide',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'suggested_unit_of_measurement': 'ppm',
}),
}),
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
@@ -18855,7 +18855,7 @@
'supported_features': 0,
'translation_key': 'carbon_dioxide',
'unique_id': 'tuya.cvowstbid97lokayjb2occo2_value',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_platform_setup_and_discovery[sensor.pth_9cw_32_carbon_dioxide-state]
@@ -19624,7 +19624,7 @@
'supported_features': 0,
'translation_key': 'battery',
'unique_id': 'tuya.gm0whbftkwbattery_percentage',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
'unit_of_measurement': '%',
})
# ---
# name: test_platform_setup_and_discovery[sensor.salon_battery-state]
@@ -19633,7 +19633,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Salon Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
'entity_id': 'sensor.salon_battery',
@@ -20694,7 +20694,7 @@
'object_id_base': 'Carbon monoxide',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'suggested_unit_of_measurement': 'ppm',
}),
}),
'original_device_class': <SensorDeviceClass.CO: 'carbon_monoxide'>,
@@ -20706,7 +20706,7 @@
'supported_features': 0,
'translation_key': 'carbon_monoxide',
'unique_id': 'tuya.swhtzki3qrz5ydchjbocco_value',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
'unit_of_measurement': 'ppm',
})
# ---
# name: test_platform_setup_and_discovery[sensor.smogo_carbon_monoxide-state]
@@ -20715,7 +20715,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_monoxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Smogo Carbon monoxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.smogo_carbon_monoxide',
-48
View File
@@ -934,54 +934,6 @@ async def test_entity_limit_per_config_entry_frees_slot_when_disabled(
assert hass.states.get("test_domain.ent3") is not None
@pytest.mark.parametrize(
"filler_ids",
[
pytest.param([], id="below_limit"),
pytest.param(["2"], id="at_limit"),
],
)
async def test_entity_limit_per_config_entry_allows_enabling_disabled_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
caplog: pytest.LogCaptureFixture,
filler_ids: list[str],
) -> None:
"""Test a user can enable a disabled entity regardless of the limit."""
config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
platform = MockEntityPlatform(hass)
platform.config_entry = config_entry
with patch.object(entity_platform, "MAX_ENABLED_ENTITIES_PER_CONFIG_ENTRY", 1):
# Register a disabled-by-default entity; fillers optionally reach the limit
await platform.async_add_entities(
[
MockEntity(
unique_id="1",
name="ent1",
entity_registry_enabled_default=False,
),
*(MockEntity(unique_id=uid, name=f"ent{uid}") for uid in filler_ids),
]
)
entity_id = entity_registry.async_get_entity_id("test_domain", "test_platform", "1")
assert entity_id is not None
assert hass.states.get("test_domain.ent1") is None
# The user enables the disabled entity
entity_registry.async_update_entity(entity_id, disabled_by=None)
# Re-adding the now-enabled entity, e.g. after a reload, adds it because it is
# already registered, even when this pushes the count over the limit
with patch.object(entity_platform, "MAX_ENABLED_ENTITIES_PER_CONFIG_ENTRY", 1):
await platform.async_add_entities([MockEntity(unique_id="1", name="ent1")])
assert hass.states.get("test_domain.ent1") is not None
assert "Reached the maximum" not in caplog.text
async def test_entity_limit_per_config_entry_allows_existing_entities(
hass: HomeAssistant,
) -> None:
+1 -8
View File
@@ -11,7 +11,6 @@ from homeassistant.components import calendar, todo
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.intent import async_register_timer_handler
from homeassistant.components.script import ScriptConfig
from homeassistant.const import EntityStateAttribute
from homeassistant.core import Context, HomeAssistant, State, SupportsResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
@@ -480,12 +479,7 @@ async def test_assist_api_prompt(
hass.states.async_set(
entry1.entity_id,
"on",
{
"friendly_name": "Kitchen",
"temperature": Decimal("0.9"),
"humidity": 65,
EntityStateAttribute.UNIT_OF_MEASUREMENT: "°C",
},
{"friendly_name": "Kitchen", "temperature": Decimal("0.9"), "humidity": 65},
)
hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"})
@@ -642,7 +636,6 @@ Live Context: An overview of the areas and the devices in this smart home:
attributes:
temperature: '0.9'
humidity: '65'
unit_of_measurement: °C
- names: Living Room
domain: light
state: 'on'
@@ -1,223 +0,0 @@
"""Tests for script.check_requirements.gate."""
from collections.abc import Callable
from types import SimpleNamespace
from unittest.mock import MagicMock
from github import GithubException
import pytest
from script.check_requirements import gate
from script.check_requirements.gate import (
decide_skip,
extract_prior_sha,
fetch_marker_comment_bodies,
)
from script.check_requirements.models import CheckRunResult
from script.check_requirements.render import render_comment
_REPO = "home-assistant/core"
_TOKEN = "test-token"
_PRIOR = "1234567890abcdef1234567890abcdef12345678"
_HEAD = "fedcba0987654321fedcba0987654321fedcba09"
InstallGithub = Callable[..., MagicMock]
def _body(sha: str | None) -> str:
body = "<!-- requirements-check -->\n## Check requirements\n"
if sha is not None:
body += (
f"\nChecked at commit "
f"[`{sha[:7]}`](https://github.com/home-assistant/core/commit/{sha})."
)
return body
def _comment(
sha: str | None, *, author: str = "github-actions[bot]"
) -> SimpleNamespace:
"""A PyGithub-like IssueComment with a body and an author login."""
return SimpleNamespace(body=_body(sha), user=SimpleNamespace(login=author))
def _file(filename: str) -> SimpleNamespace:
"""A PyGithub-like File entry from a commit comparison."""
return SimpleNamespace(filename=filename)
@pytest.fixture
def install_github(monkeypatch: pytest.MonkeyPatch) -> InstallGithub:
"""Install a fake PyGithub client and return the installer for assertions."""
def _install(
*,
comments: list[SimpleNamespace] | None = None,
files: list[SimpleNamespace] | None = None,
comments_exc: Exception | None = None,
compare_exc: Exception | None = None,
) -> MagicMock:
issue = MagicMock()
if comments_exc is not None:
issue.get_comments.side_effect = comments_exc
else:
issue.get_comments.return_value = comments or []
repo = MagicMock()
repo.get_issue.return_value = issue
if compare_exc is not None:
repo.compare.side_effect = compare_exc
else:
repo.compare.return_value = SimpleNamespace(files=files or [])
client = MagicMock()
client.get_repo.return_value = repo
monkeypatch.setattr(gate, "Github", lambda **_: client)
return client
return _install
@pytest.mark.parametrize(
("bodies", "expected"),
[
pytest.param([], None, id="no-comments"),
pytest.param(
["<!-- requirements-check -->\nNo commit link here."],
None,
id="marker-without-link",
),
pytest.param([_body(_PRIOR)], _PRIOR, id="single-comment"),
pytest.param([_body(_PRIOR), _body(_HEAD)], _HEAD, id="most-recent-wins"),
],
)
def test_extract_prior_sha(bodies: list[str], expected: str | None) -> None:
"""The last recorded commit SHA across marker comments is returned."""
assert extract_prior_sha(bodies) == expected
def test_extract_prior_sha_normalizes_case() -> None:
"""An upper-case SHA in a link is returned lower-cased."""
assert extract_prior_sha([_body(_PRIOR.upper())]) == _PRIOR
def test_extract_prior_sha_round_trips_rendered_comment() -> None:
"""The gate reads back exactly the SHA the renderer wrote, keeping them in sync."""
body = render_comment(CheckRunResult(pr_number=1, head_sha=_HEAD))
assert extract_prior_sha([body]) == _HEAD
def test_fetch_marker_comment_bodies_returns_all_bot_comments(
install_github: InstallGithub,
) -> None:
"""Every bot comment body is returned in API order; the marker is not filtered on."""
install_github(
comments=[
_comment(None), # no SHA recorded yet
_comment(_PRIOR),
SimpleNamespace(
body="chatter", user=SimpleNamespace(login="github-actions[bot]")
),
]
)
bodies = fetch_marker_comment_bodies(7, _REPO, _TOKEN)
assert bodies == [_body(None), _body(_PRIOR), "chatter"]
@pytest.mark.parametrize(
"author",
[
pytest.param("attacker", id="drive-by-commenter"),
pytest.param("dependabot[bot]", id="other-bot"),
pytest.param("maintainer", id="maintainer-account"),
],
)
def test_fetch_marker_comment_bodies_ignores_non_actions_author(
install_github: InstallGithub,
author: str,
) -> None:
"""A forged marker comment from anyone but github-actions is ignored."""
install_github(comments=[_comment(_HEAD, author=author)])
assert fetch_marker_comment_bodies(7, _REPO, _TOKEN) == []
def test_fetch_marker_comment_bodies_handles_api_error(
install_github: InstallGithub,
) -> None:
"""A GitHub API error yields no bodies (fails open) instead of raising."""
install_github(comments_exc=GithubException(500, {}, {}))
assert fetch_marker_comment_bodies(7, _REPO, _TOKEN) == []
def test_decide_skip_no_head_sha(install_github: InstallGithub) -> None:
"""An empty head SHA never skips and makes no API calls."""
client = install_github()
assert decide_skip(7, "", _REPO, _TOKEN).skip is False
client.get_repo.assert_not_called()
def test_decide_skip_no_prior_comment(install_github: InstallGithub) -> None:
"""The first run (no prior comment) runs the checks."""
install_github(
comments=[SimpleNamespace(body="hi", user=SimpleNamespace(login="x"))]
)
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is False
def test_decide_skip_head_unchanged(install_github: InstallGithub) -> None:
"""When head matches the last comment's SHA, skip without comparing."""
client = install_github(comments=[_comment(_HEAD)])
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is True
client.get_repo.return_value.compare.assert_not_called()
def test_decide_skip_tracked_files_changed(install_github: InstallGithub) -> None:
"""A requirement file changed since the comment runs the checks."""
install_github(
comments=[_comment(_PRIOR)],
files=[_file("homeassistant/foo.py"), _file("requirements_all.txt")],
)
decision = decide_skip(7, _HEAD, _REPO, _TOKEN)
assert decision.skip is False
# The untracked file must not be reported as the reason to run.
assert "requirements_all.txt" in decision.reason
assert "homeassistant/foo.py" not in decision.reason
def test_decide_skip_no_tracked_files_changed(install_github: InstallGithub) -> None:
"""Only non-requirement files changed since the comment, so skip."""
install_github(
comments=[_comment(_PRIOR)],
files=[_file("homeassistant/components/demo/light.py")],
)
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is True
def test_decide_skip_compare_unavailable_runs(install_github: InstallGithub) -> None:
"""A failed compare falls back to running the checks."""
install_github(
comments=[_comment(_PRIOR)], compare_exc=GithubException(404, {}, {})
)
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is False
def test_decide_skip_comments_error_runs(install_github: InstallGithub) -> None:
"""A failed comments fetch fails open (runs the checks), never skips."""
install_github(comments_exc=GithubException(500, {}, {}))
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is False
def test_client_uses_github_api_url(monkeypatch: pytest.MonkeyPatch) -> None:
"""GHES is supported by passing GITHUB_API_URL (trailing slash stripped)."""
captured: dict[str, object] = {}
monkeypatch.setenv("GITHUB_API_URL", "https://ghe.example.com/api/v3/")
def _fake_github(**kwargs: object) -> MagicMock:
captured.update(kwargs)
client = MagicMock()
client.get_repo.return_value.get_issue.return_value.get_comments.return_value = [
_comment(_HEAD)
]
return client
monkeypatch.setattr(gate, "Github", _fake_github)
assert decide_skip(7, _HEAD, _REPO, _TOKEN).skip is True
assert captured["base_url"] == "https://ghe.example.com/api/v3"
+15 -81
View File
@@ -6,14 +6,17 @@ from pathlib import Path
import pytest
from script.check_requirements import __main__ as main_mod
from script.check_requirements.gate import GateDecision
from script.check_requirements.pypi import ProvenanceResult, PypiPackageInfo
_SHA = "abc1234def5678abc1234def5678abc1234def56"
def _write_bump_diff(path: Path) -> None:
path.write_text(
def test_main_writes_artifact(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""The CLI parses args, runs checks, and writes a JSON artifact."""
diff_file = tmp_path / "diff.patch"
diff_file.write_text(
"diff --git a/requirements_all.txt b/requirements_all.txt\n"
"--- a/requirements_all.txt\n"
"+++ b/requirements_all.txt\n"
@@ -22,9 +25,8 @@ def _write_bump_diff(path: Path) -> None:
"+pkg==1.1.0\n",
encoding="utf-8",
)
output_file = tmp_path / "results.json"
def _mock_pypi(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"script.check_requirements.runner.fetch_package_info",
lambda name, version: PypiPackageInfo(
@@ -44,27 +46,13 @@ def _mock_pypi(monkeypatch: pytest.MonkeyPatch) -> None:
),
)
def test_main_writes_artifact(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""When the gate runs the checks, the CLI writes a non-skipped artifact."""
diff_file = tmp_path / "diff.patch"
_write_bump_diff(diff_file)
output_file = tmp_path / "results.json"
monkeypatch.setattr(
main_mod, "_resolve_skip", lambda pr, sha: GateDecision(False, "running checks")
)
_mock_pypi(monkeypatch)
sha = "abc1234def5678abc1234def5678abc1234def56"
exit_code = main_mod.main(
[
"--pr-number",
"42",
"--head-sha",
_SHA,
sha,
"--diff",
str(diff_file),
"--output",
@@ -74,70 +62,16 @@ def test_main_writes_artifact(
assert exit_code == 0
payload = json.loads(output_file.read_text(encoding="utf-8"))
assert payload["skip_aw"] is False
assert payload["pr_number"] == 42
assert payload["head_sha"] == _SHA
assert payload["head_sha"] == sha
assert payload["packages"][0]["name"] == "pkg"
assert (
f"https://github.com/home-assistant/core/commit/{_SHA}"
f"https://github.com/home-assistant/core/commit/{sha}"
in payload["rendered_comment"]
)
assert "check_requirements: 1 package change(s)" in capsys.readouterr().err
def test_main_skips_but_still_writes_artifact(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the gate skips, no checks run but a skip-flagged artifact is written."""
diff_file = tmp_path / "diff.patch"
_write_bump_diff(diff_file)
output_file = tmp_path / "results.json"
monkeypatch.setattr(
main_mod, "_resolve_skip", lambda pr, sha: GateDecision(True, "nothing changed")
)
# The checks must not run when skipping; make them explode if they do.
def _boom(name: str, version: str) -> None:
raise AssertionError("checks must not run when the gate skips")
monkeypatch.setattr("script.check_requirements.runner.fetch_package_info", _boom)
exit_code = main_mod.main(
[
"--pr-number",
"42",
"--head-sha",
_SHA,
"--diff",
str(diff_file),
"--output",
str(output_file),
]
)
assert exit_code == 0
payload = json.loads(output_file.read_text(encoding="utf-8"))
assert payload == {
"version": 1,
"pr_number": 42,
"skip_aw": True,
"head_sha": _SHA,
"needs_agent": False,
"packages": [],
"rendered_comment": "",
}
def test_resolve_skip_without_credentials_runs(monkeypatch: pytest.MonkeyPatch) -> None:
"""Missing repo/token falls open (runs) without ever calling the gate."""
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
def _boom(*args: object, **kwargs: object) -> None:
raise AssertionError("decide_skip must not be called without credentials")
monkeypatch.setattr(main_mod, "decide_skip", _boom)
assert main_mod._resolve_skip(42, _SHA).skip is False
captured = capsys.readouterr()
assert "check_requirements: 1 package change(s)" in captured.err
def test_main_missing_diff_file_exits(