mirror of
https://github.com/home-assistant/core.git
synced 2026-06-28 17:46:02 +02:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03fb4b099c | |||
| 46a6048f60 | |||
| c598d2c10e | |||
| 67bcd7550c | |||
| e44e822cec | |||
| daff150276 | |||
| 1f33859297 | |||
| 512fe8c022 | |||
| 6f038bb5b2 | |||
| d0b5162507 | |||
| 9e9978b6cb | |||
| 76feb821f4 | |||
| cd41529a89 | |||
| 8a1434332d | |||
| 36b714b513 | |||
| 46f1e4c957 | |||
| 431dcda092 | |||
| c575ef51b9 | |||
| 87690d2000 | |||
| 7afb26b1c0 | |||
| b6d5af0480 | |||
| f56098df5f | |||
| 427dd028f5 | |||
| cef9461610 | |||
| ace5398012 | |||
| 3791c83b95 | |||
| 4bdfa5c25b | |||
| 5a60771a14 | |||
| 70aba68326 | |||
| f20f86a067 | |||
| ee0c98e450 | |||
| 907a5c3c6c | |||
| a1e1b400f3 | |||
| 1544ae83dd | |||
| 145c490816 | |||
| bb7a756f84 | |||
| 183e6af8c2 | |||
| 2b66d045ff | |||
| 4841329814 | |||
| e710fc8782 | |||
| 99e18dcdd8 | |||
| eeedf28b6f | |||
| 5cca9328d6 | |||
| ebf3de3073 | |||
| 41e79927d0 | |||
| 4022eb93de | |||
| 28171dfe90 | |||
| e9af932fbe | |||
| 3212e0f051 | |||
| 87f0720450 | |||
| 534ff3f3dc | |||
| 1096c8af13 |
@@ -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
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,7 @@ on:
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.8"
|
||||
HA_SHORT_VERSION: "2026.7"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
|
||||
Generated
+2
-3
@@ -181,7 +181,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||
/homeassistant/components/atag/ @MatsNL
|
||||
/tests/components/atag/ @MatsNL
|
||||
/homeassistant/components/aten_pe/ @mtdcr
|
||||
/homeassistant/components/atome/ @baqs
|
||||
/homeassistant/components/august/ @bdraco
|
||||
/tests/components/august/ @bdraco
|
||||
@@ -790,8 +789,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/html5/ @alexyao2015 @tr4nt0r
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
/tests/components/http/ @home-assistant/core
|
||||
/homeassistant/components/huawei_lte/ @fphammerle
|
||||
/tests/components/huawei_lte/ @fphammerle
|
||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||
/tests/components/huawei_lte/ @scop @fphammerle
|
||||
/homeassistant/components/hue/ @marcelveldt
|
||||
/tests/components/hue/ @marcelveldt
|
||||
/homeassistant/components/hue_ble/ @flip-dots
|
||||
|
||||
@@ -27,7 +27,7 @@ from .const import CONDITIONS_MAP, DOMAIN, FORECAST_MAP
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_TIMEOUT: Final[int] = 120
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=20)
|
||||
|
||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||
|
||||
|
||||
@@ -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.8"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.2.2"]
|
||||
"requirements": ["pyanglianwater==3.2.3"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The ATEN PE component."""
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "aten_pe",
|
||||
"name": "ATEN Rack PDU",
|
||||
"codeowners": ["@mtdcr"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/aten_pe",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["atenpdu==0.3.6"]
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
"""The ATEN PE switch component."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from atenpdu import AtenPE, AtenPEError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTH_KEY = "auth_key"
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_PRIV_KEY = "priv_key"
|
||||
DEFAULT_COMMUNITY = "private"
|
||||
DEFAULT_PORT = "161"
|
||||
DEFAULT_USERNAME = "administrator"
|
||||
|
||||
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_AUTH_KEY): cv.string,
|
||||
vol.Optional(CONF_PRIV_KEY): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the ATEN PE switch."""
|
||||
node = config[CONF_HOST]
|
||||
serv = config[CONF_PORT]
|
||||
|
||||
dev = AtenPE(
|
||||
node=node,
|
||||
serv=serv,
|
||||
community=config[CONF_COMMUNITY],
|
||||
username=config[CONF_USERNAME],
|
||||
authkey=config.get(CONF_AUTH_KEY),
|
||||
privkey=config.get(CONF_PRIV_KEY),
|
||||
)
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(dev.initialize)
|
||||
mac = await dev.deviceMAC()
|
||||
outlets = dev.outlets()
|
||||
name = await dev.deviceName()
|
||||
model = await dev.modelName()
|
||||
sw_version = await dev.deviceFWversion()
|
||||
except AtenPEError as exc:
|
||||
_LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc))
|
||||
raise PlatformNotReady from exc
|
||||
|
||||
info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer="ATEN",
|
||||
model=model,
|
||||
name=name,
|
||||
sw_version=sw_version,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
(AtenSwitch(dev, info, mac, outlet.id, outlet.name) for outlet in outlets), True
|
||||
)
|
||||
|
||||
|
||||
class AtenSwitch(SwitchEntity):
|
||||
"""Represents an ATEN PE switch."""
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.OUTLET
|
||||
|
||||
def __init__(
|
||||
self, device: AtenPE, info: DeviceInfo, mac: str, outlet: str, name: str
|
||||
) -> None:
|
||||
"""Initialize an ATEN PE switch."""
|
||||
self._device = device
|
||||
self._outlet = outlet
|
||||
self._attr_device_info = info
|
||||
self._attr_unique_id = f"{mac}-{outlet}"
|
||||
self._attr_name = name or f"Outlet {outlet}"
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._device.setOutletStatus(self._outlet, "on")
|
||||
self._attr_is_on = True
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._device.setOutletStatus(self._outlet, "off")
|
||||
self._attr_is_on = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Process update from entity."""
|
||||
status = await self._device.displayOutletStatus(self._outlet)
|
||||
if status == "on":
|
||||
self._attr_is_on = True
|
||||
elif status == "off":
|
||||
self._attr_is_on = False
|
||||
@@ -15,15 +15,16 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfApparentPower,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfRatio,
|
||||
UnitOfReactiveEnergy,
|
||||
UnitOfReactivePower,
|
||||
UnitOfSpeed,
|
||||
@@ -52,19 +53,19 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="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,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm2_5",
|
||||
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,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="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,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
@@ -83,7 +84,7 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
@@ -178,7 +179,7 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
BleBoxSensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Support for Dovado router."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
# import dovado
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
DEVICE_DEFAULT_NAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "dovado"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Dovado component."""
|
||||
|
||||
hass.data[DOMAIN] = DovadoData(
|
||||
dovado.Dovado(
|
||||
config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD],
|
||||
config[DOMAIN].get(CONF_HOST),
|
||||
config[DOMAIN].get(CONF_PORT),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class DovadoData:
|
||||
"""Maintain a connection to the router."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Set up a new Dovado connection."""
|
||||
self._client = client
|
||||
self.state = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the router."""
|
||||
return self.state.get("product name", DEVICE_DEFAULT_NAME)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
try:
|
||||
self.state = self._client.state or {}
|
||||
if not self.state:
|
||||
return False
|
||||
self.state.update(connected=self.state.get("modem status") == "CONNECTED")
|
||||
except OSError as error:
|
||||
_LOGGER.warning("Could not contact the router: %s", error)
|
||||
return None
|
||||
_LOGGER.debug("Received: %s", self.state)
|
||||
return True
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Dovado client instance."""
|
||||
return self._client
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "dovado",
|
||||
"name": "Dovado",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dovado",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["dovado==0.4.1"]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Support for SMS notifications from the Dovado router."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.components.notify import ATTR_TARGET, 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,
|
||||
) -> DovadoSMSNotificationService:
|
||||
"""Get the Dovado Router SMS notification service."""
|
||||
return DovadoSMSNotificationService(hass.data[DOMAIN].client)
|
||||
|
||||
|
||||
class DovadoSMSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Dovado SMS component."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
@override
|
||||
def send_message(self, message: str, **kwargs: Any) -> None:
|
||||
"""Send SMS to the specified target phone number."""
|
||||
if not (target := kwargs.get(ATTR_TARGET)):
|
||||
_LOGGER.error("One target is required")
|
||||
return
|
||||
|
||||
self._client.send_sms(target, message)
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -1,143 +0,0 @@
|
||||
"""Support for sensors from the Dovado router."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import re
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_SENSORS, PERCENTAGE, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
SENSOR_UPLOAD = "upload"
|
||||
SENSOR_DOWNLOAD = "download"
|
||||
SENSOR_SIGNAL = "signal"
|
||||
SENSOR_NETWORK = "network"
|
||||
SENSOR_SMS_UNREAD = "sms"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class DovadoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Dovado sensor entity."""
|
||||
|
||||
identifier: str
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[DovadoSensorEntityDescription, ...] = (
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_NETWORK,
|
||||
key="signal strength",
|
||||
name="Network",
|
||||
icon="mdi:access-point-network",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_SIGNAL,
|
||||
key="signal strength",
|
||||
name="Signal Strength",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:signal",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_SMS_UNREAD,
|
||||
key="sms unread",
|
||||
name="SMS unread",
|
||||
icon="mdi:message-text-outline",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_UPLOAD,
|
||||
key="traffic modem tx",
|
||||
name="Sent",
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:cloud-upload",
|
||||
),
|
||||
DovadoSensorEntityDescription(
|
||||
identifier=SENSOR_DOWNLOAD,
|
||||
key="traffic modem rx",
|
||||
name="Received",
|
||||
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:cloud-download",
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)])}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dovado sensor platform."""
|
||||
dovado = hass.data[DOMAIN]
|
||||
|
||||
sensors = config[CONF_SENSORS]
|
||||
entities = [
|
||||
DovadoSensor(dovado, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in sensors
|
||||
]
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class DovadoSensor(SensorEntity):
|
||||
"""Representation of a Dovado sensor."""
|
||||
|
||||
entity_description: DovadoSensorEntityDescription
|
||||
|
||||
def __init__(self, data, description: DovadoSensorEntityDescription) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self._data = data
|
||||
|
||||
self._attr_name = f"{data.name} {description.name}"
|
||||
self._attr_native_value = self._compute_state()
|
||||
|
||||
def _compute_state(self):
|
||||
"""Compute the state of the sensor."""
|
||||
state = self._data.state.get(self.entity_description.key)
|
||||
sensor_identifier = self.entity_description.identifier
|
||||
if sensor_identifier == SENSOR_NETWORK:
|
||||
match = re.search(r"\((.+)\)", state)
|
||||
return match.group(1) if match else None
|
||||
if sensor_identifier == SENSOR_SIGNAL:
|
||||
try:
|
||||
return int(state.split()[0])
|
||||
except ValueError:
|
||||
return None
|
||||
if sensor_identifier == SENSOR_SMS_UNREAD:
|
||||
return int(state)
|
||||
if sensor_identifier in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]:
|
||||
return round(float(state) / 1e6, 1)
|
||||
return state
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update sensor values."""
|
||||
self._data.update()
|
||||
self._attr_native_value = self._compute_state()
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]}
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dropbox",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-dropbox-api==0.1.4"]
|
||||
}
|
||||
|
||||
@@ -52,9 +52,7 @@ rules:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: Integration does not make any entity updates.
|
||||
|
||||
@@ -27,6 +27,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SUPPORTED_SELECT_NODE_TYPES = {
|
||||
NodeType.BOX,
|
||||
NodeType.VLV,
|
||||
NodeType.VLVRH,
|
||||
NodeType.VLVVOC,
|
||||
NodeType.VLVCO2,
|
||||
NodeType.VLVCO2RH,
|
||||
NodeType.EAV,
|
||||
NodeType.EAVRH,
|
||||
NodeType.EAVVOC,
|
||||
NodeType.EAVCO2,
|
||||
}
|
||||
|
||||
|
||||
def _get_ventilation_options(action: ActionItem) -> tuple[str, ...] | None:
|
||||
"""Return ventilation options advertised by a node action."""
|
||||
@@ -71,7 +84,9 @@ async def async_setup_entry(
|
||||
if node.node_id in known_nodes:
|
||||
continue
|
||||
|
||||
if node.general.node_type is not NodeType.BOX:
|
||||
# Duco advertises SetVentilationState broadly, so keep the select
|
||||
# limited to the box and known valve node families.
|
||||
if node.general.node_type not in SUPPORTED_SELECT_NODE_TYPES:
|
||||
continue
|
||||
|
||||
options = options_by_node.get(node.node_id)
|
||||
|
||||
@@ -15,9 +15,10 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfRatio,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -71,7 +72,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="target_flow_level",
|
||||
translation_key="target_flow_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda node: (
|
||||
node.ventilation.flow_lvl_tgt if node.ventilation else None
|
||||
@@ -95,7 +96,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
value_fn=lambda node: node.sensor.co2 if node.sensor else None,
|
||||
node_types=(
|
||||
NodeType.BSCO2,
|
||||
@@ -107,7 +108,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_co2",
|
||||
translation_key="iaq_co2",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None,
|
||||
@@ -122,14 +123,14 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda node: node.sensor.rh if node.sensor else None,
|
||||
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="iaq_rh",
|
||||
translation_key="iaq_rh",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda node: node.sensor.iaq_rh if node.sensor else None,
|
||||
|
||||
@@ -65,6 +65,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/meters/readings",
|
||||
"/ivp/pdm/device_data",
|
||||
"/home",
|
||||
"/inventory.json?deleted=1",
|
||||
"/admin/lib/acb_config",
|
||||
"/ivp/sc/sched",
|
||||
"/admin/lib/network_display",
|
||||
"/admin/lib/wireless_display",
|
||||
"/ivp/ensemble/relay",
|
||||
"/ivp/livedata/status",
|
||||
"/ivp/pdm/energy",
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
@@ -134,16 +142,15 @@ async def async_get_config_entry_diagnostics(
|
||||
"encharge_power": envoy_data.encharge_power,
|
||||
"encharge_aggregate": envoy_data.encharge_aggregate,
|
||||
"enpower": envoy_data.enpower,
|
||||
"acb_power": envoy_data.acb_power,
|
||||
"acb_inventory": envoy_data.acb_inventory,
|
||||
"battery_aggregate": envoy_data.battery_aggregate,
|
||||
"collar": envoy_data.collar,
|
||||
"c6cc": envoy_data.c6cc,
|
||||
"system_consumption": envoy_data.system_consumption,
|
||||
"system_production": envoy_data.system_production,
|
||||
"system_consumption_phases": envoy_data.system_consumption_phases,
|
||||
"system_production_phases": envoy_data.system_production_phases,
|
||||
"ctmeter_production": envoy_data.ctmeter_production,
|
||||
"ctmeter_consumption": envoy_data.ctmeter_consumption,
|
||||
"ctmeter_storage": envoy_data.ctmeter_storage,
|
||||
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
||||
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
|
||||
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
|
||||
"ctmeters": envoy_data.ctmeters,
|
||||
"ctmeters_phases": envoy_data.ctmeters_phases,
|
||||
"dry_contact_status": envoy_data.dry_contact_status,
|
||||
|
||||
@@ -12,7 +12,6 @@ from aioesphomeapi import (
|
||||
EntityCategory as EsphomeEntityCategory,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
build_device_unique_id,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -32,7 +31,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import DeviceEntityKey, ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .entry_data import (
|
||||
DeviceEntityKey,
|
||||
ESPHomeConfigEntry,
|
||||
RuntimeEntryData,
|
||||
build_device_unique_id,
|
||||
)
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,7 +45,7 @@ from aioesphomeapi import (
|
||||
UserService,
|
||||
ValveInfo,
|
||||
WaterHeaterInfo,
|
||||
build_device_unique_id,
|
||||
build_unique_id,
|
||||
)
|
||||
from aioesphomeapi.model import ButtonInfo
|
||||
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
|
||||
@@ -103,6 +103,22 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
||||
}
|
||||
|
||||
|
||||
def build_device_unique_id(mac: str, entity_info: EntityInfo) -> str:
|
||||
"""Build unique ID for entity, appending @device_id if it belongs to a sub-device.
|
||||
|
||||
This wrapper around build_unique_id ensures that entities belonging to sub-devices
|
||||
have their device_id appended to the unique_id to handle proper migration when
|
||||
entities move between devices.
|
||||
"""
|
||||
base_unique_id = build_unique_id(mac, entity_info)
|
||||
|
||||
# If entity belongs to a sub-device, append @device_id
|
||||
if entity_info.device_id:
|
||||
return f"{base_unique_id}@{entity_info.device_id}"
|
||||
|
||||
return base_unique_id
|
||||
|
||||
|
||||
class StoreData(TypedDict, total=False):
|
||||
"""ESPHome storage data."""
|
||||
|
||||
@@ -294,31 +310,11 @@ class RuntimeEntryData:
|
||||
infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict(
|
||||
list
|
||||
)
|
||||
ent_reg = er.async_get(hass)
|
||||
registry_get_entity = ent_reg.async_get_entity_id
|
||||
for info in infos:
|
||||
info_type = type(info)
|
||||
if platform := info_types_to_platform.get(info_type):
|
||||
needed_platforms.add(platform)
|
||||
infos_by_type[info_type].append(info)
|
||||
# Migrate legacy unique ids to the version 3 format that fixes
|
||||
# UTF-8 collisions. Skip when a version 3 id already exists so a
|
||||
# downgrade then upgrade keeps the original entity. When two
|
||||
# legacy ids collided (the bug this fixes) only one registry
|
||||
# entry exists for it, so the first iterated info claims it and
|
||||
# the rest get fresh version 3 ids.
|
||||
old_unique_id = build_device_unique_id(mac, info, version=1)
|
||||
new_unique_id = build_device_unique_id(mac, info, version=3)
|
||||
if (
|
||||
old_unique_id != new_unique_id
|
||||
and (
|
||||
old_entry := registry_get_entity(
|
||||
platform, DOMAIN, old_unique_id
|
||||
)
|
||||
)
|
||||
and not registry_get_entity(platform, DOMAIN, new_unique_id)
|
||||
):
|
||||
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Entity type %s is not supported in this version of Home Assistant",
|
||||
|
||||
@@ -1417,14 +1417,13 @@ async def async_replace_device(
|
||||
upper_mac = new_mac.upper()
|
||||
old_upper_mac = old_mac.upper()
|
||||
for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
|
||||
# The mac is the leading segment of the unique id in every format,
|
||||
# so swap the prefix without parsing the rest.
|
||||
if entity.unique_id.startswith(old_upper_mac):
|
||||
new_unique_id = upper_mac + entity.unique_id[len(old_upper_mac) :]
|
||||
if new_unique_id != entity.unique_id:
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
# <upper_mac>-<entity type>-<object_id>
|
||||
old_unique_id = entity.unique_id.split("-")
|
||||
new_unique_id = "-".join([upper_mac, *old_unique_id[1:]])
|
||||
if entity.unique_id != new_unique_id and entity.unique_id.startswith(
|
||||
old_upper_mac
|
||||
):
|
||||
ent_reg.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
domain_data = DomainData.get(hass)
|
||||
store = domain_data.get_or_create_store(hass, entry)
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The greenwave component."""
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Support for Greenwave Reality (TCP Connected) lights."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, override
|
||||
|
||||
import greenwavereality as greenwave
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VERSION = "version"
|
||||
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_HOST): cv.string, vol.Required(CONF_VERSION): cv.positive_int}
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Greenwave Reality Platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
tokenfilename = hass.config.path(".greenwave")
|
||||
if config.get(CONF_VERSION) == 3:
|
||||
if os.path.exists(tokenfilename):
|
||||
with open(tokenfilename, encoding="utf8") as tokenfile:
|
||||
token = tokenfile.read()
|
||||
else:
|
||||
try:
|
||||
token = greenwave.grab_token(host, "hass", "homeassistant")
|
||||
except PermissionError:
|
||||
_LOGGER.error("The Gateway Is Not In Sync Mode")
|
||||
raise
|
||||
with open(tokenfilename, "w+", encoding="utf8") as tokenfile:
|
||||
tokenfile.write(token)
|
||||
else:
|
||||
token = None
|
||||
bulbs = greenwave.grab_bulbs(host, token)
|
||||
add_entities(
|
||||
GreenwaveLight(device, host, token, GatewayData(host, token))
|
||||
for device in bulbs.values()
|
||||
)
|
||||
|
||||
|
||||
class GreenwaveLight(LightEntity):
|
||||
"""Representation of an Greenwave Reality Light."""
|
||||
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(self, light, host, token, gatewaydata):
|
||||
"""Initialize a Greenwave Reality Light."""
|
||||
self._did = int(light["did"])
|
||||
self._attr_name = light["name"]
|
||||
self._attr_is_on = bool(int(light["state"]))
|
||||
self._attr_brightness = greenwave.hass_brightness(light)
|
||||
self._host = host
|
||||
self._attr_available = greenwave.check_online(light)
|
||||
self._token = token
|
||||
self._gatewaydata = gatewaydata
|
||||
|
||||
@override
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100)
|
||||
greenwave.set_brightness(self._host, self._did, temp_brightness, self._token)
|
||||
greenwave.turn_on(self._host, self._did, self._token)
|
||||
|
||||
@override
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
greenwave.turn_off(self._host, self._did, self._token)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Fetch new state data for this light."""
|
||||
self._gatewaydata.update()
|
||||
bulbs = self._gatewaydata.greenwave
|
||||
|
||||
self._attr_is_on = bool(int(bulbs[self._did]["state"]))
|
||||
self._attr_brightness = greenwave.hass_brightness(bulbs[self._did])
|
||||
self._attr_available = greenwave.check_online(bulbs[self._did])
|
||||
self._attr_name = bulbs[self._did]["name"]
|
||||
|
||||
|
||||
class GatewayData:
|
||||
"""Handle Gateway data and limit updates."""
|
||||
|
||||
def __init__(self, host, token):
|
||||
"""Initialize the data object."""
|
||||
self._host = host
|
||||
self._token = token
|
||||
self._greenwave = greenwave.grab_bulbs(host, token)
|
||||
|
||||
@property
|
||||
def greenwave(self):
|
||||
"""Return Gateway API object."""
|
||||
return self._greenwave
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the gateway."""
|
||||
self._greenwave = greenwave.grab_bulbs(self._host, self._token)
|
||||
return self._greenwave
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "greenwave",
|
||||
"name": "Greenwave Reality",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/greenwave",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greenwavereality"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["greenwavereality==0.5.1"]
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class SupervisorJobs:
|
||||
# We catch all errors to prevent an error in one from stopping the others
|
||||
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
|
||||
try:
|
||||
return subscription.event_callback(match)
|
||||
subscription.event_callback(match)
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error(
|
||||
"Error encountered processing Supervisor Job (%s %s %s) - %s",
|
||||
|
||||
@@ -26,17 +26,18 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
@@ -253,7 +254,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
name="Current Humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
# This sensor is only for humidity characteristics that are not part
|
||||
# of a humidity sensor service.
|
||||
probe=(lambda char: char.service.type != ServicesTypes.HUMIDITY_SENSOR),
|
||||
@@ -269,42 +270,42 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
name="PM2.5 Density",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_PM10: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_PM10,
|
||||
name="PM10 Density",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_OZONE: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_OZONE,
|
||||
name="Ozone Density",
|
||||
device_class=SensorDeviceClass.OZONE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_NO2: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_NO2,
|
||||
name="Nitrogen Dioxide Density",
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_SO2: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_SO2,
|
||||
name="Sulphur Dioxide Density",
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.DENSITY_VOC: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.DENSITY_VOC,
|
||||
name="Volatile Organic Compound Density",
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
CharacteristicsTypes.THREAD_NODE_CAPABILITIES: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES,
|
||||
@@ -362,13 +363,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
key=CharacteristicsTypes.FILTER_LIFE_LEVEL,
|
||||
name="Filter lifetime",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
CharacteristicsTypes.WATER_LEVEL: HomeKitSensorEntityDescription(
|
||||
key=CharacteristicsTypes.WATER_LEVEL,
|
||||
name="Water level",
|
||||
translation_key="water_level",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
CharacteristicsTypes.VENDOR_EVE_THERMO_VALVE_POSITION: (
|
||||
@@ -378,7 +379,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
|
||||
translation_key="valve_position",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
)
|
||||
),
|
||||
}
|
||||
@@ -408,7 +409,7 @@ class HomeKitHumiditySensor(HomeKitSensor):
|
||||
"""Representation of a Homekit humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
@override
|
||||
def get_characteristic_types(self) -> list[str]:
|
||||
@@ -480,7 +481,7 @@ class HomeKitCarbonDioxideSensor(HomeKitSensor):
|
||||
"""Representation of a Homekit Carbon Dioxide sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.CO2
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PARTS_PER_MILLION
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
|
||||
@override
|
||||
def get_characteristic_types(self) -> list[str]:
|
||||
@@ -504,7 +505,7 @@ class HomeKitBatterySensor(HomeKitSensor):
|
||||
"""Representation of a Homekit battery sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
@override
|
||||
|
||||
@@ -50,13 +50,14 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
UnitOfDensity,
|
||||
PERCENTAGE,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfRatio,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
@@ -83,7 +84,7 @@ SMOKE_DETECTOR_SENSORS: tuple[HmipSmokeDetectorSensorDescription, ...] = (
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="dirt_level",
|
||||
translation_key="smoke_detector_dirt_level",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="dirtLevel",
|
||||
@@ -531,7 +532,7 @@ class HomematicipFloorTerminalBlockMechanicChannelValve(
|
||||
):
|
||||
"""Representation of the HomematicIP floor terminal block."""
|
||||
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
@@ -580,7 +581,7 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of then HomeMaticIP access point."""
|
||||
|
||||
_attr_icon = "mdi:access-point-network"
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -599,7 +600,7 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
|
||||
class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP heating thermostat."""
|
||||
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize heating thermostat device."""
|
||||
@@ -628,7 +629,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -679,9 +680,9 @@ class HomematicipAbsoluteHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP absolute humidity sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ABSOLUTE_HUMIDITY
|
||||
_attr_native_unit_of_measurement = UnitOfDensity.GRAMS_PER_CUBIC_METER
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_GRAMS_PER_CUBIC_METER
|
||||
_attr_suggested_display_precision = 1
|
||||
_attr_suggested_unit_of_measurement = UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER
|
||||
_attr_suggested_unit_of_measurement = CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
@@ -1142,7 +1143,7 @@ class HomematicipSoilMoistureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP soil moisture sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.MOISTURE
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
|
||||
@@ -245,14 +245,11 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
assert conn
|
||||
|
||||
def _get_info_and_disconnect() -> tuple[dict, dict]:
|
||||
result = get_device_info(conn)
|
||||
self._disconnect(conn)
|
||||
return result
|
||||
|
||||
info, wlan_settings = await self.hass.async_add_executor_job(
|
||||
_get_info_and_disconnect
|
||||
get_device_info, conn
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
await self.hass.async_add_executor_job(self._disconnect, conn)
|
||||
|
||||
user_input.update(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "huawei_lte",
|
||||
"name": "Huawei LTE",
|
||||
"codeowners": ["@fphammerle"],
|
||||
"codeowners": ["@scop", "@fphammerle"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Support for sending data to Logentries webhook endpoint."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_TOKEN, EVENT_STATE_CHANGED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "logentries"
|
||||
|
||||
DEFAULT_HOST = "https://webhook.logentries.com/noformat/logs/"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_TOKEN): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Logentries component."""
|
||||
conf = config[DOMAIN]
|
||||
token = conf.get(CONF_TOKEN)
|
||||
le_wh = f"{DEFAULT_HOST}{token}"
|
||||
|
||||
def logentries_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Logentries."""
|
||||
if (state := event.data.get("new_state")) is None:
|
||||
return
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
json_body = [
|
||||
{
|
||||
"domain": state.domain,
|
||||
"entity_id": state.object_id,
|
||||
"attributes": dict(state.attributes),
|
||||
"time": str(event.time_fired),
|
||||
"value": _state,
|
||||
}
|
||||
]
|
||||
try:
|
||||
payload = {"host": le_wh, "event": json_body}
|
||||
requests.post(le_wh, data=json.dumps(payload), timeout=10)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.exception("Error sending to Logentries")
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener)
|
||||
|
||||
return True
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"domain": "logentries",
|
||||
"name": "Logentries",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/logentries",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "legacy"
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -74,7 +73,6 @@ async def async_setup_entry(
|
||||
class MealieStatisticSensors(MealieEntity, SensorEntity):
|
||||
"""Defines a Mealie sensor."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: MealieStatisticsSensorEntityDescription
|
||||
coordinator: MealieStatisticsCoordinator
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone
|
||||
) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
super().__init__(coordinator, zone, zone.zone_id) # type: ignore[arg-type] # until fix issue #139773
|
||||
super().__init__(coordinator, zone, zone.zone_id)
|
||||
thermostat = self._thermostat
|
||||
unit = thermostat.get_unit()
|
||||
min_humidity, max_humidity = thermostat.get_humidity_setpoint_limits()
|
||||
@@ -198,7 +198,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def current_temperature(self) -> float:
|
||||
def current_temperature(self) -> int:
|
||||
"""Return the current temperature."""
|
||||
return self._zone.get_temperature()
|
||||
|
||||
@@ -288,7 +288,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def target_temperature(self) -> float | None:
|
||||
def target_temperature(self) -> int | None:
|
||||
"""Temperature we try to reach."""
|
||||
current_mode = self._zone.get_current_mode()
|
||||
|
||||
@@ -300,7 +300,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def target_temperature_high(self) -> float | None:
|
||||
def target_temperature_high(self) -> int | None:
|
||||
"""Highest temperature we are trying to reach."""
|
||||
current_mode = self._zone.get_current_mode()
|
||||
|
||||
@@ -310,7 +310,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
@override
|
||||
def target_temperature_low(self) -> float | None:
|
||||
def target_temperature_low(self) -> int | None:
|
||||
"""Lowest temperature we are trying to reach."""
|
||||
current_mode = self._zone.get_current_mode()
|
||||
|
||||
|
||||
@@ -40,6 +40,4 @@ class NexiaDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from API endpoint."""
|
||||
update_data = await self.nexia_home.update() # can return None
|
||||
|
||||
return update_data or {}
|
||||
return await self.nexia_home.update()
|
||||
|
||||
@@ -56,7 +56,7 @@ class NexiaThermostatEntity(NexiaEntity):
|
||||
thermostat_id = thermostat.thermostat_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=self.coordinator.nexia_home.root_url,
|
||||
identifiers={(DOMAIN, thermostat_id)}, # type: ignore[arg-type] # until fix issue #139773
|
||||
identifiers={(DOMAIN, thermostat_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=thermostat.get_model(),
|
||||
name=thermostat.get_name(),
|
||||
@@ -110,10 +110,10 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity):
|
||||
if TYPE_CHECKING:
|
||||
assert self._attr_device_info is not None
|
||||
self._attr_device_info |= {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, zone.zone_id)}, # type: ignore[arg-type] # until fix issue #139773
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, zone.zone_id)},
|
||||
ATTR_NAME: zone_name,
|
||||
ATTR_SUGGESTED_AREA: zone_name,
|
||||
ATTR_VIA_DEVICE: (DOMAIN, zone.thermostat.thermostat_id), # type: ignore[typeddict-item] # until fix issue #139773
|
||||
ATTR_VIA_DEVICE: (DOMAIN, zone.thermostat.thermostat_id),
|
||||
}
|
||||
self._zone_signal = f"{SIGNAL_ZONE_UPDATE}-{zone.zone_id}"
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nexia"],
|
||||
"requirements": ["nexia==2.13.0"]
|
||||
"requirements": ["nexia==2.11.1"]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class NexiaAutomationScene(NexiaEntity, Scene):
|
||||
self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation
|
||||
) -> None:
|
||||
"""Initialize the automation scene."""
|
||||
super().__init__(coordinator, automation.automation_id) # type: ignore[arg-type] # until fix issue #139773
|
||||
super().__init__(coordinator, automation.automation_id)
|
||||
self._attr_name = automation.name
|
||||
self._automation = automation
|
||||
self._attr_extra_state_attributes = {ATTR_DESCRIPTION: automation.description}
|
||||
|
||||
@@ -37,7 +37,7 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data
|
||||
nexia_home = coordinator.nexia_home
|
||||
entities: list[SwitchEntity] = []
|
||||
room_iq_zones: dict[str | int, NexiaRoomIQHarmonizer] = {}
|
||||
room_iq_zones: dict[int, NexiaRoomIQHarmonizer] = {}
|
||||
for thermostat_id in nexia_home.get_thermostat_ids():
|
||||
thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
|
||||
if thermostat.has_emergency_heat():
|
||||
@@ -69,7 +69,7 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
|
||||
) -> None:
|
||||
"""Initialize the hold mode switch."""
|
||||
zone_id = zone.zone_id
|
||||
super().__init__(coordinator, zone, zone_id) # type: ignore[arg-type] # until fix issue #139773
|
||||
super().__init__(coordinator, zone, zone_id)
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -103,7 +103,7 @@ class NexiaRoomIQSwitch(NexiaThermostatZoneEntity, SwitchEntity):
|
||||
coordinator: NexiaDataUpdateCoordinator,
|
||||
zone: NexiaThermostatZone,
|
||||
sensor: NexiaSensor,
|
||||
room_iq_zones: dict[str | int, NexiaRoomIQHarmonizer],
|
||||
room_iq_zones: dict[int, NexiaRoomIQHarmonizer],
|
||||
) -> None:
|
||||
"""Initialize the RoomIQ sensor switch."""
|
||||
super().__init__(coordinator, zone, f"{sensor.id}_room_iq_sensor")
|
||||
|
||||
@@ -15,29 +15,16 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.helpers.service_info import zeroconf
|
||||
|
||||
from .const import CONF_SERIAL, DOMAIN
|
||||
|
||||
USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): TextSelector()})
|
||||
USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
AUTH_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(autocomplete="username")
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="current-password"
|
||||
)
|
||||
),
|
||||
}
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import UnitOfInformation, UnitOfRatio
|
||||
from homeassistant.const import PERCENTAGE, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -122,7 +122,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
and data.stats.memory_stats.usage > 0
|
||||
else 0.0
|
||||
),
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -151,7 +151,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
|
||||
and data.stats.cpu_stats.online_cpus > 0
|
||||
else 0.0
|
||||
),
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -22,14 +22,15 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfLength,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
@@ -125,7 +126,7 @@ _GAUGE_VARIANT_DESCRIPTIONS = {
|
||||
"AIRQUALITY": SensorEntityDescription(
|
||||
key="airquality",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"CURRENT": SensorEntityDescription(
|
||||
@@ -155,7 +156,7 @@ _GAUGE_VARIANT_DESCRIPTIONS = {
|
||||
"HUMIDITY": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"LIGHT": SensorEntityDescription(
|
||||
@@ -352,7 +353,7 @@ class QbusHumiditySensor(QbusEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
_attr_name = None
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@override
|
||||
@@ -381,7 +382,7 @@ class QbusVentilationSensor(QbusEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.CO2
|
||||
_attr_name = None
|
||||
_attr_native_unit_of_measurement = UnitOfRatio.PARTS_PER_MILLION
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_suggested_display_precision = 0
|
||||
|
||||
|
||||
@@ -286,7 +286,10 @@ async def register_callbacks(
|
||||
return async_camera_wake
|
||||
|
||||
host.api.baichuan.register_callback(
|
||||
"privacy_mode_change", async_privacy_mode_change, 623
|
||||
"privacy_mode_change_623", async_privacy_mode_change, 623
|
||||
)
|
||||
host.api.baichuan.register_callback(
|
||||
"privacy_mode_change_574", async_privacy_mode_change, 574
|
||||
)
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
@@ -306,7 +309,8 @@ async def async_unload_entry(
|
||||
|
||||
await host.stop()
|
||||
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change")
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change_623")
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change_574")
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
|
||||
|
||||
@@ -75,6 +75,7 @@ LIGHT_ENTITIES = (
|
||||
ReolinkLightEntityDescription(
|
||||
key="status_led",
|
||||
cmd_key="GetPowerLed",
|
||||
cmd_id=208,
|
||||
translation_key="status_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "power_led"),
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.21.2"]
|
||||
"requirements": ["reolink-aio==0.21.3"]
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ NUMBER_ENTITIES = (
|
||||
key="volume",
|
||||
cmd_key="GetAudioCfg",
|
||||
translation_key="volume",
|
||||
cmd_id=264,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
@@ -206,6 +207,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="volume_speak",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="volume_speak",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -218,6 +220,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="volume_doorbell",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="volume_doorbell",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -269,6 +272,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="pir_sensitivity",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -281,6 +285,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="pir_interval",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_interval",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -296,6 +301,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_face_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_face_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -310,6 +316,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_person_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_person_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -324,6 +331,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_vehicle_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_vehicle_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -338,6 +346,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_sensitivity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_non_motor_vehicle_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -355,6 +364,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_package_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_package_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -369,6 +379,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_pet_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -385,6 +396,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_animal_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
@@ -411,6 +423,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_face_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_face_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -428,6 +441,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_person_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_person_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -445,6 +459,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_non_motor_vehicle_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -464,6 +479,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_vehicle_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -481,6 +497,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_package_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_package_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -498,6 +515,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_pet_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -517,6 +535,7 @@ NUMBER_ENTITIES = (
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_pet_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
cmd_id=342,
|
||||
translation_key="ai_animal_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
|
||||
@@ -185,6 +185,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="status_led",
|
||||
cmd_key="GetPowerLed",
|
||||
cmd_id=208,
|
||||
translation_key="doorbell_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=lambda api, ch: api.doorbell_led_list(ch),
|
||||
@@ -232,6 +233,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_frame_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_frame_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -244,6 +246,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_frame_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_frame_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -256,6 +259,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_bit_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_bit_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -268,6 +272,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_bit_rate",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_bit_rate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -280,6 +285,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="main_encoding",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="main_encoding",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -291,6 +297,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="sub_encoding",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="sub_encoding",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -316,6 +323,7 @@ SELECT_ENTITIES = (
|
||||
ReolinkSelectEntityDescription(
|
||||
key="post_rec_time",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=54,
|
||||
translation_key="post_rec_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -340,6 +348,7 @@ HOST_SELECT_ENTITIES = (
|
||||
ReolinkHostSelectEntityDescription(
|
||||
key="packing_time",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=54,
|
||||
translation_key="packing_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
|
||||
@@ -74,6 +74,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="ir_lights",
|
||||
cmd_key="GetIrLights",
|
||||
cmd_id=208,
|
||||
translation_key="ir_lights",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "ir_lights"),
|
||||
@@ -83,6 +84,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="record_audio",
|
||||
cmd_key="GetEnc",
|
||||
cmd_id=56,
|
||||
translation_key="record_audio",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "audio"),
|
||||
@@ -92,6 +94,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="siren_on_event",
|
||||
cmd_key="GetAudioAlarm",
|
||||
cmd_id=232,
|
||||
translation_key="siren_on_event",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "siren"),
|
||||
@@ -136,6 +139,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="email",
|
||||
cmd_key="GetEmail",
|
||||
cmd_id=217,
|
||||
translation_key="email",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "email") and api.is_nvr,
|
||||
@@ -145,6 +149,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="ftp_upload",
|
||||
cmd_key="GetFtp",
|
||||
cmd_id=70,
|
||||
translation_key="ftp_upload",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "ftp") and api.is_nvr,
|
||||
@@ -163,6 +168,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="record",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=81,
|
||||
translation_key="record",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "rec_enable") and api.is_nvr,
|
||||
@@ -200,6 +206,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="doorbell_button_sound",
|
||||
cmd_key="GetAudioCfg",
|
||||
cmd_id=264,
|
||||
translation_key="doorbell_button_sound",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "doorbell_button_sound"),
|
||||
@@ -209,6 +216,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="pir_enabled",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_enabled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -219,6 +227,7 @@ SWITCH_ENTITIES = (
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="pir_reduce_alarm",
|
||||
cmd_key="GetPirInfo",
|
||||
cmd_id=212,
|
||||
translation_key="pir_reduce_alarm",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -260,6 +269,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="email",
|
||||
cmd_key="GetEmail",
|
||||
cmd_id=217,
|
||||
translation_key="email",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "email") and not api.is_hub,
|
||||
@@ -269,6 +279,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="ftp_upload",
|
||||
cmd_key="GetFtp",
|
||||
cmd_id=70,
|
||||
translation_key="ftp_upload",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "ftp") and not api.is_hub,
|
||||
@@ -287,6 +298,7 @@ HOST_SWITCH_ENTITIES = (
|
||||
ReolinkHostSwitchEntityDescription(
|
||||
key="record",
|
||||
cmd_key="GetRec",
|
||||
cmd_id=81,
|
||||
translation_key="record",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "rec_enable") and not api.is_hub,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==5.14.2",
|
||||
"python-roborock==5.21.0",
|
||||
"vacuum-map-parser-roborock==0.1.5"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class RoborockNumberDescription(NumberEntityDescription):
|
||||
trait: Callable[[PropertiesApi], Any | None]
|
||||
"""Function to determine if number entity is supported by the device."""
|
||||
|
||||
get_value: Callable[[Any], float]
|
||||
get_value: Callable[[Any], float | None]
|
||||
"""Function to get the value from the trait."""
|
||||
|
||||
set_value: Callable[[Any, float], Coroutine[Any, Any, None]]
|
||||
@@ -51,7 +51,9 @@ NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
trait=lambda api: api.sound_volume,
|
||||
get_value=lambda trait: float(trait.volume),
|
||||
get_value=lambda trait: (
|
||||
float(trait.volume) if trait.volume is not None else None
|
||||
),
|
||||
set_value=lambda trait, value: trait.set_volume(int(value)),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -37,7 +37,7 @@ class RoborockTimeDescription(TimeEntityDescription):
|
||||
trait: Callable[[Any], Any | None]
|
||||
"""Function to determine if time entity is supported by the device."""
|
||||
|
||||
get_value: Callable[[Any], datetime.time]
|
||||
get_value: Callable[[Any], datetime.time | None]
|
||||
"""Function to get the value from the trait."""
|
||||
|
||||
update_value: Callable[[Any, datetime.time], Coroutine[Any, Any, None]]
|
||||
@@ -58,9 +58,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=trait.end_minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.start_hour, minute=trait.start_minute
|
||||
),
|
||||
get_value=lambda trait: trait.start_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
RoborockTimeDescription(
|
||||
@@ -76,9 +74,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=desired_time.minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.end_hour, minute=trait.end_minute
|
||||
),
|
||||
get_value=lambda trait: trait.end_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
RoborockTimeDescription(
|
||||
@@ -94,9 +90,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=trait.end_minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.start_hour, minute=trait.start_minute
|
||||
),
|
||||
get_value=lambda trait: trait.start_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -113,9 +107,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
|
||||
end_minute=desired_time.minute,
|
||||
)
|
||||
),
|
||||
get_value=lambda trait: datetime.time(
|
||||
hour=trait.end_hour, minute=trait.end_minute
|
||||
),
|
||||
get_value=lambda trait: trait.end_time,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==2.3.0"]
|
||||
"requirements": ["PySwitchbot==2.2.0"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"state": {
|
||||
"cool": "mdi:fan-speed-2",
|
||||
"full_speed": "mdi:fan-speed-3",
|
||||
"low_power": "mdi:fan-chevron-down",
|
||||
"quiet": "mdi:fan-speed-1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.10.0"],
|
||||
"requirements": ["py-synologydsm-api==2.10.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:Basic:1",
|
||||
|
||||
@@ -15,6 +15,7 @@ from .coordinator import SynologyDSMCentralUpdateCoordinator, SynologyDSMConfigE
|
||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||
|
||||
FAN_SPEED_MAP = {
|
||||
FanSpeed.QUIET_STOP: "low_power",
|
||||
FanSpeed.QUIET: "quiet",
|
||||
FanSpeed.COOL: "cool",
|
||||
FanSpeed.FULL: "full_speed",
|
||||
@@ -47,7 +48,6 @@ class SynologyDSMFanSpeedMode(
|
||||
):
|
||||
"""Represent a Synology DSM fan speed mode select entity."""
|
||||
|
||||
_attr_options = list(FAN_SPEED_MAP.values())
|
||||
entity_description: SynologyDSMSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -62,6 +62,11 @@ class SynologyDSMFanSpeedMode(
|
||||
translation_key="fan_speed_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
self._attr_options = [
|
||||
val
|
||||
for fs, val in FAN_SPEED_MAP.items()
|
||||
if fs in api.dsm.hardware.supported_fan_speeds
|
||||
]
|
||||
super().__init__(api, coordinator, description)
|
||||
|
||||
@property
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"state": {
|
||||
"cool": "Cool mode",
|
||||
"full_speed": "Full-speed mode",
|
||||
"low_power": "Low-Power mode",
|
||||
"quiet": "Quiet mode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""The thermoworks_smoke component."""
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "thermoworks_smoke",
|
||||
"name": "ThermoWorks Smoke",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it creates an unresolvable dependency conflict.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["thermoworks_smoke"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["stringcase==1.2.0", "thermoworks-smoke==0.1.8"]
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
"""Support for getting the state of a Thermoworks Smoke Thermometer.
|
||||
|
||||
Requires Smoke Gateway Wifi with an internet connection.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from requests import RequestException
|
||||
from requests.exceptions import HTTPError
|
||||
from stringcase import camelcase
|
||||
import thermoworks_smoke
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
CONF_EMAIL,
|
||||
CONF_EXCLUDE,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_PASSWORD,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import snakecase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROBE_1 = "probe1"
|
||||
PROBE_2 = "probe2"
|
||||
PROBE_1_MIN = "probe1_min"
|
||||
PROBE_1_MAX = "probe1_max"
|
||||
PROBE_2_MIN = "probe2_min"
|
||||
PROBE_2_MAX = "probe2_max"
|
||||
BATTERY_LEVEL = "battery"
|
||||
FIRMWARE = "firmware"
|
||||
|
||||
SERIAL_REGEX = "^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"
|
||||
|
||||
# map types to labels
|
||||
SENSOR_TYPES = {
|
||||
PROBE_1: "Probe 1",
|
||||
PROBE_2: "Probe 2",
|
||||
PROBE_1_MIN: "Probe 1 Min",
|
||||
PROBE_1_MAX: "Probe 1 Max",
|
||||
PROBE_2_MIN: "Probe 2 Min",
|
||||
PROBE_2_MAX: "Probe 2 Max",
|
||||
}
|
||||
|
||||
# exclude these keys from thermoworks data
|
||||
EXCLUDE_KEYS = [FIRMWARE]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[PROBE_1, PROBE_2]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
),
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.matches_regex(SERIAL_REGEX)]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the thermoworks sensor."""
|
||||
|
||||
email = config[CONF_EMAIL]
|
||||
password = config[CONF_PASSWORD]
|
||||
monitored_variables = config[CONF_MONITORED_CONDITIONS]
|
||||
excluded = config[CONF_EXCLUDE]
|
||||
|
||||
try:
|
||||
mgr = thermoworks_smoke.initialize_app(email, password, True, excluded)
|
||||
except HTTPError as error:
|
||||
msg = f"{error.strerror}"
|
||||
if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg:
|
||||
_LOGGER.error("Invalid email and password combination")
|
||||
else:
|
||||
_LOGGER.error(msg)
|
||||
else:
|
||||
add_entities(
|
||||
(
|
||||
ThermoworksSmokeSensor(variable, serial, mgr)
|
||||
for serial in mgr.serials()
|
||||
for variable in monitored_variables
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class ThermoworksSmokeSensor(SensorEntity):
|
||||
"""Implementation of a thermoworks smoke sensor."""
|
||||
|
||||
def __init__(self, sensor_type, serial, mgr):
|
||||
"""Initialize the sensor."""
|
||||
self.type = sensor_type
|
||||
self.serial = serial
|
||||
self.mgr = mgr
|
||||
self._attr_name = f"{mgr.name(serial)} {SENSOR_TYPES[sensor_type]}"
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT
|
||||
self._attr_unique_id = f"{serial}-{sensor_type}"
|
||||
self._attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
self.update_unit()
|
||||
|
||||
def update_unit(self):
|
||||
"""Set the units from the data."""
|
||||
if PROBE_2 in self.type:
|
||||
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_2)
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_1)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the monitored data from firebase."""
|
||||
|
||||
try:
|
||||
values = self.mgr.data(self.serial)
|
||||
|
||||
# set state from data based on type of sensor
|
||||
self._attr_native_value = values.get(camelcase(self.type))
|
||||
|
||||
# set units
|
||||
self.update_unit()
|
||||
|
||||
# set basic attributes for all sensors
|
||||
self._attr_extra_state_attributes = {
|
||||
"time": values["time"],
|
||||
"localtime": values["localtime"],
|
||||
}
|
||||
|
||||
# set extended attributes for main probe sensors
|
||||
if self.type in (PROBE_1, PROBE_2):
|
||||
for key, val in values.items():
|
||||
# add all attributes that don't contain any probe name
|
||||
# or contain a matching probe name
|
||||
if (self.type == PROBE_1 and key.find(PROBE_2) == -1) or (
|
||||
self.type == PROBE_2 and key.find(PROBE_1) == -1
|
||||
):
|
||||
if key == BATTERY_LEVEL:
|
||||
key = ATTR_BATTERY_LEVEL
|
||||
else:
|
||||
# strip probe label and convert to snake_case
|
||||
key = snakecase(key.replace(self.type, ""))
|
||||
# add to attrs
|
||||
if key and key not in EXCLUDE_KEYS:
|
||||
self._attr_extra_state_attributes[key] = val
|
||||
# store actual unit because attributes are not converted
|
||||
self._attr_extra_state_attributes["unit_of_min_max"] = (
|
||||
self._attr_native_unit_of_measurement
|
||||
)
|
||||
|
||||
except RequestException, ValueError, KeyError:
|
||||
_LOGGER.warning("Could not update status for %s", self.name)
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.3.0"]
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfEnergy,
|
||||
UnitOfMass,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfRatio,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
@@ -80,7 +80,7 @@ VICARE_UNIT_TO_HA_UNIT = {
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_PERCENT: UnitOfRatio.PERCENTAGE,
|
||||
VICARE_PERCENT: PERCENTAGE,
|
||||
VICARE_W: UnitOfPower.WATT,
|
||||
VICARE_WH: UnitOfEnergy.WATT_HOUR,
|
||||
}
|
||||
@@ -117,7 +117,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="outside_humidity",
|
||||
translation_key="outside_humidity",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getOutsideHumidity(),
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -165,7 +165,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="primary_circuit_pump_rotation",
|
||||
translation_key="primary_circuit_pump_rotation",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getPrimaryCircuitPumpRotation(),
|
||||
unit_getter=lambda api: api.getPrimaryCircuitPumpRotationUnit(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -799,7 +799,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="ess_state_of_charge",
|
||||
translation_key="ess_state_of_charge",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getElectricalEnergySystemSOC(),
|
||||
@@ -996,7 +996,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="room_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getHumidity(),
|
||||
),
|
||||
@@ -1122,7 +1122,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="battery_level",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -1142,7 +1142,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
translation_key="zigbee_signal_strength",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getZigbeeSignalStrength(),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -1150,7 +1150,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
key="valve_position",
|
||||
translation_key="valve_position",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getValvePosition(),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -1177,7 +1177,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
key="supply_humidity",
|
||||
translation_key="supply_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getSupplyHumidity(),
|
||||
),
|
||||
@@ -1229,28 +1229,28 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm01",
|
||||
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,
|
||||
value_getter=lambda api: api.getAirborneDustPM1(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm02",
|
||||
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,
|
||||
value_getter=lambda api: api.getAirborneDustPM2d5(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="pm04",
|
||||
device_class=SensorDeviceClass.PM4,
|
||||
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_getter=lambda api: api.getAirborneDustPM4(),
|
||||
),
|
||||
ViCareSensorEntityDescription(
|
||||
key="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,
|
||||
value_getter=lambda api: api.getAirborneDustPM10(),
|
||||
),
|
||||
@@ -1293,7 +1293,7 @@ BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="burner_modulation",
|
||||
translation_key="burner_modulation",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getModulation(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@@ -1312,7 +1312,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="compressor_modulation",
|
||||
translation_key="compressor_modulation",
|
||||
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_getter=lambda api: api.getModulation(),
|
||||
unit_getter=lambda api: api.getModulationUnit(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -145,9 +145,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (
|
||||
exceptions.CannotConnect,
|
||||
exceptions.AlreadyLogged,
|
||||
exceptions.GenericLoginError,
|
||||
exceptions.VodafoneError,
|
||||
JSONDecodeError,
|
||||
) as err:
|
||||
if isinstance(err, JSONDecodeError):
|
||||
|
||||
@@ -21,8 +21,8 @@ if TYPE_CHECKING:
|
||||
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -590,12 +590,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"aten_pe": {
|
||||
"name": "ATEN Rack PDU",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"atlanticcityelectric": {
|
||||
"name": "Atlantic City Electric",
|
||||
"integration_type": "virtual",
|
||||
@@ -1489,12 +1483,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"dovado": {
|
||||
"name": "Dovado",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"downloader": {
|
||||
"name": "Downloader",
|
||||
"integration_type": "service",
|
||||
@@ -2730,12 +2718,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"greenwave": {
|
||||
"name": "Greenwave Reality",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"growatt_server": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
@@ -3947,12 +3929,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"logentries": {
|
||||
"name": "Logentries",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"logitech": {
|
||||
"name": "Logitech",
|
||||
"integrations": {
|
||||
@@ -7201,12 +7177,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"thermoworks_smoke": {
|
||||
"name": "ThermoWorks Smoke",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"thethingsnetwork": {
|
||||
"name": "The Things Network",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -71,7 +71,7 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.21
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.4.1
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.8.0.dev0"
|
||||
version = "2026.7.0b2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -77,7 +77,7 @@ dependencies = [
|
||||
"uv==0.11.21",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.7.0",
|
||||
"voluptuous-openapi==0.3.0",
|
||||
"voluptuous-openapi==0.4.1",
|
||||
"yarl==1.24.2",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.150.0",
|
||||
|
||||
Generated
+1
-1
@@ -56,7 +56,7 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.21
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.4.1
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
|
||||
Generated
+9
-15
@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
|
||||
PySrDaliGateway==0.21.0
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==2.3.0
|
||||
PySwitchbot==2.2.0
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@@ -193,7 +193,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==14.1.6
|
||||
aioamazondevices==14.1.8
|
||||
|
||||
# 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
|
||||
@@ -583,9 +583,6 @@ asyncsleepiq==1.7.1
|
||||
# homeassistant.components.sftp_storage
|
||||
asyncssh==2.21.0
|
||||
|
||||
# homeassistant.components.aten_pe
|
||||
# atenpdu==0.3.6
|
||||
|
||||
# homeassistant.components.aurora
|
||||
auroranoaa==0.0.5
|
||||
|
||||
@@ -1179,9 +1176,6 @@ greeneye_monitor==3.0.3
|
||||
# homeassistant.components.green_planet_energy
|
||||
greenplanet-energy-api==0.1.10
|
||||
|
||||
# homeassistant.components.greenwave
|
||||
greenwavereality==0.5.1
|
||||
|
||||
# homeassistant.components.pure_energie
|
||||
gridnet==5.0.1
|
||||
|
||||
@@ -1658,7 +1652,7 @@ nettigo-air-monitor==5.0.0
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.13.0
|
||||
nexia==2.11.1
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.5.1
|
||||
@@ -1948,7 +1942,7 @@ py-schluter==0.1.7
|
||||
py-sucks==0.9.11
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.10.0
|
||||
py-synologydsm-api==2.10.1
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.3.0
|
||||
@@ -2018,7 +2012,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.2.2
|
||||
pyanglianwater==3.2.3
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -2730,7 +2724,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==5.14.2
|
||||
python-roborock==5.21.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
@@ -2899,7 +2893,7 @@ renault-api==0.5.12
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.21.2
|
||||
reolink-aio==0.21.3
|
||||
|
||||
# homeassistant.components.radio_frequency
|
||||
rf-protocols==4.3.0
|
||||
@@ -3245,7 +3239,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==15.1.0
|
||||
uiprotect==15.3.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.6.1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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],
|
||||
|
||||
@@ -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
@@ -1,3 +1,2 @@
|
||||
PyGithub==2.9.1
|
||||
requests==2.34.2
|
||||
unidiff==0.7.5
|
||||
|
||||
@@ -165,7 +165,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"asterisk_mbox",
|
||||
"asuswrt",
|
||||
"atag",
|
||||
"aten_pe",
|
||||
"atome",
|
||||
"august",
|
||||
"aurora",
|
||||
@@ -274,7 +273,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"doods",
|
||||
"doorbird",
|
||||
"dormakaba_dkey",
|
||||
"dovado",
|
||||
"downloader",
|
||||
"dremel_3d_printer",
|
||||
"drop_connect",
|
||||
@@ -420,7 +418,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"graphite",
|
||||
"gree",
|
||||
"greeneye_monitor",
|
||||
"greenwave",
|
||||
"group",
|
||||
"gtfs",
|
||||
"guardian",
|
||||
@@ -548,7 +545,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"local_todo",
|
||||
"location",
|
||||
"locative",
|
||||
"logentries",
|
||||
"logi_circle",
|
||||
"london_air",
|
||||
"london_underground",
|
||||
@@ -913,7 +909,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"tesla_wall_connector",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thermoworks_smoke",
|
||||
"thethingsnetwork",
|
||||
"thingspeak",
|
||||
"thinkingcleaner",
|
||||
@@ -1113,7 +1108,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"asterisk_mbox",
|
||||
"asuswrt",
|
||||
"atag",
|
||||
"aten_pe",
|
||||
"atome",
|
||||
"august",
|
||||
"aurora",
|
||||
@@ -1225,7 +1219,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"doods",
|
||||
"doorbird",
|
||||
"dormakaba_dkey",
|
||||
"dovado",
|
||||
"downloader",
|
||||
"dremel_3d_printer",
|
||||
"drop_connect",
|
||||
@@ -1378,7 +1371,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"graphite",
|
||||
"gree",
|
||||
"greeneye_monitor",
|
||||
"greenwave",
|
||||
"group",
|
||||
"gtfs",
|
||||
"guardian",
|
||||
@@ -1513,7 +1505,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"local_todo",
|
||||
"location",
|
||||
"locative",
|
||||
"logentries",
|
||||
"logi_circle",
|
||||
"london_air",
|
||||
"london_underground",
|
||||
@@ -1896,7 +1887,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"tesla_wall_connector",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thermoworks_smoke",
|
||||
"thethingsnetwork",
|
||||
"thingspeak",
|
||||
"thinkingcleaner",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_113_humidity',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bathroom_rh_humidity-state]
|
||||
@@ -44,7 +44,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bathroom RH 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.bathroom_rh_humidity',
|
||||
@@ -90,7 +90,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_113_iaq_rh',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bathroom_rh_humidity_air_quality_index-state]
|
||||
@@ -98,7 +98,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bathroom RH Humidity air quality index',
|
||||
<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.bathroom_rh_humidity_air_quality_index',
|
||||
@@ -144,7 +144,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_humidity',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-state]
|
||||
@@ -153,7 +153,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve 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.bedroom_valve_humidity',
|
||||
@@ -199,7 +199,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_iaq_rh',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-state]
|
||||
@@ -207,7 +207,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Humidity air quality index',
|
||||
<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.bedroom_valve_humidity_air_quality_index',
|
||||
@@ -253,7 +253,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-state]
|
||||
@@ -262,7 +262,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve 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.hall_valve_carbon_dioxide',
|
||||
@@ -308,7 +308,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_co2',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_iaq_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-state]
|
||||
@@ -316,7 +316,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve CO2 air quality index',
|
||||
<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.hall_valve_co2_air_quality_index',
|
||||
@@ -362,7 +362,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_50_humidity',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity-state]
|
||||
@@ -371,7 +371,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Kitchen RH 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.kitchen_rh_humidity',
|
||||
@@ -417,7 +417,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_50_iaq_rh',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity_air_quality_index-state]
|
||||
@@ -425,7 +425,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Kitchen RH Humidity air quality index',
|
||||
<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.kitchen_rh_humidity_air_quality_index',
|
||||
@@ -635,7 +635,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'target_flow_level',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_1_target_flow_level',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_target_flow_level-state]
|
||||
@@ -643,7 +643,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Target flow 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_target_flow_level',
|
||||
@@ -781,7 +781,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_2_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.office_co2_carbon_dioxide-state]
|
||||
@@ -790,7 +790,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Office CO2 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.office_co2_carbon_dioxide',
|
||||
@@ -836,7 +836,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_co2',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_2_iaq_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.office_co2_co2_air_quality_index-state]
|
||||
@@ -844,7 +844,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Office CO2 CO2 air quality index',
|
||||
<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.office_co2_co2_air_quality_index',
|
||||
@@ -890,7 +890,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-state]
|
||||
@@ -899,7 +899,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve 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.study_valve_carbon_dioxide',
|
||||
@@ -945,7 +945,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_co2',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_co2',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-state]
|
||||
@@ -953,7 +953,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve CO2 air quality index',
|
||||
<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.study_valve_co2_air_quality_index',
|
||||
@@ -999,7 +999,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_humidity',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity-state]
|
||||
@@ -1008,7 +1008,7 @@
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve 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.study_valve_humidity',
|
||||
@@ -1054,7 +1054,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'iaq_rh',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_rh',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-state]
|
||||
@@ -1062,7 +1062,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Humidity air quality index',
|
||||
<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.study_valve_humidity_air_quality_index',
|
||||
|
||||
@@ -13,6 +13,7 @@ from duco_connectivity import (
|
||||
Node,
|
||||
NodeActionItemList,
|
||||
NodeListActionItemList,
|
||||
NodeType,
|
||||
VentilationState,
|
||||
)
|
||||
import pytest
|
||||
@@ -32,6 +33,7 @@ from . import setup_platform_integration
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_SELECT_ENTITY = "select.living_ventilation_state"
|
||||
_VALVE_SELECT_ENTITY = "select.bedroom_valve_ventilation_state"
|
||||
_UNSUPPORTED_SELECT_ENTITY = "select.office_co2_ventilation_state"
|
||||
|
||||
|
||||
@@ -120,20 +122,55 @@ async def test_select_entity_created_with_dynamic_options(
|
||||
assert hass.states.get(_UNSUPPORTED_SELECT_ENTITY) is None
|
||||
|
||||
|
||||
async def test_select_ignores_non_box_nodes_even_when_actions_exist(
|
||||
@pytest.mark.parametrize(
|
||||
"valve_node_type",
|
||||
[
|
||||
pytest.param(NodeType.VLV, id="vlv"),
|
||||
pytest.param(NodeType.VLVRH, id="vlvrh"),
|
||||
pytest.param(NodeType.VLVVOC, id="vlvvoc"),
|
||||
pytest.param(NodeType.VLVCO2, id="vlvco2"),
|
||||
pytest.param(NodeType.VLVCO2RH, id="vlvco2rh"),
|
||||
pytest.param(NodeType.EAV, id="eav"),
|
||||
pytest.param(NodeType.EAVRH, id="eavrh"),
|
||||
pytest.param(NodeType.EAVVOC, id="eavvoc"),
|
||||
pytest.param(NodeType.EAVCO2, id="eavco2"),
|
||||
],
|
||||
)
|
||||
async def test_select_creates_entities_for_controllable_valve_nodes(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
valve_node_type: NodeType,
|
||||
) -> None:
|
||||
"""Test select discovery ignores non-box nodes that expose the same action."""
|
||||
"""Test select discovery includes valve nodes when they advertise control."""
|
||||
mock_nodes = [
|
||||
replace(
|
||||
mock_sensor_nodes[0],
|
||||
general=replace(mock_sensor_nodes[0].general, node_type=valve_node_type),
|
||||
),
|
||||
*mock_sensor_nodes[1:],
|
||||
]
|
||||
mock_duco_client.async_get_nodes.return_value = mock_nodes
|
||||
mock_duco_client.async_get_node_actions.return_value = _build_multi_node_actions(
|
||||
[1, 2, 50, 113],
|
||||
[node.node_id for node in mock_nodes],
|
||||
options=["AUTO", "CNT1", "CNT2", "CNT3", "MAN1", "MAN2", "MAN3"],
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.SELECT])
|
||||
|
||||
assert hass.states.get(_SELECT_ENTITY) is not None
|
||||
valve_state = hass.states.get(_VALVE_SELECT_ENTITY)
|
||||
assert valve_state is not None
|
||||
assert valve_state.attributes[ATTR_OPTIONS] == [
|
||||
"AUTO",
|
||||
"CNT1",
|
||||
"CNT2",
|
||||
"CNT3",
|
||||
"MAN1",
|
||||
"MAN2",
|
||||
"MAN3",
|
||||
]
|
||||
assert hass.states.get(_UNSUPPORTED_SELECT_ENTITY) is None
|
||||
|
||||
|
||||
|
||||
@@ -853,12 +853,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -1768,12 +1767,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -1822,8 +1820,14 @@
|
||||
]),
|
||||
}),
|
||||
'fixtures': dict({
|
||||
'/admin/lib/acb_config': 'Testing request replies.',
|
||||
'/admin/lib/acb_config_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/network_display': 'Testing request replies.',
|
||||
'/admin/lib/network_display_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/tariff': 'Testing request replies.',
|
||||
'/admin/lib/tariff_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/admin/lib/wireless_display': 'Testing request replies.',
|
||||
'/admin/lib/wireless_display_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/api/v1/production': 'Testing request replies.',
|
||||
'/api/v1/production/inverters': 'Testing request replies.',
|
||||
'/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
@@ -1832,6 +1836,8 @@
|
||||
'/home_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/info': 'Testing request replies.',
|
||||
'/info_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/inventory.json?deleted=1': 'Testing request replies.',
|
||||
'/inventory.json?deleted=1_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/dry_contacts': 'Testing request replies.',
|
||||
'/ivp/ensemble/dry_contacts_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/generator': 'Testing request replies.',
|
||||
@@ -1840,18 +1846,26 @@
|
||||
'/ivp/ensemble/inventory_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/power': 'Testing request replies.',
|
||||
'/ivp/ensemble/power_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/relay': 'Testing request replies.',
|
||||
'/ivp/ensemble/relay_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/secctrl': 'Testing request replies.',
|
||||
'/ivp/ensemble/secctrl_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ensemble/status': 'Testing request replies.',
|
||||
'/ivp/ensemble/status_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/livedata/status': 'Testing request replies.',
|
||||
'/ivp/livedata/status_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/meters': 'Testing request replies.',
|
||||
'/ivp/meters/readings': 'Testing request replies.',
|
||||
'/ivp/meters/readings_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/meters_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/pdm/device_data': 'Testing request replies.',
|
||||
'/ivp/pdm/device_data_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/pdm/energy': 'Testing request replies.',
|
||||
'/ivp/pdm/energy_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/sc/pvlimit': 'Testing request replies.',
|
||||
'/ivp/sc/pvlimit_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/sc/sched': 'Testing request replies.',
|
||||
'/ivp/sc/sched_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ss/dry_contact_settings': 'Testing request replies.',
|
||||
'/ivp/ss/dry_contact_settings_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||
'/ivp/ss/gen_config': 'Testing request replies.',
|
||||
@@ -2727,12 +2741,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -2781,9 +2794,18 @@
|
||||
]),
|
||||
}),
|
||||
'fixtures': dict({
|
||||
'/admin/lib/acb_config_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/network_display_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/tariff_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/admin/lib/wireless_display_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/api/v1/production/inverters_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2796,6 +2818,9 @@
|
||||
'/info_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/inventory.json?deleted=1_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/dry_contacts_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2808,12 +2833,18 @@
|
||||
'/ivp/ensemble/power_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/relay_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/secctrl_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ensemble/status_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/livedata/status_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/meters/readings_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -2823,9 +2854,15 @@
|
||||
'/ivp/pdm/device_data_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/pdm/energy_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/sc/pvlimit_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/sc/sched_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
'/ivp/ss/dry_contact_settings_log': dict({
|
||||
'Error': "EnvoyError('Test')",
|
||||
}),
|
||||
@@ -3711,12 +3748,11 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': None,
|
||||
'ctmeter_consumption_phases': None,
|
||||
'ctmeter_production': None,
|
||||
'ctmeter_production_phases': None,
|
||||
'ctmeter_storage': None,
|
||||
'ctmeter_storage_phases': None,
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': None,
|
||||
'collar': None,
|
||||
'ctmeters': dict({
|
||||
}),
|
||||
'ctmeters_phases': dict({
|
||||
@@ -19745,59 +19781,16 @@
|
||||
}),
|
||||
]),
|
||||
'envoy_model_data': dict({
|
||||
'ctmeter_consumption': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
'acb_inventory': None,
|
||||
'acb_power': None,
|
||||
'battery_aggregate': None,
|
||||
'c6cc': dict({
|
||||
'__type': "<class 'pyenphase.models.c6combiner.EnvoyC6CC'>",
|
||||
'repr': "EnvoyC6CC(admin_state=82, admin_state_str='ENCMN_C6_CC_READY', firmware_loaded_date=1752945451, firmware_version='0.1.20-D1', installed_date=1752945451, last_report_date=1752945451, communicating=True, part_number='800-02403-r08', serial_number='482523040549', dmir_version='0.1.20-D1')",
|
||||
}),
|
||||
'ctmeter_consumption_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
}),
|
||||
'ctmeter_production': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance', 'power-on-unused-phase'])",
|
||||
}),
|
||||
'ctmeter_production_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance'])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['power-on-unused-phase'])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
}),
|
||||
'ctmeter_storage': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'ctmeter_storage_phases': dict({
|
||||
'L1': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L2': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'L3': dict({
|
||||
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
|
||||
'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
|
||||
}),
|
||||
'collar': dict({
|
||||
'__type': "<class 'pyenphase.models.collar.EnvoyCollar'>",
|
||||
'repr': "EnvoyCollar(admin_state=88, admin_state_str='ENCMN_MDE_ON_GRID', firmware_loaded_date=1752939759, firmware_version='3.0.6-D0', installed_date=1752939759, last_report_date=1752939759, communicating=True, mid_state='close', grid_state='on_grid', part_number='865-00400-r22', serial_number='482520020939', temperature=42, temperature_unit='C', control_error=0, collar_state='Installed')",
|
||||
}),
|
||||
'ctmeters': dict({
|
||||
'backfeed': dict({
|
||||
|
||||
@@ -607,7 +607,7 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage(
|
||||
ent_reg_entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA/0/binary_sensor/my",
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
ent_reg_entry.entity_id,
|
||||
@@ -1161,58 +1161,6 @@ async def test_entity_id_with_empty_sub_device_name(
|
||||
assert hass.states.get("binary_sensor.main_device_sensor") is not None
|
||||
|
||||
|
||||
async def test_legacy_unique_id_migrated_to_v3_sub_device(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test a legacy sub-device unique id is migrated to the version 3 format."""
|
||||
sub_devices = [
|
||||
SubDeviceInfo(device_id=22222222, name="kitchen_controller", area_id=0),
|
||||
]
|
||||
device_info = {"name": "test", "devices": sub_devices}
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
device_id=22222222,
|
||||
),
|
||||
]
|
||||
states = [BinarySensorState(key=1, state=True, missing_state=False)]
|
||||
|
||||
# Seed a registry entry in the legacy format with the @device_id suffix
|
||||
legacy_entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-temperature@22222222",
|
||||
suggested_object_id="kitchen_controller_temperature",
|
||||
)
|
||||
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
device_info=device_info,
|
||||
entity_info=entity_info,
|
||||
states=states,
|
||||
)
|
||||
|
||||
entity_entry = entity_registry.async_get(legacy_entry.entity_id)
|
||||
assert entity_entry is not None
|
||||
# The legacy id is renamed to version 3, keeping the same entity
|
||||
assert (
|
||||
entity_entry.unique_id == "11:22:33:44:55:AA/22222222/binary_sensor/Temperature"
|
||||
)
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-temperature@22222222",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
@@ -1256,8 +1204,8 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
entity_entry = entity_registry.async_get("binary_sensor.test_temperature")
|
||||
assert entity_entry is not None
|
||||
initial_unique_id = entity_entry.unique_id
|
||||
# Main device entities use device_id 0 in the unique id
|
||||
assert "/0/" in initial_unique_id
|
||||
# Initial unique_id should not have @device_id suffix since it's on main device
|
||||
assert "@" not in initial_unique_id
|
||||
|
||||
# Add sub-device to device info
|
||||
sub_devices = [
|
||||
@@ -1312,8 +1260,8 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
# Wait for entity to be updated
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The entity_id doesn't change when moving between devices,
|
||||
# only the device segment of the unique_id changes
|
||||
# The entity_id doesn't change when moving between devices
|
||||
# Only the unique_id gets updated with @device_id suffix
|
||||
state = hass.states.get("binary_sensor.test_temperature")
|
||||
assert state is not None
|
||||
|
||||
@@ -1321,8 +1269,9 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
entity_entry = entity_registry.async_get("binary_sensor.test_temperature")
|
||||
assert entity_entry is not None
|
||||
|
||||
# Unique ID device segment should now be the sub-device id
|
||||
expected_unique_id = initial_unique_id.replace("/0/", "/22222222/")
|
||||
# Unique ID should have been migrated to include @device_id
|
||||
# This is done by our build_device_unique_id wrapper
|
||||
expected_unique_id = f"{initial_unique_id}@22222222"
|
||||
assert entity_entry.unique_id == expected_unique_id
|
||||
|
||||
# Entity should now be associated with the sub-device
|
||||
@@ -1382,8 +1331,8 @@ async def test_unique_id_migration_sub_device_to_main_device(
|
||||
)
|
||||
assert entity_entry is not None
|
||||
initial_unique_id = entity_entry.unique_id
|
||||
# Sub-device entities carry the sub-device id in the unique id
|
||||
assert "/22222222/" in initial_unique_id
|
||||
# Initial unique_id should have @device_id suffix since it's on sub-device
|
||||
assert "@22222222" in initial_unique_id
|
||||
|
||||
# Update entity info - move to main device
|
||||
new_entity_info = [
|
||||
@@ -1416,8 +1365,8 @@ async def test_unique_id_migration_sub_device_to_main_device(
|
||||
)
|
||||
assert entity_entry is not None
|
||||
|
||||
# Unique ID device segment should now be the main device id 0
|
||||
expected_unique_id = initial_unique_id.replace("/22222222/", "/0/")
|
||||
# Unique ID should have been migrated to remove @device_id suffix
|
||||
expected_unique_id = initial_unique_id.replace("@22222222", "")
|
||||
assert entity_entry.unique_id == expected_unique_id
|
||||
|
||||
# Entity should now be associated with the main device
|
||||
@@ -1478,8 +1427,8 @@ async def test_unique_id_migration_between_sub_devices(
|
||||
)
|
||||
assert entity_entry is not None
|
||||
initial_unique_id = entity_entry.unique_id
|
||||
# Sub-device entities carry the sub-device id in the unique id
|
||||
assert "/22222222/" in initial_unique_id
|
||||
# Initial unique_id should have @22222222 suffix
|
||||
assert "@22222222" in initial_unique_id
|
||||
|
||||
# Update entity info - move to second sub-device
|
||||
new_entity_info = [
|
||||
@@ -1512,8 +1461,8 @@ async def test_unique_id_migration_between_sub_devices(
|
||||
)
|
||||
assert entity_entry is not None
|
||||
|
||||
# Unique ID device segment should have moved from 22222222 to 33333333
|
||||
expected_unique_id = initial_unique_id.replace("/22222222/", "/33333333/")
|
||||
# Unique ID should have been migrated from @22222222 to @33333333
|
||||
expected_unique_id = initial_unique_id.replace("@22222222", "@33333333")
|
||||
assert entity_entry.unique_id == expected_unique_id
|
||||
|
||||
# Entity should now be associated with the second sub-device
|
||||
@@ -1575,8 +1524,8 @@ async def test_entity_device_id_rename_in_yaml(
|
||||
entity_entry = entity_registry.async_get("binary_sensor.old_device_sensor")
|
||||
assert entity_entry is not None
|
||||
initial_unique_id = entity_entry.unique_id
|
||||
# Sub-device entities carry the sub-device id in the unique id
|
||||
assert "/11111111/" in initial_unique_id
|
||||
# Should have @11111111 suffix
|
||||
assert "@11111111" in initial_unique_id
|
||||
|
||||
# Simulate user renaming device_id in YAML config
|
||||
# The device_id hash changes from 11111111 to 99999999
|
||||
@@ -1638,8 +1587,9 @@ async def test_entity_device_id_rename_in_yaml(
|
||||
entity_entry = entity_registry.async_get("binary_sensor.renamed_device_sensor")
|
||||
assert entity_entry is not None
|
||||
|
||||
# Unique ID device segment should have the new device_id
|
||||
expected_unique_id = initial_unique_id.replace("/11111111/", "/99999999/")
|
||||
# Unique ID should have the new device_id
|
||||
base_unique_id = initial_unique_id.replace("@11111111", "")
|
||||
expected_unique_id = f"{base_unique_id}@99999999"
|
||||
assert entity_entry.unique_id == expected_unique_id
|
||||
|
||||
# Entity should be associated with the new device
|
||||
|
||||
@@ -21,53 +21,6 @@ from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
|
||||
from .conftest import MockGenericDeviceEntryType
|
||||
|
||||
|
||||
async def test_migrate_entity_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""Test a legacy unique id is migrated to the version 3 format."""
|
||||
entity_registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-sensor-mysensor",
|
||||
suggested_object_id="my_sensor",
|
||||
disabled_by=None,
|
||||
)
|
||||
entity_info = [
|
||||
SensorInfo(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:leaf",
|
||||
)
|
||||
]
|
||||
states = [SensorState(key=1, state=50)]
|
||||
user_service = []
|
||||
await mock_generic_device_entry(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
)
|
||||
state = hass.states.get("sensor.my_sensor")
|
||||
assert state is not None
|
||||
assert state.state == "50"
|
||||
entry = entity_registry.async_get("sensor.my_sensor")
|
||||
assert entry is not None
|
||||
# The legacy unique id should have been renamed to the version 3 format,
|
||||
# keeping the entity (and its entity_id) instead of creating a new one
|
||||
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, "11:22:33:44:55:AA-sensor-mysensor"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_entity_unique_id_downgrade_upgrade(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
@@ -75,20 +28,18 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""Test unique id migration prefers the original entity on downgrade upgrade."""
|
||||
# The original entity, already in the version 3 format
|
||||
entity_registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA/0/sensor/my sensor",
|
||||
suggested_object_id="new_sensor",
|
||||
"my_sensor",
|
||||
suggested_object_id="old_sensor",
|
||||
disabled_by=None,
|
||||
)
|
||||
# A duplicate left behind in the legacy format by a downgrade
|
||||
entity_registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-sensor-mysensor",
|
||||
suggested_object_id="old_sensor",
|
||||
suggested_object_id="new_sensor",
|
||||
disabled_by=None,
|
||||
)
|
||||
entity_info = [
|
||||
@@ -113,17 +64,17 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
|
||||
assert state.state == "50"
|
||||
entry = entity_registry.async_get("sensor.new_sensor")
|
||||
assert entry is not None
|
||||
# Confirm we did not touch the legacy entity that was created
|
||||
# Confirm we did not touch the entity that was created
|
||||
# on downgrade so when they upgrade again they can delete the
|
||||
# entity that was only created on downgrade and they keep
|
||||
# the original one.
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, "11:22:33:44:55:AA-sensor-mysensor"
|
||||
)
|
||||
entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, "my_sensor")
|
||||
is not None
|
||||
)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
|
||||
# Note that ESPHome includes the EntityInfo type in the unique id
|
||||
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
||||
|
||||
|
||||
async def test_discover_zwave() -> None:
|
||||
|
||||
@@ -139,15 +139,13 @@ async def test_device_conflict_migration(
|
||||
|
||||
ent_reg_entry = entity_registry.async_get("binary_sensor.test_my_binary_sensor")
|
||||
assert ent_reg_entry
|
||||
assert (
|
||||
ent_reg_entry.unique_id == "11:22:33:44:55:AA/0/binary_sensor/my binary_sensor"
|
||||
)
|
||||
assert ent_reg_entry.unique_id == "11:22:33:44:55:AA-binary_sensor-mybinary_sensor"
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert entries is not None
|
||||
for entry in entries:
|
||||
assert entry.unique_id.startswith("11:22:33:44:55:AA/")
|
||||
assert entry.unique_id.startswith("11:22:33:44:55:AA-")
|
||||
disconnect_done = hass.loop.create_future()
|
||||
|
||||
async def async_disconnect(*args, **kwargs) -> None:
|
||||
@@ -203,16 +201,14 @@ async def test_device_conflict_migration(
|
||||
assert mock_config_entry.unique_id == "11:22:33:44:55:ab"
|
||||
ent_reg_entry = entity_registry.async_get("binary_sensor.test_my_binary_sensor")
|
||||
assert ent_reg_entry
|
||||
assert (
|
||||
ent_reg_entry.unique_id == "11:22:33:44:55:AB/0/binary_sensor/my binary_sensor"
|
||||
)
|
||||
assert ent_reg_entry.unique_id == "11:22:33:44:55:AB-binary_sensor-mybinary_sensor"
|
||||
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert entries is not None
|
||||
for entry in entries:
|
||||
assert entry.unique_id.startswith("11:22:33:44:55:AB/")
|
||||
assert entry.unique_id.startswith("11:22:33:44:55:AB-")
|
||||
|
||||
dev_entry = device_registry.async_get_device(
|
||||
identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:44:55:ab")}
|
||||
|
||||
@@ -129,7 +129,7 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon(
|
||||
assert entry is not None
|
||||
# Note that ESPHome includes the EntityInfo type in the unique id
|
||||
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
|
||||
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
||||
assert entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ async def test_generic_numeric_sensor_state_class_measurement(
|
||||
assert entry is not None
|
||||
# Note that ESPHome includes the EntityInfo type in the unique id
|
||||
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
|
||||
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
||||
assert entry.entity_category is None
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ async def test_generic_numeric_sensor_state_class_measurement_angle(
|
||||
assert entry is not None
|
||||
# Note that ESPHome includes the EntityInfo type in the unique id
|
||||
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
|
||||
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
||||
assert entry.entity_category is None
|
||||
|
||||
|
||||
|
||||
@@ -107,12 +107,12 @@ async def test_job_manager_ws_updates(
|
||||
job_data: Job | None = None
|
||||
|
||||
@callback
|
||||
def mock_subcription_callback(job: Job) -> None:
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
nonlocal job_data
|
||||
job_data = job
|
||||
|
||||
subscription = JobSubscription(
|
||||
mock_subcription_callback, name="test_job", reference="test"
|
||||
mock_subscription_callback, name="test_job", reference="test"
|
||||
)
|
||||
unsubscribe = data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
@@ -318,11 +318,11 @@ async def test_job_manager_reload_on_supervisor_restart(
|
||||
job_data: Job | None = None
|
||||
|
||||
@callback
|
||||
def mock_subcription_callback(job: Job) -> None:
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
nonlocal job_data
|
||||
job_data = job
|
||||
|
||||
subscription = JobSubscription(mock_subcription_callback, name="test_job")
|
||||
subscription = JobSubscription(mock_subscription_callback, name="test_job")
|
||||
data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
# Send supervisor restart signal
|
||||
@@ -347,3 +347,78 @@ async def test_job_manager_reload_on_supervisor_restart(
|
||||
assert job_data.reference == "test"
|
||||
assert job_data.done is True
|
||||
assert not data_coordinator.jobs.current_jobs
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_subscribe_returns_unsubscribe_when_job_already_matches(
|
||||
hass: HomeAssistant,
|
||||
jobs_info: AsyncMock,
|
||||
hass_supervisor_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test subscribe returns a working unsubscribe even if a job already matches."""
|
||||
jobs_info.return_value = JobsInfo(
|
||||
ignore_conditions=[],
|
||||
jobs=[
|
||||
Job(
|
||||
name="test_job",
|
||||
reference="test",
|
||||
uuid=uuid4(),
|
||||
progress=0,
|
||||
stage=None,
|
||||
done=False,
|
||||
errors=[],
|
||||
created=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
extra=None,
|
||||
child_jobs=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, DOMAIN, {})
|
||||
assert result
|
||||
|
||||
client = await hass_supervisor_ws_client()
|
||||
data_coordinator: HassioMainDataUpdateCoordinator = hass.data[MAIN_COORDINATOR]
|
||||
|
||||
received: list[Job] = []
|
||||
|
||||
@callback
|
||||
def mock_subscription_callback(job: Job) -> None:
|
||||
received.append(job)
|
||||
|
||||
subscription = JobSubscription(mock_subscription_callback, name="test_job")
|
||||
unsubscribe = data_coordinator.jobs.subscribe(subscription)
|
||||
|
||||
# Existing matching job is delivered immediately, and a callable unsubscribe
|
||||
# is returned (not the None result of the callback)
|
||||
assert len(received) == 1
|
||||
assert received[0].name == "test_job"
|
||||
assert callable(unsubscribe)
|
||||
|
||||
# After unsubscribing, a new matching job update is no longer delivered
|
||||
unsubscribe()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "job",
|
||||
"data": {
|
||||
"name": "test_job",
|
||||
"reference": "test",
|
||||
"uuid": uuid4().hex,
|
||||
"progress": 50,
|
||||
"stage": None,
|
||||
"done": False,
|
||||
"errors": [],
|
||||
"created": datetime.now().isoformat(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
"extra": None,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(received) == 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user