Compare commits

...

113 Commits

Author SHA1 Message Date
J. Nick Koston cfa5ce9d91 Bump dbus-fast to 5.0.1
fixes blocking I/O in event loop

changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v5.0.0...v5.0.1
2026-05-20 10:05:41 -05:00
Manu 79fe415d6c Add exception translations to Notifications for Android TV / Fire TV (#171583) 2026-05-20 16:54:03 +02:00
J. Nick Koston 00e3a909a0 Bump bluetooth-data-tools to 1.29.11 (#170949)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-20 10:52:58 -04:00
Jan-Philipp Benecke a85fb79331 Remove duplicate constant in zha (#171586) 2026-05-20 16:25:02 +02:00
Ronald van der Meer 7f2f268fca Fix Duco VLV nodes not creating CO2 and humidity sensors (#171182) 2026-05-20 16:21:32 +02:00
Paulus Schoutsen 4c31a1737d Split BrowseMediaSource into root and source-specific classes (#170835)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-20 09:59:49 -04:00
Abílio Costa abd8d85225 Add validator subagents to github-pr-reviewer skill (#171370) 2026-05-20 14:48:29 +01:00
Jordan Harvey 626a1a5c87 Remove positional message strings when translation_key is set in nintendo_parental_controls (#171531) 2026-05-20 15:47:43 +02:00
Jarkko Pöyry 1b2e8ccc0f Avoid polling in wled integration (#161183)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-20 15:47:07 +02:00
Mike O'Driscoll 4da2cd465a casper_glow: fix missing translation for exception (#171534) 2026-05-20 15:47:01 +02:00
Thomas55555 e3c31a3482 Allow setting a custom laqi in Google Air Quality (#160681)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-05-20 15:41:45 +02:00
A. Gideonse a0b52e0f58 Bump indevolt-api to 1.8.1 (#171472) 2026-05-20 14:37:01 +01:00
Andrew Jackson 75dd509c7b Add translations to Mastodon exceptions (#171528) 2026-05-20 15:33:01 +02:00
Max Michels 6540ccd52a Replace duplicate constants with homeassistant.const imports in citybikes (#171478) 2026-05-20 15:18:12 +02:00
Ronald van der Meer a35ad41495 Fix untranslated config entry error in Duco (#171514) 2026-05-20 15:17:32 +02:00
Max Michels cedf5a5861 Replace duplicate constants with homeassistant.const imports hddtemp (#171517) 2026-05-20 15:15:55 +02:00
Alexey Masolov 16f4dc74bf Add TIMEOUT constant to CalDAV integration (#171463)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 15:08:42 +02:00
Franck Nijhof c5f22936e4 Use HA timezone for date in saj (#171450) 2026-05-20 14:57:34 +02:00
Jan Čermák aa23b3176c Bump base image to 2026.05.0 with Python 3.14.5, use 3.14.5 in CI (#171482) 2026-05-20 14:43:54 +02:00
Franck Nijhof a144bbab2b Fix Wyoming satellite crash when TTS is not configured (#171513) 2026-05-20 14:30:20 +02:00
Erik Montnemery 6a20b99252 Adjust device_registry.async_setup (#167653) 2026-05-20 14:28:10 +02:00
Franck Nijhof 8a12c06116 Fix PowerView cover crash when shade position is unavailable (#171471) 2026-05-20 13:50:49 +02:00
Alistair Francis 5dc057b36d husqvarna_automower_ble: Gracefully handle unreachable device (#171479)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2026-05-20 13:48:11 +02:00
Robert Resch 6d6f14a0aa Revert "Bump py-opendisplay to 7.0.0" (#171477) 2026-05-20 13:42:15 +02:00
dependabot[bot] 7bd81aeb9f Bump actions/ai-inference from 2.0.7 to 2.1.0 (#171475)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 13:06:59 +02:00
Sören 8dc29b5411 Bump avea to 1.8.0 (#171473) 2026-05-20 11:55:49 +01:00
Sören 1cabcf522e Fix Avea stale brightness restore (#171139) 2026-05-20 12:53:45 +02:00
Erik Montnemery ff8d244839 Remove advanced mode from tts service actions (#171462) 2026-05-20 12:11:30 +02:00
J. Nick Koston d1a5b0dbd3 Bump aiodns to 4.0.4 (#171420)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
2026-05-20 12:10:03 +02:00
Erik Montnemery 86bab0c0f6 Remove advanced mode from zwave_js service actions (#171465) 2026-05-20 11:57:05 +02:00
J. Nick Koston 7f320a5a41 Bump zeroconf to 0.149.7 (#171054)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2026-05-20 11:45:24 +02:00
Erik Montnemery 309ce5545e Set breaks_in_ha_version on issue about not running core in venv or container (#171426) 2026-05-20 11:35:55 +02:00
Erik Montnemery 293d7851ba Remove advanced mode from webostv service actions (#171464) 2026-05-20 12:33:21 +03:00
Erik Montnemery eeb9270241 Rename advanced options section to additional options in light service actions (#171444) 2026-05-20 11:30:30 +02:00
Erik Montnemery b841a26aff Remove advanced mode from fan service actions (#171439) 2026-05-20 11:29:41 +02:00
Erik Montnemery 40f7a2f50f Remove advanced mode from climate service actions (#171437) 2026-05-20 11:29:00 +02:00
Willem-Jan van Rootselaar 15b230c4e7 Bump python-bsblan to version 6.0.1 (#171447) 2026-05-20 11:24:24 +02:00
Erik Montnemery 49a14112b7 Remove advanced mode from sharkiq service actions (#171456) 2026-05-20 11:23:23 +02:00
Erik Montnemery e59e631a87 Remove advanced mode from squeezebox service actions (#171457) 2026-05-20 11:22:58 +02:00
Erik Montnemery c1d0c9fb9f Rename advanced options section to additional options in kitchen_sink service actions (#171443) 2026-05-20 10:57:35 +02:00
Andrew Jackson ee7461ed9c Remove duplicate const in time_date (#171438) 2026-05-20 10:43:27 +02:00
AlCalzone 9757f8b574 Add support for Z-Wave credential management (#168360)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-05-20 10:41:02 +02:00
Thomas Bouron 5ee96a3616 Improve temperature unit handling in Tuya numbers and sensors (#170432) 2026-05-20 09:59:27 +02:00
Tomer 703ac31bd1 Use CONF_MODEL from homeassistant.const in victron_gx (#171434) 2026-05-20 09:56:23 +02:00
Erik Montnemery 52bf0b0ee0 Remove references to the removed toon.update service (#171435) 2026-05-20 09:55:45 +02:00
Robert Resch 29db335930 Remove advanced mode dependency from version config flow (#171215) 2026-05-20 09:50:16 +02:00
Denis Shulyaka 8257107462 Replace duplicate constants with homeassistant.const imports in humidifier (#171354)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-05-20 09:27:05 +02:00
Jan Bouwhuis c7618949da Remove deprecated advanced flags from MQTT service actions services.yaml (#171430) 2026-05-20 09:24:48 +02:00
Marcos A L M Macedo 592154bd27 Add fixture for Tuya INTELAR IR288 (#171412)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-05-20 09:11:11 +02:00
epenet 7919330ae0 Bump renault-api to 0.5.9 (#171428) 2026-05-20 09:10:44 +02:00
epenet 6ffe1bab9a Fix duplicate-const in Renault services (#171429) 2026-05-20 08:58:36 +02:00
Michael Heyman 91705ef821 Replace duplicate ATTR_TEMPERATURE constant with homeassistant.const (#171423)
Co-authored-by: Michael Heyman <michaelheyman@users.noreply.github.com>
2026-05-20 08:58:11 +02:00
J. Nick Koston e15797af14 Bump aiodiscover to 3.2.0 (#171401) 2026-05-20 08:43:10 +02:00
Russell VanderMey a966ce4586 Use homeassistant.const CONF_TOKEN for triggercmd (#171411) 2026-05-20 08:42:44 +02:00
Paulus Schoutsen ee2de6641f Use CONF_MODEL from homeassistant.const in marantz_infrared (#171415)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 08:42:11 +02:00
J. Nick Koston 5f85ae6f95 Bump dbus-fast to 5.0.0 (#171421) 2026-05-20 08:40:57 +02:00
Phil-Rad 275e0b3dd1 Add reconfigure flow to cert_expiry (#170888) 2026-05-20 08:39:32 +02:00
Erik Montnemery a9475683e1 Add new device tracker base entity BaseScannerEntity (#171063) 2026-05-20 08:09:28 +02:00
Petro31 d4e1a7075e Clean up legacy template entity code (#170016) 2026-05-20 07:44:32 +02:00
Adam Katic 2a3d75eb2b Add missing speedtestdotnet options flow translation (#171153) 2026-05-20 08:26:37 +03:00
Tsvi Mostovicz 9212d2300c Replace duplicate constants with homeassistant.const imports in jewish calendar (#171403) 2026-05-19 21:18:18 -04:00
J. Nick Koston 6836b27ba6 Bump yarl to 1.24.2 (#171407) 2026-05-19 21:17:58 -04:00
TimL 8656f52d7a Add buzzer action play_rtttl to SMLIGHT (#166665)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 23:24:49 +02:00
Paul Bottein 6a07ca93e9 Migrate Freebox to has_entity_name and key-based unique IDs (#169860) 2026-05-19 23:17:57 +02:00
Jan Bouwhuis 77990c8808 Remove duplicate constants for homeassistant integration (#171363) 2026-05-19 23:00:55 +02:00
Jan Rieger e4227ee1d4 Replace duplicate constant with homeassistant.const import in gpsd (#171355) 2026-05-19 22:59:48 +02:00
Denis Shulyaka 87aca7416f Replace duplicate constants with homeassistant.const imports in generic_hygrostat (#171358) 2026-05-19 22:59:00 +02:00
Glenn Waters 1ec6619a20 ElkM1 integration: Fix duplicate constants (#171364) 2026-05-19 22:57:15 +02:00
Denis Shulyaka 85013282e4 Explicitly set parallel-updates for Anthropic (#171387) 2026-05-19 22:32:40 +02:00
Chris ceab93ab83 refactor: remove redundant CONF_ID constant from OpenEVSE integration (#171366) 2026-05-19 22:14:54 +02:00
Tomas Kislan ac636ce54f Use homeassistant.const CONF_HOST and CONF_PORT in minio (#171395)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 22:13:59 +02:00
Jan-Philipp Benecke 3287b01ed1 Group Nuki executor jobs (#171391) 2026-05-19 22:06:46 +02:00
Jens Timmerman 3acc7d08b3 fix translations (#171157) 2026-05-19 22:04:31 +02:00
Jan-Philipp Benecke 16eb5dce63 Fix language handling in jewish_calendar tests (#171383) 2026-05-19 21:47:37 +02:00
Petro31 3fee05db71 Add entity_platform helper function to create issues when platform setup is not supported by integration (#171105)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 21:08:03 +02:00
Artur Pragacz f823ef639a Prefix area to entity ID (#170560) 2026-05-19 20:53:26 +02:00
mithomas da4263b95c Fix missing delay and repeat support in LG Netcast remote (#170324)
Co-authored-by: Copilot <copilot@github.com>
2026-05-19 20:49:43 +02:00
Robert Resch 29e2184163 Fix workflow run (#171367) 2026-05-19 20:49:10 +02:00
Robert Resch 816c3ff939 Adjust aw check requirements checks (#171389)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-19 20:48:53 +02:00
karwosts 2348ccc76e Use modern batteries for demo integration (#171376) 2026-05-19 19:23:11 +01:00
Manu 4202686a0d Remove duplicate constants in Mobile App integration (#171379) 2026-05-19 20:17:18 +02:00
dontinelli dd1437f5f2 Remove obsolete local const in slide_local (#171386) 2026-05-19 20:16:09 +02:00
Willem-Jan van Rootselaar 1a1c9d935c Remove duplicate constant in bsblan integration (#171385) 2026-05-19 19:48:17 +02:00
javicalle 4c0e7eb92d Migrate RFLink YAML configuration (ADR0007) (#161822)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 19:44:03 +02:00
Josh Gustafson d288645f0e Declare PARALLEL_UPDATES on arcam_fmj platforms (#171151)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:26:57 +02:00
Jan-Philipp Benecke 66aad8d3c5 Fix solaredge tests (#171378) 2026-05-19 18:38:10 +02:00
peteS-UK 89e15b9eae Remove unused ATTR_TIME from squeezebox const.py (#171374) 2026-05-19 18:29:03 +02:00
Nick Haghiri 489b831a4b Use homeassistant.const CONF_PREFIX in backblaze_b2 (#171365) 2026-05-19 18:24:56 +02:00
Manu f1854e1816 Remove duplicate constant in Notifications for Android TV / Fire TV integration (#171377) 2026-05-19 18:18:36 +02:00
Manu 8931ce561c Remove duplicate constant in ntfy integration (#171375) 2026-05-19 18:08:46 +02:00
Petro31 4d19cec214 Replace duplicate constants with homeassistant.const imports in template (#171349)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 18:08:20 +02:00
Manu e111678c40 Remove duplicate constant from HTML5 integration (#171373) 2026-05-19 18:07:07 +02:00
Jan Bouwhuis 69de70407b Remove duplicate constants for MQTT (#171359) 2026-05-19 18:05:16 +02:00
Andrew Jackson 64d17521a4 Remove duplicate const in Mastodon (#171357) 2026-05-19 16:38:11 +01:00
Erik Montnemery b52476a37e Remove use of advanced mode from the homekit integration (#171200) 2026-05-19 16:27:58 +02:00
Denis Shulyaka 58c906a2d1 Replace duplicate constants with homeassistant.const imports in anthropic (#171316) 2026-05-19 16:17:15 +02:00
Denis Shulyaka 3b2fa3f5b7 Replace duplicate constants with homeassistant.const imports in openai_conversation (#171348)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 16:15:12 +02:00
Paul Bottein 0dae4689cf Use CONF_CODE in Novy Cooker Hood (#171350)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 16:15:08 +02:00
Joost Lekkerkerker cd7fe836b0 Fix CI (#171351) 2026-05-19 16:07:27 +02:00
Robert Resch e3bae0dbda Use multistage workflow to run agentic workflow on forks (#171186)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 16:06:14 +02:00
Mika 7cf3cba27b Split SolarEdge power-flow attributes into sensor entities (#170457)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 15:42:36 +02:00
Tsvi Mostovicz de70d9ed82 Jewish Calendar: add a calendar entity (#145140)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-19 15:40:26 +02:00
Onero-testdev eb0c1700b7 Add support for SwitchBot Lock Vision (Pro) and Lock Pro Wifi (#170470) 2026-05-19 15:36:33 +02:00
Franck Nijhof 6fa5fc77aa Add pylint checker for duplicate homeassistant.const definitions (#170848)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 15:10:13 +02:00
Åke Strandberg c705e8ff56 Cleanup miele API timeouts (#171172) 2026-05-19 14:19:01 +02:00
Jan Bouwhuis ee248b536e Fix subscription ID for restored subscriptions (#171130) 2026-05-19 14:08:36 +02:00
Åke Strandberg 7bfd11cf2e Add missing Miele Dishwasher codes (#171175) 2026-05-19 14:01:35 +02:00
iluvdata 2dae262135 Address future error in RepairFlow for Anthropic (#171156) 2026-05-19 14:53:18 +03:00
Aidan Timson 76a463dd50 Bump aiolyric to 2.1.1, Update OAuth URL for lyric (#171181) 2026-05-19 13:16:47 +02:00
epenet 0d83b1cbe8 Handle temperature unit mismatch in Tuya climate (#171183) 2026-05-19 13:16:02 +02:00
Artur Pragacz ae622a7cd4 Fix zwave_js fixture path resolution (#171196) 2026-05-19 13:11:43 +02:00
Przemko92 3f0af1e5b7 Add Compit switch (#164053) 2026-05-19 13:00:39 +02:00
G Johansson 742e63d02c Fix exception handling in command_line notify service (#170709)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-19 12:49:19 +02:00
728 changed files with 24801 additions and 12429 deletions
@@ -18,6 +18,13 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
+1 -1
View File
@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.04.0"
BASE_IMAGE_VERSION: "2026.05.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
@@ -0,0 +1,31 @@
name: Check requirements (changes detection)
# Stage 1 of the agentic Check requirements workflow.
# Just kicks off Stage 2 (`check-requirements-dispatcher.yml`) which starts the agentic workflow
# yamllint disable-line rule:truthy
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
- "pyproject.toml"
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
changes:
name: Requirements files changed
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Record PR number
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |-
echo "Requirements files changed in PR #${PR_NUMBER}"
@@ -0,0 +1,73 @@
name: Check requirements (dispatcher)
# Stage 2 of the agentic Check requirements workflow. Runs on completion of
# stage 1 (`check-requirements-changes.yml`) and dispatches stage 3
# (`check-requirements.lock.yml`)
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
# yamllint disable-line rule:truthy
on: # zizmor: ignore[dangerous-triggers]
# workflow_run is safe here: this workflow does not check out PR code or run
# any code from the triggering PR. It only resolves the PR number from the
# head SHA and dispatches `check-requirements.lock.yml` with that number as
# a sanitized string input. The PR code is analysed downstream in the
# agentic workflow (`check-requirements.lock.yml`)
workflow_run:
workflows: ["Check requirements (changes detection)"]
types: [completed]
permissions: {}
jobs:
dispatch:
name: Dispatch agentic requirements check
if: >
github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write # For triggering the downstream workflow
pull-requests: read # For querying PRs by commit SHA
steps:
- name: Resolve PR number from head SHA and trigger agentic requirements check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
const headBranch = context.payload.workflow_run.head_branch;
const headRepository = context.payload.workflow_run.head_repository;
const headRepo = headRepository.full_name;
// Query the head repository (which may be a fork). When the PR comes
// from a fork, the upstream's listPullRequestsAssociatedWithCommit
// returns no results for the fork's commit SHA.
const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: headRepository.owner.login,
repo: headRepository.name,
commit_sha: headSha,
});
const matches = pulls.filter(p =>
p.state === 'open'
&& p.head.ref === headBranch
&& p.head.repo?.full_name === headRepo
);
if (matches.length === 0) {
core.info(`No open PR found for head SHA ${headSha} on ${headRepo}:${headBranch}; nothing to dispatch.`);
return;
}
const defaultBranch = context.payload.workflow_run.repository.default_branch;
for (const pr of matches) {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'check-requirements.lock.yml',
ref: defaultBranch,
inputs: {
pull_request_number: String(pr.number),
},
});
core.info(`Dispatched check-requirements.lock.yml for PR #${pr.number}.`);
}
+66 -72
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"70aa938926d4aac250b6d7aca7251f663476fc2da39a29d1ffd569dc725c133a","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"62eb6e3d38092bd041a0c1ddfdaef94cf4b9c694b2d2bcac6cbbecd6810230ca","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# 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":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"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":"d3abfe96a194bce3a523ed2093ddedd5704cdf62","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
@@ -34,7 +34,6 @@
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
@@ -47,20 +46,8 @@
# - ghcr.io/github/github-mcp-server:v1.0.4
# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
name: "Requirements License and Availability Check"
name: "Check requirements"
on:
pull_request:
# forks: # Fork filtering applied via job conditions
# - "*" # Fork filtering applied via job conditions
paths:
- requirements*.txt
- homeassistant/package_constraints.txt
- pyproject.toml
types:
- opened
- synchronize
- reopened
# roles: all # Roles processed as role check in pre-activation job
workflow_dispatch:
inputs:
aw_context:
@@ -76,10 +63,10 @@ on:
permissions: {}
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}"
cancel-in-progress: true
group: ${{ github.workflow }}-${{ inputs.pull_request_number }}
run-name: "Requirements License and Availability Check"
run-name: "Check requirements"
jobs:
activation:
@@ -88,7 +75,6 @@ jobs:
actions: read
contents: read
outputs:
body: ${{ steps.sanitized.outputs.body }}
comment_id: ""
comment_repo: ""
engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
@@ -99,8 +85,6 @@ jobs:
setup-span-id: ${{ steps.setup.outputs.span-id }}
setup-trace-id: ${{ steps.setup.outputs.trace-id }}
stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
text: ${{ steps.sanitized.outputs.text }}
title: ${{ steps.sanitized.outputs.title }}
steps:
- name: Setup Scripts
id: setup
@@ -109,7 +93,7 @@ jobs:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -122,7 +106,7 @@ jobs:
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_AGENT_VERSION: "1.0.48"
GH_AW_INFO_CLI_VERSION: "v0.74.4"
GH_AW_INFO_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_INFO_WORKFLOW_NAME: "Check requirements"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
GH_AW_INFO_STAGED: "false"
@@ -187,17 +171,6 @@ jobs:
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs');
await main();
- name: Compute current body text
id: sanitized
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
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"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs');
await main();
- name: Create prompt with built-in context
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
@@ -210,24 +183,25 @@ jobs:
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
# poutine:ignore untrusted_checkout_exec
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
<system>
GH_AW_PROMPT_f9148b37e60758ba_EOF
GH_AW_PROMPT_2df1318dbe2d4011_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
<safe-output-tools>
Tools: add_comment, missing_tool, missing_data, noop
</safe-output-tools>
GH_AW_PROMPT_f9148b37e60758ba_EOF
GH_AW_PROMPT_2df1318dbe2d4011_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -256,18 +230,19 @@ jobs:
{{/if}}
</github-context>
GH_AW_PROMPT_f9148b37e60758ba_EOF
GH_AW_PROMPT_2df1318dbe2d4011_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
cat << 'GH_AW_PROMPT_f9148b37e60758ba_EOF'
cat << 'GH_AW_PROMPT_2df1318dbe2d4011_EOF'
</system>
{{#runtime-import .github/workflows/check-requirements.md}}
GH_AW_PROMPT_f9148b37e60758ba_EOF
GH_AW_PROMPT_2df1318dbe2d4011_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_ENGINE_ID: "copilot"
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
@@ -286,6 +261,7 @@ jobs:
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_INPUTS_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools'
with:
script: |
@@ -306,6 +282,7 @@ jobs:
GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
GH_AW_INPUTS_PULL_REQUEST_NUMBER: process.env.GH_AW_INPUTS_PULL_REQUEST_NUMBER,
GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST
}
});
@@ -375,7 +352,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -430,16 +407,13 @@ jobs:
GH_HOST: github.com
- name: Install AWF binary
run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
- name: Determine automatic lockdown mode for GitHub MCP Server
id: determine-automatic-lockdown
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
- name: Parse integrity filter lists
id: parse-guard-vars
env:
GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
with:
script: |
const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');
await determineAutomaticLockdown(github, context, core);
GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }}
GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }}
GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }}
run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh"
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -459,19 +433,21 @@ jobs:
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46 ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 ghcr.io/github/github-mcp-server:v1.0.4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
- name: Generate Safe Outputs Config
env:
GH_AW_INPUT_PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
run: |
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_e3fcd372e2542806_EOF'
{"add_comment":{"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_e3fcd372e2542806_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_c7878b8b9775118a_EOF
{"add_comment":{"max":1,"target":"${GH_AW_INPUT_PULL_REQUEST_NUMBER}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_c7878b8b9775118a_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
{
"description_suffixes": {
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ inputs.pull_request_number }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -627,8 +603,6 @@ jobs:
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }}
GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }}
GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -eo pipefail
@@ -659,7 +633,7 @@ jobs:
mkdir -p /home/runner/.copilot
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
cat << GH_AW_MCP_CONFIG_710f47825b96ccb9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
cat << GH_AW_MCP_CONFIG_103328ae7b98b0c7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
"github": {
@@ -673,8 +647,11 @@ jobs:
},
"guard-policies": {
"allow-only": {
"min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
"repos": "$GITHUB_MCP_GUARD_REPOS"
"approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }},
"blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }},
"min-integrity": "unapproved",
"repos": "all",
"trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }}
}
}
},
@@ -700,7 +677,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
GH_AW_MCP_CONFIG_710f47825b96ccb9_EOF
GH_AW_MCP_CONFIG_103328ae7b98b0c7_EOF
- name: Mount MCP servers as CLIs
id: mount-mcp-clis
continue-on-error: true
@@ -899,6 +876,21 @@ jobs:
if [ ! -f /tmp/gh-aw/agent_output.json ]; then
echo '{"items":[]}' > /tmp/gh-aw/agent_output.json
fi
- if: always()
name: Verify agent produced an add_comment safe-output
run: |-
OUTPUT=/tmp/gh-aw/agent_output.json
if [ ! -f "${OUTPUT}" ]; then
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
exit 1
fi
if ! grep -q '"add_comment"' "${OUTPUT}"; then
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
echo "Agent output:"
cat "${OUTPUT}"
exit 1
fi
- name: Upload agent artifacts
if: always()
continue-on-error: true
@@ -910,6 +902,8 @@ jobs:
/tmp/gh-aw/sandbox/agent/logs/
/tmp/gh-aw/redacted-urls.log
/tmp/gh-aw/mcp-logs/
/tmp/gh-aw/proxy-logs/
!/tmp/gh-aw/proxy-logs/proxy-tls/
/tmp/gh-aw/agent_usage.json
/tmp/gh-aw/agent-stdio.log
/tmp/gh-aw/pre-agent-audit.txt
@@ -959,7 +953,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -983,7 +977,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: "1"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
@@ -999,7 +993,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
@@ -1016,7 +1010,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_MISSING_TOOL_CREATE_ISSUE: "true"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
@@ -1030,7 +1024,7 @@ jobs:
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
@@ -1044,7 +1038,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
GH_AW_WORKFLOW_ID: "check-requirements"
@@ -1098,7 +1092,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -1166,7 +1160,7 @@ jobs:
if: always() && steps.detection_guard.outputs.run_detection == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
WORKFLOW_NAME: "Requirements License and Availability Check"
WORKFLOW_NAME: "Check requirements"
WORKFLOW_DESCRIPTION: "Checks changed Python package requirements on PRs targeting the core repo (including PRs opened from forks) and verifies licenses match PyPI metadata, source repositories are publicly accessible, PyPI releases were uploaded via automated CI (Trusted Publisher attestation), the package's release pipeline uses OIDC or equivalent automated credentials (not static tokens), and the PR description contains the required links."
HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
with:
@@ -1296,7 +1290,7 @@ jobs:
GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }}
GH_AW_ENGINE_VERSION: "1.0.48"
GH_AW_WORKFLOW_ID: "check-requirements"
GH_AW_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_WORKFLOW_NAME: "Check requirements"
outputs:
code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
@@ -1316,7 +1310,7 @@ jobs:
trace-id: ${{ needs.activation.outputs.setup-trace-id }}
parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
env:
GH_AW_SETUP_WORKFLOW_NAME: "Requirements License and Availability Check"
GH_AW_SETUP_WORKFLOW_NAME: "Check requirements"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/check-requirements.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.48"
GH_AW_INFO_ENGINE_ID: "copilot"
@@ -1351,7 +1345,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},\"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\":\"${{ inputs.pull_request_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: |
+74 -63
View File
@@ -1,19 +1,11 @@
---
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
- "pyproject.toml"
forks: ["*"]
workflow_dispatch:
inputs:
pull_request_number:
description: "Pull request number to (re-)check"
required: true
type: number
roles: all
permissions:
contents: read
pull-requests: read
@@ -25,9 +17,29 @@ tools:
web-fetch: {}
github:
toolsets: [default]
min-integrity: unapproved
safe-outputs:
add-comment:
max: 1
target: ${{ inputs.pull_request_number }}
concurrency:
group: ${{ github.workflow }}-${{ inputs.pull_request_number }}
cancel-in-progress: true
post-steps:
- name: Verify agent produced an add_comment safe-output
if: always()
run: |
OUTPUT=/tmp/gh-aw/agent_output.json
if [ ! -f "${OUTPUT}" ]; then
echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion."
exit 1
fi
if ! grep -q '"add_comment"' "${OUTPUT}"; then
echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR."
echo "Agent output:"
cat "${OUTPUT}"
exit 1
fi
description: >
Checks changed Python package requirements on PRs targeting the core repo
(including PRs opened from forks) and verifies licenses match PyPI metadata, source
@@ -37,7 +49,7 @@ description: >
description contains the required links.
---
# Requirements License and Availability Check
# Check requirements
You are a code review assistant for the Home Assistant project. Your job is to
review changes to Python package requirements and verify they meet the project's
@@ -57,8 +69,14 @@ standards.
## Step 1 — Identify Changed Packages
Use the GitHub tool to fetch the PR diff. Look for lines that were added (`+`)
or removed (`-`) in **any** of these files:
This workflow is triggered via `workflow_dispatch`. The PR number to check is
**#${{ inputs.pull_request_number }}**. Use that PR number for **every** GitHub
API call in the steps below (fetching the diff, the PR body, etc.). Do **not**
rely on `github.event.pull_request` — it is not populated for
`workflow_dispatch` runs.
Use the GitHub tool to fetch the PR diff for that PR number. Look for
lines that were added (`+`) or removed (`-`) in **any** of these files:
- `requirements.txt`
- `requirements_all.txt`
- `requirements_test.txt`
@@ -116,7 +134,7 @@ manually.
repository.
- If at least one distribution file has a valid Trusted Publisher attestation,
mark ✅ CI-uploaded.
- If no attestation is found for any file (404 for all), mark — "Release
- If no attestation is found for any file (404 for all), mark ⚠️ — "Release
has no provenance attestation; it may have been uploaded manually".
- If an attestation exists but the `publisher` does not identify a recognized
CI system or Trusted Publisher, mark ⚠️ — "Attestation present but
@@ -138,10 +156,8 @@ For each new or bumped package:
## Step 4 — Check PR Description
Read the PR body from the GitHub API using the PR number from the workflow
context (`pull-request-number`). If that value is absent, use the
`workflow_dispatch` input `pull_request_number`.
Extract all URLs present in the PR body.
Read the PR body from the GitHub API for PR
#${{ inputs.pull_request_number }}. Extract all URLs present in the PR body.
### 4a — New packages: repository link required
@@ -161,39 +177,29 @@ must point directly to the source repository (e.g. a GitHub or GitLab URL).
"PR description must link to the source repository at `<repo_url>` (found
via PyPI). A PyPI page link is not sufficient."
### 4b — Version bumps: changelog or diff link required
### 4b — Version bumps: changelog or diff link matching the bump
For **version bumps**: the PR description must contain a link to a changelog,
release notes page, or a diff/comparison URL that references the **correct
versions** being bumped (old → new).
release notes page, or a diff/comparison URL that references the **exact
versions** being bumped (old → new) as recorded in the diff from Step 1.
Checks to perform for each bumped package (old version = X, new version = Y):
1. Extract all URLs from the PR body that contain the repository's domain or
path (as identified in Step 3).
2. Verify that at least one such URL includes both the old version string and
new version string in some form — e.g. a GitHub compare URL like
2. Verify that at least one such URL includes both the old version (X) and the
new version (Y) in some form — e.g. a GitHub compare URL like
`compare/vX...vY`, a releases URL mentioning version Y, or a
`CHANGELOG.md` anchor referencing Y.
3. If no URL matches, check if the PR body contains any changelog/diff link at
all for this package.
3. Confirm the link's version range matches the actual bump in the diff. If
the link references versions different from X → Y (for example, the PR
bumps `1.2.3 → 1.3.0` but the link points to `compare/v1.2.0...v1.2.4`),
the link does not match the bump.
Outcome:
- ✅ — a URL pointing to the correct repo with version references covering the
exact bump (X → Y).
- ⚠️a changelog/diff link exists but does not clearly reference the correct
versions or the correct repository; explain what was found and what is
expected.
- ❌ — no changelog or diff link found at all in the PR description for this
package.
### 4c — Diff consistency check
For each **version bump**, verify that the version change recorded in the diff
(Step 1) is internally consistent:
- The `-` line must contain the old version and the `+` line must contain the
new version for the same package name.
- Flag ❌ if the diff shows a downgrade (new version < old version) without an
explanation, or if the version strings cannot be parsed.
- ✅ — a URL pointing to the correct repo with version references that match
the exact bump (X → Y).
- no changelog/diff link is found, or the link does not match the actual
bump (X → Y). Explain what was found and what is expected.
## Step 5 — Verify Source Repository is Publicly Accessible
@@ -238,9 +244,12 @@ workflow is sane. The checks differ by hosting provider.
- `pypa/gh-action-pypi-publish` action
- `actions/attest-build-provenance` action
- Any step that sets `TWINE_PASSWORD` from `secrets.PYPI_TOKEN` directly
(flag ❌ if a long-lived API token is used instead of OIDC).
Mark ✅ if OIDC is used, ⚠️ if the publish method cannot be determined,
❌ if a static secret token is the only credential.
(treat this as a static long-lived API token rather than OIDC).
Mark ✅ if OIDC is used, ⚠️ if the publish method cannot be determined.
If a static secret token is the only credential, mark ⚠️ for version
bumps (the package was already accepted at a previous version; suggest
the upstream maintainer switch to OIDC / Trusted Publisher for better
security) and ❌ for new packages.
c. **No manual upload bypass**: Verify there is no step that calls
`twine upload` or `pip upload` outside of a properly gated job (e.g., one
that requires an environment approval). Flag ⚠️ if such steps exist.
@@ -267,9 +276,11 @@ workflow is sane. The checks differ by hosting provider.
b. **Automated credentials**: The job should use GitLab's OIDC ID token
(`id_tokens:` block) and `pypa/gh-action-pypi-publish` equivalent, or
reference `secrets.PYPI_TOKEN` / `$PYPI_TOKEN` injected from GitLab CI/CD
protected variables (flag ❌ if the token is hard-coded or unprotected).
Mark ✅ if OIDC or protected CI variables are used, ⚠️ if the method
cannot be determined, ❌ if credentials appear to be insecure.
protected variables. Flag ❌ if the token is hard-coded or unprotected.
Mark ✅ if OIDC is used, ⚠️ if the method cannot be determined. If a
protected static token is the only credential, mark ⚠️ for version bumps
(suggest the upstream maintainer switch to OIDC / Trusted Publisher for
better security) and ❌ for new packages.
c. **No manual upload bypass**: Flag ⚠️ if any job calls `twine upload`
without being behind a protected-variable or environment guard.
5. If no publish job is found, mark ⚠️ — "No publish job found in .gitlab-ci.yml;
@@ -325,13 +336,13 @@ when the repository is not publicly accessible).
```
<!-- requirements-check -->
## Requirements Check
## Check requirements
| Package | Type | Old→New | License | Repo Public | CI Upload | Release Pipeline | PR Link | Diff Consistent |
|---------|------|---------|---------|-------------|-----------|------------------|---------|-----------------|
| PackageA | bump | 1.2.3→1.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| PackageB | new | —→4.5.6 | ❌ | ✅ | | ⚠️ | ❌ | ✅ |
| PackageC | bump | 2.0.0→2.1.0 | ✅ | ❌ | — | — | ⚠️ | ✅ |
| Package | Type | Old→New | License | Repo Public | CI Upload | Release Pipeline | PR Link |
|---------|------|---------|---------|-------------|-----------|------------------|---------|
| PackageA | bump | 1.2.3→1.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ |
| PackageB | new | —→4.5.6 | ❌ | ✅ | ⚠️ | ⚠️ | ❌ |
| PackageC | bump | 2.0.0→2.1.0 | ✅ | ❌ | — | — | |
```
### 7c — Per-package detail sections
@@ -345,8 +356,8 @@ After the table, add one collapsible `<details>` block per package.
Each block must include the full detail for every check: the license found, the
repository URL, whether a provenance attestation was found, the release
pipeline findings, the PR link found (or missing), and whether the diff is
consistent. For failed or warned checks, explain exactly what the contributor
pipeline findings, and the PR link found (or missing, or mismatched with the
actual bump). For failed or warned checks, explain exactly what the contributor
must fix, including the expected source repository URL, expected version range,
etc.
@@ -358,10 +369,9 @@ Template (repeat for each package):
- **License**: ❌ License is `UNKNOWN` — not in the approved list. Check PyPI metadata and `script/licenses.py`.
- **Repository Public**: ✅ https://github.com/example/packageb is publicly accessible.
- **CI Upload**: No provenance attestation found for any distribution file. The release may have been uploaded manually.
- **CI Upload**: ⚠️ No provenance attestation found for any distribution file. The release may have been uploaded manually.
- **Release Pipeline**: ⚠️ No publish workflow found in the repository; it is unclear how this package is released to PyPI.
- **PR Link**: ❌ PR description must link to the source repository at https://github.com/example/packageb (a PyPI page link is not sufficient).
- **Diff Consistent**: ✅
</details>
```
@@ -377,7 +387,6 @@ Collapsed example (all checks passed):
- **CI Upload**: ✅ Trusted Publisher attestation found (GitHub Actions).
- **Release Pipeline**: ✅ OIDC via `pypa/gh-action-pypi-publish`; triggered on `release: published`; `environment: release` gate.
- **PR Link**: ✅ https://github.com/example/packagea/compare/v1.2.3...v1.3.0
- **Diff Consistent**: ✅
</details>
```
@@ -396,10 +405,12 @@ Collapsed example (all checks passed):
description checks as for production dependencies.
- A package that appears in both a production file and a test file should only
be reported once; use the production file entry as the canonical one.
- This workflow is only triggered when a commit actually changes one of the
tracked requirements files (for `synchronize` events GitHub compares the
before/after SHAs of the push, not the entire PR diff). Members can manually
retrigger the workflow via `workflow_dispatch` with the PR number to re-run
the check after updating the PR description or fixing issues without changing
any requirements files. On a retrigger the existing comment is updated in
place so there is always exactly one requirements-check comment in the PR.
- This workflow is invoked exclusively via `workflow_dispatch`. The stage-1
workflow `Check requirements (changes detection)` runs on `pull_request` with
a paths filter on the tracked requirements files, and its completion triggers
the dispatcher (`Check requirements (dispatcher)`) which calls this workflow
with the PR number. Members can also dispatch this workflow manually with the
PR number to re-run the check after updating the PR description or fixing
issues without changing any requirements files. On a retrigger the existing
comment is updated in place so there is always exactly one requirements-check
comment in the PR.
@@ -236,7 +236,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
with:
model: openai/gpt-4o
system-prompt: |
@@ -62,7 +62,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
with:
model: openai/gpt-4o-mini
system-prompt: |
+1 -1
View File
@@ -1 +1 @@
3.14.4
3.14.5
+1
View File
@@ -19,6 +19,7 @@ from .hub import AdsHub
DEFAULT_NAME = "ADS select"
# pylint: disable-next=home-assistant-duplicate-const
CONF_OPTIONS = "options"
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
@@ -2,4 +2,5 @@
DOMAIN = "altruist"
# pylint: disable-next=home-assistant-duplicate-const
CONF_HOST = "host"
@@ -16,6 +16,8 @@ from .entity import AnthropicBaseLLMEntity
if TYPE_CHECKING:
from . import AnthropicConfigEntry
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
@@ -24,6 +24,7 @@ from homeassistant.const import (
CONF_API_KEY,
CONF_LLM_HASS_API,
CONF_NAME,
CONF_PROMPT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, llm
@@ -43,7 +44,6 @@ from .const import (
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_PROMPT_CACHING,
CONF_RECOMMENDED,
CONF_THINKING_BUDGET,
@@ -10,7 +10,6 @@ DEFAULT_CONVERSATION_NAME = "Claude conversation"
DEFAULT_AI_TASK_NAME = "Claude AI Task"
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
CONF_CODE_EXECUTION = "code_execution"
CONF_MAX_TOKENS = "max_tokens"
@@ -4,14 +4,16 @@ from typing import Literal
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AnthropicConfigEntry
from .const import CONF_PROMPT, DOMAIN
from .const import DOMAIN
from .entity import AnthropicBaseLLMEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
@@ -5,11 +5,10 @@ from typing import TYPE_CHECKING, Any
from anthropic import __title__, __version__
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY
from homeassistant.const import CONF_API_KEY, CONF_PROMPT
from homeassistant.helpers import entity_registry as er
from .const import (
CONF_PROMPT,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_REGION,
@@ -38,10 +38,7 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates:
status: exempt
comment: |
The API does not limit parallel updates.
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
@@ -40,9 +40,11 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
self._current_subentry_id = None
self._model_list_cache = None
async def async_step_init(self, user_input: dict[str, str]) -> RepairsFlowResult:
async def async_step_init(
self, user_input: dict[str, str] | None
) -> RepairsFlowResult:
"""Handle the steps of a fix flow."""
if user_input.get(CONF_CHAT_MODEL):
if user_input and user_input.get(CONF_CHAT_MODEL):
self._async_update_current_subentry(user_input)
target = await self._async_next_target()
@@ -16,6 +16,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
@@ -25,6 +25,10 @@ from .entity import ArcamFmjEntity, convert_exception
_LOGGER = logging.getLogger(__name__)
# arcam-fmj serializes commands on a single TCP writer at the library
# layer; serialize at HA's layer to match the device's contract.
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
@@ -22,6 +22,9 @@ from .entity import ArcamFmjEntity
_LOGGER = logging.getLogger(__name__)
# Read-only, coordinator-driven entities; no per-entity I/O to bound.
PARALLEL_UPDATES = 0
def _enum_options(value: type[IntOrTypeEnum]) -> list[str]:
return [
@@ -19,6 +19,8 @@ DEVICES = "devices"
MANUFACTURER = "ABB"
ATTR_DEVICE_NAME = "device_name"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_DEVICE_ID = "device_id"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
ATTR_FIRMWARE = "firmware"
+2 -2
View File
@@ -205,9 +205,9 @@ class AveaLight(LightEntity):
def turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
if self._attr_brightness:
self._last_brightness = self._attr_brightness
self._light.set_brightness(0)
self._attr_is_on = False
self._attr_brightness = 0
def update(self) -> None:
"""Fetch new state data for this light."""
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["avea"],
"requirements": ["avea==1.6.1"]
"requirements": ["avea==1.8.0"]
}
+1
View File
@@ -11,6 +11,7 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
AWS_DOMAIN = "amazonaws.com"
@@ -20,12 +20,12 @@ from homeassistant.components.backup import (
OnProgressCallback,
suggested_filename,
)
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.async_iterator import AsyncIteratorReader
from . import BackblazeConfigEntry
from .const import (
CONF_PREFIX,
DATA_BACKUP_AGENT_LISTENERS,
DOMAIN,
METADATA_FILE_SUFFIX,
@@ -8,6 +8,7 @@ from b2sdk.v2 import exception
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PREFIX
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
@@ -22,7 +23,6 @@ from .const import (
CONF_APPLICATION_KEY,
CONF_BUCKET,
CONF_KEY_ID,
CONF_PREFIX,
DOMAIN,
)
@@ -10,7 +10,6 @@ DOMAIN: Final = "backblaze_b2"
CONF_KEY_ID = "key_id"
CONF_APPLICATION_KEY = "application_key"
CONF_BUCKET = "bucket"
CONF_PREFIX = "prefix"
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
f"{DOMAIN}.backup_agent_listeners"
@@ -16,6 +16,7 @@ CONF_DETAILS = "details"
CONF_PASSIVE = "passive"
# pylint: disable-next=home-assistant-duplicate-const
CONF_SOURCE: Final = "source"
CONF_SOURCE_DOMAIN: Final = "source_domain"
CONF_SOURCE_MODEL: Final = "source_model"
@@ -19,8 +19,8 @@
"bleak-retry-connector==4.6.0",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==4.0.4",
"bluetooth-data-tools==1.29.11",
"dbus-fast==5.0.1",
"habluetooth==6.1.0"
]
}
@@ -6,6 +6,7 @@ from typing import Final
ATTR_CID: Final = "cid"
ATTR_MAC: Final = "macAddr"
ATTR_MANUFACTURER: Final = "Sony"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL: Final = "model"
CONF_NICKNAME: Final = "nickname"
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.2.1"],
"requirements": ["python-bsblan==6.0.1"],
"zeroconf": [
{
"name": "bsb-lan*",
+1 -1
View File
@@ -8,6 +8,7 @@ from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -20,7 +21,6 @@ if TYPE_CHECKING:
LOGGER = logging.getLogger(__name__)
ATTR_DEVICE_ID = "device_id"
ATTR_MONDAY_SLOTS = "monday_slots"
ATTR_TUESDAY_SLOTS = "tuesday_slots"
ATTR_WEDNESDAY_SLOTS = "wednesday_slots"
+3 -1
View File
@@ -17,6 +17,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import TIMEOUT
type CalDavConfigEntry = ConfigEntry[caldav.DAVClient]
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
ssl_verify_cert=entry.data[CONF_VERIFY_SSL],
timeout=30,
timeout=TIMEOUT,
)
try:
await hass.async_add_executor_job(client.principal)
@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .const import DOMAIN, TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -65,6 +65,7 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
ssl_verify_cert=user_input[CONF_VERIFY_SSL],
timeout=TIMEOUT,
)
try:
await self.hass.async_add_executor_job(client.principal)
@@ -75,6 +76,9 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
# AuthorizationError can be raised if the url is incorrect or
# on some other unexpected server response.
return "cannot_connect"
except requests.Timeout as err:
_LOGGER.warning("Timeout connecting to CalDAV server: %s", err)
return "cannot_connect"
except requests.ConnectionError as err:
_LOGGER.warning("Connection Error connecting to CalDAV server: %s", err)
return "cannot_connect"
+1
View File
@@ -3,3 +3,4 @@
from typing import Final
DOMAIN: Final = "caldav"
TIMEOUT: Final = 30
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
DOMAIN = "calendar"
DATA_COMPONENT: HassKey[EntityComponent[CalendarEntity]] = HassKey(DOMAIN)
# pylint: disable-next=home-assistant-duplicate-const
CONF_EVENT = "event"
@@ -7,6 +7,7 @@ from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [
@@ -24,7 +25,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Casper Glow device with address {address}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"address": address},
)
glow = CasperGlow(ble_device)
@@ -54,6 +54,9 @@
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Casper Glow: {error}"
},
"device_not_found": {
"message": "Could not find Casper Glow device with address {address}"
}
}
}
@@ -95,3 +95,42 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=self._errors,
)
async def async_step_reconfigure(
self,
user_input: Mapping[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
self._errors = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
host = user_input[CONF_HOST]
port = user_input.get(CONF_PORT, DEFAULT_PORT)
if (
host != reconfigure_entry.data[CONF_HOST]
or port != reconfigure_entry.data[CONF_PORT]
):
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
if await self._test_connection(user_input):
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={CONF_HOST: host, CONF_PORT: port},
unique_id=f"{host}:{port}",
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
),
user_input or reconfigure_entry.data,
),
errors=self._errors,
)
@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed"
"import_failed": "Import from config failed",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"connection_refused": "Connection refused when connecting to host",
@@ -11,6 +12,13 @@
"resolve_failed": "This host cannot be resolved"
},
"step": {
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"title": "Reconfigure the certificate to test"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
+2 -2
View File
@@ -17,6 +17,8 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
APPLICATION_NAME,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
@@ -45,8 +47,6 @@ HA_USER_AGENT = (
)
ATTR_UID = "uid"
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_FREE_EBIKES = "free_ebikes"
ATTR_TIMESTAMP = "timestamp"
@@ -29,6 +29,7 @@ BASE_API_URL = "https://rest.clicksend.com/v3"
HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
# pylint: disable-next=home-assistant-duplicate-const
CONF_LANGUAGE = "language"
CONF_VOICE = "voice"
+22 -22
View File
@@ -32,28 +32,28 @@ set_temperature:
max: 250
step: 0.1
mode: box
target_temp_high:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
target_temp_low:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
temperature_range:
fields:
target_temp_high:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
target_temp_low:
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
selector:
number:
min: 0
max: 250
step: 0.1
mode: box
hvac_mode:
selector:
state:
@@ -373,7 +373,12 @@
"name": "Target temperature"
}
},
"name": "Set thermostat target temperature"
"name": "Set thermostat target temperature",
"sections": {
"temperature_range": {
"name": "Temperature range"
}
}
},
"toggle": {
"description": "Toggles a thermostat on/off.",
@@ -11,6 +11,7 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
# R2 is S3-compatible. Endpoint should be like:
@@ -6,4 +6,5 @@ ATTR_URL = "color_extract_url"
DOMAIN = "color_extractor"
DEFAULT_NAME = "Color extractor"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_TURN_ON = "turn_on"
@@ -10,10 +10,11 @@ from homeassistant.components.notify import (
)
from homeassistant.const import CONF_COMMAND
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.process import kill_subprocess
from .const import CONF_COMMAND_TIMEOUT, LOGGER
from .const import CONF_COMMAND_TIMEOUT, DOMAIN, LOGGER
from .utils import create_platform_yaml_not_supported_issue, render_template_args
_LOGGER = logging.getLogger(__name__)
@@ -66,9 +67,18 @@ class CommandLineNotificationService(BaseNotificationService):
proc.returncode,
command,
)
# pylint: disable-next=home-assistant-action-swallowed-exception
except subprocess.TimeoutExpired:
_LOGGER.error("Timeout for command: %s", command)
except subprocess.TimeoutExpired as err:
_LOGGER.debug("Timeout for command: %s", command)
kill_subprocess(proc)
except subprocess.SubprocessError:
_LOGGER.error("Error trying to exec command: %s", command)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_error",
translation_placeholders={"command": command},
) from err
except subprocess.SubprocessError as err:
_LOGGER.debug("Error trying to exec command: %s", command)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={"command": command, "error": str(err)},
) from err
@@ -1,8 +1,10 @@
{
"issues": {
"platform_yaml_not_supported": {
"description": "Platform YAML setup is not supported.\nChange from configuring it using the `{platform}:` key to using the `command_line:` key directly in configuration.yaml and restart Home Assistant to resolve the issue.\nTo see the detailed documentation, select Learn more.",
"title": "Platform YAML is not supported in Command Line"
"exceptions": {
"command_error": {
"message": "Error trying to execute command: {command}. Error: {error}"
},
"timeout_error": {
"message": "Timeout trying to execute command: {command}"
}
},
"services": {
@@ -4,7 +4,9 @@ import asyncio
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.entity_platform import (
async_create_platform_config_not_supported_issue,
)
from homeassistant.helpers.template import Template
from .const import DOMAIN, LOGGER
@@ -98,13 +100,11 @@ def create_platform_yaml_not_supported_issue(
hass: HomeAssistant, platform_domain: str
) -> None:
"""Create an issue when platform yaml is used."""
async_create_issue(
async_create_platform_config_not_supported_issue(
hass,
DOMAIN,
f"{platform_domain}_platform_yaml_not_supported",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="platform_yaml_not_supported",
translation_placeholders={"platform": platform_domain},
platform_domain,
yaml_config_under_integration_supported=True,
learn_more_url="https://www.home-assistant.io/integrations/command_line/",
logger=LOGGER,
)
@@ -91,6 +91,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Compensation sensor."""
hass.data[DATA_COMPENSATION] = {}
# Exit early if no compensations are configured using the compensation: key in configuration.yaml.
# This allows us to create an issue if platform: compensation is present in the sensor: section.
if DOMAIN not in config:
return True
for compensation, conf in config[DOMAIN].items():
_LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
@@ -8,6 +8,7 @@ import numpy as np
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
SensorEntity,
)
from homeassistant.const import (
@@ -31,7 +32,10 @@ from homeassistant.core import (
State,
callback,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_create_platform_config_not_supported_issue,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -41,6 +45,7 @@ from .const import (
CONF_PRECISION,
DATA_COMPENSATION,
DEFAULT_NAME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@@ -58,6 +63,14 @@ async def async_setup_platform(
) -> None:
"""Set up the Compensation sensor."""
if discovery_info is None:
async_create_platform_config_not_supported_issue(
hass,
DOMAIN,
SENSOR_DOMAIN,
yaml_config_under_integration_supported=True,
learn_more_url="https://www.home-assistant.io/integrations/compensation/",
logger=_LOGGER,
)
return
compensation: str = discovery_info[CONF_COMPENSATION]
@@ -16,6 +16,7 @@ PLATFORMS = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.WATER_HEATER,
]
@@ -279,6 +279,20 @@
"no_alarm": "mdi:check-circle"
}
}
},
"switch": {
"device_on_off": {
"default": "mdi:power",
"state": {
"off": "mdi:power-off"
}
},
"force_dhw": {
"default": "mdi:water-boiler",
"state": {
"off": "mdi:water-boiler-off"
}
}
}
}
}
@@ -421,6 +421,14 @@
"weather_curve": {
"name": "Weather curve"
}
},
"switch": {
"device_on_off": {
"name": "Device on/off"
},
"force_dhw": {
"name": "Force domestic hot water"
}
}
}
}
+132
View File
@@ -0,0 +1,132 @@
"""Switch platform for Compit integration."""
from dataclasses import dataclass
from typing import Any
from compit_inext_api.consts import CompitParameter
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class CompitDeviceDescription:
"""Class to describe a Compit device."""
name: str
"""Name of the device."""
parameters: list[SwitchEntityDescription]
"""Parameters of the device."""
DESCRIPTIONS: dict[CompitParameter, SwitchEntityDescription] = {
CompitParameter.DEVICE_ON_OFF: SwitchEntityDescription(
key=CompitParameter.DEVICE_ON_OFF.value,
translation_key="device_on_off",
),
CompitParameter.FORCE_DHW: SwitchEntityDescription(
key=CompitParameter.FORCE_DHW.value,
translation_key="force_dhw",
),
}
DEVICE_DEFINITIONS: dict[int, CompitDeviceDescription] = {
210: CompitDeviceDescription(
name="EL750",
parameters=[DESCRIPTIONS[CompitParameter.DEVICE_ON_OFF]],
),
224: CompitDeviceDescription(
name="R 900",
parameters=[
DESCRIPTIONS[CompitParameter.FORCE_DHW],
],
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Compit switch entities from a config entry."""
coordinator = entry.runtime_data
async_add_entities(
CompitSwitch(
coordinator,
device_id,
device_definition.name,
entity_description,
)
for device_id, device in coordinator.connector.all_devices.items()
if (device_definition := DEVICE_DEFINITIONS.get(device.definition.code))
for entity_description in device_definition.parameters
)
class CompitSwitch(CoordinatorEntity[CompitDataUpdateCoordinator], SwitchEntity):
"""Representation of a Compit switch entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
device_name: str,
entity_description: SwitchEntityDescription,
) -> None:
"""Initialize the switch entity."""
super().__init__(coordinator)
self.device_id = device_id
self.entity_description = entity_description
self._attr_unique_id = f"{device_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=device_name,
manufacturer=MANUFACTURER_NAME,
model=device_name,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.connector.get_device(self.device_id) is not None
)
@property
def is_on(self) -> bool | None:
"""Return the state of the switch."""
value = self.coordinator.connector.get_current_option(
self.device_id, CompitParameter(self.entity_description.key)
)
return True if value == STATE_ON else False if value == STATE_OFF else None
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.coordinator.connector.select_device_option(
self.device_id, CompitParameter(self.entity_description.key), STATE_ON
)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.coordinator.connector.select_device_option(
self.device_id, CompitParameter(self.entity_description.key), STATE_OFF
)
self.async_write_ha_state()
@@ -19,6 +19,7 @@ ATTR_AGENT_ID = "agent_id"
ATTR_CONVERSATION_ID = "conversation_id"
SERVICE_PROCESS = "process"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_RELOAD = "reload"
DATA_COMPONENT: HassKey[EntityComponent[ConversationEntity]] = HassKey(DOMAIN)
+1
View File
@@ -43,6 +43,7 @@ PLATFORMS = [
]
ATTR_DARK = "dark"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCKED = "locked"
ATTR_OFFSET = "offset"
ATTR_ON = "on"
+35 -21
View File
@@ -12,9 +12,9 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
@@ -37,49 +37,71 @@ async def async_setup_entry(
async_add_entities(
[
DemoSensor(
"sensor_1",
"sensor_1",
"Outside Temperature",
15.6,
SensorDeviceClass.TEMPERATURE,
SensorStateClass.MEASUREMENT,
UnitOfTemperature.CELSIUS,
12,
),
DemoSensor(
"battery_1",
"sensor_1",
"Outside Temperature",
12,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
DemoSensor(
"sensor_2",
"sensor_2",
"Outside Humidity",
54,
SensorDeviceClass.HUMIDITY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
None,
),
DemoSensor(
"sensor_3",
"sensor_3",
"Carbon monoxide",
54,
SensorDeviceClass.CO,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
None,
),
DemoSensor(
"sensor_4",
"sensor_4",
"Carbon dioxide",
54,
SensorDeviceClass.CO2,
SensorStateClass.MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
14,
),
DemoSensor(
"battery_4",
"sensor_4",
"Carbon dioxide",
99,
SensorDeviceClass.BATTERY,
SensorStateClass.MEASUREMENT,
PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery",
),
DemoSensor(
"sensor_5",
"sensor_5",
"Power consumption",
100,
SensorDeviceClass.POWER,
SensorStateClass.MEASUREMENT,
UnitOfPower.WATT,
None,
),
DemoSumSensor(
"sensor_6",
@@ -88,7 +110,6 @@ async def async_setup_entry(
SensorDeviceClass.ENERGY,
SensorStateClass.TOTAL,
UnitOfEnergy.KILO_WATT_HOUR,
None,
"total_energy_kwh",
),
DemoSumSensor(
@@ -98,7 +119,6 @@ async def async_setup_entry(
SensorDeviceClass.ENERGY,
SensorStateClass.TOTAL,
UnitOfEnergy.MEGA_WATT_HOUR,
None,
"total_energy_mwh",
),
DemoSumSensor(
@@ -108,7 +128,6 @@ async def async_setup_entry(
SensorDeviceClass.GAS,
SensorStateClass.TOTAL,
UnitOfVolume.CUBIC_METERS,
None,
"total_gas_m3",
),
DemoSumSensor(
@@ -118,17 +137,16 @@ async def async_setup_entry(
SensorDeviceClass.GAS,
SensorStateClass.TOTAL,
UnitOfVolume.CUBIC_FEET,
None,
"total_gas_ft3",
),
DemoSensor(
unique_id="sensor_10",
device_id="sensor_10",
device_name="Thermostat",
state="eco",
device_class=SensorDeviceClass.ENUM,
state_class=None,
unit_of_measurement=None,
battery=None,
options=["away", "comfort", "eco", "sleep"],
translation_key="thermostat_mode",
),
@@ -140,20 +158,21 @@ class DemoSensor(SensorEntity):
"""Representation of a Demo sensor."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
device_id: str,
device_name: str | None,
state: float | str | None,
device_class: SensorDeviceClass,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
battery: int | None,
options: list[str] | None = None,
translation_key: str | None = None,
entity_category: EntityCategory | None = None,
entity_name: str | None = None,
) -> None:
"""Initialize the sensor."""
self._attr_device_class = device_class
@@ -163,15 +182,14 @@ class DemoSensor(SensorEntity):
self._attr_unique_id = unique_id
self._attr_options = options
self._attr_translation_key = translation_key
self._attr_entity_category = entity_category
self._attr_name = entity_name
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
identifiers={(DOMAIN, device_id)},
name=device_name,
)
if battery:
self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery}
class DemoSumSensor(RestoreSensor):
"""Representation of a Demo sensor."""
@@ -187,7 +205,6 @@ class DemoSumSensor(RestoreSensor):
device_class: SensorDeviceClass,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
battery: int | None,
suggested_entity_id: str,
) -> None:
"""Initialize the sensor."""
@@ -204,9 +221,6 @@ class DemoSumSensor(RestoreSensor):
name=device_name,
)
if battery:
self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery}
@callback
def _async_bump_sum(self, now: datetime) -> None:
"""Bump the sum."""
@@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .config_entry import ( # noqa: F401
BaseScannerEntity,
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -166,7 +166,11 @@ def _async_register_mac(
class BaseTrackerEntity(Entity):
"""Represent a tracked device."""
"""Represent a tracked device.
Not intended to be directly inherited by integrations. Integrations should
inherit TrackerEntity, BaseScannerEntity or ScannerEntity instead.
"""
_attr_device_info: None = None
_attr_entity_category = EntityCategory.DIAGNOSTIC
@@ -304,6 +308,28 @@ class TrackerEntity(
return attr
class BaseScannerEntity(BaseTrackerEntity):
"""Base class for a tracked device that can be connected or disconnected.
Unlike ScannerEntity, this entity does not make assumptions about MAC
addresses being used to identify the device.
"""
@property
def state(self) -> str | None:
"""Return the state of the device."""
if self.is_connected is None:
return None
if self.is_connected:
return STATE_HOME
return STATE_NOT_HOME
@property
def is_connected(self) -> bool | None:
"""Return true if the device is connected."""
raise NotImplementedError
class ScannerEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes tracker entities."""
@@ -316,7 +342,7 @@ CACHED_SCANNER_PROPERTIES_WITH_ATTR_ = {
class ScannerEntity(
BaseTrackerEntity, cached_properties=CACHED_SCANNER_PROPERTIES_WITH_ATTR_
BaseScannerEntity, cached_properties=CACHED_SCANNER_PROPERTIES_WITH_ATTR_
):
"""Base class for a tracked device that is on a scanned network."""
@@ -341,18 +367,6 @@ class ScannerEntity(
"""Return hostname of the device."""
return self._attr_hostname
@property
def state(self) -> str:
"""Return the state of the device."""
if self.is_connected:
return STATE_HOME
return STATE_NOT_HOME
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
raise NotImplementedError
@property
def unique_id(self) -> str | None:
"""Return unique ID of the entity."""
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==2.7.1",
"aiodiscover==3.2.0",
"cached-ipaddress==1.0.1"
]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.3"]
"requirements": ["aiodns==4.0.4"]
}
@@ -4,6 +4,7 @@
CONF_COMMAND_TOPIC = "drop_command_topic"
CONF_DATA_TOPIC = "drop_data_topic"
CONF_DEVICE_DESC = "device_desc"
# pylint: disable-next=home-assistant-duplicate-const
CONF_DEVICE_ID = "device_id"
CONF_DEVICE_TYPE = "device_type"
CONF_HUB_ID = "drop_hub_id"
@@ -49,6 +49,7 @@ CURRENT_SYSTEM_PRESSURE = "current_system_pressure"
HIGH_SYSTEM_PRESSURE = "high_system_pressure"
LOW_SYSTEM_PRESSURE = "low_system_pressure"
BATTERY = "battery"
# pylint: disable-next=home-assistant-duplicate-const
TEMPERATURE = "temperature"
INLET_TDS = "inlet_tds"
OUTLET_TDS = "outlet_tds"
+5 -1
View File
@@ -60,7 +60,11 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
translation_placeholders={"error": repr(err)},
) from err
except DucoError as err:
raise ConfigEntryError(f"Duco API error: {err}") from err
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"error": repr(err)},
) from err
async def _async_update_data(self) -> DucoData:
"""Fetch node data from the Duco box."""
+4 -4
View File
@@ -95,7 +95,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
value_fn=lambda node: node.sensor.co2 if node.sensor else None,
node_types=(NodeType.UCCO2,),
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
),
DucoSensorEntityDescription(
key="iaq_co2",
@@ -104,7 +104,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None,
node_types=(NodeType.UCCO2,),
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
),
DucoSensorEntityDescription(
key="humidity",
@@ -112,7 +112,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda node: node.sensor.rh if node.sensor else None,
node_types=(NodeType.BSRH, NodeType.UCRH),
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
),
DucoSensorEntityDescription(
key="iaq_rh",
@@ -121,7 +121,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda node: node.sensor.iaq_rh if node.sensor else None,
node_types=(NodeType.BSRH, NodeType.UCRH),
node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH),
),
)
@@ -1,8 +1,10 @@
"""Constants for the ElevenLabs text-to-speech integration."""
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
CONF_VOICE = "voice"
# pylint: disable-next=home-assistant-duplicate-const
CONF_MODEL = "model"
CONF_CONFIGURE_VOICE = "configure_voice"
CONF_STABILITY = "stability"
@@ -16,6 +16,7 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import SERVICE_ALARM_ARM_VACATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -43,7 +44,6 @@ DISPLAY_MESSAGE_SERVICE_SCHEMA: VolDictType = {
}
SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message"
SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation"
SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant"
SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant"
SERVICE_ALARM_BYPASS = "alarm_bypass"
@@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE,
CONF_HOST,
CONF_PASSWORD,
CONF_PREFIX,
@@ -32,8 +33,6 @@ from .discovery import (
async_update_entry_from_discovery,
)
CONF_DEVICE = "device"
NON_SECURE_PORT = 2101
SECURE_PORT = 2601
STANDARD_PORTS = {NON_SECURE_PORT, SECURE_PORT}
@@ -8,6 +8,7 @@ NAME: Final = "EnergyID"
# --- Config Flow and Entry Data ---
CONF_PROVISIONING_KEY: Final = "provisioning_key"
CONF_PROVISIONING_SECRET: Final = "provisioning_secret"
# pylint: disable-next=home-assistant-duplicate-const
CONF_DEVICE_ID: Final = "device_id"
CONF_DEVICE_NAME: Final = "device_name"
+20 -16
View File
@@ -99,14 +99,16 @@ increase_speed:
supported_features:
- fan.FanEntityFeature.SET_SPEED
fields:
percentage_step:
advanced: true
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
additional_fields:
collapsed: true
fields:
percentage_step:
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
decrease_speed:
target:
@@ -115,11 +117,13 @@ decrease_speed:
supported_features:
- fan.FanEntityFeature.SET_SPEED
fields:
percentage_step:
advanced: true
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
additional_fields:
collapsed: true
fields:
percentage_step:
required: false
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
+13 -2
View File
@@ -2,6 +2,7 @@
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"section_additional_fields_name": "Additional options",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -109,7 +110,12 @@
"name": "Decrement"
}
},
"name": "Decrease fan speed"
"name": "Decrease fan speed",
"sections": {
"additional_fields": {
"name": "[%key:component::fan::common::section_additional_fields_name%]"
}
}
},
"increase_speed": {
"description": "Increases the speed of a fan.",
@@ -119,7 +125,12 @@
"name": "Increment"
}
},
"name": "Increase fan speed"
"name": "Increase fan speed",
"sections": {
"additional_fields": {
"name": "[%key:component::fan::common::section_additional_fields_name%]"
}
}
},
"oscillate": {
"description": "Controls the oscillation of a fan.",
@@ -5,12 +5,15 @@ from typing import Literal
DOMAIN = "fish_audio"
# pylint: disable-next=home-assistant-duplicate-const
CONF_NAME: Literal["name"] = "name"
CONF_USER_ID: Literal["user_id"] = "user_id"
# pylint: disable-next=home-assistant-duplicate-const
CONF_API_KEY: Literal["api_key"] = "api_key"
CONF_VOICE_ID: Literal["voice_id"] = "voice_id"
CONF_BACKEND: Literal["backend"] = "backend"
CONF_SELF_ONLY: Literal["self_only"] = "self_only"
# pylint: disable-next=home-assistant-duplicate-const
CONF_LANGUAGE: Literal["language"] = "language"
CONF_SORT_BY: Literal["sort_by"] = "sort_by"
CONF_LATENCY: Literal["latency"] = "latency"
+1
View File
@@ -13,6 +13,7 @@ ATTR_LAST_SAVED_AT: Final = "last_saved_at"
ATTR_DURATION: Final = "duration"
ATTR_DISTANCE: Final = "distance"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ELEVATION: Final = "elevation"
ATTR_HEIGHT: Final = "height"
ATTR_WEIGHT: Final = "weight"
+65 -2
View File
@@ -1,19 +1,82 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
from datetime import timedelta
import logging
from freebox_api.exceptions import HttpRequestError
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.event import async_track_time_interval
from .const import PLATFORMS
from .const import DOMAIN, PLATFORMS
from .router import FreeboxConfigEntry, FreeboxRouter, get_api
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
# Old entity name suffixes that need rewriting to the entity description key.
# Format: (platform, old name suffix, new key)
_STATIC_UNIQUE_ID_MIGRATIONS: tuple[tuple[Platform, str, str], ...] = (
(Platform.SENSOR, "Freebox download speed", "rate_down"),
(Platform.SENSOR, "Freebox upload speed", "rate_up"),
(Platform.SENSOR, "Freebox missed calls", "missed"),
(Platform.BUTTON, "Reboot Freebox", "reboot"),
(Platform.BUTTON, "Mark calls as read", "mark_calls_as_read"),
(Platform.SWITCH, "Freebox WiFi", "wifi"),
)
async def async_migrate_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool:
"""Migrate old config entries."""
if entry.version < 2:
api = await get_api(hass, entry.data[CONF_HOST])
try:
await api.open(entry.data[CONF_HOST], entry.data[CONF_PORT])
freebox_config = await api.system.get_config()
except HttpRequestError:
_LOGGER.warning(
"Unable to migrate Freebox entry to version 2: cannot reach the router"
)
return False
finally:
await api.close()
mac: str = freebox_config["mac"]
entity_registry = er.async_get(hass)
migrations: list[tuple[Platform, str, str]] = [
(platform, f"{mac} {old_suffix}", f"{mac} {new_key}")
for platform, old_suffix, new_key in _STATIC_UNIQUE_ID_MIGRATIONS
]
migrations.extend(
(
Platform.SENSOR,
f"{mac} Freebox {sensor['name']}",
f"{mac} {sensor['id']}",
)
for sensor in freebox_config.get("sensors", [])
)
for platform, old_uid, new_uid in migrations:
if entity_id := entity_registry.async_get_entity_id(
platform, DOMAIN, old_uid
):
entity_registry.async_update_entity(entity_id, new_unique_id=new_uid)
_LOGGER.debug(
"Migrated %s unique_id from %s to %s",
entity_id,
old_uid,
new_uid,
)
hass.config_entries.async_update_entry(entry, version=2)
return True
async def async_setup_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool:
"""Set up Freebox entry."""
@@ -48,6 +48,7 @@ class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity):
"""Representation of a Freebox alarm."""
_attr_code_arm_required = False
_attr_name = None
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
"""Initialize an alarm."""
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
RAID_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="raid_degraded",
name="degraded",
translation_key="raid_degraded",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -68,7 +68,7 @@ async def async_setup_entry(
class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
"""Representation of a Freebox binary sensor."""
_sensor_name = "trigger"
_endpoint_name = "trigger"
def __init__(
self,
@@ -79,9 +79,11 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
"""Initialize a Freebox binary sensor."""
super().__init__(router, node, sub_node)
self._command_id = self.get_command_id(
node["type"]["endpoints"], "signal", self._sensor_name
node["type"]["endpoints"], "signal", self._endpoint_name
)
self._attr_is_on = self._edit_state(
self.get_value("signal", self._endpoint_name)
)
self._attr_is_on = self._edit_state(self.get_value("signal", self._sensor_name))
async def async_update_signal(self) -> None:
"""Update name & state."""
@@ -91,10 +93,10 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity):
await FreeboxHomeEntity.async_update_signal(self)
def _edit_state(self, state: bool | None) -> bool | None:
"""Edit state depending on sensor name."""
"""Edit state depending on endpoint name."""
if state is None:
return None
if self._sensor_name == "trigger":
if self._endpoint_name == "trigger":
return not state
return state
@@ -103,12 +105,14 @@ class FreeboxPirSensor(FreeboxHomeBinarySensor):
"""Representation of a Freebox motion binary sensor."""
_attr_device_class = BinarySensorDeviceClass.MOTION
_attr_name = None
class FreeboxDwsSensor(FreeboxHomeBinarySensor):
"""Representation of a Freebox door opener binary sensor."""
_attr_device_class = BinarySensorDeviceClass.DOOR
_attr_name = None
class FreeboxCoverSensor(FreeboxHomeBinarySensor):
@@ -121,14 +125,15 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor):
_attr_device_class = BinarySensorDeviceClass.SAFETY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
_attr_translation_key = "cover"
_sensor_name = "cover"
_endpoint_name = "cover"
def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None:
"""Initialize a cover for another device."""
cover_node = next(
filter(
lambda x: x["name"] == self._sensor_name and x["ep_type"] == "signal",
lambda x: x["name"] == self._endpoint_name and x["ep_type"] == "signal",
node["type"]["endpoints"],
),
None,
@@ -153,7 +158,7 @@ class FreeboxRaidDegradedSensor(BinarySensorEntity):
self._router = router
self._attr_device_info = router.device_info
self._raid = raid
self._attr_name = f"Raid array {raid['id']} {description.name}"
self._attr_translation_placeholders = {"id": str(raid["id"])}
self._attr_unique_id = (
f"{router.mac} {description.key} {raid['name']} {raid['id']}"
)
+3 -3
View File
@@ -25,14 +25,13 @@ class FreeboxButtonEntityDescription(ButtonEntityDescription):
BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = (
FreeboxButtonEntityDescription(
key="reboot",
name="Reboot Freebox",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
async_press=lambda router: router.reboot(),
),
FreeboxButtonEntityDescription(
key="mark_calls_as_read",
name="Mark calls as read",
translation_key="mark_calls_as_read",
entity_category=EntityCategory.DIAGNOSTIC,
async_press=lambda router: router.call.mark_calls_log_as_read(),
),
@@ -55,6 +54,7 @@ async def async_setup_entry(
class FreeboxButton(ButtonEntity):
"""Representation of a Freebox button."""
_attr_has_entity_name = True
entity_description: FreeboxButtonEntityDescription
def __init__(
@@ -64,7 +64,7 @@ class FreeboxButton(ButtonEntity):
self.entity_description = description
self._router = router
self._attr_device_info = router.device_info
self._attr_unique_id = f"{router.mac} {description.name}"
self._attr_unique_id = f"{router.mac} {description.key}"
async def async_press(self) -> None:
"""Press the button."""
+50 -30
View File
@@ -1,17 +1,16 @@
"""Support for Freebox cameras."""
import logging
from typing import Any
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=home-assistant-component-root-import
DEFAULT_ARGUMENTS,
FFmpegCamera,
)
from homeassistant.const import CONF_NAME
from aiohttp import web
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager, async_get_image
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -19,7 +18,7 @@ from .const import ATTR_DETECTION, FreeboxHomeCategory
from .entity import FreeboxHomeEntity
from .router import FreeboxConfigEntry, FreeboxRouter
_LOGGER = logging.getLogger(__name__)
_FFMPEG_ARGUMENTS = "-pred 1"
async def async_setup_entry(
@@ -63,25 +62,21 @@ def add_entities(
async_add_entities(new_tracked, True)
class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera):
class FreeboxCamera(FreeboxHomeEntity, Camera):
"""Representation of a Freebox camera."""
_attr_name = None
_attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
def __init__(
self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any]
) -> None:
"""Initialize a camera."""
super().__init__(router, node)
device_info = {
CONF_NAME: node["label"].strip(),
CONF_INPUT: node["props"]["Stream"],
CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS,
}
FFmpegCamera.__init__(self, hass, device_info)
Camera.__init__(self)
self._supported_features = (
CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
)
self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG]
self._input: str = node["props"]["Stream"]
self._command_motion_detection = self.get_command_id(
node["type"]["endpoints"], "slot", ATTR_DETECTION
@@ -89,6 +84,39 @@ class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera):
self._attr_extra_state_attributes = {}
self.update_node(node)
async def stream_source(self) -> str:
"""Return the stream source."""
return self._input.split(" ")[-1]
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
return await async_get_image(
self.hass,
self._input,
output_format=IMAGE_JPEG,
extra_cmd=_FFMPEG_ARGUMENTS,
)
async def handle_async_mjpeg_stream(
self, request: web.Request
) -> web.StreamResponse:
"""Generate an HTTP MJPEG stream from the camera."""
stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(self._input, extra_cmd=_FFMPEG_ARGUMENTS)
try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self.hass,
request,
stream_reader,
self._ffmpeg.ffmpeg_stream_content_type,
)
finally:
await stream.close()
async def async_enable_motion_detection(self) -> None:
"""Enable motion detection in the camera."""
if await self.set_home_endpoint_value(self._command_motion_detection, True):
@@ -102,25 +130,17 @@ class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera):
async def async_update_signal(self) -> None:
"""Update the camera node."""
self.update_node(self._router.home_devices[self._id])
self.async_write_ha_state()
await super().async_update_signal()
def update_node(self, node: dict[str, Any]) -> None:
"""Update params."""
self._name = node["label"].strip()
self._attr_is_streaming = node["status"] == "active"
# Get status
if self._node["status"] == "active":
self._attr_is_streaming = True
else:
self._attr_is_streaming = False
# Parse all endpoints values
for endpoint in filter(
lambda x: x["ep_type"] == "signal", node["show_endpoints"]
):
self._attr_extra_state_attributes[endpoint["name"]] = endpoint["value"]
# Get motion detection status
self._attr_motion_detection_enabled = self._attr_extra_state_attributes[
ATTR_DETECTION
]
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
class FreeboxFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
VERSION = 2
def __init__(self) -> None:
"""Initialize config flow."""
@@ -55,6 +55,7 @@ def add_entities(
class FreeboxDevice(ScannerEntity):
"""Representation of a Freebox device."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None:
+11 -11
View File
@@ -3,6 +3,7 @@
import logging
from typing import Any
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -16,6 +17,8 @@ _LOGGER = logging.getLogger(__name__)
class FreeboxHomeEntity(Entity):
"""Representation of a Freebox base entity."""
_attr_has_entity_name = True
def __init__(
self,
router: FreeboxRouter,
@@ -27,12 +30,9 @@ class FreeboxHomeEntity(Entity):
self._node = node
self._sub_node = sub_node
self._id = node["id"]
self._attr_name = node["label"].strip()
self._device_name = self._attr_name
self._attr_unique_id = f"{self._router.mac}-node_{self._id}"
if sub_node is not None:
self._attr_name += " " + sub_node["label"].strip()
self._attr_unique_id += "-" + sub_node["name"].strip()
self._available = True
@@ -52,7 +52,7 @@ class FreeboxHomeEntity(Entity):
identifiers={(DOMAIN, self._id)},
manufacturer=self._manufacturer,
model=self._model,
name=self._device_name,
name=node["label"].strip(),
sw_version=self._firmware,
via_device=(DOMAIN, router.mac),
)
@@ -60,13 +60,13 @@ class FreeboxHomeEntity(Entity):
async def async_update_signal(self) -> None:
"""Update signal."""
self._node = self._router.home_devices[self._id]
# Update name
if self._sub_node is None:
self._attr_name = self._node["label"].strip()
else:
self._attr_name = (
self._node["label"].strip() + " " + self._sub_node["label"].strip()
)
# Propagate Freebox device label changes to the device registry so
# the entity stays in sync when users rename it on the Freebox app.
device_registry = dr.async_get(self.hass)
if device := device_registry.async_get_device(identifiers={(DOMAIN, self._id)}):
new_name = self._node["label"].strip()
if device.name != new_name:
device_registry.async_update_device(device.id, name=new_name)
self.async_write_ha_state()
async def set_home_endpoint_value(
+22 -3
View File
@@ -1,7 +1,26 @@
{
"services": {
"reboot": {
"service": "mdi:restart"
"entity": {
"sensor": {
"missed": {
"default": "mdi:phone-missed"
},
"partition_free_space": {
"default": "mdi:harddisk"
},
"rate_down": {
"default": "mdi:download-network"
},
"rate_up": {
"default": "mdi:upload-network"
}
},
"switch": {
"wifi": {
"default": "mdi:wifi",
"state": {
"off": "mdi:wifi-off"
}
}
}
}
}
+4 -1
View File
@@ -125,6 +125,7 @@ class FreeboxRouter:
self.supports_raid = True
self.raids: dict[int, dict[str, Any]] = {}
self.sensors_temperature: dict[str, int] = {}
self.sensors_temperature_names: dict[str, str] = {}
self.sensors_connection: dict[str, float] = {}
self.call_list: list[dict[str, Any]] = []
self.home_granted = True
@@ -185,7 +186,9 @@ class FreeboxRouter:
# temperature sensors in celsius degree.
# Name and id of sensors may vary under Freebox devices.
for sensor in syst_datas["sensors"]:
self.sensors_temperature[sensor["name"]] = sensor.get("value")
sensor_id = sensor["id"]
self.sensors_temperature[sensor_id] = sensor.get("value")
self.sensors_temperature_names[sensor_id] = sensor["name"]
# Connection sensors
connection_datas: dict[str, Any] = await self._api.connection.get_status()
+12 -13
View File
@@ -25,36 +25,34 @@ _LOGGER = logging.getLogger(__name__)
CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="rate_down",
name="Freebox download speed",
translation_key="rate_down",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:download-network",
),
SensorEntityDescription(
key="rate_up",
name="Freebox upload speed",
translation_key="rate_up",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:upload-network",
),
)
CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="missed",
name="Freebox missed calls",
icon="mdi:phone-missed",
translation_key="missed",
native_unit_of_measurement="calls",
state_class=SensorStateClass.MEASUREMENT,
),
)
DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="partition_free_space",
name="free space",
translation_key="partition_free_space",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk",
),
)
@@ -77,14 +75,14 @@ async def async_setup_entry(
FreeboxSensor(
router,
SensorEntityDescription(
key=sensor_name,
name=f"Freebox {sensor_name}",
key=sensor_id,
name=sensor_name,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
)
for sensor_name in router.sensors_temperature
for sensor_id, sensor_name in router.sensors_temperature_names.items()
]
entities.extend(
@@ -121,6 +119,7 @@ class FreeboxSensor(SensorEntity):
"""Representation of a Freebox sensor."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self, router: FreeboxRouter, description: SensorEntityDescription
@@ -128,7 +127,7 @@ class FreeboxSensor(SensorEntity):
"""Initialize a Freebox sensor."""
self.entity_description = description
self._router = router
self._attr_unique_id = f"{router.mac} {description.name}"
self._attr_unique_id = f"{router.mac} {description.key}"
self._attr_device_info = router.device_info
@callback
@@ -204,7 +203,7 @@ class FreeboxDiskSensor(FreeboxSensor):
super().__init__(router, description)
self._disk_id = disk["id"]
self._partition_id = partition["id"]
self._attr_name = f"{partition['label']} {description.name}"
self._attr_translation_placeholders = {"partition": partition["label"]}
self._attr_unique_id = (
f"{router.mac} {description.key} {disk['id']} {partition['id']}"
)
@@ -1,3 +0,0 @@
# Freebox service entries description.
reboot:
+32 -4
View File
@@ -25,10 +25,38 @@
}
}
},
"services": {
"reboot": {
"description": "Reboots the Freebox.",
"name": "Reboot"
"entity": {
"binary_sensor": {
"cover": {
"name": "Cover"
},
"raid_degraded": {
"name": "RAID array {id} degraded"
}
},
"button": {
"mark_calls_as_read": {
"name": "Mark calls as read"
}
},
"sensor": {
"missed": {
"name": "Missed calls"
},
"partition_free_space": {
"name": "{partition} free space"
},
"rate_down": {
"name": "Download speed"
},
"rate_up": {
"name": "Upload speed"
}
},
"switch": {
"wifi": {
"name": "Wi-Fi"
}
}
}
}
+4 -2
View File
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
SWITCH_DESCRIPTIONS = [
SwitchEntityDescription(
key="wifi",
name="Freebox WiFi",
translation_key="wifi",
entity_category=EntityCategory.CONFIG,
)
]
@@ -41,6 +41,8 @@ async def async_setup_entry(
class FreeboxSwitch(SwitchEntity):
"""Representation of a freebox switch."""
_attr_has_entity_name = True
def __init__(
self, router: FreeboxRouter, entity_description: SwitchEntityDescription
) -> None:
@@ -48,7 +50,7 @@ class FreeboxSwitch(SwitchEntity):
self.entity_description = entity_description
self._router = router
self._attr_device_info = router.device_info
self._attr_unique_id = f"{router.mac} {entity_description.name}"
self._attr_unique_id = f"{router.mac} {entity_description.key}"
async def _async_set_state(self, enabled: bool) -> None:
"""Turn the switch on or off."""
@@ -7,6 +7,7 @@ API_REFRESH = timedelta(minutes=5)
DOMAIN = "fujitsu_fglair"
# pylint: disable-next=home-assistant-duplicate-const
CONF_REGION = "region"
CONF_EUROPE = "is_europe"
REGION_EU = "eu"
@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
@@ -28,7 +28,6 @@ CONF_SENSOR = "target_sensor"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
CONF_DEVICE_CLASS = "device_class"
CONF_MIN_DUR = "min_cycle_duration"
CONF_DRY_TOLERANCE = "dry_tolerance"
CONF_WET_TOLERANCE = "wet_tolerance"
@@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components import fan, switch
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import CONF_NAME, PERCENTAGE
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, PERCENTAGE
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
@@ -16,7 +16,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
)
from . import (
CONF_DEVICE_CLASS,
CONF_DRY_TOLERANCE,
CONF_HUMIDIFIER,
CONF_MIN_DUR,
@@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_START,
@@ -56,7 +57,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_AWAY_FIXED,
CONF_AWAY_HUMIDITY,
CONF_DEVICE_CLASS,
CONF_DRY_TOLERANCE,
CONF_HUMIDIFIER,
CONF_INITIAL_STATE,
@@ -5,7 +5,11 @@ from typing import Any
from google_air_quality_api.api import GoogleAirQualityApi
from google_air_quality_api.auth import Auth
from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.exceptions import (
GoogleAirQualityApiError,
InvalidCustomLAQIConfigurationError,
)
from google_air_quality_api.mapping import AQICategoryMapping
import voluptuous as vol
from homeassistant.config_entries import (
@@ -18,6 +22,7 @@ from homeassistant.config_entries import (
)
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
@@ -26,11 +31,28 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
from homeassistant.helpers.selector import (
CountrySelector,
LocationSelector,
LocationSelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CONF_REFERRER,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
SECTION_API_KEY_OPTIONS,
)
_LOGGER = logging.getLogger(__name__)
AIR_QUALITY_COVERAGE_URL = (
"https://developers.google.com/maps/documentation/air-quality/coverage"
)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
@@ -50,10 +72,31 @@ async def _validate_input(
description_placeholders: dict[str, str],
) -> bool:
try:
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS) or {}
enable_custom_laqi = custom_options.get(CONF_ENABLE_CUSTOM_LAQI)
if enable_custom_laqi:
country = custom_options.get(CONF_COUNTRY)
custom_laqi = custom_options.get(CUSTOM_LAQI)
# When custom LAQI is enabled, both country and custom_laqi must be provided
if not country or not custom_laqi:
errors[CUSTOM_LOCAL_AQI_OPTIONS] = "missing_custom_laqi_options"
return False
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
region_code=country,
custom_local_aqi=custom_laqi,
)
else:
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except InvalidCustomLAQIConfigurationError:
errors["base"] = "mismatch_country_and_laqi"
except GoogleAirQualityApiError as err:
errors["base"] = "cannot_connect"
description_placeholders["error_message"] = str(err)
@@ -79,6 +122,25 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
CONF_LONGITUDE: hass.config.longitude,
},
): LocationSelector(LocationSelectorConfig(radius=False)),
vol.Optional(CUSTOM_LOCAL_AQI_OPTIONS): section(
vol.Schema(
{
vol.Required(CONF_ENABLE_CUSTOM_LAQI, default=False): bool,
vol.Optional(
CONF_COUNTRY, default=hass.config.country
): CountrySelector(),
vol.Optional(CUSTOM_LAQI): SelectSelector(
SelectSelectorConfig(
options=sorted(
AQICategoryMapping.get_all_laq_indices()
),
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
SectionConfig(collapsed=True),
),
}
)
@@ -123,6 +185,7 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"api_key_url": "https://developers.google.com/maps/documentation/air-quality/get-api-key",
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL,
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
}
if user_input is not None:
@@ -132,10 +195,13 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
return self.async_abort(reason="already_configured")
session = async_get_clientsession(self.hass)
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
auth = Auth(session, user_input[CONF_API_KEY], referrer=referrer)
api = GoogleAirQualityApi(auth)
if await _validate_input(user_input, api, errors, description_placeholders):
subentry_data = dict(user_input[CONF_LOCATION])
custom_opts = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_opts and custom_opts.get(CONF_ENABLE_CUSTOM_LAQI):
subentry_data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_opts
return self.async_create_entry(
title="Google Air Quality",
data={
@@ -145,7 +211,7 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
subentries=[
{
"subentry_type": "location",
"data": user_input[CONF_LOCATION],
"data": subentry_data,
"title": user_input[CONF_NAME],
"unique_id": None,
},
@@ -185,7 +251,9 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL
}
if user_input is not None:
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
errors["base"] = "location_already_configured"
@@ -202,9 +270,13 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
description_placeholders=description_placeholders,
)
if await _validate_input(user_input, api, errors, description_placeholders):
data = dict(user_input[CONF_LOCATION])
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_options and custom_options.get(CONF_ENABLE_CUSTOM_LAQI):
data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_options
return self.async_create_entry(
title=user_input[CONF_NAME],
data=user_input[CONF_LOCATION],
data=data,
)
else:
user_input = {}
@@ -2,6 +2,9 @@
from typing import Final
DOMAIN = "google_air_quality"
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
CONF_ENABLE_CUSTOM_LAQI: Final = "enable_custom_laqi"
CONF_REFERRER: Final = "referrer"
CUSTOM_LAQI: Final = "custom_laqi"
CUSTOM_LOCAL_AQI_OPTIONS: Final = "custom_local_aqi_options"
DOMAIN: Final = "google_air_quality"
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
@@ -10,11 +10,16 @@ from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.model import AirQualityCurrentConditionsData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@@ -49,11 +54,27 @@ class GoogleAirQualityUpdateCoordinator(
subentry = config_entry.subentries[subentry_id]
self.lat = subentry.data[CONF_LATITUDE]
self.long = subentry.data[CONF_LONGITUDE]
self.custom_local_aqi: str | None = None
self.region_code: str | None = None
options = subentry.data.get(CUSTOM_LOCAL_AQI_OPTIONS)
if isinstance(options, dict) and options.get(CONF_ENABLE_CUSTOM_LAQI):
custom_laqi = options.get(CUSTOM_LAQI)
region_code = options.get(CONF_COUNTRY)
if custom_laqi is not None and region_code is not None:
self.custom_local_aqi = custom_laqi
self.region_code = region_code
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
"""Fetch air quality data for this coordinate."""
try:
return await self.client.async_get_current_conditions(self.lat, self.long)
return await self.client.async_get_current_conditions(
lat=self.lat,
lon=self.long,
region_code=self.region_code,
custom_local_aqi=self.custom_local_aqi,
)
except GoogleAirQualityApiError as ex:
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
raise UpdateFailed(
@@ -204,7 +204,6 @@ async def async_setup_entry(
for subentry_id, subentry in entry.subentries.items():
coordinator = coordinators[subentry_id]
_LOGGER.debug("subentry.data: %s", subentry.data)
async_add_entities(
(
AirQualitySensorEntity(coordinator, description, subentry_id, subentry)
@@ -15,6 +15,8 @@
},
"error": {
"cannot_connect": "Unable to connect to the Google Air Quality API:\n\n{error_message}",
"mismatch_country_and_laqi": "This local AQI is not available for the selected country. Please select an available combination.",
"missing_custom_laqi_options": "Please provide both country and custom local AQI when custom local AQI is enabled.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
@@ -39,6 +41,20 @@
"referrer": "Specify this only if the API key has a [website application restriction]({restricting_api_keys_url})."
},
"name": "Optional API key options"
},
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "Custom local AQI",
"enable_custom_laqi": "Enable custom local AQI"
},
"data_description": {
"country": "Country of the location",
"custom_laqi": "The target air quality index",
"enable_custom_laqi": "Select to enable a custom local air quality index"
},
"description": "Country and custom local AQI must match. You can find the available combinations here: {air_quality_coverage_url}",
"name": "Custom local AQI options"
}
}
}
@@ -51,8 +67,11 @@
},
"entry_type": "Air quality location",
"error": {
"cannot_connect": "[%key:component::google_air_quality::config::error::cannot_connect%]",
"location_already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"location_name_already_configured": "Location name already configured.",
"mismatch_country_and_laqi": "[%key:component::google_air_quality::config::error::mismatch_country_and_laqi%]",
"missing_custom_laqi_options": "[%key:component::google_air_quality::config::error::missing_custom_laqi_options%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
@@ -69,6 +88,22 @@
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"sections": {
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::enable_custom_laqi%]"
},
"data_description": {
"country": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::enable_custom_laqi%]"
},
"description": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::description%]",
"name": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::name%]"
}
},
"title": "Air quality data location"
}
}
@@ -15,6 +15,7 @@ DEFAULT_STT_NAME = "Google AI STT"
DEFAULT_TTS_NAME = "Google AI TTS"
DEFAULT_AI_TASK_NAME = "Google AI Task"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PROMPT = "prompt"
DEFAULT_STT_PROMPT = "Transcribe the attached audio"
@@ -12,6 +12,7 @@ DOMAIN = "google_travel_time"
ATTRIBUTION = "Powered by Google"
CONF_DESTINATION = "destination"
# pylint: disable-next=home-assistant-duplicate-const
CONF_OPTIONS = "options"
CONF_ORIGIN = "origin"
CONF_AVOID = "avoid"

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