mirror of
https://github.com/home-assistant/core.git
synced 2026-06-27 17:15:23 +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 |
@@ -51,7 +51,6 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/switch/**
|
||||
- homeassistant/components/text/**
|
||||
- homeassistant/components/time/**
|
||||
- homeassistant/components/timer_list/**
|
||||
- homeassistant/components/todo/**
|
||||
- homeassistant/components/tts/**
|
||||
- homeassistant/components/update/**
|
||||
|
||||
@@ -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
-7
@@ -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
|
||||
@@ -1031,8 +1030,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/local_calendar/ @allenporter
|
||||
/homeassistant/components/local_ip/ @issacg
|
||||
/tests/components/local_ip/ @issacg
|
||||
/homeassistant/components/local_timer_list/ @home-assistant/core @synesthesiam
|
||||
/tests/components/local_timer_list/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/local_todo/ @allenporter
|
||||
/tests/components/local_todo/ @allenporter
|
||||
/homeassistant/components/lock/ @home-assistant/core
|
||||
@@ -1834,8 +1831,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/time/ @home-assistant/core
|
||||
/homeassistant/components/time_date/ @fabaff
|
||||
/tests/components/time_date/ @fabaff
|
||||
/homeassistant/components/timer_list/ @home-assistant/core @synesthesiam
|
||||
/tests/components/timer_list/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/tmb/ @alemuro
|
||||
/homeassistant/components/todo/ @home-assistant/core
|
||||
/tests/components/todo/ @home-assistant/core
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.1.3"]
|
||||
"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,
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260624.0"]
|
||||
"requirements": ["home-assistant-frontend==20260624.1"]
|
||||
}
|
||||
|
||||
@@ -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,22 +0,0 @@
|
||||
"""The Local Timer list integration.
|
||||
|
||||
Creates an in-memory timer list entity (provided by the ``timer_list`` base
|
||||
platform) for each config entry, so users can create timer lists from the UI.
|
||||
"""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.TIMER_LIST]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Local Timer list from a config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,33 +0,0 @@
|
||||
"""Config flow for the Local Timer list integration."""
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import CONF_TIMER_LIST_NAME, DOMAIN
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TIMER_LIST_NAME): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LocalTimerListConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Local Timer list."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_TIMER_LIST_NAME], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA)
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Constants for the Local Timer list integration."""
|
||||
|
||||
DOMAIN = "local_timer_list"
|
||||
|
||||
CONF_TIMER_LIST_NAME = "timer_list_name"
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "local_timer_list",
|
||||
"name": "Local Timer list",
|
||||
"codeowners": ["@home-assistant/core", "@synesthesiam"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["timer_list"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_timer_list",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"timer_list_name": "Name"
|
||||
},
|
||||
"description": "Choose a name for the new timer list. The entity ID is derived from this name.",
|
||||
"submit": "Create"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Local timer list platform."""
|
||||
|
||||
from homeassistant.components.timer_list import TimerListEntity, TimerListEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_TIMER_LIST_NAME
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the local timer list entity from a config entry."""
|
||||
async_add_entities([LocalTimerListEntity(config_entry)])
|
||||
|
||||
|
||||
class LocalTimerListEntity(TimerListEntity):
|
||||
"""A local, in-memory timer list."""
|
||||
|
||||
_attr_supported_features = (
|
||||
TimerListEntityFeature.START_TIMER
|
||||
| TimerListEntityFeature.PAUSE_TIMER
|
||||
| TimerListEntityFeature.CANCEL_TIMER
|
||||
| TimerListEntityFeature.ADD_TIME
|
||||
)
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the timer list."""
|
||||
super().__init__()
|
||||
self._attr_name = config_entry.data[CONF_TIMER_LIST_NAME]
|
||||
self._attr_unique_id = config_entry.entry_id
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Support for Mycroft AI."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "mycroft"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Mycroft component."""
|
||||
hass.data[DOMAIN] = config[DOMAIN][CONF_HOST]
|
||||
discovery.load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config)
|
||||
return True
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "mycroft",
|
||||
"name": "Mycroft",
|
||||
"codeowners": [],
|
||||
"disabled": "Dependencies not compatible with the new pip resolver",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mycroft",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["mycroftapi"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["mycroftapi==2.0"]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Mycroft AI notification platform."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from mycroftapi import MycroftAPI
|
||||
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> MycroftNotificationService:
|
||||
"""Get the Mycroft notification service."""
|
||||
return MycroftNotificationService(hass.data[DOMAIN])
|
||||
|
||||
|
||||
class MycroftNotificationService(BaseNotificationService):
|
||||
"""The Mycroft Notification Service."""
|
||||
|
||||
def __init__(self, mycroft_ip: str) -> None:
|
||||
"""Initialize the service."""
|
||||
self.mycroft_ip = mycroft_ip
|
||||
|
||||
@override
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message mycroft to speak on instance."""
|
||||
|
||||
text = message
|
||||
mycroft = MycroftAPI(self.mycroft_ip)
|
||||
if mycroft is not None:
|
||||
mycroft.speak_text(text)
|
||||
else:
|
||||
_LOGGER.warning("Could not reach this instance of mycroft")
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -365,5 +365,7 @@ async def create_rexel_client(
|
||||
gateway_id=entry.data[CONF_GATEWAY_ID],
|
||||
),
|
||||
session=async_create_clientsession(hass),
|
||||
settings=OverkizClientSettings(action_queue=ActionQueueSettings()),
|
||||
settings=OverkizClientSettings(
|
||||
action_queue=ActionQueueSettings(), default_rts_command_duration=0
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,34 +85,18 @@ def sun(
|
||||
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
|
||||
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
|
||||
|
||||
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
|
||||
after_sunrise = sunrise is not None and today > dt_util.as_local(sunrise).date()
|
||||
if after_sunrise and has_sunrise_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
|
||||
|
||||
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
|
||||
after_sunset = sunset is not None and today > dt_util.as_local(sunset).date()
|
||||
if after_sunset and has_sunset_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
|
||||
|
||||
# Special case: before sunrise OR after sunset
|
||||
# This will handle the very rare case in the polar region when the sun rises/sets
|
||||
# but does not set/rise.
|
||||
# However this entire condition does not handle those full days of darkness
|
||||
# or light, the following should be used instead:
|
||||
#
|
||||
# condition:
|
||||
# condition: state
|
||||
# entity_id: sun.sun
|
||||
# state: 'above_horizon' (or 'below_horizon')
|
||||
#
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
# A missing sunrise/sunset means the sun doesn't rise/set on this day, which
|
||||
# happens in polar regions.
|
||||
if sunrise is None and has_sunrise_condition:
|
||||
# There is no sunrise today
|
||||
condition_trace_set_result(False, message="no sunrise today")
|
||||
@@ -123,6 +107,16 @@ def sun(
|
||||
condition_trace_set_result(False, message="no sunset today")
|
||||
return False
|
||||
|
||||
# "before: sunrise" combined with "after: sunset" describes the dark period
|
||||
# around midnight, so it is evaluated as an OR (true before sunrise or after
|
||||
# sunset) rather than the usual AND of the two bounds.
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
if before == SUN_EVENT_SUNRISE:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
|
||||
@@ -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)
|
||||
@@ -1,547 +0,0 @@
|
||||
"""The Timer list integration.
|
||||
|
||||
A timer list entity holds many independent countdown timers (its *items*),
|
||||
mirroring how a to-do list holds many to-do items. The entity state is the
|
||||
number of active timers. Timers are kept in memory only: they do not survive a
|
||||
restart of Home Assistant in this first version (see the module-level notes on
|
||||
``async_will_remove_from_hass``).
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, final, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util, ulid as ulid_util
|
||||
|
||||
from .const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_FINISH_ACTION,
|
||||
ATTR_STATUS,
|
||||
ATTR_TIMER_ID,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
TimerFinishAction,
|
||||
TimerListEntityFeature,
|
||||
TimerListEventType,
|
||||
TimerListServices,
|
||||
TimerStatus,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
|
||||
_FINISHED_STATUSES = (TimerStatus.FINISHED, TimerStatus.CANCELLED)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TimerItem:
|
||||
"""A single timer within a timer list."""
|
||||
|
||||
timer_id: str
|
||||
"""Generated unique id of the timer."""
|
||||
|
||||
name: str | None
|
||||
"""Optional user-provided name."""
|
||||
|
||||
status: TimerStatus
|
||||
"""Current status of the timer."""
|
||||
|
||||
finish_action: TimerFinishAction
|
||||
"""What happens to the timer once it finishes."""
|
||||
|
||||
duration: timedelta
|
||||
"""Original duration the timer was created with (used by ``restart``)."""
|
||||
|
||||
created_at: datetime
|
||||
"""When the timer was (re)started, in UTC."""
|
||||
|
||||
finishes_at: datetime | None = None
|
||||
"""Absolute time the timer will finish, in UTC. ``None`` unless active."""
|
||||
|
||||
remaining: timedelta | None = None
|
||||
"""Remaining time captured while paused. ``None`` unless paused."""
|
||||
|
||||
finished_at: datetime | None = None
|
||||
"""When the timer finished or was cancelled, in UTC."""
|
||||
|
||||
def remaining_at(self, now: datetime) -> timedelta:
|
||||
"""Return the time left on the timer relative to ``now``."""
|
||||
if self.status == TimerStatus.ACTIVE and self.finishes_at is not None:
|
||||
return max(timedelta(0), self.finishes_at - now)
|
||||
if self.status == TimerStatus.PAUSED and self.remaining is not None:
|
||||
return self.remaining
|
||||
return timedelta(0)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class TimerListEvent:
|
||||
"""A change to a timer, pushed to subscribers and triggers."""
|
||||
|
||||
event_type: TimerListEventType
|
||||
item: TimerItem
|
||||
|
||||
|
||||
@callback
|
||||
def timer_to_dict(item: TimerItem, now: datetime) -> dict[str, Any]:
|
||||
"""Serialize a timer item for the websocket API and triggers."""
|
||||
return {
|
||||
ATTR_TIMER_ID: item.timer_id,
|
||||
ATTR_NAME: item.name,
|
||||
ATTR_STATUS: item.status.value,
|
||||
ATTR_FINISH_ACTION: item.finish_action.value,
|
||||
"duration": item.duration.total_seconds(),
|
||||
"created_at": item.created_at.isoformat(),
|
||||
"finishes_at": item.finishes_at.isoformat() if item.finishes_at else None,
|
||||
"finished_at": item.finished_at.isoformat() if item.finished_at else None,
|
||||
"remaining": item.remaining_at(now).total_seconds(),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Timer list component."""
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[TimerListEntity](
|
||||
_LOGGER, DOMAIN, hass
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_handle_subscribe)
|
||||
websocket_api.async_register_command(hass, websocket_handle_list)
|
||||
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.START_TIMER,
|
||||
{
|
||||
vol.Optional(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_DURATION): cv.positive_time_period,
|
||||
vol.Optional(
|
||||
ATTR_FINISH_ACTION, default=TimerFinishAction.REMOVE
|
||||
): vol.Coerce(TimerFinishAction),
|
||||
},
|
||||
_async_start_timer,
|
||||
required_features=[TimerListEntityFeature.START_TIMER],
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.PAUSE_TIMER,
|
||||
{vol.Required(ATTR_TIMER_ID): cv.string},
|
||||
"async_pause_timer",
|
||||
required_features=[TimerListEntityFeature.PAUSE_TIMER],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.UNPAUSE_TIMER,
|
||||
{vol.Required(ATTR_TIMER_ID): cv.string},
|
||||
"async_unpause_timer",
|
||||
required_features=[TimerListEntityFeature.PAUSE_TIMER],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.CANCEL_TIMER,
|
||||
{vol.Required(ATTR_TIMER_ID): cv.string},
|
||||
"async_cancel_timer",
|
||||
required_features=[TimerListEntityFeature.CANCEL_TIMER],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.CANCEL_ALL_TIMERS,
|
||||
None,
|
||||
"async_cancel_all_timers",
|
||||
required_features=[TimerListEntityFeature.CANCEL_TIMER],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.ADD_TIME,
|
||||
{
|
||||
vol.Required(ATTR_TIMER_ID): cv.string,
|
||||
vol.Required(ATTR_DURATION): cv.positive_time_period,
|
||||
},
|
||||
_async_add_time,
|
||||
required_features=[TimerListEntityFeature.ADD_TIME],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.REMOVE_TIME,
|
||||
{
|
||||
vol.Required(ATTR_TIMER_ID): cv.string,
|
||||
vol.Required(ATTR_DURATION): cv.positive_time_period,
|
||||
},
|
||||
_async_remove_time,
|
||||
required_features=[TimerListEntityFeature.ADD_TIME],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.REMOVE_TIMER,
|
||||
{vol.Required(ATTR_TIMER_ID): cv.string},
|
||||
"async_remove_timer",
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.CLEAR_FINISHED_TIMERS,
|
||||
None,
|
||||
"async_clear_finished_timers",
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
TimerListServices.GET_TIMERS,
|
||||
{vol.Optional(ATTR_STATUS): vol.All(cv.ensure_list, [vol.Coerce(TimerStatus)])},
|
||||
_async_get_timers,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
class TimerListEntity(Entity):
|
||||
"""An entity that holds a list of independent countdown timers."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the timer list."""
|
||||
self._timers: dict[str, TimerItem] = {}
|
||||
self._cancel_callbacks: dict[str, CALLBACK_TYPE] = {}
|
||||
self._update_listeners: list[Callable[[TimerListEvent], None]] = []
|
||||
|
||||
@property
|
||||
@override
|
||||
def state(self) -> int:
|
||||
"""Return the number of active timers."""
|
||||
return sum(
|
||||
timer.status == TimerStatus.ACTIVE for timer in self._timers.values()
|
||||
)
|
||||
|
||||
@property
|
||||
def timers(self) -> list[TimerItem]:
|
||||
"""Return the timers in the list."""
|
||||
return list(self._timers.values())
|
||||
|
||||
async def async_start_timer(
|
||||
self,
|
||||
*,
|
||||
name: str | None,
|
||||
duration: timedelta,
|
||||
finish_action: TimerFinishAction,
|
||||
) -> str:
|
||||
"""Create and start a new timer, returning its id."""
|
||||
now = dt_util.utcnow()
|
||||
timer_id = ulid_util.ulid_now()
|
||||
timer = TimerItem(
|
||||
timer_id=timer_id,
|
||||
name=name,
|
||||
status=TimerStatus.ACTIVE,
|
||||
finish_action=finish_action,
|
||||
duration=duration,
|
||||
created_at=now,
|
||||
finishes_at=now + duration,
|
||||
)
|
||||
self._timers[timer_id] = timer
|
||||
self._schedule(timer)
|
||||
self._notify(TimerListEventType.STARTED, timer)
|
||||
return timer_id
|
||||
|
||||
async def async_pause_timer(self, timer_id: str) -> None:
|
||||
"""Pause an active timer."""
|
||||
timer = self._get_timer(timer_id)
|
||||
if timer.status != TimerStatus.ACTIVE or timer.finishes_at is None:
|
||||
return
|
||||
timer.remaining = max(timedelta(0), timer.finishes_at - dt_util.utcnow())
|
||||
timer.finishes_at = None
|
||||
timer.status = TimerStatus.PAUSED
|
||||
self._unschedule(timer_id)
|
||||
self._notify(TimerListEventType.UPDATED, timer)
|
||||
|
||||
async def async_unpause_timer(self, timer_id: str) -> None:
|
||||
"""Resume a paused timer."""
|
||||
timer = self._get_timer(timer_id)
|
||||
if timer.status != TimerStatus.PAUSED or timer.remaining is None:
|
||||
return
|
||||
timer.finishes_at = dt_util.utcnow() + timer.remaining
|
||||
timer.remaining = None
|
||||
timer.status = TimerStatus.ACTIVE
|
||||
self._schedule(timer)
|
||||
self._notify(TimerListEventType.UPDATED, timer)
|
||||
|
||||
async def async_cancel_timer(self, timer_id: str) -> None:
|
||||
"""Cancel a timer.
|
||||
|
||||
The timer is retained in the ``cancelled`` state only when its finish
|
||||
action is ``archive``; otherwise it is removed.
|
||||
"""
|
||||
timer = self._get_timer(timer_id)
|
||||
self._unschedule(timer_id)
|
||||
timer.status = TimerStatus.CANCELLED
|
||||
timer.finishes_at = None
|
||||
timer.remaining = None
|
||||
timer.finished_at = dt_util.utcnow()
|
||||
self._notify(TimerListEventType.CANCELLED, timer)
|
||||
if timer.finish_action != TimerFinishAction.ARCHIVE:
|
||||
del self._timers[timer_id]
|
||||
self._notify(TimerListEventType.REMOVED, timer)
|
||||
|
||||
async def async_cancel_all_timers(self) -> None:
|
||||
"""Cancel every active or paused timer."""
|
||||
for timer_id in [
|
||||
timer.timer_id
|
||||
for timer in self._timers.values()
|
||||
if timer.status in (TimerStatus.ACTIVE, TimerStatus.PAUSED)
|
||||
]:
|
||||
await self.async_cancel_timer(timer_id)
|
||||
|
||||
async def async_add_time(self, timer_id: str, duration: timedelta) -> None:
|
||||
"""Add (or, with a negative duration, remove) time on a timer."""
|
||||
timer = self._get_timer(timer_id)
|
||||
if timer.status == TimerStatus.ACTIVE and timer.finishes_at is not None:
|
||||
now = dt_util.utcnow()
|
||||
finishes_at = timer.finishes_at + duration
|
||||
if finishes_at <= now:
|
||||
self._unschedule(timer_id)
|
||||
self._async_timer_finished(timer_id, now)
|
||||
return
|
||||
timer.finishes_at = finishes_at
|
||||
self._schedule(timer)
|
||||
elif timer.status == TimerStatus.PAUSED and timer.remaining is not None:
|
||||
timer.remaining = max(timedelta(0), timer.remaining + duration)
|
||||
else:
|
||||
return
|
||||
self._notify(TimerListEventType.UPDATED, timer)
|
||||
|
||||
async def async_remove_timer(self, timer_id: str) -> None:
|
||||
"""Remove a timer from the list regardless of its status."""
|
||||
timer = self._get_timer(timer_id)
|
||||
self._unschedule(timer_id)
|
||||
del self._timers[timer_id]
|
||||
self._notify(TimerListEventType.REMOVED, timer)
|
||||
|
||||
async def async_clear_finished_timers(self) -> None:
|
||||
"""Remove all finished and cancelled (archived) timers."""
|
||||
for timer_id in [
|
||||
timer.timer_id
|
||||
for timer in self._timers.values()
|
||||
if timer.status in _FINISHED_STATUSES
|
||||
]:
|
||||
timer = self._timers.pop(timer_id)
|
||||
self._notify(TimerListEventType.REMOVED, timer)
|
||||
|
||||
def _get_timer(self, timer_id: str) -> TimerItem:
|
||||
"""Return a timer by id or raise if it does not exist."""
|
||||
if (timer := self._timers.get(timer_id)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timer_not_found",
|
||||
translation_placeholders={"timer_id": timer_id},
|
||||
)
|
||||
return timer
|
||||
|
||||
@callback
|
||||
def _schedule(self, timer: TimerItem) -> None:
|
||||
"""Schedule (or reschedule) the finish callback for a timer."""
|
||||
self._unschedule(timer.timer_id)
|
||||
assert timer.finishes_at is not None
|
||||
self._cancel_callbacks[timer.timer_id] = async_track_point_in_utc_time(
|
||||
self.hass,
|
||||
partial(self._async_timer_finished, timer.timer_id),
|
||||
timer.finishes_at,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _unschedule(self, timer_id: str) -> None:
|
||||
"""Cancel a pending finish callback, if any."""
|
||||
if cancel := self._cancel_callbacks.pop(timer_id, None):
|
||||
cancel()
|
||||
|
||||
@callback
|
||||
def _async_timer_finished(self, timer_id: str, now: datetime) -> None:
|
||||
"""Handle a timer reaching its finish time."""
|
||||
self._cancel_callbacks.pop(timer_id, None)
|
||||
if (timer := self._timers.get(timer_id)) is None:
|
||||
return
|
||||
|
||||
timer.status = TimerStatus.FINISHED
|
||||
timer.finishes_at = None
|
||||
timer.remaining = None
|
||||
timer.finished_at = dt_util.utcnow()
|
||||
self._notify(TimerListEventType.FINISHED, timer)
|
||||
|
||||
if timer.finish_action == TimerFinishAction.REMOVE:
|
||||
self._timers.pop(timer_id, None)
|
||||
self._notify(TimerListEventType.REMOVED, timer)
|
||||
elif timer.finish_action == TimerFinishAction.RESTART:
|
||||
restarted_at = dt_util.utcnow()
|
||||
timer.status = TimerStatus.ACTIVE
|
||||
timer.created_at = restarted_at
|
||||
timer.finishes_at = restarted_at + timer.duration
|
||||
timer.finished_at = None
|
||||
self._schedule(timer)
|
||||
self._notify(TimerListEventType.STARTED, timer)
|
||||
|
||||
@final
|
||||
@callback
|
||||
def async_subscribe_updates(
|
||||
self, listener: Callable[[TimerListEvent], None]
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to timer change events.
|
||||
|
||||
Only future changes are pushed; the current set of timers is not
|
||||
replayed on subscribe.
|
||||
"""
|
||||
self._update_listeners.append(listener)
|
||||
|
||||
@callback
|
||||
def unsubscribe() -> None:
|
||||
self._update_listeners.remove(listener)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
@callback
|
||||
def _notify(self, event_type: TimerListEventType, timer: TimerItem) -> None:
|
||||
"""Push a change event to subscribers and write entity state."""
|
||||
event = TimerListEvent(event_type=event_type, item=copy.copy(timer))
|
||||
for listener in list(self._update_listeners):
|
||||
listener(event)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Cancel all pending finish callbacks."""
|
||||
for cancel in self._cancel_callbacks.values():
|
||||
cancel()
|
||||
self._cancel_callbacks.clear()
|
||||
|
||||
|
||||
async def _async_start_timer(
|
||||
entity: TimerListEntity, call: ServiceCall
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the start_timer service."""
|
||||
timer_id = await entity.async_start_timer(
|
||||
name=call.data.get(ATTR_NAME),
|
||||
duration=call.data[ATTR_DURATION],
|
||||
finish_action=call.data[ATTR_FINISH_ACTION],
|
||||
)
|
||||
return {ATTR_TIMER_ID: timer_id}
|
||||
|
||||
|
||||
async def _async_add_time(entity: TimerListEntity, call: ServiceCall) -> None:
|
||||
"""Handle the add_time service."""
|
||||
await entity.async_add_time(call.data[ATTR_TIMER_ID], call.data[ATTR_DURATION])
|
||||
|
||||
|
||||
async def _async_remove_time(entity: TimerListEntity, call: ServiceCall) -> None:
|
||||
"""Handle the remove_time service."""
|
||||
await entity.async_add_time(call.data[ATTR_TIMER_ID], -call.data[ATTR_DURATION])
|
||||
|
||||
|
||||
async def _async_get_timers(
|
||||
entity: TimerListEntity, call: ServiceCall
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the get_timers service."""
|
||||
now = dt_util.utcnow()
|
||||
statuses: list[TimerStatus] | None = call.data.get(ATTR_STATUS)
|
||||
return {
|
||||
"timers": [
|
||||
timer_to_dict(timer, now)
|
||||
for timer in entity.timers
|
||||
if not statuses or timer.status in statuses
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "timer_list/item/subscribe",
|
||||
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_handle_subscribe(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Subscribe to timer changes for a timer list, with an initial snapshot."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
if not (entity := hass.data[DATA_COMPONENT].get_entity(entity_id)):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_FOUND,
|
||||
f"Timer list entity not found: {entity_id}",
|
||||
)
|
||||
return
|
||||
|
||||
@callback
|
||||
def forward_event(event: TimerListEvent) -> None:
|
||||
"""Forward a timer change event to the websocket connection."""
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
{
|
||||
"type": "change",
|
||||
"event_type": event.event_type.value,
|
||||
"timer": timer_to_dict(event.item, dt_util.utcnow()),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
connection.subscriptions[msg["id"]] = entity.async_subscribe_updates(forward_event)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
now = dt_util.utcnow()
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
{
|
||||
"type": "timers",
|
||||
"timers": [timer_to_dict(timer, now) for timer in entity.timers],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "timer_list/item/list",
|
||||
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_handle_list(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Return the current timers for a timer list."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
if not (entity := hass.data[DATA_COMPONENT].get_entity(entity_id)):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_FOUND,
|
||||
f"Timer list entity not found: {entity_id}",
|
||||
)
|
||||
return
|
||||
|
||||
now = dt_util.utcnow()
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{"timers": [timer_to_dict(timer, now) for timer in entity.timers]},
|
||||
)
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Constants for the Timer list integration."""
|
||||
|
||||
from enum import IntFlag, StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import TimerListEntity
|
||||
|
||||
DOMAIN = "timer_list"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[TimerListEntity]] = HassKey(DOMAIN)
|
||||
|
||||
ATTR_TIMER_ID = "timer_id"
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_FINISH_ACTION = "finish_action"
|
||||
ATTR_FINISHES_AT = "finishes_at"
|
||||
ATTR_CREATED_AT = "created_at"
|
||||
ATTR_FINISHED_AT = "finished_at"
|
||||
ATTR_REMAINING = "remaining"
|
||||
ATTR_STATUS = "status"
|
||||
ATTR_TIMER = "timer"
|
||||
ATTR_TIMERS = "timers"
|
||||
|
||||
|
||||
class TimerListServices(StrEnum):
|
||||
"""Services for the Timer list integration."""
|
||||
|
||||
START_TIMER = "start_timer"
|
||||
PAUSE_TIMER = "pause_timer"
|
||||
UNPAUSE_TIMER = "unpause_timer"
|
||||
CANCEL_TIMER = "cancel_timer"
|
||||
CANCEL_ALL_TIMERS = "cancel_all_timers"
|
||||
ADD_TIME = "add_time"
|
||||
REMOVE_TIME = "remove_time"
|
||||
REMOVE_TIMER = "remove_timer"
|
||||
CLEAR_FINISHED_TIMERS = "clear_finished_timers"
|
||||
GET_TIMERS = "get_timers"
|
||||
|
||||
|
||||
class TimerStatus(StrEnum):
|
||||
"""Status of a single timer in a timer list."""
|
||||
|
||||
ACTIVE = "active"
|
||||
PAUSED = "paused"
|
||||
FINISHED = "finished"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class TimerFinishAction(StrEnum):
|
||||
"""What happens to a timer once it finishes."""
|
||||
|
||||
REMOVE = "remove"
|
||||
ARCHIVE = "archive"
|
||||
RESTART = "restart"
|
||||
|
||||
|
||||
class TimerListEventType(StrEnum):
|
||||
"""Type of change pushed to timer list subscribers."""
|
||||
|
||||
STARTED = "started"
|
||||
UPDATED = "updated"
|
||||
FINISHED = "finished"
|
||||
CANCELLED = "cancelled"
|
||||
REMOVED = "removed"
|
||||
|
||||
|
||||
class TimerListEntityFeature(IntFlag):
|
||||
"""Supported features of a timer list entity."""
|
||||
|
||||
START_TIMER = 1
|
||||
PAUSE_TIMER = 2
|
||||
CANCEL_TIMER = 4
|
||||
ADD_TIME = 8
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:timer-outline"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"add_time": {
|
||||
"service": "mdi:timer-plus-outline"
|
||||
},
|
||||
"cancel_all_timers": {
|
||||
"service": "mdi:timer-cancel-outline"
|
||||
},
|
||||
"cancel_timer": {
|
||||
"service": "mdi:timer-cancel-outline"
|
||||
},
|
||||
"clear_finished_timers": {
|
||||
"service": "mdi:timer-remove-outline"
|
||||
},
|
||||
"get_timers": {
|
||||
"service": "mdi:timer-outline"
|
||||
},
|
||||
"pause_timer": {
|
||||
"service": "mdi:timer-pause-outline"
|
||||
},
|
||||
"remove_time": {
|
||||
"service": "mdi:timer-minus-outline"
|
||||
},
|
||||
"remove_timer": {
|
||||
"service": "mdi:timer-remove-outline"
|
||||
},
|
||||
"start_timer": {
|
||||
"service": "mdi:timer-plus-outline"
|
||||
},
|
||||
"unpause_timer": {
|
||||
"service": "mdi:timer-play-outline"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"timer_cancelled": {
|
||||
"trigger": "mdi:timer-cancel-outline"
|
||||
},
|
||||
"timer_finished": {
|
||||
"trigger": "mdi:timer-check-outline"
|
||||
},
|
||||
"timer_started": {
|
||||
"trigger": "mdi:timer-play-outline"
|
||||
},
|
||||
"timer_updated": {
|
||||
"trigger": "mdi:timer-edit-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "timer_list",
|
||||
"name": "Timer list",
|
||||
"codeowners": ["@home-assistant/core", "@synesthesiam"],
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/timer_list",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
start_timer:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.START_TIMER
|
||||
fields:
|
||||
name:
|
||||
example: "Pasta"
|
||||
selector:
|
||||
text:
|
||||
duration:
|
||||
required: true
|
||||
example: "00:05:00"
|
||||
selector:
|
||||
duration:
|
||||
finish_action:
|
||||
default: remove
|
||||
selector:
|
||||
select:
|
||||
translation_key: finish_action
|
||||
options:
|
||||
- remove
|
||||
- archive
|
||||
- restart
|
||||
pause_timer:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.PAUSE_TIMER
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
unpause_timer:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.PAUSE_TIMER
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
cancel_timer:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.CANCEL_TIMER
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
cancel_all_timers:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.CANCEL_TIMER
|
||||
add_time:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.ADD_TIME
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
duration:
|
||||
required: true
|
||||
example: "00:01:00"
|
||||
selector:
|
||||
duration:
|
||||
remove_time:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
supported_features:
|
||||
- timer_list.TimerListEntityFeature.ADD_TIME
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
duration:
|
||||
required: true
|
||||
example: "00:01:00"
|
||||
selector:
|
||||
duration:
|
||||
remove_timer:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
fields:
|
||||
timer_id:
|
||||
required: true
|
||||
example: "01HZ8ABCDEF0123456789ABCDE"
|
||||
selector:
|
||||
text:
|
||||
clear_finished_timers:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
get_timers:
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
fields:
|
||||
status:
|
||||
example: "active"
|
||||
selector:
|
||||
select:
|
||||
translation_key: status
|
||||
multiple: true
|
||||
options:
|
||||
- active
|
||||
- paused
|
||||
- finished
|
||||
- cancelled
|
||||
@@ -1,154 +0,0 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::timer_list::title%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"timer_not_found": {
|
||||
"message": "Unable to find timer with id: {timer_id}"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"finish_action": {
|
||||
"options": {
|
||||
"archive": "Archive",
|
||||
"remove": "Remove",
|
||||
"restart": "Restart"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"options": {
|
||||
"active": "Active",
|
||||
"cancelled": "Cancelled",
|
||||
"finished": "Finished",
|
||||
"paused": "Paused"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"add_time": {
|
||||
"description": "Adds time to a timer.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "How much time to add to the timer.",
|
||||
"name": "Duration"
|
||||
},
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to add time to.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Add time"
|
||||
},
|
||||
"cancel_all_timers": {
|
||||
"description": "Cancels every active and paused timer on a timer list.",
|
||||
"name": "Cancel all timers"
|
||||
},
|
||||
"cancel_timer": {
|
||||
"description": "Cancels a timer.",
|
||||
"fields": {
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to cancel.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Cancel timer"
|
||||
},
|
||||
"clear_finished_timers": {
|
||||
"description": "Removes all finished and cancelled timers from a timer list.",
|
||||
"name": "Clear finished timers"
|
||||
},
|
||||
"get_timers": {
|
||||
"description": "Gets the timers on a timer list.",
|
||||
"fields": {
|
||||
"status": {
|
||||
"description": "Only return timers with the specified statuses.",
|
||||
"name": "Status"
|
||||
}
|
||||
},
|
||||
"name": "Get timers"
|
||||
},
|
||||
"pause_timer": {
|
||||
"description": "Pauses an active timer.",
|
||||
"fields": {
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to pause.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Pause timer"
|
||||
},
|
||||
"remove_time": {
|
||||
"description": "Removes time from a timer.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "How much time to remove from the timer.",
|
||||
"name": "Duration"
|
||||
},
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to remove time from.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Remove time"
|
||||
},
|
||||
"remove_timer": {
|
||||
"description": "Removes a timer from a timer list.",
|
||||
"fields": {
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to remove.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Remove timer"
|
||||
},
|
||||
"start_timer": {
|
||||
"description": "Creates and starts a new timer on a timer list.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "How long the timer should run for.",
|
||||
"name": "Duration"
|
||||
},
|
||||
"finish_action": {
|
||||
"description": "What happens to the timer once it finishes.",
|
||||
"name": "Finish action"
|
||||
},
|
||||
"name": {
|
||||
"description": "Optional name for the timer.",
|
||||
"name": "Name"
|
||||
}
|
||||
},
|
||||
"name": "Start timer"
|
||||
},
|
||||
"unpause_timer": {
|
||||
"description": "Resumes a paused timer.",
|
||||
"fields": {
|
||||
"timer_id": {
|
||||
"description": "The id of the timer to resume.",
|
||||
"name": "Timer ID"
|
||||
}
|
||||
},
|
||||
"name": "Resume timer"
|
||||
}
|
||||
},
|
||||
"title": "Timer list",
|
||||
"triggers": {
|
||||
"timer_cancelled": {
|
||||
"description": "Triggers when a timer is cancelled on a timer list.",
|
||||
"name": "Timer cancelled"
|
||||
},
|
||||
"timer_finished": {
|
||||
"description": "Triggers when a timer finishes on a timer list.",
|
||||
"name": "Timer finished"
|
||||
},
|
||||
"timer_started": {
|
||||
"description": "Triggers when a timer is started on a timer list.",
|
||||
"name": "Timer started"
|
||||
},
|
||||
"timer_updated": {
|
||||
"description": "Triggers when a timer is paused, resumed, or has time added or removed.",
|
||||
"name": "Timer updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
"""Provides triggers for timer lists."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
TriggerActionRunner,
|
||||
TriggerConfig,
|
||||
TriggerNotTriggeredReporter,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TimerListEvent, timer_to_dict
|
||||
from .const import ATTR_TIMER, DATA_COMPONENT, DOMAIN, TimerListEventType
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
vol.Required(CONF_OPTIONS, default={}): {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TimerEventListener(TargetEntityChangeTracker):
|
||||
"""Subscribe to timer change events for the targeted timer list entities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
target_selection: TargetSelection,
|
||||
listener: Callable[[str, TimerListEvent], None],
|
||||
) -> None:
|
||||
"""Initialize the listener."""
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
super().__init__(hass, target_selection, entity_filter)
|
||||
self._listener = listener
|
||||
self._unsubscribe_listeners: list[CALLBACK_TYPE] = []
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
|
||||
"""Resubscribe when the set of tracked entities changes."""
|
||||
for unsub in self._unsubscribe_listeners:
|
||||
unsub()
|
||||
self._unsubscribe_listeners = []
|
||||
|
||||
component = self._hass.data[DATA_COMPONENT]
|
||||
for entity_id in tracked_entities:
|
||||
if (entity := component.get_entity(entity_id)) is None:
|
||||
continue
|
||||
self._unsubscribe_listeners.append(
|
||||
entity.async_subscribe_updates(partial(self._listener, entity_id))
|
||||
)
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _unsubscribe(self) -> None:
|
||||
"""Unsubscribe from all events."""
|
||||
super()._unsubscribe()
|
||||
for unsub in self._unsubscribe_listeners:
|
||||
unsub()
|
||||
self._unsubscribe_listeners = []
|
||||
|
||||
|
||||
class TimerEventTrigger(Trigger):
|
||||
"""Trigger that fires on a specific timer change event type."""
|
||||
|
||||
_event_type: TimerListEventType
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self,
|
||||
run_action: TriggerActionRunner,
|
||||
did_not_trigger: TriggerNotTriggeredReporter | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
target_selection = TargetSelection(self._target)
|
||||
if not target_selection.has_any_target:
|
||||
raise HomeAssistantError(f"No target defined in {self._target}")
|
||||
|
||||
@callback
|
||||
def handle_event(entity_id: str, event: TimerListEvent) -> None:
|
||||
if event.event_type != self._event_type:
|
||||
return
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_TIMER: timer_to_dict(event.item, dt_util.utcnow()),
|
||||
},
|
||||
f"timer {self._event_type.value} on {entity_id}",
|
||||
)
|
||||
|
||||
listener = TimerEventListener(self._hass, target_selection, handle_event)
|
||||
return await listener.async_setup()
|
||||
|
||||
|
||||
class TimerStartedTrigger(TimerEventTrigger):
|
||||
"""Trigger when a timer starts."""
|
||||
|
||||
_event_type = TimerListEventType.STARTED
|
||||
|
||||
|
||||
class TimerUpdatedTrigger(TimerEventTrigger):
|
||||
"""Trigger when a timer is paused, resumed, or has time added/removed."""
|
||||
|
||||
_event_type = TimerListEventType.UPDATED
|
||||
|
||||
|
||||
class TimerFinishedTrigger(TimerEventTrigger):
|
||||
"""Trigger when a timer finishes."""
|
||||
|
||||
_event_type = TimerListEventType.FINISHED
|
||||
|
||||
|
||||
class TimerCancelledTrigger(TimerEventTrigger):
|
||||
"""Trigger when a timer is cancelled."""
|
||||
|
||||
_event_type = TimerListEventType.CANCELLED
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"timer_started": TimerStartedTrigger,
|
||||
"timer_updated": TimerUpdatedTrigger,
|
||||
"timer_finished": TimerFinishedTrigger,
|
||||
"timer_cancelled": TimerCancelledTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for timer lists."""
|
||||
return TRIGGERS
|
||||
@@ -1,9 +0,0 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: timer_list
|
||||
|
||||
timer_started: *trigger_common
|
||||
timer_updated: *trigger_common
|
||||
timer_finished: *trigger_common
|
||||
timer_cancelled: *trigger_common
|
||||
@@ -44,7 +44,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["tuya_sharing"],
|
||||
"requirements": [
|
||||
"tuya-device-handlers==0.0.22",
|
||||
"tuya-device-sharing-sdk==0.2.8"
|
||||
"tuya-device-handlers==0.0.24",
|
||||
"tuya-device-sharing-sdk==0.2.10"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==15.0.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):
|
||||
|
||||
@@ -149,7 +149,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_DISPLAY_ORIENTATION,
|
||||
attr_name=ATTR_DISPLAY_ORIENTATION,
|
||||
name="Display Orientation",
|
||||
options_map={
|
||||
"Portrait": "Forward",
|
||||
"LandscapeLeft": "Left",
|
||||
@@ -165,7 +164,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_MODE,
|
||||
attr_name=ATTR_MODE,
|
||||
name="Mode",
|
||||
set_method="set_mode",
|
||||
set_method_error_message="Setting the mode of the fan failed.",
|
||||
icon="mdi:fan",
|
||||
@@ -176,7 +174,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_LED_BRIGHTNESS,
|
||||
attr_name=ATTR_LED_BRIGHTNESS,
|
||||
name="Led Brightness",
|
||||
set_method="set_led_brightness",
|
||||
set_method_error_message="Setting the led brightness failed.",
|
||||
icon="mdi:brightness-6",
|
||||
@@ -187,7 +184,6 @@ SELECTOR_TYPES = (
|
||||
XiaomiMiioSelectDescription(
|
||||
key=ATTR_PTC_LEVEL,
|
||||
attr_name=ATTR_PTC_LEVEL,
|
||||
name="Auxiliary Heat Level",
|
||||
set_method="set_ptc_level",
|
||||
set_method_error_message="Setting the ptc level failed.",
|
||||
icon="mdi:fire-circle",
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
},
|
||||
"select": {
|
||||
"airpurifier_mode": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"favorite": "Favorite",
|
||||
@@ -145,6 +146,7 @@
|
||||
}
|
||||
},
|
||||
"display_orientation": {
|
||||
"name": "Display orientation",
|
||||
"state": {
|
||||
"forward": "Forward",
|
||||
"left": "Left",
|
||||
@@ -152,6 +154,7 @@
|
||||
}
|
||||
},
|
||||
"led_brightness": {
|
||||
"name": "LED brightness",
|
||||
"state": {
|
||||
"bright": "Bright",
|
||||
"dim": "Dim",
|
||||
@@ -159,6 +162,7 @@
|
||||
}
|
||||
},
|
||||
"ptc_level": {
|
||||
"name": "Auxiliary heat level",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
-1
@@ -12,7 +12,6 @@ FLOWS = {
|
||||
"group",
|
||||
"history_stats",
|
||||
"integration",
|
||||
"local_timer_list",
|
||||
"min_max",
|
||||
"mold_indicator",
|
||||
"otp",
|
||||
|
||||
-1
@@ -46,7 +46,6 @@ class EntityPlatforms(StrEnum):
|
||||
SWITCH = "switch"
|
||||
TEXT = "text"
|
||||
TIME = "time"
|
||||
TIMER_LIST = "timer_list"
|
||||
TODO = "todo"
|
||||
TTS = "tts"
|
||||
UPDATE = "update"
|
||||
|
||||
@@ -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": {
|
||||
@@ -4587,12 +4563,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"mycroft": {
|
||||
"name": "Mycroft",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"myneomitis": {
|
||||
"name": "MyNeomitis",
|
||||
"integration_type": "hub",
|
||||
@@ -7207,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",
|
||||
@@ -8448,12 +8412,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"local_timer_list": {
|
||||
"name": "Local Timer list",
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"manual": {
|
||||
"name": "Manual Alarm Control Panel",
|
||||
"integration_type": "helper",
|
||||
|
||||
@@ -740,7 +740,7 @@ def _get_exposed_entities(
|
||||
|
||||
if include_state and (
|
||||
attributes := {
|
||||
attr_name: (
|
||||
str(attr_name): (
|
||||
str(attr_value)
|
||||
if isinstance(attr_value, (Enum, Decimal, int))
|
||||
else attr_value
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.25.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.8.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260624.0
|
||||
home-assistant-frontend==20260624.1
|
||||
home-assistant-intents==2026.6.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -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
|
||||
|
||||
@@ -101,7 +101,6 @@ _ENTITY_COMPONENTS: set[str] = set(ENTITY_COMPONENTS).union(
|
||||
"script",
|
||||
"tag",
|
||||
"timer",
|
||||
"timer_list",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260624.0"
|
||||
FRONTEND_VERSION: Final[str] = "20260624.1"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+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
+10
-16
@@ -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.3
|
||||
aioamazondevices==14.1.8
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -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
|
||||
|
||||
@@ -1272,7 +1266,7 @@ hole==0.9.2
|
||||
holidays==0.99
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260624.0
|
||||
home-assistant-frontend==20260624.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.6.24
|
||||
@@ -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
|
||||
@@ -3221,10 +3215,10 @@ ttls==1.8.3
|
||||
ttn_client==1.3.0
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-handlers==0.0.22
|
||||
tuya-device-handlers==0.0.24
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.2.8
|
||||
tuya-device-sharing-sdk==0.2.10
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==3.0.0
|
||||
@@ -3245,7 +3239,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==15.0.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."
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user