Compare commits

..

52 Commits

Author SHA1 Message Date
Franck Nijhof 03fb4b099c Bump version to 2026.7.0b2 2026-06-27 11:31:01 +00:00
Manu 46a6048f60 Remove ATEN Rack PDU integration (#174940) 2026-06-27 11:29:22 +00:00
Manu c598d2c10e Remove Logentries (#174939) 2026-06-27 11:29:20 +00:00
Raphael Hehl 67bcd7550c Bump uiprotect to 15.3.0 (#174938) 2026-06-27 11:29:18 +00:00
Manu e44e822cec Remove Dovado integration (#174933) 2026-06-27 11:29:16 +00:00
Manu daff150276 Remove Greenwave Reality (#174929) 2026-06-27 11:29:15 +00:00
Michael 1f33859297 Check for supported fan speed modes in Synology DSM (#174925) 2026-06-27 11:29:13 +00:00
Simone Chemelli 512fe8c022 Bump aioamazondevices to 14.1.8 (#174924) 2026-06-27 11:29:11 +00:00
Raphael Hehl 6f038bb5b2 Bump uiprotect to 15.2.0 (#174922) 2026-06-27 11:29:09 +00:00
Allen Porter d0b5162507 Refactor Roborock time platform to use library property APIs (#174921) 2026-06-27 11:29:08 +00:00
Allen Porter 9e9978b6cb Bump voluptuous-openapi to 0.4.1 (#174912) 2026-06-27 11:29:06 +00:00
Jordan Harvey 76feb821f4 Bump pyanglianwater to 3.2.3 (#174902) 2026-06-27 11:29:04 +00:00
Ronald van der Meer cd41529a89 Fix Duco ventilation state select not being created for valve nodes (#174901) 2026-06-27 11:29:02 +00:00
starkillerOG 8a1434332d Bump reolink_aio to 0.21.3 (#174879) 2026-06-27 11:29:00 +00:00
starkillerOG 36b714b513 Add Reolink push command IDs (#174876) 2026-06-27 11:28:59 +00:00
Paulus Schoutsen 46f1e4c957 Fix Roborock time entity crash when timer value is missing (#174873)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 11:28:57 +00:00
Paulus Schoutsen 431dcda092 Fix Roborock number entity crash when volume is None (#174872)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 11:28:55 +00:00
Mick Vleeshouwer c575ef51b9 Set RTS command duration for Overkiz Rexel client (#174863) 2026-06-27 11:28:54 +00:00
Simone Chemelli 87690d2000 Handle all login exceptions in Vodafone Station (#174852)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-27 11:28:52 +00:00
Manu 7afb26b1c0 Remove Mycroft integration (#174849) 2026-06-27 11:28:50 +00:00
Simone Chemelli b6d5af0480 Bump aioamazondevices to 14.1.6 (#174848) 2026-06-27 11:28:48 +00:00
Raphael Hehl f56098df5f Bump uiprotect to 15.1.0 (#174846) 2026-06-27 11:28:46 +00:00
Manu 427dd028f5 Remove ThermoWorks Smoke (#174845) 2026-06-27 11:28:44 +00:00
Ludovic BOUÉ cef9461610 Bump roborock dependencies to 5.21.0 (#174841) 2026-06-27 11:28:42 +00:00
epenet ace5398012 Bump tuya-device-handlers to 0.0.24 (#174840)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-27 11:28:41 +00:00
Bram Kragten 3791c83b95 Update frontend to 20260624.1 (#174831) 2026-06-27 11:28:39 +00:00
epenet 4bdfa5c25b Bump tuya-device-sharing-sdk to 0.2.10 (#174827) 2026-06-27 11:28:37 +00:00
Nicolas Mowen 5a60771a14 Handle case where GetLiveContext includes an entity with StrEnum key (#174822) 2026-06-27 11:28:35 +00:00
Erik Montnemery 70aba68326 Fix exception in legacy sun condition (#174811) 2026-06-27 11:28:33 +00:00
Paul Bottein f20f86a067 Fix missing translated names for Xiaomi Miio select entities (#174810) 2026-06-27 11:28:32 +00:00
Erik Montnemery ee0c98e450 Improve tests of sun conditions and triggers (#174805)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-27 11:28:30 +00:00
Rafa PA 907a5c3c6c [aemet] Increase weather update interval to 20 minutes (#174803) 2026-06-27 11:28:28 +00:00
Erik Montnemery a1e1b400f3 Catch errors when evaluating automation conditions (#174799) 2026-06-27 11:28:27 +00:00
Erik Montnemery 1544ae83dd Improve tests of entity limits (#174793) 2026-06-27 11:28:25 +00:00
Raphael Hehl 145c490816 Bump uiprotect to 15.0.0 (#174709) 2026-06-27 11:28:23 +00:00
Samuel Xiao bb7a756f84 Bump switchbot-api to 2.12.0 (#174705) 2026-06-27 11:28:21 +00:00
J. Nick Koston 183e6af8c2 Bump habluetooth to 6.25.1 (#174700) 2026-06-27 11:28:19 +00:00
Franck Nijhof 2b66d045ff Add missing unit of measurement to Home Connect battery sensor (#174694)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-27 11:28:17 +00:00
Michael Hansen 4841329814 Bump intents and fix broken tests (#174689) 2026-06-27 11:28:16 +00:00
J. Nick Koston e710fc8782 Bump habluetooth to 6.24.0 (#174688) 2026-06-27 11:28:14 +00:00
Franck Nijhof 99e18dcdd8 Add delegated charging mode to Renault integration (#174687) 2026-06-27 11:28:12 +00:00
Simone Chemelli eeedf28b6f Fix async_get_entity_id() params for Alexa Devices (#174641) 2026-06-27 11:28:09 +00:00
Arie Catsman 5cca9328d6 Update enphase_envoy diagnostics for pyenphase lib v3.0.0 (#174524) 2026-06-27 11:28:08 +00:00
Erik Montnemery ebf3de3073 Add WS command recorder/entity_options/get (#174134) 2026-06-27 11:28:06 +00:00
Stefan Agner 41e79927d0 Fix hassio job subscribe returning None instead of unsubscribe callback (#174063)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-27 11:28:04 +00:00
Franck Nijhof 4022eb93de Bump version to 2026.7.0b1 2026-06-24 21:29:59 +00:00
Christian Lackas 28171dfe90 Bump homematicip to 2.13.2 (#174673) 2026-06-24 21:29:51 +00:00
Erwin Douna e9af932fbe Vera core group executor job (#174669) 2026-06-24 21:29:49 +00:00
Erwin Douna 3212e0f051 Tami4 group executor job (#174668) 2026-06-24 21:29:47 +00:00
TheJulianJES 87f0720450 Bump zha-quirks to 2.1.0 (#174662) 2026-06-24 21:29:45 +00:00
Brandon Rothweiler 534ff3f3dc Add missing scope and authorize param to Dropbox OAuth (#174587) 2026-06-24 21:29:43 +00:00
Franck Nijhof 1096c8af13 Bump version to 2026.7.0b0 2026-06-24 16:36:39 +00:00
141 changed files with 1162 additions and 4072 deletions
-1
View File
@@ -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
View File
@@ -1,5 +1,5 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"36a7fc263a2ce868d74a266f23eb7772d82fd397806464384fe087479ddd4a70","body_hash":"bba8c011f2b82bb4d9847a359f43f0e7d91245b280678c20e5112b3c9e77d5cd","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5c2fe865bb4dc46e1450f6ee0d0541d759aea73a","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7b142e96e0f8b454cdcc9c0c25070cf9a52c44d83a6b1fbc3ad6725b6567337c","body_hash":"3894ded07d5934ac5f29d160ffb1f9115cf72b6da8a7e453a4d4f69e8641a48e","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.79.6","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -31,12 +31,12 @@
# - GITHUB_TOKEN
#
# Custom actions used:
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
# - github/gh-aw-actions/setup@v0.79.6
#
# Container images used:
# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -155,7 +155,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- name: Checkout .github and .agents folders
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
@@ -344,8 +344,9 @@ jobs:
agent:
needs:
- activation
- prepare
if: (needs.prepare.outputs.skip != 'true') && (needs.activation.outputs.daily_effective_workflow_exceeded != 'true')
- extract_pr_number
- gate
if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true'
runs-on: ubuntu-latest
permissions:
actions: read
@@ -382,7 +383,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -403,7 +404,7 @@ jobs:
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
} >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Create gh-aw temp directory
@@ -488,15 +489,15 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_823c5547a5e52957_EOF'
{"add_comment":{"max":1,"target":"${{ needs.prepare.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_823c5547a5e52957_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f496a449c5dccca1_EOF'
{"add_comment":{"max":1,"target":"${{ needs.extract_pr_number.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_f496a449c5dccca1_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
{
"description_suffixes": {
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.prepare.outputs.pr_number }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -993,7 +994,8 @@ jobs:
- activation
- agent
- detection
- prepare
- extract_pr_number
- gate
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1016,7 +1018,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1206,7 +1208,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1234,7 +1236,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
- name: Checkout repository for patch context
if: needs.agent.outputs.has_patch == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# --- Threat Detection ---
@@ -1427,6 +1429,111 @@ jobs:
}
}
extract_pr_number:
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
gate:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
- name: Decide whether requirements changed since the last comment
id: gate
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
pre_activation:
runs-on: ubuntu-slim
outputs:
@@ -1438,7 +1545,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1461,48 +1568,12 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs');
await main();
prepare:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
pr_number: ${{ steps.prepare.outputs.pr_number }}
skip: ${{ steps.prepare.outputs.skip }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Resolve skip and PR number from the artifact
id: prepare
run: |
echo "skip=$(jq -r '.skip_aw' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
echo "pr_number=$(jq -r '.pr_number' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
safe_outputs:
needs:
- activation
- agent
- detection
- prepare
- extract_pr_number
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
@@ -1538,7 +1609,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
uses: github/gh-aw-actions/setup@v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1583,7 +1654,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,files.pythonhosted.org,github.com,host.docker.internal,pip.pypa.io,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,telemetry.enterprise.githubcopilot.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.prepare.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.extract_pr_number.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+68 -15
View File
@@ -15,41 +15,94 @@ tools:
github:
toolsets: [repos, pull_requests]
min-integrity: unapproved
if: needs.prepare.outputs.skip != 'true'
safe-outputs:
add-comment:
max: 1
target: "${{ needs.prepare.outputs.pr_number }}"
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
needs:
- prepare
- extract_pr_number
jobs:
prepare:
# The deterministic stage always uploads an artifact; its `skip_aw` flag is
# true when no tracked requirement file changed since the last comment,
# which is our cue to skip the (token-spending) agent. Recover the PR number
# to comment on either way.
gate:
# Skip the (token-spending) agent when no tracked requirement file changed
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.prepare.outputs.skip }}
pr_number: ${{ steps.prepare.outputs.pr_number }}
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Decide whether requirements changed since the last comment
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
extract_pr_number:
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Download deterministic-results artifact
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve skip and PR number from the artifact
id: prepare
- name: Extract PR number from artifact
id: extract
run: |
echo "skip=$(jq -r '.skip_aw' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
echo "pr_number=$(jq -r '.pr_number' /tmp/deterministic/results.json)" >> "${GITHUB_OUTPUT}"
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.id }}
cancel-in-progress: true
+1 -1
View File
@@ -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
View File
@@ -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"]
}
-119
View File
@@ -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
+8 -7
View File
@@ -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"]
}
-38
View File
@@ -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"
]
-143
View File
@@ -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.
+16 -1
View File
@@ -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)
+7 -6
View File
@@ -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."""
-123
View File
@@ -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"]
}
+1 -1
View File
@@ -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"
}
+6 -4
View File
@@ -12,9 +12,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -30,7 +32,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.AIR_HUMIDITY: SensorEntityDescription(
key="air_humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.AIR_PRESSURE: SensorEntityDescription(
@@ -47,7 +49,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.ECO2: SensorEntityDescription(
key="eco2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.LIGHT: SensorEntityDescription(
@@ -65,7 +67,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.VOC: SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -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}
)
+3 -1
View File
@@ -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
),
)
+3 -3
View File
@@ -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,
+6 -5
View File
@@ -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
+6 -2
View File
@@ -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"
]
}
+4 -2
View File
@@ -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)),
)
]
+5 -13
View File
@@ -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
+14 -20
View File
@@ -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
+2 -2
View File
@@ -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"]
}
+17 -17
View File
@@ -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%]",
+2 -2
View File
@@ -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)
-1
View File
@@ -12,7 +12,6 @@ FLOWS = {
"group",
"history_stats",
"integration",
"local_timer_list",
"min_max",
"mold_indicator",
"otp",
-1
View File
@@ -46,7 +46,6 @@ class EntityPlatforms(StrEnum):
SWITCH = "switch"
TEXT = "text"
TIME = "time"
TIMER_LIST = "timer_list"
TODO = "todo"
TTS = "tts"
UPDATE = "update"
-42
View File
@@ -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",
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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
+10 -16
View File
@@ -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
-1
View File
@@ -19,7 +19,6 @@ mock-open==1.4.0
mypy==2.1.0
prek==0.2.28
pydantic==2.13.4
PyGithub==2.9.1
pylint==4.0.6
pylint-per-file-ignores==3.2.1
pipdeptree==2.26.1
+14 -38
View File
@@ -2,28 +2,12 @@
import argparse
import json
import os
from pathlib import Path
import sys
from .gate import GateDecision, decide_skip
from .models import CheckRunResult
from .runner import run_checks
def _resolve_skip(pr_number: int, head_sha: str | None) -> GateDecision:
"""Decide whether this run can skip re-checking the PR.
Needs the repo and a token (from the Actions environment) to read prior
comments; without them it falls open and runs the checks.
"""
repo = os.environ.get("GITHUB_REPOSITORY")
token = os.environ.get("GITHUB_TOKEN")
if not head_sha or not repo or not token:
return GateDecision(False, "Gate inputs unavailable; running checks.")
return decide_skip(pr_number, head_sha, repo, token)
def main(argv: list[str] | None = None) -> int:
"""Run the deterministic check_requirements stage and write its artifact."""
parser = argparse.ArgumentParser(prog="python -m script.check_requirements")
@@ -47,32 +31,24 @@ def main(argv: list[str] | None = None) -> int:
)
args = parser.parse_args(argv)
decision = _resolve_skip(args.pr_number, args.head_sha)
print(decision.reason, file=sys.stderr)
if decision.skip:
result = CheckRunResult(
pr_number=args.pr_number, head_sha=args.head_sha, skip_aw=True
)
else:
try:
diff_text = args.diff.read_text(encoding="utf-8")
except FileNotFoundError:
parser.error(f"input file {args.diff} not found")
result = run_checks(
pr_number=args.pr_number,
diff_text=diff_text,
head_sha=args.head_sha,
)
print(
f"check_requirements: {len(result.packages)} package change(s); "
f"needs_agent={result.needs_agent}",
file=sys.stderr,
)
try:
diff_text = args.diff.read_text(encoding="utf-8")
except FileNotFoundError:
parser.error(f"input file {args.diff} not found")
result = run_checks(
pr_number=args.pr_number,
diff_text=diff_text,
head_sha=args.head_sha,
)
args.output.write_text(
json.dumps(result.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
print(
f"check_requirements: {len(result.packages)} package change(s); "
f"needs_agent={result.needs_agent}",
file=sys.stderr,
)
return 0
+2 -4
View File
@@ -17,13 +17,11 @@ from .models import PackageChange
# of truth for pinned package changes.
TRACKED_PATTERNS = (
"requirements*.txt",
"**/requirements*.txt",
"homeassistant/package_constraints.txt",
)
def is_tracked(path: str) -> bool:
"""Return True if `path` is a requirement file the checks care about."""
def _is_tracked(path: str) -> bool:
return any(fnmatchcase(path, pattern) for pattern in TRACKED_PATTERNS)
@@ -63,7 +61,7 @@ def parse_diff(diff_text: str) -> list[PackageChange]:
added: dict[str, _Pin] = {}
removed: dict[str, _Pin] = {}
for patched_file in PatchSet(diff_text):
if not is_tracked(patched_file.path):
if not _is_tracked(patched_file.path):
continue
for hunk in patched_file:
for line in hunk:
-107
View File
@@ -1,107 +0,0 @@
"""Decide whether the deterministic stage can skip re-checking a PR.
The deterministic stage re-runs on every `synchronize` where the PR touches a
tracked requirement file, even when the latest push changed only unrelated
files. This module answers "did a tracked requirement file actually change
since we last commented?" so the stage can skip the PyPI work and flag the
uploaded artifact as skipped, telling the agentic stage to no-op.
"""
from dataclasses import dataclass
import logging
import os
import re
from github import Auth, Github, GithubException
from github.IssueComment import IssueComment
from .diff import is_tracked
from .render import COMMIT_PATH
_LOGGER = logging.getLogger(__name__)
# The "Checked at commit [`abc1234`](...COMMIT_PATH<40-hex>)." link rendered by
# render._intro is the only place the head SHA is recorded in the comment.
_COMMIT_SHA_RE = re.compile(re.escape(COMMIT_PATH) + r"([0-9a-f]{40})", re.IGNORECASE)
_TRUSTED_AUTHOR = "github-actions[bot]"
def _is_trusted_author(comment: IssueComment) -> bool:
"""True only for the github-actions bot that posts the check comment."""
return comment.user is not None and comment.user.login == _TRUSTED_AUTHOR
@dataclass(slots=True, frozen=True)
class GateDecision:
"""Whether to skip the deterministic checks, with a human-readable reason."""
skip: bool
reason: str
def _client(token: str) -> Github:
"""A lazy GitHub client on the configured (possibly GHES) API base."""
base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/")
return Github(auth=Auth.Token(token), base_url=base_url, lazy=True)
def fetch_marker_comment_bodies(pr_number: int, repo: str, token: str) -> list[str]:
"""Return the trusted requirements-check comment bodies, oldest-first."""
try:
comments = _client(token).get_repo(repo).get_issue(pr_number).get_comments()
return [comment.body for comment in comments if _is_trusted_author(comment)]
except GithubException as err:
_LOGGER.warning("Could not read comments for PR #%s: %s", pr_number, err)
return []
def extract_prior_sha(bodies: list[str]) -> str | None:
"""Return the head SHA recorded in the most recent marker comment."""
shas = [
match.group(1).lower()
for body in bodies
for match in _COMMIT_SHA_RE.finditer(body)
]
return shas[-1] if shas else None
def compare_changed_files(
base: str, head: str, repo: str, token: str
) -> list[str] | None:
"""Return filenames changed between two commits, or None if unavailable."""
try:
comparison = _client(token).get_repo(repo).compare(base, head)
return [changed.filename for changed in comparison.files]
except GithubException as err:
_LOGGER.warning("Could not compare %s...%s: %s", base, head, err)
return None
def decide_skip(pr_number: int, head_sha: str, repo: str, token: str) -> GateDecision:
"""Decide whether requirements changed since the last comment."""
if not head_sha:
return GateDecision(False, "No head SHA available; running checks.")
prior = extract_prior_sha(fetch_marker_comment_bodies(pr_number, repo, token))
if prior is None:
return GateDecision(
False, "No previous requirements-check comment; running checks."
)
if prior == head_sha.lower():
return GateDecision(
True, f"Head {head_sha} unchanged since the last comment; skipping."
)
changed = compare_changed_files(prior, head_sha, repo, token)
if changed is None:
return GateDecision(
False, f"Could not compare {prior}...{head_sha}; running checks."
)
tracked = [path for path in changed if is_tracked(path)]
if tracked:
return GateDecision(
False,
f"Tracked requirement files changed since {prior}; running checks: "
+ ", ".join(tracked),
)
return GateDecision(
True, f"No tracked requirement files changed since {prior}; skipping."
)

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