Compare commits

..

86 Commits

Author SHA1 Message Date
Willem-Jan van Rootselaar 486385e308 Bump python-bsblan to version 6.1.4 (#174974) 2026-06-27 18:02:38 +03:00
Raj Laud 11113ce1f4 Add Orion XS and Inverter support to Victron BLE (#169834)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-27 16:48:19 +02:00
Øyvind Matheson Wergeland 31dc1f9519 Drop nobo_hub custom select device class (#170135)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-27 16:44:57 +02:00
On Freund 926900d0ca Bump pymonoprice to 0.6.1 (#174951)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 09:49:03 -04:00
OzGav 06b429d0b7 Update music-assistant-client to 1.3.6 (#174968) 2026-06-27 15:31:17 +02:00
Åke Strandberg 9fb2d14fdb Handle SSE updates of single miele devices (#174955) 2026-06-27 15:26:57 +02:00
Manu 0e6279e273 Remove Watson TTS integration (#174967) 2026-06-27 15:17:41 +02:00
TimL 02ddd4c3f4 Bump Pysmlight to v0.5.0 (#174947) 2026-06-27 15:58:27 +03:00
Michael c147321aba Migrate demo to UnitOfRatio enums (#174966) 2026-06-27 15:56:34 +03:00
Michael f0e75ebb83 Bump aioimmich to 0.15.1 (#174962) 2026-06-27 15:55:18 +03:00
Manu 7ae85d8d97 Remove BlinkStick (#174961) 2026-06-27 14:29:30 +02:00
g4bri3lDev 43802961e9 Fix swallowed exception in opendisplay upload_image action handler (#170660) 2026-06-27 14:19:47 +02:00
Øyvind Matheson Wergeland 97844d7ee0 Filter already-configured hubs from nobo_hub config flow (#169593)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-27 14:18:53 +02:00
renovate[bot] e8e5eedd7e Update pytest-github-actions-annotate-failures to 0.4.2 (#174941) 2026-06-27 10:41:27 +02:00
Erwin Douna bbf7a60a9e ProxmoxVE refactor to use text selectors (#174949) 2026-06-27 10:19:38 +02:00
Erwin Douna fac6e8feb6 MELCloud Home add current password config flow (#174950) 2026-06-27 09:26:20 +02:00
Arie Catsman 86d282ac2a remove pyenphase library deprecated data fields from enphase_envoy fixtures (#174928) 2026-06-27 08:30:48 +02:00
Manuel Stahl cb407b8163 Use async config flow in stiebel_eltron integration (#174937) 2026-06-26 21:25:53 -04:00
Manu 61b3dda20f Remove Dovado integration (#174933) 2026-06-26 21:23:51 -04:00
Manu 951ec34e7c Remove Logentries (#174939) 2026-06-26 21:23:29 -04:00
Manu 57a441aa4a Remove ATEN Rack PDU integration (#174940) 2026-06-26 21:23:07 -04:00
Raphael Hehl 1e71607063 Bump uiprotect to 15.3.0 (#174938) 2026-06-27 02:48:58 +02:00
Manuel Stahl 952fdec1ad Extract Modbus schemas to schemas.py (#174935)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-26 20:26:56 -04:00
Erwin Douna c9bc041cac ProxmoxVE add typing to auth kwargs (#174930) 2026-06-26 23:56:55 +02:00
Erwin Douna 2047042803 ProxmoxVE add stale device removal (#174932) 2026-06-26 23:51:13 +02:00
Manuel Stahl 9f92c8438e Use async coordinator in stiebel_eltron integration (#174850)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:32:25 -04:00
Allen Porter a0c6d4fb52 Refactor Roborock time platform to use library property APIs (#174921) 2026-06-26 17:29:02 -04:00
Manu f315273404 Remove Greenwave Reality (#174929) 2026-06-26 22:28:43 +02:00
Michael 38baeba81c Check for supported fan speed modes in Synology DSM (#174925) 2026-06-26 21:45:32 +02:00
Raphael Hehl d246a7a7e5 Bump uiprotect to 15.2.0 (#174922) 2026-06-26 21:44:09 +02:00
Simone Chemelli 29f12d6e80 Bump aioamazondevices to 14.1.8 (#174924) 2026-06-26 20:47:21 +02:00
Jordan Harvey e01ed866d9 Bump pyanglianwater to 3.2.3 (#174902) 2026-06-26 18:30:37 +02:00
Arie Catsman 3ce5630809 remove enphase_envoy test for to-be-deprecated pyenphase library data (#174908) 2026-06-26 18:29:24 +02:00
Ronald van der Meer 344ae6f604 Fix Duco ventilation state select not being created for valve nodes (#174901) 2026-06-26 18:28:28 +02:00
Simone Chemelli e8b1acea6c Clarify and align domain/platform usage in Alexa Devices (#174914) 2026-06-26 18:27:08 +02:00
Simone Chemelli 24d6a0420c Clarify and align domain/platform usage in Fritz (#174915) 2026-06-26 18:26:40 +02:00
TheOtherAdam 393424fa88 Allow disabling managed log file (#170374)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-26 16:45:55 +01:00
Manu 585dd72366 Add reconfigure to Steam integration (#174696) 2026-06-26 17:01:36 +02:00
A. Gideonse 7e2343243d Add charge/discharge remaining time to Indevolt (#174600) 2026-06-26 16:42:47 +02:00
Simone Chemelli de8ed15ea8 Handle all login exceptions in Vodafone Station (#174852)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-26 16:34:52 +02:00
Allen Porter 9a21d92908 Bump voluptuous-openapi to 0.4.1 (#174912) 2026-06-26 16:12:28 +02:00
GSzabados 12ea0c4d77 Update units UnitOfDensity / UnitOfRatio for airnow (#174905) 2026-06-26 14:57:50 +02:00
prana-dev-official 94c4483735 Update units UnitOfRatio enums for Prana (#174907)
Co-authored-by: yuriipopow <yurapopov522@gmail.com>
2026-06-26 14:23:04 +02:00
GSzabados 053efdf662 Update units UnitOfDensity / UnitOfRatio for airthings_ble (#174906) 2026-06-26 14:16:49 +02:00
GSzabados 3c9f55f7b2 Update units UnitOfDensity / UnitOfRatio for ambient_station (#174900) 2026-06-26 12:53:23 +02:00
GSzabados a46b434930 Update measurement units UnitOfDensity / UnitOfRatio for Tomorrow.io (#174899) 2026-06-26 12:42:49 +02:00
Renat Sibgatulin 031e764957 Migrate the airq to the new enums (UnitOfDensity / UnitOfRatio) (#174896) 2026-06-26 12:26:52 +02:00
Paulus Schoutsen d9f0faf365 Fix Roborock time entity crash when timer value is missing (#174873)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 06:25:55 -04:00
Joost Lekkerkerker a14e5d8a0c Change Airthings BLE level entities in enum device class (#174815) 2026-06-26 12:14:49 +02:00
Manu f356a1cd0d Remove ThermoWorks Smoke (#174845) 2026-06-26 11:39:56 +02:00
starkillerOG 50c12d85f8 Add Reolink push command IDs (#174876) 2026-06-26 11:38:48 +02:00
javicalle a88b43d845 Migrate rflink to UnitOfRatio enum (#174886) 2026-06-26 11:21:21 +02:00
GSzabados b9e59522e3 Update measurement units UnitOfDensity / UnitOfRatio for EcoWitt sensors (#174887) 2026-06-26 11:19:21 +02:00
Ludovic BOUÉ 1484384d63 Bump roborock dependencies to 5.21.0 (#174841) 2026-06-26 11:06:00 +02:00
Paulus Schoutsen 1d1ab798df Fix Roborock number entity crash when volume is None (#174872)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 11:05:12 +02:00
Erwin Douna 926d2f1e21 Shelly refactor UnitsOf (#174883) 2026-06-26 11:01:23 +02:00
dependabot[bot] b341228b4b Bump actions/checkout from 6.0.3 to 7.0.0 (#174875)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-26 10:58:47 +02:00
renovate[bot] 241f850e90 Update pytest-github-actions-annotate-failures to 0.4.1 (#174868) 2026-06-26 10:34:57 +02:00
Rafa PA 257040ac51 [aemet] Increase weather update interval to 20 minutes (#174803) 2026-06-26 10:01:09 +02:00
Arie Catsman e3c17026d0 Update enphase_envoy diagnostics for pyenphase lib v3.0.0 (#174524) 2026-06-26 10:00:35 +02:00
Stefan Agner 9e4550dd14 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-26 09:56:50 +02:00
starkillerOG 37f441d3da Bump reolink_aio to 0.21.3 (#174879) 2026-06-26 09:53:04 +02:00
Maciej Bieniek 0099100d14 Use UnitOfDensity enum in GIOS (#174855) 2026-06-26 07:59:40 +02:00
Maciej Bieniek e20f74dac5 Use new unit enums in NAM (#174856) 2026-06-26 07:59:37 +02:00
John Hillery f7aa6ef384 Bump nexia to 2.13.0 (#174652) 2026-06-26 07:07:25 +02:00
Raphael Hehl 71342ef1f6 Bump uiprotect to 15.1.0 (#174846) 2026-06-25 22:27:17 -04:00
Simone Chemelli 544dccd50b Bump aioamazondevices to 14.1.6 (#174848) 2026-06-25 22:26:39 -04:00
Manu 5f6508c424 Remove Mycroft integration (#174849) 2026-06-25 22:25:47 -04:00
Maciej Bieniek 70f3526be3 Use UnitOfDensity / UnitOfRatio enums in Airly (#174854) 2026-06-25 22:25:28 -04:00
Mick Vleeshouwer f5f2ecadfd Migrate to UnitOfRatio enum in Overkiz (#174862) 2026-06-25 22:25:11 -04:00
Mick Vleeshouwer a8de57a1c6 Set RTS command duration for Overkiz Rexel client (#174863) 2026-06-25 22:24:43 -04:00
Erwin Douna 8b08f10f78 Tuya refactor UnitOfs (#174835) 2026-06-25 20:33:07 +02:00
Erik Montnemery 34679b033a Improve tests of entity limits (#174793) 2026-06-25 20:23:50 +02:00
Paul Bottein 014f785050 Fix missing translated names for Xiaomi Miio select entities (#174810) 2026-06-25 20:23:13 +02:00
epenet 1c53ada438 Bump tuya-device-handlers to 0.0.24 (#174840)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-25 20:22:42 +02:00
Erwin Douna a88de3efab Matter refactor to UnitOfs (#174824) 2026-06-25 19:54:53 +02:00
Nicolas Mowen 10ce428387 Handle case where GetLiveContext includes an entity with StrEnum key (#174822) 2026-06-25 19:53:32 +02:00
Erwin Douna dae8b60ec4 Tradfri refactor UnitOfs (#174834) 2026-06-25 19:08:12 +02:00
Bram Kragten e098cc384c Update frontend to 20260624.1 (#174831) 2026-06-25 18:47:17 +02:00
J. Nick Koston 6007bfa1cd Bump aioesphomeapi to 45.5.2 (#174826) 2026-06-25 18:31:51 +02:00
epenet 16262362e1 Bump tuya-device-sharing-sdk to 0.2.10 (#174827) 2026-06-25 17:57:33 +02:00
Erik Montnemery 01350e8f15 Fix exception in legacy sun condition (#174811) 2026-06-25 17:40:36 +02:00
Manu 8977fc6f67 Migrate entity unique_id in Steam integration (#174701) 2026-06-25 16:54:53 +02:00
Robert Resch 6411cc5c48 Improve the gate on check requirements aw to avoid useless runs (#174599) 2026-06-25 16:48:51 +02:00
Erik Montnemery 9ce56183ea Add WS command recorder/entity_options/get (#174134) 2026-06-25 15:59:29 +02:00
MoonDevLT 5117c0b964 Migrate lunatone to UnitOfRatio enums (#174817) 2026-06-25 15:01:10 +02:00
216 changed files with 8979 additions and 3989 deletions
@@ -12,6 +12,7 @@ on:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "**/requirements*.txt"
- "homeassistant/package_constraints.txt"
workflow_dispatch:
inputs:
@@ -58,6 +59,7 @@ 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: |
+54 -125
View File
@@ -1,5 +1,5 @@
# 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"}]}
# 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"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -36,7 +36,7 @@
# - 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@v0.79.6
# - github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # 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@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -344,9 +344,8 @@ jobs:
agent:
needs:
- activation
- extract_pr_number
- gate
if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true'
- prepare
if: (needs.prepare.outputs.skip != 'true') && (needs.activation.outputs.daily_effective_workflow_exceeded != 'true')
runs-on: ubuntu-latest
permissions:
actions: read
@@ -383,7 +382,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -489,15 +488,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_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
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
- 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.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.prepare.outputs.pr_number }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -994,8 +993,7 @@ jobs:
- activation
- agent
- detection
- extract_pr_number
- gate
- prepare
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1018,7 +1016,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1208,7 +1206,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1429,111 +1427,6 @@ 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:
@@ -1545,7 +1438,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1568,12 +1461,48 @@ 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
- extract_pr_number
- prepare
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
@@ -1609,7 +1538,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@v0.79.6
uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1654,7 +1583,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.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_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\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+15 -68
View File
@@ -15,94 +15,41 @@ tools:
github:
toolsets: [repos, pull_requests]
min-integrity: unapproved
if: needs.prepare.outputs.skip != 'true'
safe-outputs:
add-comment:
max: 1
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
target: "${{ needs.prepare.outputs.pr_number }}"
needs:
- extract_pr_number
- prepare
jobs:
gate:
# Skip the (token-spending) agent when no tracked requirement file changed
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.
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: 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 }}
skip: ${{ steps.prepare.outputs.skip }}
pr_number: ${{ steps.prepare.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: Extract PR number from artifact
id: extract
- name: Resolve skip and PR number from the artifact
id: prepare
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
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}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.id }}
cancel-in-progress: true
Generated
-2
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
@@ -1987,7 +1986,6 @@ CLAUDE.md @home-assistant/core
/tests/components/waterfurnace/ @sdague @masterkoppa
/homeassistant/components/watergate/ @adam-the-hero
/tests/components/watergate/ @adam-the-hero
/homeassistant/components/watson_tts/ @rutkai
/homeassistant/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
/tests/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
/homeassistant/components/watttime/ @bachya
+36 -3
View File
@@ -66,6 +66,7 @@ from .const import (
BASE_PLATFORMS,
FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING,
KEY_DATA_LOGGING_DISABLED_REASON as DATA_LOGGING_DISABLED_REASON,
SIGNAL_BOOTSTRAP_INTEGRATIONS,
)
from .core_config import async_process_ha_core_config
@@ -129,6 +130,11 @@ SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
ERROR_LOG_FILENAME = "home-assistant.log"
ENV_DISABLE_LOG_FILE = "HA_DISABLE_LOG_FILE"
ENV_DUPLICATE_LOG_FILE = "HA_DUPLICATE_LOG_FILE"
ENV_SUPERVISOR = "SUPERVISOR"
LOG_FILE_DISABLED_REASON_ENVIRONMENT = "environment"
LOG_FILE_DISABLED_REASON_SUPERVISOR = "supervisor"
# hass.data key for logging information.
DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded")
@@ -642,10 +648,12 @@ async def async_enable_logging(
logger.setLevel(logging.INFO if verbose else logging.WARNING)
if log_file is None:
disabled_log_file_reason = _log_file_disabled_reason()
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
if disabled_log_file_reason:
# Rename the default log file if it exists, since previous versions created
# it even on Supervisor
# it before Supervisor disabled duplicate file logging or
# HA_DISABLE_LOG_FILE disabled the log file.
def rename_old_file() -> None:
"""Rename old log file in executor."""
if os.path.isfile(default_log_path):
@@ -657,6 +665,7 @@ async def async_enable_logging(
else:
err_log_path = default_log_path
else:
disabled_log_file_reason = None
err_log_path = os.path.abspath(log_file)
if err_log_path:
@@ -669,10 +678,34 @@ async def async_enable_logging(
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
elif disabled_log_file_reason == LOG_FILE_DISABLED_REASON_ENVIRONMENT:
hass.data[DATA_LOGGING_DISABLED_REASON] = disabled_log_file_reason
async_activate_log_queue_handler(hass)
def _log_file_disabled_reason() -> str | None:
"""Return why the log file is disabled."""
if ENV_SUPERVISOR in os.environ and ENV_DUPLICATE_LOG_FILE not in os.environ:
return LOG_FILE_DISABLED_REASON_SUPERVISOR
disable_log_file = os.environ.get(ENV_DISABLE_LOG_FILE)
if disable_log_file is None:
return None
try:
if cv.boolean(disable_log_file):
return LOG_FILE_DISABLED_REASON_ENVIRONMENT
except vol.Invalid:
_LOGGER.warning(
"Ignoring invalid %s value: %s. Expected a boolean value: "
"1/0, true/false, yes/no, on/off, or enable/disable",
ENV_DISABLE_LOG_FILE,
disable_log_file,
)
return None
def _create_log_file(
err_log_path: str, log_rotate_days: int | None
) -> RotatingFileHandler | TimedRotatingFileHandler:
@@ -734,7 +767,7 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
domains.update(DEFAULT_INTEGRATIONS_RECOVERY_MODE)
# Add domains depending on if the Supervisor is used or not
if "SUPERVISOR" in os.environ:
if ENV_SUPERVISOR in os.environ:
domains.update(DEFAULT_INTEGRATIONS_SUPERVISOR)
return domains
@@ -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]
+10 -10
View File
@@ -11,10 +11,10 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
PERCENTAGE,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -76,14 +76,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM1,
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
AirlySensorEntityDescription(
key=ATTR_API_PM25,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -94,7 +94,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_PM10,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -105,7 +105,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
@@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_CO,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -137,7 +137,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_NO2,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -148,7 +148,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_SO2,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
@@ -159,7 +159,7 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_O3,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
attrs=lambda data: {
+4 -8
View File
@@ -12,11 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
ATTR_TIME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
)
from homeassistant.const import ATTR_TIME, UnitOfDensity, UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -95,7 +91,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_PM10,
translation_key="pm10",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM10,
value_fn=lambda data: data.get(ATTR_API_PM10),
@@ -104,7 +100,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_PM25,
translation_key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
value_fn=lambda data: data.get(ATTR_API_PM25),
@@ -113,7 +109,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_O3,
translation_key="o3",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.get(ATTR_API_O3),
extra_state_attributes_fn=None,
+2 -2
View File
@@ -8,7 +8,7 @@ from typing import override
from aioairq.core import AirQ
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import PERCENTAGE
from homeassistant.const import UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -32,7 +32,7 @@ AIRQ_LED_BRIGHTNESS = AirQBrightnessDescription(
native_min_value=0.0,
native_max_value=100.0,
native_step=1.0,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=lambda data: data["brightness"],
set_value=lambda device, value: device.set_current_brightness(value),
)
+47 -51
View File
@@ -12,13 +12,9 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfSoundPressure,
UnitOfTemperature,
)
@@ -44,70 +40,70 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="c2h4o",
translation_key="acetaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4o"),
),
AirQEntityDescription(
key="nh3_MR100",
translation_key="ammonia",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("nh3_MR100"),
),
AirQEntityDescription(
key="ash3",
translation_key="arsine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ash3"),
),
AirQEntityDescription(
key="br2",
translation_key="bromine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("br2"),
),
AirQEntityDescription(
key="ch4s",
translation_key="methanethiol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4s"),
),
AirQEntityDescription(
key="cl2_M20",
translation_key="chlorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cl2_M20"),
),
AirQEntityDescription(
key="clo2",
translation_key="chlorine_dioxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("clo2"),
),
AirQEntityDescription(
key="co",
translation_key="carbon_monoxide",
native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co"),
),
AirQEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co2"),
),
AirQEntityDescription(
key="cs2",
translation_key="carbon_disulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cs2"),
),
@@ -122,182 +118,182 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="ethanol",
translation_key="ethanol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ethanol"),
),
AirQEntityDescription(
key="c2h4",
translation_key="ethylene",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4"),
),
AirQEntityDescription(
key="ch2o_M10",
translation_key="formaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch2o_M10"),
),
AirQEntityDescription(
key="f2",
translation_key="fluorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("f2"),
),
AirQEntityDescription(
key="h2s",
translation_key="hydrogen_sulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2s"),
),
AirQEntityDescription(
key="hcl",
translation_key="hydrochloric_acid",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcl"),
),
AirQEntityDescription(
key="hcn",
translation_key="hydrogen_cyanide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcn"),
),
AirQEntityDescription(
key="hf",
translation_key="hydrogen_fluoride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hf"),
),
AirQEntityDescription(
key="health",
translation_key="health_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("health", 0.0) / 10.0,
),
AirQEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity"),
),
AirQEntityDescription(
key="humidity_abs",
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
),
AirQEntityDescription(
key="h2_M1000",
translation_key="hydrogen",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2_M1000"),
),
AirQEntityDescription(
key="h2o2",
translation_key="hydrogen_peroxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2o2"),
),
AirQEntityDescription(
key="ch4_MIPEX",
translation_key="methane",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4_MIPEX"),
),
AirQEntityDescription(
key="mold",
translation_key="mold",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("mold"),
),
AirQEntityDescription(
key="n2o",
device_class=SensorDeviceClass.NITROUS_OXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("n2o"),
),
AirQEntityDescription(
key="no_M250",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no_M250"),
),
AirQEntityDescription(
key="no2",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no2"),
),
AirQEntityDescription(
key="acid_M100",
translation_key="organic_acid",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("acid_M100"),
),
AirQEntityDescription(
key="oxygen",
translation_key="oxygen",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("oxygen"),
),
AirQEntityDescription(
key="o3",
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("o3"),
),
AirQEntityDescription(
key="performance",
translation_key="performance_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("performance", 0.0) / 10.0,
),
AirQEntityDescription(
key="ph3",
translation_key="hydrogen_phosphide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ph3"),
),
AirQEntityDescription(
key="pm1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm1"),
),
AirQEntityDescription(
key="pm2_5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm2_5"),
),
AirQEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm10"),
),
@@ -319,42 +315,42 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="c3h8_MIPEX",
translation_key="propane",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c3h8_MIPEX"),
),
AirQEntityDescription(
key="r32",
translation_key="r32",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r32"),
),
AirQEntityDescription(
key="r454b",
translation_key="r454b",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r454b"),
),
AirQEntityDescription(
key="r454c",
translation_key="r454c",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r454c"),
),
AirQEntityDescription(
key="sih4",
translation_key="silicon_hydride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sih4"),
),
AirQEntityDescription(
key="so2",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("so2"),
),
@@ -391,7 +387,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="tvoc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc"),
),
@@ -399,14 +395,14 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
key="tvoc_ionsc",
translation_key="industrial_volatile_organic_compounds",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc_ionsc"),
),
AirQEntityDescription(
key="virus",
translation_key="virus_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("virus", 0.0),
),
@@ -1,6 +1,8 @@
"""Support for airthings ble sensors."""
from collections.abc import Callable
import dataclasses
from dataclasses import dataclass
import logging
from typing import override
@@ -13,13 +15,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfRatio,
UnitOfSoundPressure,
UnitOfTemperature,
)
@@ -46,87 +46,108 @@ CONNECTIVITY_MODE_MAP = {
AirthingsConnectivityMode.NOT_CONFIGURED.value: "not_configured",
}
SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
"radon_1day_avg": SensorEntityDescription(
def get_connectivity_mode(value: str | float | None) -> str | None:
"""Get connectivity mode."""
if not isinstance(value, str):
return None
return CONNECTIVITY_MODE_MAP.get(value)
@dataclass(frozen=True, kw_only=True)
class AirthingsBLESensorEntityDescription(SensorEntityDescription):
"""Describes Airthings BLE sensor entity."""
value_fn: Callable[[str | float | None], str | float | None] = lambda x: x
SENSORS_MAPPING_TEMPLATE: dict[str, AirthingsBLESensorEntityDescription] = {
"radon_1day_avg": AirthingsBLESensorEntityDescription(
key="radon_1day_avg",
translation_key="radon_1day_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
"radon_longterm_avg": SensorEntityDescription(
"radon_longterm_avg": AirthingsBLESensorEntityDescription(
key="radon_longterm_avg",
translation_key="radon_longterm_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
"radon_1day_level": SensorEntityDescription(
"radon_1day_level": AirthingsBLESensorEntityDescription(
key="radon_1day_level",
translation_key="radon_1day_level",
device_class=SensorDeviceClass.ENUM,
options=["good", "fair", "poor"],
value_fn=lambda value: value if value != "unknown" else None,
),
"radon_longterm_level": SensorEntityDescription(
"radon_longterm_level": AirthingsBLESensorEntityDescription(
key="radon_longterm_level",
translation_key="radon_longterm_level",
device_class=SensorDeviceClass.ENUM,
options=["good", "fair", "poor"],
value_fn=lambda value: value if value != "unknown" else None,
),
"temperature": SensorEntityDescription(
"temperature": AirthingsBLESensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"humidity": SensorEntityDescription(
"humidity": AirthingsBLESensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"pressure": SensorEntityDescription(
"pressure": AirthingsBLESensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"battery": SensorEntityDescription(
"battery": AirthingsBLESensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
),
"co2": SensorEntityDescription(
"co2": AirthingsBLESensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"voc": SensorEntityDescription(
"voc": AirthingsBLESensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"illuminance": SensorEntityDescription(
"illuminance": AirthingsBLESensorEntityDescription(
key="illuminance",
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"lux": SensorEntityDescription(
"lux": AirthingsBLESensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"noise": SensorEntityDescription(
"noise": AirthingsBLESensorEntityDescription(
key="noise",
translation_key="ambient_noise",
device_class=SensorDeviceClass.SOUND_PRESSURE,
@@ -134,13 +155,14 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"connectivity_mode": SensorEntityDescription(
"connectivity_mode": AirthingsBLESensorEntityDescription(
key="connectivity_mode",
translation_key="connectivity_mode",
device_class=SensorDeviceClass.ENUM,
options=list(CONNECTIVITY_MODE_MAP.values()),
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=get_connectivity_mode,
),
}
@@ -228,12 +250,13 @@ class AirthingsSensor(
"""Airthings BLE sensors for the device."""
_attr_has_entity_name = True
entity_description: AirthingsBLESensorEntityDescription
def __init__(
self,
coordinator: AirthingsBLEDataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
entity_description: AirthingsBLESensorEntityDescription,
) -> None:
"""Populate the airthings entity with relevant data."""
super().__init__(coordinator)
@@ -272,11 +295,4 @@ class AirthingsSensor(
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
value = self.coordinator.data.sensors[self.entity_description.key]
# Map connectivity mode to enum values
if self.entity_description.key == "connectivity_mode":
if not isinstance(value, str):
return None
return CONNECTIVITY_MODE_MAP.get(value)
return value
return self.entity_description.value_fn(value)
@@ -45,13 +45,23 @@
"name": "Radon 1-day average"
},
"radon_1day_level": {
"name": "Radon 1-day level"
"name": "Radon 1-day level",
"state": {
"fair": "Fair",
"good": "Good",
"poor": "Poor"
}
},
"radon_longterm_avg": {
"name": "Radon longterm average"
},
"radon_longterm_level": {
"name": "Radon longterm level"
"name": "Radon longterm level",
"state": {
"fair": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::fair%]",
"good": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::good%]",
"poor": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::poor%]"
}
}
}
},
@@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.entity_registry as er
@@ -118,7 +118,7 @@ async def async_setup_entry(
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{sensor_desc.key}"
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
Platform.BINARY_SENSOR, DOMAIN, unique_id
):
_LOGGER.debug("Removing deprecated entity %s", entity_id)
entity_registry.async_remove(entity_id)
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.1.3"]
"requirements": ["aioamazondevices==14.1.8"]
}
@@ -7,7 +7,7 @@ from aioamazondevices.const.schedules import (
NOTIFICATION_TIMER,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
@@ -70,7 +70,7 @@ async def async_remove_unsupported_notification_sensors(
):
unique_id = f"{serial_num}-{notification_key}"
entity_id = entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, unique_id=unique_id
Platform.SENSOR, DOMAIN, unique_id=unique_id
)
is_unsupported = not coordinator.data[serial_num].notifications_supported
@@ -11,15 +11,14 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UnitOfDensity,
UnitOfIrradiance,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -157,13 +156,13 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM25_IN_AQIN,
translation_key="pm25_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM25_IN_24H_AQIN,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
translation_key="pm25_indoor_24h_aqin",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
@@ -171,28 +170,28 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM10_IN_AQIN,
translation_key="pm10_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM10_IN_24H_AQIN,
translation_key="pm10_indoor_24h_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_CO2_IN_AQIN,
translation_key="co2_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_CO2_IN_24H_AQIN,
translation_key="co2_indoor_24h_aqin",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -206,7 +205,7 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM_IN_HUMIDITY_AQIN,
translation_key="pm_indoor_humidity_aqin",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -250,7 +249,7 @@ SENSOR_DESCRIPTIONS = (
),
SensorEntityDescription(
key=TYPE_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -291,83 +290,83 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_HUMIDITY10,
translation_key="humidity_10",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY1,
translation_key="humidity_1",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY2,
translation_key="humidity_2",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY3,
translation_key="humidity_3",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY4,
translation_key="humidity_4",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY5,
translation_key="humidity_5",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY6,
translation_key="humidity_6",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY7,
translation_key="humidity_7",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY8,
translation_key="humidity_8",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY9,
translation_key="humidity_9",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITYIN,
translation_key="humidity_indoor",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -417,95 +416,95 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM25_24H,
translation_key="pm25_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
),
SensorEntityDescription(
key=TYPE_PM25_IN,
translation_key="pm25_indoor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM25_IN_24H,
translation_key="pm25_indoor_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
),
SensorEntityDescription(
key=TYPE_PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM10,
translation_key="soil_humidity_10",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM1,
translation_key="soil_humidity_1",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM2,
translation_key="soil_humidity_2",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM3,
translation_key="soil_humidity_3",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM4,
translation_key="soil_humidity_4",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM5,
translation_key="soil_humidity_5",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM6,
translation_key="soil_humidity_6",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM7,
translation_key="soil_humidity_7",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM8,
translation_key="soil_humidity_8",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM9,
translation_key="soil_humidity_9",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -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
@@ -1 +0,0 @@
"""The BlinkStick integration."""
@@ -1,87 +0,0 @@
"""Support for BlinkStick lights."""
# mypy: ignore-errors
from typing import Any
# from blinkstick import blinkstick
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
)
from homeassistant.const import CONF_NAME
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 color as color_util
CONF_SERIAL = "serial"
DEFAULT_NAME = "Blinkstick"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_SERIAL): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up BlinkStick device specified by serial number."""
name = config[CONF_NAME]
serial = config[CONF_SERIAL]
stick = blinkstick.find_by_serial(serial)
add_entities([BlinkStickLight(stick, name)], True)
class BlinkStickLight(LightEntity):
"""Representation of a BlinkStick light."""
_attr_color_mode = ColorMode.HS
_attr_supported_color_modes = {ColorMode.HS}
def __init__(self, stick, name):
"""Initialize the light."""
self._stick = stick
self._attr_name = name
def update(self) -> None:
"""Read back the device state."""
rgb_color = self._stick.get_color()
hsv = color_util.color_RGB_to_hsv(*rgb_color)
self._attr_hs_color = hsv[:2]
self._attr_brightness = int(hsv[2])
self._attr_is_on = self.brightness is not None and self.brightness > 0
def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if ATTR_HS_COLOR in kwargs:
self._attr_hs_color = kwargs[ATTR_HS_COLOR]
brightness: int = kwargs.get(ATTR_BRIGHTNESS, 255)
self._attr_brightness = brightness
self._attr_is_on = bool(brightness)
assert self.hs_color
rgb_color = color_util.color_hsv_to_RGB(
self.hs_color[0], self.hs_color[1], brightness / 255 * 100
)
self._stick.set_color(red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2])
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self._stick.turn_off()
@@ -1,11 +0,0 @@
{
"domain": "blinksticklight",
"name": "BlinkStick",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/blinksticklight",
"iot_class": "local_polling",
"loggers": ["blinkstick"],
"quality_scale": "legacy",
"requirements": ["BlinkStick==1.2.0"]
}
@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==6.1.3"],
"requirements": ["python-bsblan==6.1.4"],
"zeroconf": [
{
"name": "bsb-lan*",
+6 -7
View File
@@ -12,11 +12,10 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfRatio,
UnitOfTemperature,
UnitOfVolume,
)
@@ -52,7 +51,7 @@ async def async_setup_entry(
12,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
@@ -63,7 +62,7 @@ async def async_setup_entry(
54,
SensorDeviceClass.HUMIDITY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
),
DemoSensor(
"sensor_3",
@@ -72,7 +71,7 @@ async def async_setup_entry(
54,
SensorDeviceClass.CO,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_MILLION,
),
DemoSensor(
"sensor_4",
@@ -81,7 +80,7 @@ async def async_setup_entry(
54,
SensorDeviceClass.CO2,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_MILLION,
),
DemoSensor(
"battery_4",
@@ -90,7 +89,7 @@ async def async_setup_entry(
99,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
@@ -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"]}
+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)
+11 -12
View File
@@ -14,18 +14,17 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UV_INDEX,
EntityCategory,
UnitOfDensity,
UnitOfElectricPotential,
UnitOfIrradiance,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -99,7 +98,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.HUMIDITY: SensorEntityDescription(
key="HUMIDITY",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DEGREE: SensorEntityDescription(
@@ -122,19 +121,19 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.PM25: SensorEntityDescription(
key="PM25",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.PM10: SensorEntityDescription(
key="PM10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.BATTERY_PERCENTAGE: SensorEntityDescription(
key="BATTERY_PERCENTAGE",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -149,7 +148,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.CO2_PPM: SensorEntityDescription(
key="CO2_PPM",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.LUX: SensorEntityDescription(
@@ -263,13 +262,13 @@ ECOWITT_SENSORS_MAPPING: Final = {
),
EcoWittSensorTypes.PERCENTAGE: SensorEntityDescription(
key="PERCENTAGE",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.SOIL_MOISTURE: SensorEntityDescription(
key="SOIL_MOISTURE",
device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DISTANCE_MM: SensorEntityDescription(
@@ -286,13 +285,13 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.PM1: SensorEntityDescription(
key="PM1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.PM4: SensorEntityDescription(
key="PM4",
device_class=SensorDeviceClass.PM4,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -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,
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==45.3.1",
"aioesphomeapi==45.5.2",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.9.4"
],
+3 -3
View File
@@ -10,7 +10,7 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
@@ -75,7 +75,7 @@ def repair_issue_cleanup(hass: HomeAssistant, avm_wrapper: AvmWrapper) -> None:
if (
(
entity_button := entity_registry.async_get_entity_id(
"button", DOMAIN, f"{avm_wrapper.unique_id}-cleanup"
Platform.BUTTON, DOMAIN, f"{avm_wrapper.unique_id}-cleanup"
)
)
and (entity_entry := entity_registry.async_get(entity_button))
@@ -102,7 +102,7 @@ def repair_issue_firmware_update(hass: HomeAssistant, avm_wrapper: AvmWrapper) -
if (
(
entity_button := entity_registry.async_get_entity_id(
"button", DOMAIN, f"{avm_wrapper.unique_id}-firmware_update"
Platform.BUTTON, DOMAIN, f"{avm_wrapper.unique_id}-firmware_update"
)
)
and (entity_entry := entity_registry.async_get(entity_button))
+2 -2
View File
@@ -7,13 +7,13 @@ from typing import override
from requests.exceptions import RequestException
from homeassistant.components.image import ImageEntity
from homeassistant.const import EntityCategory
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util, slugify
from .const import DOMAIN, Platform
from .const import DOMAIN
from .coordinator import AvmWrapper, FritzConfigEntry
from .entity import FritzBoxBaseEntity
@@ -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"]
}
+10 -10
View File
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.const import UnitOfDensity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -63,7 +63,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
key=ATTR_C6H6,
value=lambda sensors: sensors.c6h6.value if sensors.c6h6 else None,
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
translation_key="c6h6",
),
@@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.co.value if sensors.co else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -80,7 +80,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.no.value if sensors.no else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -88,7 +88,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.no2.value if sensors.no2 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
translation_key=ATTR_NOX,
value=lambda sensors: sensors.nox.value if sensors.nox else None,
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -112,7 +112,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.o3.value if sensors.o3 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -128,7 +128,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.pm10.value if sensors.pm10 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.pm25.value if sensors.pm25 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -160,7 +160,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.so2.value if sensors.so2 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -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",
@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.15.0"]
"requirements": ["aioimmich==0.15.1"]
}
@@ -161,5 +161,7 @@ SENSOR_KEYS: Final[dict[int, list[str]]] = {
IndevoltSolar.CUMULATIVE_PRODUCTION,
IndevoltBattery.GEN_2_CYCLE_COUNT,
IndevoltBattery.GEN_2_TRANSFORMER_TEMPERATURE,
IndevoltBattery.REMAINING_CHARGING_TIME,
IndevoltBattery.REMAINING_DISCHARGING_TIME,
],
}
@@ -27,6 +27,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -45,6 +46,7 @@ class IndevoltSensorEntityDescription(SensorEntityDescription):
state_mapping: dict[str | int, str] = field(default_factory=dict)
generation: tuple[int, ...] = (1, 2)
energy_mode: IndevoltEnergyMode | None = None
charge_discharge_state: int | None = None
SENSORS: Final = (
@@ -242,6 +244,26 @@ SENSORS: Final = (
state_mapping={1000: "static", 1001: "charging", 1002: "discharging"},
device_class=SensorDeviceClass.ENUM,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.REMAINING_CHARGING_TIME,
generation=(2,),
translation_key="remaining_charging_time",
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
charge_discharge_state=1001,
entity_registry_enabled_default=False,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.REMAINING_DISCHARGING_TIME,
generation=(2,),
translation_key="remaining_discharging_time",
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
charge_discharge_state=1002,
entity_registry_enabled_default=False,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.SOC,
translation_key="battery_soc",
@@ -948,6 +970,14 @@ class IndevoltSensorEntity(IndevoltEntity, SensorEntity):
if energy_mode != self.entity_description.energy_mode:
return False
# Check whether the battery is not in the required charge/discharge state
if (
self.entity_description.charge_discharge_state is not None
and self.coordinator.data.get(IndevoltBattery.CHARGE_DISCHARGE_STATE)
!= self.entity_description.charge_discharge_state
):
return False
# Check whether inverter is reporting 0 degrees with heater not active (thus reporting to indicate "idle")
# Pending fix by Indevolt: https://discord.com/channels/1417471269942591571/1510277757689659522
if self.entity_description.key == IndevoltBattery.GEN_1_INVERTER_TEMPERATURE:
@@ -365,6 +365,12 @@
"realtime_target_soc": {
"name": "Real-time target SOC"
},
"remaining_charging_time": {
"name": "Remaining charging time"
},
"remaining_discharging_time": {
"name": "Remaining discharging time"
},
"serial_number": {
"name": "Serial number"
},
@@ -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"
}
+4 -6
View File
@@ -12,11 +12,9 @@ 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
@@ -32,7 +30,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.AIR_HUMIDITY: SensorEntityDescription(
key="air_humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.AIR_PRESSURE: SensorEntityDescription(
@@ -49,7 +47,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.ECO2: SensorEntityDescription(
key="eco2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
SensorType.LIGHT: SensorEntityDescription(
@@ -67,7 +65,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
SensorType.VOC: SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
),
}
+20 -21
View File
@@ -22,20 +22,19 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
Platform,
UnitOfApparentPower,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfReactivePower,
UnitOfTemperature,
UnitOfTime,
@@ -444,7 +443,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="HumiditySensor",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
device_to_ha=lambda x: x / HUMIDITY_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
@@ -459,7 +458,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SoilMoistureSensor",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.MOISTURE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -484,7 +483,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PowerSource",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
# value has double precision
@@ -625,7 +624,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="EveThermoValvePosition",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
),
entity_class=MatterSensor,
required_attributes=(EveCluster.Attributes.ValvePosition,),
@@ -658,7 +657,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="CarbonDioxideSensor",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -671,7 +670,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="TotalVolatileOrganicCompoundsSensor",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -699,7 +698,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM1Sensor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -712,7 +711,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM25Sensor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -725,7 +724,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PM10Sensor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -750,7 +749,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="CarbonMonoxideSensor",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -763,7 +762,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="NitrogenDioxideSensor",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -776,7 +775,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="OzoneConcentrationSensor",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.OZONE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -802,7 +801,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="HepaFilterCondition",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="hepa_filter_condition",
),
@@ -813,7 +812,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ActivatedCarbonFilterCondition",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="activated_carbon_filter_condition",
),
@@ -1297,7 +1296,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="ThermostatPIHeatingDemand",
translation_key="pi_heating_demand",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterSensor,
@@ -1379,7 +1378,7 @@ DISCOVERY_SCHEMAS = [
entity_registry_enabled_default=False,
translation_key="window_covering_target_position",
device_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
),
entity_class=MatterSensor,
required_attributes=(
@@ -1464,7 +1463,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="EnergyEvseStateOfCharge",
translation_key="evse_soc",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -1488,7 +1487,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterSensorEntityDescription(
key="WaterHeaterManagementTankPercentage",
translation_key="tank_percentage",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
@@ -31,7 +31,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
TextSelectorConfig(type=TextSelectorType.EMAIL, autocomplete="username")
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
TextSelectorConfig(
type=TextSelectorType.PASSWORD, autocomplete="current-password"
)
),
}
)
+10 -4
View File
@@ -114,20 +114,26 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
async def callback_update_data(self, devices_json: dict[str, dict]) -> None:
"""Handle data update from the API."""
devices = {
updated_devices = {
device_id: MieleDevice(device) for device_id, device in devices_json.items()
}
self.async_set_updated_data(
MieleCoordinatorData(devices=devices, actions=self.data.actions)
MieleCoordinatorData(
devices={**self.data.devices, **updated_devices},
actions=self.data.actions,
)
)
async def callback_update_actions(self, actions_json: dict[str, dict]) -> None:
"""Handle data update from the API."""
actions = {
updated_actions = {
device_id: MieleAction(action) for device_id, action in actions_json.items()
}
self.async_set_updated_data(
MieleCoordinatorData(devices=self.data.devices, actions=actions)
MieleCoordinatorData(
devices=self.data.devices,
actions={**self.data.actions, **updated_actions},
)
)
+3 -522
View File
@@ -2,539 +2,20 @@
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.components.cover import (
DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import (
DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_BINARY_SENSORS,
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
CONF_COUNT,
CONF_COVERS,
CONF_DELAY,
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_LIGHTS,
CONF_METHOD,
CONF_NAME,
CONF_OFFSET,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_SLAVE,
CONF_STRUCTURE,
CONF_SWITCHES,
CONF_TEMPERATURE_UNIT,
CONF_TIMEOUT,
CONF_TYPE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
)
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
CONF_BAUDRATE,
CONF_BRIGHTNESS_REGISTER,
CONF_BYTESIZE,
CONF_CLIMATES,
CONF_COLOR_TEMP_REGISTER,
CONF_CURRENT_TEMP_OFFSET,
CONF_CURRENT_TEMP_SCALE,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_DIFFUSE,
CONF_FAN_MODE_FOCUS,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_LOW,
CONF_FAN_MODE_MEDIUM,
CONF_FAN_MODE_MIDDLE,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_TOP,
CONF_FAN_MODE_VALUES,
CONF_FANS,
CONF_HVAC_ACTION_COOLING,
CONF_HVAC_ACTION_DEFROSTING,
CONF_HVAC_ACTION_DRYING,
CONF_HVAC_ACTION_FAN,
CONF_HVAC_ACTION_HEATING,
CONF_HVAC_ACTION_IDLE,
CONF_HVAC_ACTION_OFF,
CONF_HVAC_ACTION_PREHEATING,
CONF_HVAC_ACTION_REGISTER,
CONF_HVAC_ACTION_VALUES,
CONF_HVAC_MODE_AUTO,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_DRY,
CONF_HVAC_MODE_FAN_ONLY,
CONF_HVAC_MODE_HEAT,
CONF_HVAC_MODE_HEAT_COOL,
CONF_HVAC_MODE_OFF,
CONF_HVAC_MODE_REGISTER,
CONF_HVAC_MODE_VALUES,
CONF_HVAC_OFF_VALUE,
CONF_HVAC_ON_VALUE,
CONF_HVAC_ONOFF_COIL,
CONF_HVAC_ONOFF_REGISTER,
CONF_INPUT_TYPE,
CONF_MAX_TEMP,
CONF_MAX_VALUE,
CONF_MIN_TEMP,
CONF_MIN_VALUE,
CONF_MSG_WAIT,
CONF_NAN_VALUE,
CONF_PARITY,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OFF,
CONF_STATE_ON,
CONF_STATE_OPEN,
CONF_STATE_OPENING,
CONF_STATUS_REGISTER,
CONF_STATUS_REGISTER_TYPE,
CONF_STEP,
CONF_STOPBITS,
CONF_SWAP,
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_SWING_MODE_REGISTER,
CONF_SWING_MODE_SWING_BOTH,
CONF_SWING_MODE_SWING_HORIZ,
CONF_SWING_MODE_SWING_OFF,
CONF_SWING_MODE_SWING_ON,
CONF_SWING_MODE_SWING_VERT,
CONF_SWING_MODE_VALUES,
CONF_TARGET_TEMP,
CONF_TARGET_TEMP_OFFSET,
CONF_TARGET_TEMP_SCALE,
CONF_TARGET_TEMP_WRITE_REGISTERS,
CONF_VERIFY,
CONF_VIRTUAL_COUNT,
CONF_WRITE_REGISTERS,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
DEFAULT_HUB,
DEFAULT_HVAC_OFF_VALUE,
DEFAULT_HVAC_ON_VALUE,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TEMP_UNIT,
DOMAIN,
RTUOVERTCP,
SERIAL,
TCP,
UDP,
DataType,
)
from .const import DOMAIN
from .modbus import DATA_MODBUS_HUBS, ModbusHub, async_modbus_setup
from .validators import (
duplicate_fan_mode_validator,
duplicate_swing_mode_validator,
ensure_and_check_conflicting_scales_and_offsets,
hvac_fixedsize_reglist_validator,
nan_validator,
not_zero_value,
register_int_list_validator,
struct_validator,
)
from .schemas import CONFIG_SCHEMA # noqa: F401
_LOGGER = logging.getLogger(__name__)
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
BASE_COMPONENT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.positive_int,
vol.Exclusive(CONF_DEVICE_ADDRESS, "slave_addr"): cv.positive_int,
vol.Exclusive(CONF_SLAVE, "slave_addr"): cv.positive_int,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
vol.Optional(CONF_COUNT): cv.positive_int,
vol.Optional(CONF_DATA_TYPE, default=DataType.INT16): vol.In(
[
DataType.INT16,
DataType.INT32,
DataType.INT64,
DataType.UINT16,
DataType.UINT32,
DataType.UINT64,
DataType.FLOAT16,
DataType.FLOAT32,
DataType.FLOAT64,
DataType.STRING,
DataType.CUSTOM,
]
),
vol.Optional(CONF_STRUCTURE): cv.string,
vol.Optional(CONF_SCALE): vol.All(
vol.Coerce(float), lambda v: not_zero_value(v, "Scale cannot be zero.")
),
vol.Optional(CONF_OFFSET): vol.Coerce(float),
vol.Optional(CONF_PRECISION): cv.positive_int,
vol.Optional(
CONF_SWAP,
): vol.In(
[
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
]
),
}
)
BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_COIL,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
]
),
vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
vol.Optional(CONF_VERIFY): vol.Maybe(
{
vol.Optional(CONF_ADDRESS): cv.positive_int,
vol.Optional(CONF_INPUT_TYPE): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_INPUT,
CALL_TYPE_COIL,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
]
),
vol.Optional(CONF_STATE_OFF): vol.All(
cv.ensure_list, [cv.positive_int]
),
vol.Optional(CONF_STATE_ON): vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
}
),
}
)
CLIMATE_SCHEMA = vol.All(
BASE_STRUCT_SCHEMA.extend(
{
vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(int),
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int,
vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int,
vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(
v, "Current temperature scale cannot be zero."
),
),
vol.Optional(CONF_TARGET_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(v, "Target temperature scale cannot be zero."),
),
vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
): cv.positive_int,
vol.Optional(
CONF_HVAC_OFF_VALUE, default=DEFAULT_HVAC_OFF_VALUE
): cv.positive_int,
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_HVAC_MODE_REGISTER): vol.Maybe(
{
CONF_ADDRESS: cv.positive_int,
CONF_HVAC_MODE_VALUES: {
vol.Optional(CONF_HVAC_MODE_OFF): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_HEAT): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_COOL): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_HEAT_COOL): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_AUTO): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_DRY): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_FAN_ONLY): vol.Any(
cv.positive_int, [cv.positive_int]
),
},
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
}
),
vol.Optional(CONF_HVAC_ACTION_REGISTER): vol.Maybe(
{
CONF_ADDRESS: cv.positive_int,
CONF_HVAC_ACTION_VALUES: {
vol.Optional(CONF_HVAC_ACTION_COOLING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_DEFROSTING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_DRYING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_FAN): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_HEATING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_IDLE): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_OFF): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_PREHEATING): vol.Any(
cv.positive_int, [cv.positive_int]
),
},
vol.Optional(
CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING
): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
}
),
vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
vol.All(
{
vol.Required(CONF_ADDRESS): register_int_list_validator,
CONF_FAN_MODE_VALUES: {
vol.Optional(CONF_FAN_MODE_ON): cv.positive_int,
vol.Optional(CONF_FAN_MODE_OFF): cv.positive_int,
vol.Optional(CONF_FAN_MODE_AUTO): cv.positive_int,
vol.Optional(CONF_FAN_MODE_LOW): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MEDIUM): cv.positive_int,
vol.Optional(CONF_FAN_MODE_HIGH): cv.positive_int,
vol.Optional(CONF_FAN_MODE_TOP): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MIDDLE): cv.positive_int,
vol.Optional(CONF_FAN_MODE_FOCUS): cv.positive_int,
vol.Optional(CONF_FAN_MODE_DIFFUSE): cv.positive_int,
},
},
duplicate_fan_mode_validator,
),
),
vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe(
vol.All(
{
vol.Required(CONF_ADDRESS): register_int_list_validator,
CONF_SWING_MODE_VALUES: {
vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int,
},
},
duplicate_swing_mode_validator,
)
),
},
),
ensure_and_check_conflicting_scales_and_offsets,
)
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(
CONF_INPUT_TYPE,
default=CALL_TYPE_REGISTER_HOLDING,
): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_COIL,
]
),
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
vol.Optional(
CONF_STATUS_REGISTER_TYPE,
default=CALL_TYPE_REGISTER_HOLDING,
): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
}
)
SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
}
)
LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend(
{
vol.Optional(CONF_BRIGHTNESS_REGISTER): cv.positive_int,
vol.Optional(CONF_COLOR_TEMP_REGISTER): cv.positive_int,
vol.Optional(CONF_MIN_TEMP): cv.positive_int,
vol.Optional(CONF_MAX_TEMP): cv.positive_int,
}
)
FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({})
SENSOR_SCHEMA = vol.All(
BASE_STRUCT_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
vol.Optional(CONF_MIN_VALUE): vol.Coerce(float),
vol.Optional(CONF_MAX_VALUE): vol.Coerce(float),
vol.Optional(CONF_NAN_VALUE): nan_validator,
vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
}
),
)
BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
[
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_bin_count"): cv.positive_int,
vol.Exclusive(CONF_SLAVE_COUNT, "vir_bin_count"): cv.positive_int,
}
)
MODBUS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
vol.Optional(CONF_MSG_WAIT): cv.positive_int,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
),
vol.Optional(CONF_CLIMATES): vol.All(
cv.ensure_list, [vol.All(CLIMATE_SCHEMA, struct_validator)]
),
vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.All(SENSOR_SCHEMA, struct_validator)]
),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]),
},
extra=vol.ALLOW_EXTRA,
)
SERIAL_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_TYPE): SERIAL,
vol.Required(CONF_BAUDRATE): cv.positive_int,
vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_PARITY): vol.Any("E", "O", "N"),
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
}
)
ETHERNET_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any(TCP, UDP, RTUOVERTCP),
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
],
),
},
extra=vol.ALLOW_EXTRA,
)
def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
"""Return modbus hub with name."""
return hass.data[DATA_MODBUS_HUBS][name]
+537
View File
@@ -0,0 +1,537 @@
"""Voluptuous schemas for the Modbus integration."""
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.components.cover import (
DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import (
DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_BINARY_SENSORS,
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
CONF_COUNT,
CONF_COVERS,
CONF_DELAY,
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_LIGHTS,
CONF_METHOD,
CONF_NAME,
CONF_OFFSET,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_SLAVE,
CONF_STRUCTURE,
CONF_SWITCHES,
CONF_TEMPERATURE_UNIT,
CONF_TIMEOUT,
CONF_TYPE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
Platform,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import VolSchemaType
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
CONF_BAUDRATE,
CONF_BRIGHTNESS_REGISTER,
CONF_BYTESIZE,
CONF_CLIMATES,
CONF_COLOR_TEMP_REGISTER,
CONF_CURRENT_TEMP_OFFSET,
CONF_CURRENT_TEMP_SCALE,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_DIFFUSE,
CONF_FAN_MODE_FOCUS,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_LOW,
CONF_FAN_MODE_MEDIUM,
CONF_FAN_MODE_MIDDLE,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_TOP,
CONF_FAN_MODE_VALUES,
CONF_FANS,
CONF_HVAC_ACTION_COOLING,
CONF_HVAC_ACTION_DEFROSTING,
CONF_HVAC_ACTION_DRYING,
CONF_HVAC_ACTION_FAN,
CONF_HVAC_ACTION_HEATING,
CONF_HVAC_ACTION_IDLE,
CONF_HVAC_ACTION_OFF,
CONF_HVAC_ACTION_PREHEATING,
CONF_HVAC_ACTION_REGISTER,
CONF_HVAC_ACTION_VALUES,
CONF_HVAC_MODE_AUTO,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_DRY,
CONF_HVAC_MODE_FAN_ONLY,
CONF_HVAC_MODE_HEAT,
CONF_HVAC_MODE_HEAT_COOL,
CONF_HVAC_MODE_OFF,
CONF_HVAC_MODE_REGISTER,
CONF_HVAC_MODE_VALUES,
CONF_HVAC_OFF_VALUE,
CONF_HVAC_ON_VALUE,
CONF_HVAC_ONOFF_COIL,
CONF_HVAC_ONOFF_REGISTER,
CONF_INPUT_TYPE,
CONF_MAX_TEMP,
CONF_MAX_VALUE,
CONF_MIN_TEMP,
CONF_MIN_VALUE,
CONF_MSG_WAIT,
CONF_NAN_VALUE,
CONF_PARITY,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
CONF_STATE_OFF,
CONF_STATE_ON,
CONF_STATE_OPEN,
CONF_STATE_OPENING,
CONF_STATUS_REGISTER,
CONF_STATUS_REGISTER_TYPE,
CONF_STEP,
CONF_STOPBITS,
CONF_SWAP,
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_SWING_MODE_REGISTER,
CONF_SWING_MODE_SWING_BOTH,
CONF_SWING_MODE_SWING_HORIZ,
CONF_SWING_MODE_SWING_OFF,
CONF_SWING_MODE_SWING_ON,
CONF_SWING_MODE_SWING_VERT,
CONF_SWING_MODE_VALUES,
CONF_TARGET_TEMP,
CONF_TARGET_TEMP_OFFSET,
CONF_TARGET_TEMP_SCALE,
CONF_TARGET_TEMP_WRITE_REGISTERS,
CONF_VERIFY,
CONF_VIRTUAL_COUNT,
CONF_WRITE_REGISTERS,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
DEFAULT_HUB,
DEFAULT_HVAC_OFF_VALUE,
DEFAULT_HVAC_ON_VALUE,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TEMP_UNIT,
DOMAIN,
RTUOVERTCP,
SERIAL,
TCP,
UDP,
DataType,
)
from .validators import (
duplicate_fan_mode_validator,
duplicate_swing_mode_validator,
ensure_and_check_conflicting_scales_and_offsets,
hvac_fixedsize_reglist_validator,
nan_validator,
not_zero_value,
register_int_list_validator,
struct_validator,
)
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
BASE_COMPONENT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.positive_int,
vol.Exclusive(CONF_DEVICE_ADDRESS, "slave_addr"): cv.positive_int,
vol.Exclusive(CONF_SLAVE, "slave_addr"): cv.positive_int,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
vol.Optional(CONF_COUNT): cv.positive_int,
vol.Optional(CONF_DATA_TYPE, default=DataType.INT16): vol.In(
[
DataType.INT16,
DataType.INT32,
DataType.INT64,
DataType.UINT16,
DataType.UINT32,
DataType.UINT64,
DataType.FLOAT16,
DataType.FLOAT32,
DataType.FLOAT64,
DataType.STRING,
DataType.CUSTOM,
]
),
vol.Optional(CONF_STRUCTURE): cv.string,
vol.Optional(CONF_SCALE): vol.All(
vol.Coerce(float), lambda v: not_zero_value(v, "Scale cannot be zero.")
),
vol.Optional(CONF_OFFSET): vol.Coerce(float),
vol.Optional(CONF_PRECISION): cv.positive_int,
vol.Optional(
CONF_SWAP,
): vol.In(
[
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
]
),
}
)
BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_COIL,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
]
),
vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
vol.Optional(CONF_VERIFY): vol.Maybe(
{
vol.Optional(CONF_ADDRESS): cv.positive_int,
vol.Optional(CONF_INPUT_TYPE): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_INPUT,
CALL_TYPE_COIL,
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
]
),
vol.Optional(CONF_STATE_OFF): vol.All(
cv.ensure_list, [cv.positive_int]
),
vol.Optional(CONF_STATE_ON): vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
}
),
}
)
CLIMATE_SCHEMA = vol.All(
BASE_STRUCT_SCHEMA.extend(
{
vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(int),
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int,
vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int,
vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(
v, "Current temperature scale cannot be zero."
),
),
vol.Optional(CONF_TARGET_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(v, "Target temperature scale cannot be zero."),
),
vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
): cv.positive_int,
vol.Optional(
CONF_HVAC_OFF_VALUE, default=DEFAULT_HVAC_OFF_VALUE
): cv.positive_int,
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_HVAC_MODE_REGISTER): vol.Maybe(
{
CONF_ADDRESS: cv.positive_int,
CONF_HVAC_MODE_VALUES: {
vol.Optional(CONF_HVAC_MODE_OFF): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_HEAT): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_COOL): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_HEAT_COOL): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_AUTO): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_DRY): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_MODE_FAN_ONLY): vol.Any(
cv.positive_int, [cv.positive_int]
),
},
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
}
),
vol.Optional(CONF_HVAC_ACTION_REGISTER): vol.Maybe(
{
CONF_ADDRESS: cv.positive_int,
CONF_HVAC_ACTION_VALUES: {
vol.Optional(CONF_HVAC_ACTION_COOLING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_DEFROSTING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_DRYING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_FAN): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_HEATING): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_IDLE): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_OFF): vol.Any(
cv.positive_int, [cv.positive_int]
),
vol.Optional(CONF_HVAC_ACTION_PREHEATING): vol.Any(
cv.positive_int, [cv.positive_int]
),
},
vol.Optional(
CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING
): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
}
),
vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
vol.All(
{
vol.Required(CONF_ADDRESS): register_int_list_validator,
CONF_FAN_MODE_VALUES: {
vol.Optional(CONF_FAN_MODE_ON): cv.positive_int,
vol.Optional(CONF_FAN_MODE_OFF): cv.positive_int,
vol.Optional(CONF_FAN_MODE_AUTO): cv.positive_int,
vol.Optional(CONF_FAN_MODE_LOW): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MEDIUM): cv.positive_int,
vol.Optional(CONF_FAN_MODE_HIGH): cv.positive_int,
vol.Optional(CONF_FAN_MODE_TOP): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MIDDLE): cv.positive_int,
vol.Optional(CONF_FAN_MODE_FOCUS): cv.positive_int,
vol.Optional(CONF_FAN_MODE_DIFFUSE): cv.positive_int,
},
},
duplicate_fan_mode_validator,
),
),
vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe(
vol.All(
{
vol.Required(CONF_ADDRESS): register_int_list_validator,
CONF_SWING_MODE_VALUES: {
vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int,
vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int,
},
},
duplicate_swing_mode_validator,
)
),
},
),
ensure_and_check_conflicting_scales_and_offsets,
)
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(
CONF_INPUT_TYPE,
default=CALL_TYPE_REGISTER_HOLDING,
): vol.In(
[
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_COIL,
]
),
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
vol.Optional(
CONF_STATUS_REGISTER_TYPE,
default=CALL_TYPE_REGISTER_HOLDING,
): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
}
)
SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
}
)
LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend(
{
vol.Optional(CONF_BRIGHTNESS_REGISTER): cv.positive_int,
vol.Optional(CONF_COLOR_TEMP_REGISTER): cv.positive_int,
vol.Optional(CONF_MIN_TEMP): cv.positive_int,
vol.Optional(CONF_MAX_TEMP): cv.positive_int,
}
)
FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({})
SENSOR_SCHEMA = vol.All(
BASE_STRUCT_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
vol.Optional(CONF_MIN_VALUE): vol.Coerce(float),
vol.Optional(CONF_MAX_VALUE): vol.Coerce(float),
vol.Optional(CONF_NAN_VALUE): nan_validator,
vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
}
),
)
BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
[
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
]
),
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_bin_count"): cv.positive_int,
vol.Exclusive(CONF_SLAVE_COUNT, "vir_bin_count"): cv.positive_int,
}
)
MODBUS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
vol.Optional(CONF_MSG_WAIT): cv.positive_int,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
),
vol.Optional(CONF_CLIMATES): vol.All(
cv.ensure_list, [vol.All(CLIMATE_SCHEMA, struct_validator)]
),
vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.All(SENSOR_SCHEMA, struct_validator)]
),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]),
},
extra=vol.ALLOW_EXTRA,
)
SERIAL_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_TYPE): SERIAL,
vol.Required(CONF_BAUDRATE): cv.positive_int,
vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_PARITY): vol.Any("E", "O", "N"),
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
}
)
ETHERNET_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any(TCP, UDP, RTUOVERTCP),
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
],
),
},
extra=vol.ALLOW_EXTRA,
)
# Per-platform schema for a single entity config, used to validate the entity
# lists stored in a device subentry.
PLATFORM_SCHEMAS: dict[Platform, VolSchemaType] = {
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMA,
Platform.CLIMATE: vol.All(CLIMATE_SCHEMA, struct_validator),
Platform.COVER: COVERS_SCHEMA,
Platform.FAN: FAN_SCHEMA,
Platform.LIGHT: LIGHT_SCHEMA,
Platform.SENSOR: vol.All(SENSOR_SCHEMA, struct_validator),
Platform.SWITCH: SWITCH_SCHEMA,
}
@@ -4,7 +4,7 @@ from dataclasses import dataclass
import logging
from pymonoprice import Monoprice, get_monoprice
from serial import SerialException
from serialx import SerialException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT, Platform
@@ -4,7 +4,7 @@ import logging
from typing import Any, override
from pymonoprice import get_monoprice
from serial import SerialException
from serialx import SerialException
import voluptuous as vol
from homeassistant.config_entries import (
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pymonoprice"],
"requirements": ["pymonoprice==0.5"]
"requirements": ["pymonoprice==0.6.1"]
}
@@ -3,7 +3,7 @@
import logging
from typing import override
from serial import SerialException
from serialx import SerialException
from homeassistant import core
from homeassistant.components.media_player import (
@@ -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")
+16 -17
View File
@@ -16,13 +16,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -97,7 +96,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_BME280_HUMIDITY,
translation_key="bme280_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.bme280_humidity,
@@ -169,7 +168,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_HECA_HUMIDITY,
translation_key="heca_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.heca_humidity,
@@ -187,7 +186,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_MHZ14A_CARBON_DIOXIDE,
translation_key="mhz14a_carbon_dioxide",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.mhz14a_carbon_dioxide,
@@ -208,7 +207,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P0,
translation_key="pmsx003_pm1",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p0,
@@ -217,7 +216,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P1,
translation_key="pmsx003_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p1,
@@ -226,7 +225,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P2,
translation_key="pmsx003_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p2,
@@ -247,7 +246,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SDS011_P1,
translation_key="sds011_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sds011_p1,
@@ -256,7 +255,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SDS011_P2,
translation_key="sds011_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sds011_p2,
@@ -265,7 +264,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SHT3X_HUMIDITY,
translation_key="sht3x_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sht3x_humidity,
@@ -295,7 +294,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P0,
translation_key="sps30_pm1",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p0,
@@ -304,7 +303,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P1,
translation_key="sps30_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p1,
@@ -313,7 +312,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P2,
translation_key="sps30_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p2,
@@ -322,7 +321,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P4,
translation_key="sps30_pm4",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM4,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p4,
@@ -331,7 +330,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_DHT22_HUMIDITY,
translation_key="dht22_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.dht22_humidity,
+5 -5
View File
@@ -165,7 +165,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone
) -> None:
"""Initialize the thermostat."""
super().__init__(coordinator, zone, zone.zone_id)
super().__init__(coordinator, zone, zone.zone_id) # type: ignore[arg-type] # until fix issue #139773
thermostat = self._thermostat
unit = thermostat.get_unit()
min_humidity, max_humidity = thermostat.get_humidity_setpoint_limits()
@@ -198,7 +198,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
@property
@override
def current_temperature(self) -> int:
def current_temperature(self) -> float:
"""Return the current temperature."""
return self._zone.get_temperature()
@@ -288,7 +288,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
@property
@override
def target_temperature(self) -> int | None:
def target_temperature(self) -> float | None:
"""Temperature we try to reach."""
current_mode = self._zone.get_current_mode()
@@ -300,7 +300,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
@property
@override
def target_temperature_high(self) -> int | None:
def target_temperature_high(self) -> float | None:
"""Highest temperature we are trying to reach."""
current_mode = self._zone.get_current_mode()
@@ -310,7 +310,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
@property
@override
def target_temperature_low(self) -> int | None:
def target_temperature_low(self) -> float | None:
"""Lowest temperature we are trying to reach."""
current_mode = self._zone.get_current_mode()
@@ -40,4 +40,6 @@ class NexiaDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@override
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from API endpoint."""
return await self.nexia_home.update()
update_data = await self.nexia_home.update() # can return None
return update_data or {}
+3 -3
View File
@@ -56,7 +56,7 @@ class NexiaThermostatEntity(NexiaEntity):
thermostat_id = thermostat.thermostat_id
self._attr_device_info = DeviceInfo(
configuration_url=self.coordinator.nexia_home.root_url,
identifiers={(DOMAIN, thermostat_id)},
identifiers={(DOMAIN, thermostat_id)}, # type: ignore[arg-type] # until fix issue #139773
manufacturer=MANUFACTURER,
model=thermostat.get_model(),
name=thermostat.get_name(),
@@ -110,10 +110,10 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity):
if TYPE_CHECKING:
assert self._attr_device_info is not None
self._attr_device_info |= {
ATTR_IDENTIFIERS: {(DOMAIN, zone.zone_id)},
ATTR_IDENTIFIERS: {(DOMAIN, zone.zone_id)}, # type: ignore[arg-type] # until fix issue #139773
ATTR_NAME: zone_name,
ATTR_SUGGESTED_AREA: zone_name,
ATTR_VIA_DEVICE: (DOMAIN, zone.thermostat.thermostat_id),
ATTR_VIA_DEVICE: (DOMAIN, zone.thermostat.thermostat_id), # type: ignore[typeddict-item] # until fix issue #139773
}
self._zone_signal = f"{SIGNAL_ZONE_UPDATE}-{zone.zone_id}"
+1 -1
View File
@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["nexia"],
"requirements": ["nexia==2.11.1"]
"requirements": ["nexia==2.13.0"]
}
+1 -1
View File
@@ -42,7 +42,7 @@ class NexiaAutomationScene(NexiaEntity, Scene):
self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation
) -> None:
"""Initialize the automation scene."""
super().__init__(coordinator, automation.automation_id)
super().__init__(coordinator, automation.automation_id) # type: ignore[arg-type] # until fix issue #139773
self._attr_name = automation.name
self._automation = automation
self._attr_extra_state_attributes = {ATTR_DESCRIPTION: automation.description}
+3 -3
View File
@@ -37,7 +37,7 @@ async def async_setup_entry(
coordinator = config_entry.runtime_data
nexia_home = coordinator.nexia_home
entities: list[SwitchEntity] = []
room_iq_zones: dict[int, NexiaRoomIQHarmonizer] = {}
room_iq_zones: dict[str | int, NexiaRoomIQHarmonizer] = {}
for thermostat_id in nexia_home.get_thermostat_ids():
thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
if thermostat.has_emergency_heat():
@@ -69,7 +69,7 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
) -> None:
"""Initialize the hold mode switch."""
zone_id = zone.zone_id
super().__init__(coordinator, zone, zone_id)
super().__init__(coordinator, zone, zone_id) # type: ignore[arg-type] # until fix issue #139773
@property
@override
@@ -103,7 +103,7 @@ class NexiaRoomIQSwitch(NexiaThermostatZoneEntity, SwitchEntity):
coordinator: NexiaDataUpdateCoordinator,
zone: NexiaThermostatZone,
sensor: NexiaSensor,
room_iq_zones: dict[int, NexiaRoomIQHarmonizer],
room_iq_zones: dict[str | int, NexiaRoomIQHarmonizer],
) -> None:
"""Initialize the RoomIQ sensor switch."""
super().__init__(coordinator, zone, f"{sensor.id}_room_iq_sensor")
@@ -25,6 +25,8 @@ from .const import (
DOMAIN,
OVERRIDE_TYPE_CONSTANT,
OVERRIDE_TYPE_NOW,
SERIAL_LENGTH,
SERIAL_PREFIX_LENGTH,
)
DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation"
@@ -49,7 +51,20 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the initial step."""
if self._discovered_hubs is None:
self._discovered_hubs = dict(await nobo.async_discover_hubs())
# Wait 5s — real-world gaps up to ~4s have been observed.
discovered = dict(await nobo.async_discover_hubs(autodiscover_wait=5.0))
# Hide hubs that already have a config entry. Include matching on IP
# as serial prefix is not unique.
configured = {
(entry.data[CONF_IP_ADDRESS], entry.unique_id[:SERIAL_PREFIX_LENGTH])
for entry in self._async_current_entries(include_ignore=False)
if entry.unique_id
}
self._discovered_hubs = {
ip: prefix
for ip, prefix in discovered.items()
if (ip, prefix) not in configured
}
if not self._discovered_hubs:
# No hubs auto discovered
@@ -227,7 +242,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
)
async def _test_connection(self, serial: str, ip_address: str) -> str:
if not len(serial) == 12 or not serial.isdigit():
if len(serial) != SERIAL_LENGTH or not serial.isdigit():
raise NoboHubConnectError("invalid_serial")
try:
socket.inet_aton(ip_address)
@@ -9,6 +9,11 @@ CONF_OVERRIDE_TYPE = "override_type"
OVERRIDE_TYPE_CONSTANT = "constant"
OVERRIDE_TYPE_NOW = "now"
# Hub serial: 9-digit batch prefix + 3-digit per-hub suffix. Discovery
# broadcasts only the prefix; the user supplies the suffix.
SERIAL_PREFIX_LENGTH = 9
SERIAL_LENGTH = SERIAL_PREFIX_LENGTH + 3
NOBO_MANUFACTURER = "Glen Dimplex Nordic AS"
ATTR_HARDWARE_VERSION: Final = "hardware_version"
ATTR_SOFTWARE_VERSION: Final = "software_version"
@@ -64,11 +64,7 @@ rules:
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class:
status: todo
comment: >
Custom device class on global override select being dropped in
PR #170135.
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
@@ -53,7 +53,6 @@ class NoboGlobalSelector(NoboBaseEntity, SelectEntity):
"""Global override selector for Nobø Ecohub."""
_attr_translation_key = "global_override"
_attr_device_class = "nobo_hub__override"
_modes = {
nobo.API.OVERRIDE_MODE_NORMAL: "none",
nobo.API.OVERRIDE_MODE_AWAY: "away",
@@ -2,7 +2,6 @@
import asyncio
from collections.abc import Callable
import contextlib
from datetime import timedelta
from enum import IntEnum
import io
@@ -188,9 +187,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
current = asyncio.current_task()
if (prev := entry.runtime_data.upload_task) is not None and not prev.done():
prev.cancel()
# pylint: disable-next=home-assistant-action-swallowed-exception
with contextlib.suppress(asyncio.CancelledError):
await prev
await asyncio.wait({prev})
entry.runtime_data.upload_task = current
try:
+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
),
)
+6 -7
View File
@@ -15,13 +15,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
@@ -55,7 +54,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
OverkizSensorDescription(
key=OverkizState.CORE_BATTERY_LEVEL,
name="Battery level",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -329,7 +328,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
native_value=lambda value: round(cast(float, value), 2),
device_class=SensorDeviceClass.HUMIDITY,
# core:MeasuredValueType = core:RelativeValueInPercentage
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
# TemperatureSensor/TemperatureSensor
@@ -369,7 +368,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
key=OverkizState.CORE_CO_CONCENTRATION,
name="CO concentration",
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
# AirSensor/CO2Sensor
@@ -377,7 +376,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
key=OverkizState.CORE_CO2_CONCENTRATION,
name="CO2 concentration",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
# SunSensor/SunEnergySensor
@@ -489,7 +488,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
OverkizSensorDescription(
key=OverkizState.CORE_TARGET_CLOSURE,
name="Target closure",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_registry_enabled_default=False,
),
# ThreeWayWindowHandle/WindowHandle
+4 -10
View File
@@ -12,13 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
StateType,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.const import UnitOfPressure, UnitOfRatio, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -54,13 +48,13 @@ ENTITIES: tuple[PranaSensorEntityDescription, ...] = (
PranaSensorEntityDescription(
key=PranaSensorType.HUMIDITY,
value_fn=lambda coord: coord.data.humidity,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
),
PranaSensorEntityDescription(
key=PranaSensorType.VOC,
value_fn=lambda coord: coord.data.voc,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
),
PranaSensorEntityDescription(
@@ -72,7 +66,7 @@ ENTITIES: tuple[PranaSensorEntityDescription, ...] = (
PranaSensorEntityDescription(
key=PranaSensorType.CO2,
value_fn=lambda coord: coord.data.co2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
),
PranaSensorEntityDescription(
@@ -25,6 +25,9 @@ from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .common import sanitize_config_entry
@@ -58,7 +61,9 @@ BASE_SCHEMA = vol.Schema(
)
),
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT, autocomplete="username")
),
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_TOKEN, default=False): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
@@ -67,7 +72,12 @@ BASE_SCHEMA = vol.Schema(
PASSWORD_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
}
)
TOKEN_SCHEMA = vol.Schema(
@@ -20,8 +20,9 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .common import sanitize_config_entry
@@ -201,7 +202,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
def _init_proxmox(self) -> None:
"""Initialize ProxmoxAPI instance."""
data = sanitize_config_entry(self.config_entry.data)
auth_kwargs = {
auth_kwargs: dict[str, Any] = {
"password": data.get(CONF_PASSWORD),
}
if data.get(CONF_TOKEN):
@@ -331,6 +332,41 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
for storages_callback in self.new_storages_callbacks:
storages_callback(new_storage_data)
self._async_remove_stale_devices(data)
@callback
def _async_remove_stale_devices(self, data: dict[str, ProxmoxNodeData]) -> None:
"""Remove devices for nodes/VMs/containers/storages no longer present."""
valid_identifiers: set[str] = set()
for node_data in data.values():
valid_identifiers.add(
f"{self.config_entry.entry_id}_node_{node_data.node['id']}"
)
valid_identifiers.update(
f"{self.config_entry.entry_id}_vm_{vmid}" for vmid in node_data.vms
)
valid_identifiers.update(
f"{self.config_entry.entry_id}_container_{vmid}"
for vmid in node_data.containers
)
valid_identifiers.update(
f"{self.config_entry.entry_id}_storage_{storage}"
for storage in node_data.storages
)
registry = dr.async_get(self.hass)
for device in dr.async_entries_for_config_entry(
registry, self.config_entry.entry_id
):
if not any(
identifier[0] == DOMAIN and identifier[1] in valid_identifiers
for identifier in device.identifiers
):
_LOGGER.debug("Removing stale device: %s", device.identifiers)
registry.async_update_device(
device.id, remove_config_entry_id=self.config_entry.entry_id
)
class ProxmoxSetupError(Exception):
"""Base exception for Proxmox setup issues."""
@@ -77,7 +77,7 @@ rules:
icon-translations: done
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo
stale-devices: done
# Platinum
async-dependency:
@@ -30,6 +30,7 @@ from homeassistant.util.event_type import EventType
# startup
from . import (
backup, # noqa: F401
entity_options,
entity_registry,
websocket_api,
)
@@ -42,6 +43,7 @@ from .const import ( # noqa: F401
SupportedDialect,
)
from .core import Recorder
from .entity_options import is_entity_recorded # noqa: F401
from .services import async_setup_services
from .tasks import AddRecorderPlatformTask
from .util import get_instance
@@ -125,15 +127,6 @@ CONFIG_SCHEMA = vol.Schema(
)
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
"""Check if an entity is being recorded.
Async friendly.
"""
instance = get_instance(hass)
return instance.entity_filter is None or instance.entity_filter(entity_id)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the recorder."""
conf = config[DOMAIN]
@@ -167,6 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
get_instance.cache_clear()
entity_registry.async_setup(hass)
entity_options.async_setup(hass)
instance.async_initialize()
instance.async_register()
instance.start()
@@ -0,0 +1,68 @@
"""Control recorder entity options."""
import dataclasses
from enum import StrEnum
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from .util import get_instance
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
"""Check if an entity is being recorded.
Async friendly.
"""
instance = get_instance(hass)
return instance.entity_filter is None or instance.entity_filter(entity_id)
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the recorder entity options."""
websocket_api.async_register_command(hass, ws_get_entity_options)
class EntityRecordingDisabler(StrEnum):
"""What disabled recording of an entity."""
USER = "user"
@dataclasses.dataclass(frozen=True)
class RecorderEntityOptions:
"""Recorder options for an entity."""
recording_disabled_by: EntityRecordingDisabler | None = None
def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage."""
return {
"recording_disabled_by": self.recording_disabled_by,
}
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/entity_options/get",
vol.Required("entity_id"): cv.strict_entity_id,
}
)
@callback
def ws_get_entity_options(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Get recorder settings for a single entity."""
entity_id: str = msg["entity_id"]
recording_disabled = (
None if is_entity_recorded(hass, entity_id) else EntityRecordingDisabler.USER
)
options = RecorderEntityOptions(recording_disabled_by=recording_disabled)
connection.send_result(msg["id"], options.to_json())
+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,
+3 -4
View File
@@ -15,14 +15,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_DEVICES,
CONF_NAME,
CONF_SENSOR_TYPE,
CONF_UNIT_OF_MEASUREMENT,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UV_INDEX,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -30,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -85,7 +84,7 @@ SENSOR_TYPES = (
name="CO2 air quality",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
),
SensorEntityDescription(
key="command",
@@ -140,7 +139,7 @@ SENSOR_TYPES = (
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
),
SensorEntityDescription(
key="humidity_status",
@@ -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,
),
+5 -5
View File
@@ -17,7 +17,7 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.const import EntityCategory, UnitOfRatio, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -183,7 +183,7 @@ BLOCK_NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
("device", "valvePos"): BlockNumberDescription(
key="device|valvepos",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
available=lambda block: cast(int, block.valveError) != 1,
entity_category=EntityCategory.CONFIG,
native_min_value=0,
@@ -291,7 +291,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="blu_trv_set_valve_position",
removal_condition=lambda config, _, key: (
config[key].get("enable", True) is True
@@ -307,7 +307,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="cury_set",
slot="left",
available=lambda status: (
@@ -325,7 +325,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="cury_set",
slot="right",
available=lambda status: (
+13 -14
View File
@@ -17,10 +17,8 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfApparentPower,
@@ -30,6 +28,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
@@ -203,7 +202,7 @@ class RpcBluTrvSensor(RpcSensor):
BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("device", "battery"): BlockSensorDescription(
key="device|battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda settings, _: settings.get("external_power") == 1,
@@ -350,7 +349,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("sensor", "concentration"): BlockSensorDescription(
key="sensor|concentration",
translation_key="gas_concentration",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
("sensor", "temp"): BlockSensorDescription(
@@ -373,7 +372,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
),
("sensor", "humidity"): BlockSensorDescription(
key="sensor|humidity",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -398,7 +397,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("relay", "totalWorkTime"): BlockSensorDescription(
key="relay|totalWorkTime",
translation_key="lamp_life",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=get_shelly_air_lamp_life,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -1255,7 +1254,7 @@ RPC_SENSORS: Final = {
"humidity_rh": RpcSensorDescription(
key="humidity",
sub_key="rh",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1266,7 +1265,7 @@ RPC_SENSORS: Final = {
"battery": RpcSensorDescription(
key="devicepower",
sub_key="battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=lambda status, _: status["percent"],
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1296,7 +1295,7 @@ RPC_SENSORS: Final = {
key="input",
sub_key="percent",
translation_key="analog",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, _, key: (
config[key]["type"] != "analog" or config[key]["enable"] is False
@@ -1389,7 +1388,7 @@ RPC_SENSORS: Final = {
key="blutrv",
sub_key="pos",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
removal_condition=lambda config, _, key: (
@@ -1400,7 +1399,7 @@ RPC_SENSORS: Final = {
"blutrv_battery": RpcSensorDescription(
key="blutrv",
sub_key="battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -1448,7 +1447,7 @@ RPC_SENSORS: Final = {
"number_current_humidity": RpcSensorDescription(
key="number",
sub_key="value",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1686,7 +1685,7 @@ RPC_SENSORS: Final = {
translation_key="left_slot_level",
value=lambda status, _: status["left"]["vial"]["level"],
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
available=lambda status: (
(left := status["left"]) is not None
@@ -1710,7 +1709,7 @@ RPC_SENSORS: Final = {
translation_key="right_slot_level",
value=lambda status, _: status["right"]["vial"]["level"],
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
available=lambda status: (
(right := status["right"]) is not None
@@ -12,7 +12,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.4.0"],
"requirements": ["pysmlight==0.5.0"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."
@@ -1,7 +1,8 @@
"""The Steam integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .coordinator import SteamConfigEntry, SteamDataUpdateCoordinator
@@ -21,3 +22,22 @@ 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
@@ -7,11 +7,13 @@ import steam.api
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
@@ -41,6 +43,8 @@ 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
@@ -94,12 +98,22 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a reauthorization flow request."""
return await self.async_step_reauth_confirm()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfigure flow."""
return await self.async_step_reauth_confirm(user_input)
async def async_step_reauth_confirm(
self, user_input: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors: dict[str, str] = {}
entry = self._get_reauth_entry()
entry = (
self._get_reauth_entry()
if self.source == SOURCE_REAUTH
else self._get_reconfigure_entry()
)
if user_input is not None:
try:
@@ -120,12 +134,14 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
entry, data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
step_id=(
"reauth_confirm" if self.source == SOURCE_REAUTH else SOURCE_RECONFIGURE
),
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input
),
errors=errors,
description_placeholders=PLACEHOLDERS,
description_placeholders={CONF_NAME: entry.title, **PLACEHOLDERS},
)
@@ -150,7 +166,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"sensor.steam_{_id}"
Platform.SENSOR, DOMAIN, f"{_id}_account"
)
):
er.async_get(self.hass).async_remove(entity_id)
@@ -1,5 +1,6 @@
"""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
@@ -12,9 +13,17 @@ class SteamEntity(CoordinatorEntity[SteamDataUpdateCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: SteamDataUpdateCoordinator) -> None:
def __init__(
self,
coordinator: SteamDataUpdateCoordinator,
steamid: str,
description: SensorEntityDescription,
) -> 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,10 +80,7 @@ class SteamSensorEntity(SteamEntity, SensorEntity):
description: SteamSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._steamid = steamid
self.entity_description = description
self._attr_unique_id = f"sensor.steam_{steamid}"
super().__init__(coordinator, steamid, description)
self._attr_name = self.entity_description.name_fn(coordinator.data[steamid])
@property
@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -21,6 +22,16 @@
"description": "The Steam integration requires re-authentication.\n\nYou can find your Steam Web API key [**here**]({api_key_url}).",
"title": "[%key:common::config_flow::title::reauth%]"
},
"reconfigure": {
"data": {
"api_key": "[%key:component::steam_online::config::step::user::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::steam_online::config::step::user::data_description::api_key%]"
},
"description": "You can find your Steam Web API key [**here**]({api_key_url}).",
"title": "Reconfigure {name}"
},
"user": {
"data": {
"account": "Steam ID",

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