Compare commits

..

191 Commits

Author SHA1 Message Date
Paulus Schoutsen b28e6502a3 tests: sandbox_v2 integration tests
Tests for the HA-core side of sandbox_v2 (the client-side
hass_client/tests/ shipped with the previous commit).

134 tests across:
- test_classifier.py — manifest-based routing rules.
- test_router.py — flow create / setup / unload intercepts.
- test_manager.py — subprocess lifecycle + crash/restart + token factory.
- test_proxy_flow.py — `SandboxFlowProxy` + flow marshalling.
- test_channel.py — concurrent channel dispatcher + close semantics.
- test_bridge.py — entity / service / event mirror handlers on main.
- test_phase4_subprocess.py — real-subprocess flow handshake.
- test_phase9_shutdown.py — graceful shutdown + restore_state hand-off.
- test_phase13_proxies.py — parametrised smoke per supported entity domain.
- test_phase14.py — flow schema bridge + unique_id propagation +
  async_unload core hook + perf benchmark.
- test_store.py — `_SandboxStoreServer` path scoping + key validation.
- test_init.py — `SandboxV2Data` shape + integration wiring.
- test_auth.py — sandbox-scoped access token issuance.
- test_testing_plugins.py — in-process + subprocess pytest plugins +
  autotag fixture.
- test_spike.py — Phase 1 entity-bridge spike (Option A vs B).
- test_perf.py — 200-light area-call batching benchmark.
- _helpers.py — shared `make_channel_pair` test helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:42:55 -04:00
Paulus Schoutsen e3aafaedb1 Add sandbox_v2 client library, docs, and compat sweep tooling
The client-library side of sandbox v2, plus the full architecture +
phase-by-phase narrative + per-failure compat tooling.

`sandbox_v2/hass_client/` is a separate uv-managed Python package that
the HA-core sandbox_v2 integration spawns as a subprocess per sandbox
group. It hosts a private `HomeAssistant`, drives each sandboxed
integration's `ConfigFlow` and `async_setup_entry`, mirrors entity /
service / event registrations back to main over a stdio JSON-line
`Channel`, and routes Store reads/writes through main via `RemoteStore`.

`sandbox_v2/docs/`:
- `entity-bridge-decision.md` — Phase 1 spike: why Option B
  (action-call forwarding via `sandbox_v2/call_service`).
- `auth-scoping-decision.md` — Phase 7: why `RefreshToken.scopes` is
  a generic primitive (vs a sandbox-private subclass).
- `FOLLOWUPS.md` — narrative of Phases 12–17 (concurrent dispatcher,
  28-domain proxy fill-in, flow-schema bridge, baseline compat sweep,
  cross-integration BACKLOG generation, `ConfigEntry.sandbox` field).

Compat sweep tooling:
- `run_compat.py` — Phase 15: v1's 37-integration baseline runner;
  output to `COMPAT.md` (curated) + `COMPAT.csv`.
- `run_compat_full.py` — Phase 16: 807-integration cross-sweep at
  asyncio concurrency=6 (~12 min wall); output to `COMPAT_FULL.md`
  + `COMPAT_FULL.csv`.
- `categorize_failures.py` — regex-rule failure categoriser feeding
  `BACKLOG.md` + `BACKLOG_FAILURES.json`.
- `generate_backlog.py` — auto-draft skeleton for BACKLOG.md.

Headline result (after Phase 17): 99.67% test-level pass rate across
807 integrations; baseline 99.97%. Both clear the 99.5% v1-removal
threshold.

`sandbox_v2/STATUS-phase-{3..18}.md` are the authoritative landing
notes for each phase — every "Things to flag" surfaced is in there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:42:36 -04:00
Paulus Schoutsen 9f32319481 Add sandbox_v2 integration (HA-core side)
The HA-core side of the sandbox v2 rewrite: routing, lifecycle, flow
forwarding, entity bridging, service/event mirroring, scoped auth,
opt-in data sharing, Store routing, graceful shutdown.

Lives at `homeassistant/components/sandbox_v2/`. Designed alongside the
client library at `sandbox_v2/`; see `sandbox_v2/OVERVIEW.md` for the
full architecture and `sandbox_v2/docs/FOLLOWUPS.md` for the phase-by-
phase narrative.

Built on the core hooks added in the preceding commits:
`ConfigEntries.router` + `ConfigEntry.sandbox` + `RefreshToken.scopes`
+ `EntityComponent.async_register_remote_platform`.

32 domain proxy classes under `entity/` cover every entity domain v2
supports. Bridge translates each proxy method into a
`sandbox_v2/call_service` RPC via a per-loop-tick batcher (coalesces
multi-entity area calls into single RPCs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:41:48 -04:00
Paulus Schoutsen ddd9c5ab61 hassfest: tolerate sandbox v1 errors; add sandbox_v2 to NO_QUALITY_SCALE
Adds an `IGNORE_INTEGRATIONS_WITH_ERRORS` set to hassfest's main loop
so v1 sandbox's pre-existing hassfest gates (CONFIG_SCHEMA, manifest
version, missing services.yaml, mypy signature drift in entity proxies)
don't block validation of the rest of the tree. v1 is being superseded
by sandbox_v2 (see `sandbox_v2/OVERVIEW.md`) — accepting v1's existing
state for now is preferable to either fixing every gate in code that
will be removed, or skipping hooks.

Also adds `sandbox_v2` to `NO_QUALITY_SCALE` (internal integration)
and ships an empty `sandbox_v2/services.yaml` placeholder — `bridge.py`
calls `hass.services.async_register` dynamically per sandboxed
integration; those services are owned by the sandboxed integrations.

`homeassistant/generated/config_flows.py` is regenerated to include
`sandbox` (v1 had drifted out of the registry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:41:18 -04:00
Paulus Schoutsen 4936885598 config_entries + entity_component: hooks for runtime-routed integrations
Three small additive surfaces that the sandbox_v2 integration plugs
into. Each is additive and a no-op when nothing registers against it.

config_entries.py:
- `ConfigEntries.router: ConfigEntryRouter | None` attribute + the
  `ConfigEntryRouter` Protocol. Consulted from three sites:
  `ConfigEntriesFlowManager.async_create_flow`, `ConfigEntries.async_setup`,
  and `ConfigEntries.async_unload`. Returning `None` falls through to
  the existing path.
- `ConfigEntry.sandbox: str | None` optional field. Carries the routing
  tag without polluting `entry.data`. Persisted via `as_dict` /
  `as_storage_fragment` only when non-None; read via `dict.get` so
  pre-existing stored entries load with `sandbox=None`. Mutable via
  `ConfigEntries.async_update_entry(entry, sandbox=)`. `ConfigFlowResult`
  gains a `sandbox` TypedDict key the framework reads at entry
  construction (same plumbing shape as `minor_version` / `options` /
  `subentries`).

entity_component.py:
- `EntityComponent.async_register_remote_platform(config_entry, platform)`
  lets sandbox_v2 attach a pre-built remote `EntityPlatform` without
  re-discovering the local integration. Mirrors `async_setup_entry`'s
  `_platforms[entry_id] = platform` assignment as a public hook.

Tests:
- `MockConfigEntry` picks up a `sandbox=` kwarg threaded through to
  `ConfigEntry.__init__`.
- Six new `test_config_entries.py` cases for the `sandbox` field:
  default-none + omitted-from-storage, persisted-when-set, round-trip,
  absent-from-storage-loads-as-none, async_update_entry-sets-sandbox,
  cannot-be-set-directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:25:54 -04:00
Paulus Schoutsen 67fff835b2 auth: optional scopes on RefreshToken + dispatcher enforcement
Adds an optional `scopes: frozenset[str] | None` attribute to
`RefreshToken` and threads it through `AuthManager.async_create_refresh_token`
and `AuthStore` (sorted list on disk, optional on read — no version bump).

`ActiveConnection` reads scopes off the connecting token and a new
`_scope_allows` helper in the websocket dispatcher rejects out-of-scope
commands with `ERR_UNAUTHORIZED`. Existing unscoped tokens (`scopes is
None`) are unaffected — the gate is a no-op for them.

This is the primitive the sandbox_v2 integration uses to issue
namespace-scoped tokens (`{"sandbox_v2/", "auth/current_user"}`) to
sandbox subprocesses, so a sandbox-resident integration cannot escalate
to the rest of the websocket API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:25:31 -04:00
Paulus Schoutsen 7b19a3a71b Update SANDBOX_COMPAT for newly-installable deps
After 'uv pip install -r requirements_ha.txt' (which pulls in
requirements_all.txt), the integrations previously listed as
'Not Tested (missing dependencies)' import and run:

  - rest: 10/10 pass        (needed xmltodict)
  - logbook: 55/55 pass      (needed sqlalchemy + numpy + turbojpeg)
  - command_line: 7/7 pass
  - trend: 9/9 pass

Promote them into the main pass table; the totals now read 35 of 37
fully pass, 955/957 tests (99.8%).

conversation imports too (hassil was already in pyproject.toml deps
but the report listed it as missing) but 8 of 21 tests fail and the
run deadlocks at tests 20-21 — moved into a new 'Newly runnable, still
investigating' section instead of the pass table.

Add a Setup section pointing at requirements_ha.txt and the pyitachip2ir
macOS caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:12:21 -04:00
Paulus Schoutsen 7994744bea Add requirements_ha.txt to pull in HA Core integration deps
The sandbox client's pyproject.toml only carries the minimal set of
packages needed to run the client library and its own tests. Running
HA Core's per-integration test suites through the sandbox plugin needs
the full integration dependency tree (hassil for conversation,
xmltodict for rest, sqlalchemy+numpy+turbojpeg for logbook, …).

requirements_ha.txt pulls in ../../requirements_all.txt and
../../requirements_test.txt with paths relative to the file, so it
keeps working from any cwd. Comment notes the macOS pyitachip2ir
build caveat and the workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:44:30 -04:00
Paulus Schoutsen e9e5bda3f6 Drop .sh from doc references to the test runner
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:31:30 -04:00
Paulus Schoutsen 3d807de32d Remove obsolete run_all_sandbox_tests.sh
The shell version required a manually-prepared
/tmp/all_integrations.txt and used a perl-based timeout shim.
run_all_sandbox_tests.py auto-discovers integrations from the core
tests directory and uses subprocess timeouts, so the .sh is no longer
needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:43:45 -04:00
Paulus Schoutsen fa60ef5477 Consolidate sandbox docs: fold ARCHITECTURE.md into OVERVIEW.md
architecture.html already covers system diagrams, flow diagrams, file
structure, websocket API, key classes, and test results, so the prose
deep-dive in ARCHITECTURE.md was largely overlapping. Keep the bits
that weren't already in OVERVIEW.md and drop the rest:

- Startup sequence (host startup, sandbox process startup, host/sandbox
  entity platform setup) as a new section after High-Level Flow.
- The RemoteLightEntity worked example plus the static/dynamic property
  caching rationale, inside Entity Platform Architecture.
- Entity Method Compatibility (which domains already expose async
  wrappers; the cover.toggle gap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:42:18 -04:00
Paulus Schoutsen 3046996869 Add sandbox/README.md as the directory's overview
Pointers to OVERVIEW.md, ARCHITECTURE.md, architecture.html, the
test driver scripts, and SANDBOX_COMPAT.md; quick-start for running
the sandbox client and the core test suites through it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:59:46 -04:00
Paulus Schoutsen 9930d7dad4 Consolidate sandbox docs and test drivers under core/sandbox/
Move ARCHITECTURE.md, OVERVIEW.md, CLAUDE.md, the architecture HTML,
the test-runner scripts and TEST_RESULTS.csv into this directory next
to the hass_client subtree, so the entire sandbox project lives on the
sandbox branch of core (only the HA integration at
homeassistant/components/sandbox/ stays put for HA's loader).

Adjust the relative paths the moved files used to point at the old
sibling checkouts:
- hass_client/pyproject.toml: uv source homeassistant -> ../..
- run_all_sandbox_tests.{py,sh}: cd into ./hass_client and walk to
  ../../tests/components/ for the core test suites
- analyze_failures.py: write TEST_RESULTS.csv next to the script

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:34:54 -04:00
Paulus Schoutsen e18dd7e906 Add 'sandbox/hass_client/' from commit '8f1a294efecab03343748950da428bd18d92fffe'
git-subtree-dir: sandbox/hass_client
git-subtree-mainline: d12fb7814a
git-subtree-split: 8f1a294efe
2026-05-23 11:32:40 -04:00
Paulus Schoutsen d12fb7814a Replace subscribe_service_calls with explicit register/call/result API
Restructure the sandbox websocket API around three commands instead of
a single event subscription: sandbox/register_service registers a
proxy service on the host that forwards calls into the sandbox,
sandbox/call_service lets the sandbox invoke a host service while
preserving its context, and sandbox/service_call_result returns the
sandbox's response back to the originating host caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:31:47 -04:00
Paulus Schoutsen 8e6be68fe3 Remove per-domain platform setup files
These 32 files (light.py, sensor.py, etc.) each only registered an
async_add_entities callback. Now that RemoteHostEntityPlatform adds
proxy entities directly to the EntityComponent, they are dead code.

Also removes the unused register_platform_callback and
AddEntitiesCallback from SandboxEntityManager.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:47 -04:00
Paulus Schoutsen c1a71bed25 Add RemoteHostEntityPlatform for sandbox entities
Replace the async_forward_entry_setups + per-domain platform file
approach with RemoteHostEntityPlatform. This EntityPlatform subclass
is added directly to the domain's EntityComponent and manages proxy
entities without needing 32 identical platform files.

The platform is created on-demand when the first entity for a domain
is registered by the sandbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:47 -04:00
Paulus Schoutsen ee82ca9677 Support sandbox grouping by string option value
Config entries can now set options["sandbox"] = "group_name" to be
assigned to a named sandbox group. Entries sharing the same group
string run in the same sandbox process. The sandbox config entry
discovers group members via entry.data["group"].

The explicit entries list (entry.data["entries"]) still works for
test infrastructure compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:47 -04:00
Paulus Schoutsen b51067d37d Refactor sandbox entity proxies into entity/ package
Split the monolithic entity.py (1900 lines) into a per-platform
package structure under entity/. Each domain gets its own file,
making the codebase easier to navigate and extend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:47 -04:00
Paulus Schoutsen 12f24ac6bf Add device_tracker and todo proxy entity support
Brings total supported platforms to 32. Device tracker supports
both TrackerEntity (GPS) and ScannerEntity (router/BLE).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:46 -04:00
Paulus Schoutsen 6b92011cae Add proxy entity support for 24 additional HA platforms
Implements sandbox proxy entities for: alarm_control_panel, button,
calendar, climate, cover, date, datetime, fan, humidifier, lawn_mower,
lock, media_player, notify, number, remote, select, siren, text, time,
update, vacuum, valve, water_heater, weather.

Total supported platforms: 30 (up from 6).

Each proxy class caches state from sandbox pushes and forwards service
calls back to the sandbox via the existing websocket command channel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:46 -04:00
Paulus Schoutsen c88253752f Add proxy entity support for all Hue platforms
Adds SandboxBinarySensorEntity, SandboxSensorEntity, SandboxSwitchEntity,
SandboxSceneEntity, and SandboxEventEntity proxy classes. Also adds
device_class and state_class to entity registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:46 -04:00
Paulus Schoutsen 4f43b99540 Add sandbox integration with entity proxy architecture
Implements the sandbox integration that manages config entries running
in isolated processes. Proxy entities on the host forward service calls
to sandbox processes via websocket and cache state pushed back.

Supports entity, device, and area targeting for service calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-23 11:31:46 -04:00
Paulus Schoutsen 8f1a294efe Extract HybridServiceRegistry and improve sandbox error translation
Move HybridServiceRegistry out of runtime.py into its own
sandbox_service_registry.py module, expand the websocket API error
translator to handle ServiceNotSupported and sandbox/call_service, and
extend conftest_sandbox with additional fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:31:00 -04:00
Max Michels f88b7bcdf6 Replace duplicate constants in olama with homeassistant.const imports (#171949)
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2026-05-23 17:03:44 +02:00
Max Michels 05009871aa Replace duplicate constants in numato with homeassistant.const imports (#171950) 2026-05-23 16:57:46 +02:00
Max Michels 4aa7323af2 Replace duplicate constants in nmbs with homeassistant.const imports (#171951) 2026-05-23 16:57:07 +02:00
Max Michels bcacf3a73c Remove unused duplicate constants in nice_go with homeassistant.const imports (#171952) 2026-05-23 16:56:16 +02:00
Maciej Bieniek 96a6babaef Remove Shelly temperature and humidity sensors with error (#170900) 2026-05-23 14:02:32 +02:00
Max Michels e856271a5a Replace duplicate constants in motioneye with homeassistant.const imports (#171954)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-05-23 12:05:02 +02:00
Max Michels add023ed74 Replace duplicate constants in openerz with homeassistant.const imports (#171946) 2026-05-23 11:58:51 +02:00
Max Michels 8d456cb24f Replace duplicate constants osoenergy with homeassistant.const imports (#171944) 2026-05-23 11:52:54 +02:00
Max Michels 5ebd95eb34 Replace duplicate constants in netatmo with homeassistant.const imports (#171953) 2026-05-23 11:47:58 +02:00
Max Michels 228d7189c3 Replace duplicate constants in profiler with homeassistant.const imports (#171943) 2026-05-23 11:01:02 +02:00
Max Michels a8e141a48a Replace duplicate constants in rainmachine with homeassistant.const imports (#171942)
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2026-05-23 10:59:53 +02:00
Max Michels d42d52a0f7 Replace duplicate constants in onkyo with homeassistant.const imports (#171947) 2026-05-23 10:57:48 +02:00
Mick Vleeshouwer cee0fe071d Fix tilt-only DynamicPergola covers in Overkiz (#171898) 2026-05-23 10:52:16 +02:00
Josh Gustafson e3593c3076 Arcam reconfig flow (#171767) 2026-05-23 10:24:28 +02:00
Martin Hjelmare 5498de07ff Remove legacy Konnected integration (#171896) 2026-05-23 10:19:35 +02:00
J. Nick Koston ac3f973d7d Bump aioesphomeapi to 45.1.0 (#171935) 2026-05-23 09:47:15 +02:00
J. Nick Koston 6b8a2a4032 Bump bleak-esphome to 3.8.1 (#171936) 2026-05-23 09:46:49 +02:00
Artur Pragacz 74e40af4bb Remove CLOUD_NEVER_EXPOSED_ENTITIES (#171933) 2026-05-23 00:26:45 -04:00
J. Nick Koston 833e15d6f2 Bump habluetooth to 6.4.0 (#171918) 2026-05-23 00:10:50 -04:00
Matt ee56fd1eb0 Fix two HEOS bugs: host set construction and missing error decorator (#171913) 2026-05-22 18:42:43 -05:00
Felipe Santos e6528bae8a Add missing translation for connection failure on OpenRGB (#171892) 2026-05-22 21:59:39 +02:00
Joost Lekkerkerker a17eb65498 Refactor labs websocket API tests to use async_setup_component (#171891) 2026-05-22 21:53:52 +02:00
Joost Lekkerkerker 912a839d66 Don't call migrate entry in generic thermostat tests directly (#171887) 2026-05-22 21:44:10 +02:00
Martin Hjelmare 4306863729 Fix homekit test_reload flaky test (#171878) 2026-05-22 14:33:27 -05:00
Martin Hjelmare ba2f66e751 Remove not needed default force_update in flo (#171854) 2026-05-22 20:15:00 +02:00
Manu 94581d8ab6 Move service registration in System Bridge integration to async_setup (#171761)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-22 18:43:22 +02:00
Ingo Fischer 7d6ec7fc58 Bump matter-python-client to 0.7.1 (#171764)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-05-22 17:34:20 +01:00
Jan Bouwhuis f49de3548e Add MQTT message expiry interval option (#171143) 2026-05-22 18:27:22 +02:00
Manu 49ab42d3a2 Fix dead link in System Bridge service action (#171855) 2026-05-22 17:00:30 +02:00
Franck Nijhof 383f6142f0 Fix ZBT-2 hardware page crash when entry data is missing VID (#171828) 2026-05-22 16:58:01 +02:00
Kamil Breguła 2f120cf604 Fix rgb_color passed as RGBColor NamedTuple instead of plain tuple to light entity turn_on (#171795)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:56:34 +02:00
Franck Nijhof 37288849b3 Register Insteon modem device before platform setup (#171839) 2026-05-22 10:23:47 -04:00
zhangluofeng aa8659f507 Add xthings cloud lock (#171176)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-22 15:54:37 +02:00
DeerMaximum 40c0d79d1d Replaced duplicate constant with homeassistant.const in NINA (#171852) 2026-05-22 15:41:04 +02:00
Franck Nijhof bef8632d78 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 15:23:42 +02:00
Duco Sebel f00decfaa3 Use uptime device class for HomeWizard uptime sensor (#171830) 2026-05-22 15:23:09 +02:00
Manu 42e7add026 Add selector options translations to System Bridge integration (#171771) 2026-05-22 15:22:22 +02:00
Franck Nijhof 263aa3f16e Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 15:18:00 +02:00
mhuiskes 03b364dcf0 Refactor zeversolar tests: use fixtures, patch at use site, add unique_id (#171697) 2026-05-22 14:58:56 +02:00
Duco Sebel 3b1aaf39af Bumb python homewizard energy 10.1.0 (#171826) 2026-05-22 14:51:58 +02:00
Franck Nijhof b82ba43fa4 Add pylint checker for invalid MDI icon references (#171824)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 13:45:40 +02:00
starkillerOG d81ef5593c Bump reolink_aio to 0.20.0: Reolink battery camera support (#171836) 2026-05-22 12:59:33 +02:00
Manu 5c5e50f024 Fix platform unloading in System Bridge integration (#171822) 2026-05-22 12:56:03 +02:00
Lex Postma e796d9c467 Update strings.json to align with HomeWizard app (#171740)
Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com>
2026-05-22 12:54:58 +02:00
Karl Beecken 342f23526f Remove empty requirements_test_all.txt (#171530 follow-up) (#171834) 2026-05-22 12:38:58 +02:00
Erik Montnemery 814ec697cf Remove advanced mode from hue service actions (#171442) 2026-05-22 11:45:33 +02:00
Erik Montnemery 120f1446d4 Rename advanced section to additional options in telegram_bot service actions (#171460) 2026-05-22 11:44:05 +02:00
Franck Nijhof 170af75b7d Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 11:37:05 +02:00
Ariel Ebersberger 5432d29489 Use is/is not for same-enum identity comparisons (tests) (#171689) 2026-05-22 11:32:27 +02:00
Franck Nijhof 8098f4f6bc Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 11:25:28 +02:00
Simone Chemelli 6a70077687 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 11:17:17 +02:00
Max Michels 5dbb0464ba Replace duplicate constants with homeassistant.const imports (#171815) 2026-05-22 11:10:21 +02:00
dependabot[bot] 1df165ea02 Bump j178/prek-action from 2.0.3 to 2.0.4 (#171812)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 10:39:43 +02:00
Manu 62542eb911 Replace duplicate constants with homeassistant.const imports in xiaomi_miio (#171823) 2026-05-22 10:39:09 +02:00
Max Michels a842cac34c Replace duplicate constants with homeassistant.const imports (#171817) 2026-05-22 10:38:06 +02:00
Simone Chemelli 2460f688e3 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 10:34:00 +02:00
Simone Chemelli a868ea443c Fix hardcoded exception strings in uptimerobot (#171744) 2026-05-22 10:33:07 +02:00
Franck Nijhof 1d8565483b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 10:31:10 +02:00
dependabot[bot] 1ef3301253 Bump github/codeql-action from 4.35.4 to 4.35.5 (#171813)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 09:47:16 +02:00
Manu 525952f016 Add entity translations to System Bridge integration (#171807) 2026-05-22 09:00:54 +02:00
Shay Levy 3257275c5a Fix LG webOS TV hardcoded exception strings (#171777) 2026-05-22 08:28:19 +02:00
Max Michels cb54fd4921 Replace duplicate constants with homeassistant.const imports (#171809) 2026-05-22 07:57:08 +02:00
Max Michels b391fc61ea Replace duplicate constants with homeassistant.const imports (#171808) 2026-05-22 07:56:29 +02:00
J. Nick Koston fcd4e4939c Bump habluetooth to 6.2.0 (#171800) 2026-05-21 23:08:17 -05:00
J. Nick Koston deb8b5da05 Parallelize pytest --collect-only in split_tests.py (#171772) 2026-05-21 22:58:01 -04:00
g4bri3lDev c7754a6ce9 Bump py-opendisplay to 7.2.3 (#171775) 2026-05-21 22:52:36 -04:00
J. Nick Koston 242724bd50 Bump aiodiscover to 3.2.3 (#171803) 2026-05-21 22:51:54 -04:00
Max Michels 42454563db Replace duplicate constants with homeassistant.const imports (#171790)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-21 22:51:34 -04:00
J. Nick Koston bf03d0c216 Bump dbus-fast to 5.0.3 (#171595) 2026-05-21 21:11:35 -05:00
Max Michels 568107e06b Replace duplicate constants with homeassistant.const imports (#171784) 2026-05-22 01:33:48 +03:00
Jens Timmerman 7da44428b6 Bump guntamatic to v1.9.0 (#171631) 2026-05-21 22:55:29 +01:00
Max Michels 0a27f31949 Replace duplicate constants with homeassistant.const imports (#171781) 2026-05-21 22:53:07 +01:00
Erwin Douna 905b868c82 Add recreate services to Portainer (#167225)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2026-05-21 22:52:07 +01:00
Max Michels 3187289913 Replace duplicate constants with homeassistant.const imports (#171776) 2026-05-22 00:18:54 +03:00
Max Michels 87cecd4a44 Replace duplicate constants with homeassistant.const imports (#171778) 2026-05-22 00:18:23 +03:00
Robert Svensson fed38b0e38 Replace duplicate ATTR_LOCKED constant with homeassistant.const import in deconz (#171779) 2026-05-22 00:17:22 +03:00
Raphael Hehl 6a36d1260b Bump uiprotect to 10.5.0 (#171768)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 15:42:31 -05:00
Raphael Hehl 49fc1b413d Bump pydantic to 2.13.4 (#171763)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 14:42:06 -05:00
Abílio Costa bffb0417cc Instruct agents to run prek after doing changes (#171757) 2026-05-21 20:16:26 +01:00
G Johansson 8b8c687fc3 Remove not needed exception handling in dnsip (#171758) 2026-05-21 20:58:32 +02:00
Lukas e3dd6b5fc5 Fix hardcoded exception strings in pooldose integration (#171652)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 20:36:17 +02:00
Ariel Ebersberger 94d620438b Use is/is not for same-enum identity comparisons (source) (#171591) 2026-05-21 19:30:55 +02:00
Erik Montnemery 8867b792dc Remove use of advanced mode from the zha integration (#171753) 2026-05-21 19:26:24 +02:00
G Johansson 97967abfeb Fix missing string in smhi (#171756) 2026-05-21 19:25:03 +02:00
mhuiskes af8fea272d Declare Bronze quality scale for Zeversolar integration (#170410) 2026-05-21 19:12:54 +02:00
Simone Chemelli 2db0eed570 Fix hardcoded exception strings in samsungtv (#171745) 2026-05-21 18:59:37 +02:00
Erwin Douna ded1628c20 Downloader add missing data description (#171727) 2026-05-21 18:59:24 +02:00
Petro31 a02e54f332 Update documentation link to point to each domain/platform (#171734)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 18:47:39 +02:00
Erik Montnemery 1858649bc7 Improve tests of trigger variables (#171742) 2026-05-21 17:55:41 +02:00
Leonardo Merza 109e09c3ec Add fan minimum on time number entity to ecobee (#171419)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 17:53:02 +02:00
Manu ad139b259b Add notify entity to System Bridge integration (#171736)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 17:49:25 +02:00
Scott Giminiani a1a76874fd Fix name of config flow form field (#171741) 2026-05-21 17:33:53 +02:00
Ariel Ebersberger e7bd56325b Use is for IntentResponseType identity check in conversation (#171699)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arturpragacz <49985303+arturpragacz@users.noreply.github.com>
2026-05-21 17:31:14 +02:00
J. Nick Koston ef2ef0c8ba Bump zeroconf to 0.149.16 (#171737) 2026-05-21 17:28:26 +02:00
Max Michels a8381e923a Replace duplicate constants with homeassistant.const imports (#171675) 2026-05-21 17:13:31 +02:00
Max Michels b7adba559b Replace duplicate constants with homeassistant.const imports (#171677) 2026-05-21 17:13:20 +02:00
Matthias Alphart 5cf6dceb04 Normalize empty string to None in knx config flow (#171693) 2026-05-21 17:13:11 +02:00
Paul Bottein 975bcc5431 Reorganize Freebox entity categories (#171480) 2026-05-21 16:55:27 +02:00
Michael Barrett f24a44e81f Update aioghost to 0.4.16 (#171690) 2026-05-21 16:53:45 +02:00
Phil-Rad 43c91843cd Remove unreachable import config flow path from cert_expiry (#171733) 2026-05-21 16:50:49 +02:00
Chris dbce1d328a Bump python-openevse-http to 0.3.4 (#171621) 2026-05-21 16:46:54 +02:00
Petro31 d294b04b79 Add EntityComponent to device_tracker (#171507) 2026-05-21 16:10:20 +02:00
Markus Tuominen 8b0e9060b3 Set _attr_has_entity_name on tplink_omada OmadaClientScannerEntity (#171680) 2026-05-21 16:41:37 +03:00
MoonDevLT 39066b6e3a Fix missing exceptions translation key missing_device_info in lunatone (#171569) 2026-05-21 14:59:48 +02:00
Max Michels a23a9b350b Replace duplicate constants with homeassistant.const imports (#171701) 2026-05-21 14:57:58 +02:00
chiro79 fdaa807ca8 Switch to aiopvpc-ng (#171025) 2026-05-21 14:54:23 +02:00
A. Gideonse f290dcc03f Update Indevolt integration quality scale to platinum (#170320) 2026-05-21 14:53:06 +02:00
Markus Tuominen 654408cc76 Set _attr_has_entity_name on sonos SonosFavoritesEntity (#171678) 2026-05-21 13:42:21 +02:00
Max Michels 1f814faad8 Replace duplicate constants with homeassistant.const imports (#171702) 2026-05-21 13:36:14 +02:00
Markus Tuominen 6e00eecfcd Set _attr_has_entity_name on lunatone LunatoneLineBroadcastLight (#171682) 2026-05-21 13:19:42 +02:00
Robert Resch 8c8620c511 Add check requirements yanked and CVE check (#171641)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-05-21 12:54:15 +02:00
Wendelin cca8825ca5 Add comment optional attribute to automation items (#171091) 2026-05-21 12:52:54 +02:00
Max Michels 92fbcc29a5 Replace duplicate constants with homeassistant.const imports (#171700) 2026-05-21 12:51:19 +02:00
Shay Levy 1c28833f39 Fix LG WebOS TV translation placeholders mismatches (#171696) 2026-05-21 13:33:36 +03:00
Christian Lackas cfdef77222 homematicip_cloud: migrate entity names to has_entity_name (#169273) 2026-05-21 12:29:43 +02:00
epenet 49720475da Bump renault-api to 0.5.10 (#171692) 2026-05-21 12:16:29 +02:00
Markus Tuominen 7967b84cc6 Set _attr_has_entity_name on omie OMIEPriceSensor (#171671) 2026-05-21 12:14:03 +02:00
Markus Tuominen c715557813 Set _attr_has_entity_name on smartthings SmartThingsScene (#171672) 2026-05-21 12:10:06 +02:00
Markus Tuominen 79e5330782 Set _attr_has_entity_name on ekeybionyx EkeyEvent (#171668) 2026-05-21 12:03:01 +02:00
Max Michels 5210ca64b1 Replace duplicate constants with homeassistant.const imports (#171669) 2026-05-21 12:02:12 +02:00
Markus Tuominen 65283e3d77 Set _attr_has_entity_name on fitbit battery sensors (#171670) 2026-05-21 12:01:27 +02:00
mhuiskes 427cb9f8db Remove unnecessary intermediate variables in zeversolar diagnostics (#171691) 2026-05-21 11:55:34 +02:00
Erik Montnemery a09e042d42 Add test of FlowHandler show_advanced_options property (#171681) 2026-05-21 11:47:42 +02:00
Shay Levy 072e9b51a2 Fix Shelly translation placeholders mismatches (#171685) 2026-05-21 11:47:20 +02:00
Erik Montnemery b96342c4f3 Remove use of advanced mode from the knx integration (#171674) 2026-05-21 11:26:22 +02:00
Erik Montnemery 56eae8c808 Fix min value for music_assistant.get_library offset (#171664) 2026-05-21 10:25:08 +02:00
Erik Montnemery 9fbdf86104 Rename advanced options section to additional options in opendisplay service actions (#171452) 2026-05-21 10:18:31 +02:00
Jan Bouwhuis 8ff5da59c4 Fix hardcoded exception strings in incomfort (#171616) 2026-05-21 10:09:04 +02:00
Andres Ruiz 298f4f8ed0 Remove National Grid US virtual integration (#171204) 2026-05-21 09:53:59 +02:00
Willem-Jan van Rootselaar 6fdc0bb90b Fix bsblan set data error translation (#171529) 2026-05-21 09:51:54 +02:00
Mick Vleeshouwer 94c3ad2cb2 Bump pyOverkiz to 1.20.4 (#171626) 2026-05-21 09:50:54 +02:00
Martin Hjelmare d83d44648c Fix Home Connect exception translation placeholder mismatch (#171655) 2026-05-21 09:38:22 +02:00
Erik Montnemery 279b614b7c Remove advanced mode from music_assistant service actions (#171451) 2026-05-21 09:35:48 +02:00
Erik Montnemery 244dfe014a Remove advanced mode from mqtt service actions (#171448)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 09:33:10 +02:00
Markus Tuominen 6b379e50cf Add has-entity-name pylint quality scale checker (#171486) 2026-05-21 10:21:06 +03:00
epenet 1368cd15da Remove myself from samsungtv code-owners (#171654) 2026-05-21 08:54:59 +02:00
Franck Nijhof 8c8cc3acb9 Fix habitica ignoring zero values for interval and streak (#171468) 2026-05-21 08:06:08 +02:00
Franck Nijhof b0634bea35 Fix SmartThings crash when timestamp attribute is None (#171467) 2026-05-21 08:05:42 +02:00
Raphael Hehl 5ae31cad6f Fix unifiprotect exception translations (#171510) (#171619)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 08:04:21 +02:00
Brandon Rothweiler b45aaaa177 Update py-aosmith to 1.0.18 (#171647) 2026-05-21 07:42:07 +02:00
Jan-Philipp Benecke 6560496440 Add missing WebDAV exception translation (#171614) 2026-05-20 20:46:31 -04:00
Erwin Douna 489dda8efb SMA refactor to new pylint (#171630) 2026-05-20 20:45:39 -04:00
Alexey Masolov 30c942d139 Catch requests.Timeout and apply TIMEOUT constant across CalDAV integration (#171632)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 20:45:08 -04:00
On Freund c735e47e23 Bump pyrisco to 0.7.0 (#171644) 2026-05-20 20:42:29 -04:00
Robert Resch 3856405c72 Add aw check requirements async block check (#171642) 2026-05-21 01:28:32 +02:00
Robert Resch 323479ca44 Fix aw check requirements safe output (#171643) 2026-05-21 01:16:25 +02:00
Raphael Hehl c8bfe56975 Fix hardcoded exception strings in unifi_access (#171629)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 00:37:08 +02:00
A. Gideonse ab214b64f2 Implement final Indevolt exceptions translations (#171635) 2026-05-21 00:35:01 +02:00
Max Michels fea673d93a Replace duplicate constants with homeassistant.const imports (#171639) 2026-05-21 00:24:05 +02:00
Max Michels 5405151112 Replace duplicate constants with homeassistant.const imports (#171637) 2026-05-21 00:12:23 +02:00
Max Michels b3c210ef24 Replace duplicate constants with homeassistant.const imports (#171638) 2026-05-21 00:11:59 +02:00
Robert Resch 5f5d74cfbd Remove requirements_test_all file (#171530) 2026-05-20 23:54:31 +02:00
Josh Gustafson c188fdcc8b Clean up arcam_fmj config flow (#171161)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:58:10 +02:00
Michael Hansen a3b43fc19b Handle multiple intents in Wyoming conversation (#171615)
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 16:48:56 -04:00
Maciej Bieniek 894a68acb6 Fix media_image_hash and validate the MIME type in the Shelly media player (#171585) 2026-05-20 22:25:30 +02:00
Kamil Breguła 30bc3fc412 Bump wled to 0.23.0 and remove backoff exception (#171622) 2026-05-20 22:16:43 +02:00
Paulus Schoutsen f07d650de8 Remove per-domain platform setup files
These 32 files (light.py, sensor.py, etc.) each only registered an
async_add_entities callback. Now that RemoteHostEntityPlatform adds
proxy entities directly to the EntityComponent, they are dead code.

Also removes the unused register_platform_callback from
SandboxEntityManager.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-16 09:30:59 -04:00
Paulus Schoutsen f494fa2909 Add RemoteClientEntityPlatform for sandbox entity interception
New class that wraps an EntityPlatform on the sandbox side to intercept
async_add_entities calls. When an integration adds entities, they are:
1. Added locally as normal
2. Registered with the host via sandbox/register_entity
3. State changes forwarded to the host
4. Method calls from the host dispatched to local entities

This replaces the post-setup iteration approach in SandboxEntityBridge
with a clean intercept at the async_add_entities boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-16 09:29:40 -04:00
Paulus Schoutsen b81a221c20 Add Hue and Picnic as tested config-entry integrations
Both pass fully through the real sandbox websocket:
- Philips Hue: 112 tests (lights, sensors, switches, scenes, device
  triggers, services, config flow, diagnostics)
- Picnic: 40 tests (sensors, services, todo)

Validates that the full config entry path works: async_setup_entry,
entity platforms, device registry, mocked HTTP APIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 21:02:12 -04:00
Paulus Schoutsen f852c33cf8 Fix host HA teardown and service fallback, expand to 33 integrations
Three fixes:
- Stop host HA explicitly after tests to cancel lingering timers that
  caused verify_cleanup teardown errors (scene, todo, etc.)
- Guard HybridServiceRegistry remote fallback: only try remote for
  services that exist in the remote cache, preventing wrong
  ServiceNotFound errors in nested service calls
- Remove manual INSTANCES.remove; let async_stop handle cleanup

31 of 33 integrations fully pass (878/880 tests, 99.8%).
The 2 remaining failures are pre-existing logbook platform issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 18:18:04 -04:00
Paulus Schoutsen 7b60f912a7 Fix schedule test hangs by detecting freezer fixture and falling back
Tests using pytest-freezer's `freezer.move_to()` hang when a live
websocket is active because time jumps break async heartbeat timers.
Detect the freezer fixture in pytest_runtest_setup and fall back to
the base plugin (no websocket) for those tests.

All 9 input helper integrations now pass (189/189 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 18:05:35 -04:00
Paulus Schoutsen da978415a8 Add sandbox test infrastructure for running core tests through websocket
New pytest plugin (hass_client.testing.conftest_sandbox) that boots a host
HA Core with websocket_api + sandbox integration, creates a sandbox auth
token, and connects a RemoteHomeAssistant to it via a live websocket. This
allows running the full HA Core input_boolean test suite (16/16 tests)
through a real sandbox round-trip.

Key pieces:
- conftest_sandbox.py: pytest plugin that patches async_test_home_assistant
  to create host + sandbox HA instances with real TCP websocket
- conftest.py: adds core/tests to sys.path for test infrastructure imports
- pyproject.toml: point homeassistant dep at local core checkout, add test deps

Usage: pytest -p hass_client.testing.conftest_sandbox \
              ../core/tests/components/input_boolean/test_init.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 17:33:54 -04:00
Paulus Schoutsen 64750386cb Add sandbox client and end-to-end tests
SandboxClient connects to HA Core via a sandbox token, fetches assigned
config entries, sets up input helper integrations locally, registers
entities back to the host, pushes state changes, and subscribes to
service call forwarding.

Three e2e tests validate: token/instance creation, state updates, and
unload cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 17:33:42 -04:00
Paulus Schoutsen 0c45d006f7 Add sandbox websocket API methods and fix RemoteHomeAssistant.__new__
Add sandbox API methods to HomeAssistantAPI for communicating with HA Core's
sandbox integration: get_entries, update_entry, register/update/remove device,
register/update/remove entity, update_state, and subscribe_service_calls.

Override __new__ on RemoteHomeAssistant to accept extra keyword arguments,
since HomeAssistant.__new__ has a strict (config_dir: str) signature that
rejects the remote_config kwarg in Python 3.14.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 17:33:34 -04:00
Paulus Schoutsen cd81c61509 WIP 2026-04-01 09:51:35 -04:00
Paulus Schoutsen 81bca02aed Expand core and helper test compatibility 2026-03-18 12:52:17 +09:00
Paulus Schoutsen cc2428c2b5 Initial hass-client compatibility harness 2026-03-18 11:56:47 +09:00
896 changed files with 54490 additions and 11584 deletions
+1 -1
View File
@@ -15,11 +15,11 @@ Dockerfile.dev linguist-language=Dockerfile
# Generated files
CODEOWNERS linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
requirements_test_all.txt linguist-generated=true
requirements_test_pre_commit.txt linguist-generated=true
script/hassfest/docker/Dockerfile linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true
+1
View File
@@ -25,6 +25,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
+55 -23
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1ad29b8fb97f5df4466be54051779a3188f094d7efb041a8ed55211eab33c5f5","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"75b8b624ba0c144fb4b28cba143d16a47c30de8afae568fa3256c6febe01a68a","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"}]}
# ___ _ _
# / _ \ | | (_)
@@ -65,7 +65,9 @@ run-name: "Check requirements (AW)"
jobs:
activation:
needs: pre_activation
needs:
- extract_pr_number
- pre_activation
# zizmor: ignore[dangerous-triggers] - workflow_run trigger is secured with role and fork validation
if: >
(needs.pre_activation.outputs.activated == 'true') && (github.event_name != 'workflow_run' || github.event.workflow_run.repository.id == github.repository_id &&
@@ -189,20 +191,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
<system>
GH_AW_PROMPT_bb296919e461941b_EOF
GH_AW_PROMPT_198418d99edc7d5b_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_bb296919e461941b_EOF'
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
<safe-output-tools>
Tools: add_comment, missing_tool, missing_data, noop
</safe-output-tools>
GH_AW_PROMPT_bb296919e461941b_EOF
GH_AW_PROMPT_198418d99edc7d5b_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -231,12 +233,12 @@ jobs:
{{/if}}
</github-context>
GH_AW_PROMPT_bb296919e461941b_EOF
GH_AW_PROMPT_198418d99edc7d5b_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
</system>
{{#runtime-import .github/workflows/check-requirements.md}}
GH_AW_PROMPT_bb296919e461941b_EOF
GH_AW_PROMPT_198418d99edc7d5b_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -314,7 +316,9 @@ jobs:
retention-days: 1
agent:
needs: activation
needs:
- activation
- extract_pr_number
runs-on: ubuntu-latest
permissions:
actions: read
@@ -385,11 +389,6 @@ jobs:
name: check-requirements-deterministic
path: /tmp/gh-aw/deterministic
run-id: ${{ github.event.workflow_run.id }}
- if: github.event.workflow_run.conclusion == 'success'
name: Extract PR number from artifact
run: |-
PR=$(python3 -c 'import json,sys;print(json.load(open("/tmp/gh-aw/deterministic/results.json"))["pr_number"])')
echo "PR_NUMBER=${PR}" >> "${GITHUB_ENV}"
- name: Configure Git credentials
env:
@@ -454,15 +453,15 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF'
{"add_comment":{"max":1,"target":"${{ env.PR_NUMBER }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF'
{"add_comment":{"max":1,"target":"${{ needs.extract_pr_number.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
{
"description_suffixes": {
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ env.PR_NUMBER }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -648,7 +647,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_d12799b4d7ffe5c2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
cat << GH_AW_MCP_CONFIG_175174907e5a28b4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
"github": {
@@ -692,7 +691,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
GH_AW_MCP_CONFIG_d12799b4d7ffe5c2_EOF
GH_AW_MCP_CONFIG_175174907e5a28b4_EOF
- name: Mount MCP servers as CLIs
id: mount-mcp-clis
continue-on-error: true
@@ -939,6 +938,7 @@ jobs:
- activation
- agent
- detection
- extract_pr_number
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1283,6 +1283,37 @@ jobs:
}
}
extract_pr_number:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
pre_activation:
runs-on: ubuntu-slim
outputs:
@@ -1321,6 +1352,7 @@ jobs:
- activation
- agent
- detection
- extract_pr_number
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
@@ -1393,7 +1425,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,files.pythonhosted.org,github.com,host.docker.internal,pip.pypa.io,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,telemetry.enterprise.githubcopilot.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ env.PR_NUMBER }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.extract_pr_number.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+138 -13
View File
@@ -19,7 +19,30 @@ tools:
safe-outputs:
add-comment:
max: 1
target: "${{ env.PR_NUMBER }}"
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
needs:
- extract_pr_number
jobs:
extract_pr_number:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: true
@@ -32,11 +55,6 @@ steps:
path: /tmp/gh-aw/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract PR number from artifact
if: github.event.workflow_run.conclusion == 'success'
run: |
PR=$(python3 -c 'import json,sys;print(json.load(open("/tmp/gh-aw/deterministic/results.json"))["pr_number"])')
echo "PR_NUMBER=${PR}" >> "${GITHUB_ENV}"
post-steps:
- name: Verify agent produced an add_comment safe-output
if: always() && github.event.workflow_run.conclusion == 'success'
@@ -80,10 +98,11 @@ The deterministic stage uploaded its results to the runner at
The JSON has this shape:
- `pr_number` — the PR being checked. The `add_comment` safe-output is
already targeted at this PR (the workflow extracted `pr_number` from
the artifact and wired it into the safe-output config), so **you do
not need to set `item_number` yourself** — just emit `add_comment`
with the rendered body.
already targeted at this PR (a pre-job extracts `pr_number` from the
artifact and the workflow wires it into the safe-output config via
`needs.extract_pr_number.outputs.pr_number`), so **you do not need to
set `item_number` yourself** — just emit `add_comment` with the
rendered body.
- `needs_agent``true` iff any package's check needs resolution.
- `packages[]` — one entry per changed package. Each entry has:
- `name`, `old_version` (`null` for a newly added package; otherwise the
@@ -161,9 +180,10 @@ Verify that the package's source repository is publicly reachable.
- Any other inconclusive result → ⚠️ with a one-line description.
If `repo_public` resolves to ❌ for a package, **also** mark that
package's `release_pipeline` cell/detail as `` (em dash) and explain
`Skipped because the source repository is not publicly accessible.` —
because the release pipeline cannot be inspected without a public repo.
package's `release_pipeline` and `async_blocking` cells/details as ``
(em dash) and explain `Skipped because the source repository is not
publicly accessible.` — neither check can be performed without a public
repo.
### Check kind: `pr_link`
@@ -239,6 +259,111 @@ host from `package.repo_url`, then apply the corresponding checklist.
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
inspected; hosting provider is not GitHub or GitLab.`
### Check kind: `async_blocking`
Verify whether the dependency performs blocking I/O inside async code
paths. Home Assistant runs on a single asyncio event loop, so a library
that exposes an `async` surface must not call blocking APIs from inside
its `async def` functions — that stalls the whole loop. A purely sync
library is fine: Home Assistant integrations are expected to wrap such
calls in an executor.
**Two modes — pick by inspecting `package.old_version`:**
- `old_version` is `null` → **new package**: review the *entire current
source tree*. Nothing about this dependency has been vetted before.
- `old_version` is a string → **version bump**: review only the *diff
between `old_version` and `new_version`*. The previous version was
already accepted, so blocking calls that were present in
`old_version` are not regressions; report only what `new_version`
introduces.
#### Step 1 — Decide whether the library exposes an async surface
Use the `github` MCP tool (for `github.com` repos) or `web-fetch`
(other hosts) on `package.repo_url`. Always inspect the tag /
ref matching `new_version` (e.g. `v{new_version}` or `{new_version}`).
- Locate the top-level package directory (usually named after the
import name, often equal or close to `package.name`).
- Check `pyproject.toml` / `setup.py` / `setup.cfg` / `README*` for
async indicators (`Framework :: AsyncIO` trove classifier, `asyncio`
/ `aiohttp` / `httpx` / `anyio` in dependencies, an async usage
example in the README).
- Grep the package source for `async def`. A handful of `async def`
entries in the public modules is enough to treat the library as
having an async surface.
If the library is **sync-only** (no `async def` in its public modules
and no async framework dependency) → ✅
`Sync-only library; Home Assistant integrations must wrap calls in an
executor.` *This verdict is the same in both modes.*
#### Step 2a — Mode: new package (`old_version` is `null`)
Inspect **every `async def` in the public modules** for blocking
patterns. Walk transitively into helpers the async functions call.
#### Step 2b — Mode: version bump (`old_version` is a string)
Fetch the diff between the two tags and review **only changed lines**:
- GitHub: `GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag}` via
the `github` MCP tool, or
`https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff`
via `web-fetch`. Try the common tag formats in order until one
resolves: `v{version}`, `{version}`, `release-{version}`.
- GitLab: `https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff`.
- Other hosts: use the project's equivalent compare URL via
`web-fetch`.
If neither tag format resolves on the host, fall back to a full review
(Step 2a) and mention in the detail that the diff was unavailable.
When reviewing the diff, only flag blocking patterns that appear in
**added lines** *inside or reachable from* an `async def`. A blocking
call that existed in `old_version` and is unchanged is not a regression
for this bump.
#### Step 3 — Blocking patterns to look for
In both modes, the patterns to flag inside `async def` bodies are:
- Sync HTTP: `requests.`, `urllib.request`, `urllib3.` direct use,
`http.client.`, sync `httpx.Client(` / `httpx.get(` (NOT the
`AsyncClient`), `pycurl`.
- `time.sleep(` (must be `await asyncio.sleep(`).
- Sync sockets: bare `socket.socket` reads/writes, `ssl.wrap_socket`,
blocking `select.select`.
- File I/O: `open(` / `pathlib.Path.read_*` / `.write_*` for
non-trivial sizes (small one-shot reads during import are
acceptable; reads/writes on the request path are not — prefer
`aiofiles` / executor).
- Sync DB drivers used directly: `sqlite3`, `psycopg2`, `pymysql`,
`pymongo` (sync client), `redis.Redis` (sync client).
- `subprocess.run` / `subprocess.call` / `os.system` (must be
`asyncio.create_subprocess_*`).
A call that is clearly dispatched to an executor
(`run_in_executor`, `asyncio.to_thread`, `anyio.to_thread.run_sync`)
does NOT count as blocking.
#### Step 4 — Verdict
- ✅ — no offending blocking pattern in the surface being reviewed
(whole tree for a new package, added lines for a bump). For a bump,
phrase the detail as `No new blocking calls introduced in
{old_version} → {new_version}.`.
- ⚠️ — blocking calls exist only in sync helpers that the async API
does not call, or only on a clearly non-hot path (e.g. one-shot
setup before the event loop is running). Cite at least one
`<file>:<line>` and explain why it is not on the hot path.
- ❌ — a blocking call is reachable from an `async def` that is part
of the public API on the request / polling path (for a bump: the
call was introduced or moved onto the hot path by this version).
Cite the offending `<file>:<line>` as a clickable link on the repo
host so the contributor can jump to it.
## Notes
- Be constructive and helpful. Reference the inspected workflow / CI
+2 -2
View File
@@ -281,7 +281,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -302,7 +302,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
extra-args: --all-files zizmor
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:python"
+3 -3
View File
@@ -132,7 +132,7 @@
"problemMatcher": []
},
{
"label": "Install all Requirements",
"label": "Install all production Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt",
"group": {
@@ -146,9 +146,9 @@
"problemMatcher": []
},
{
"label": "Install all Test Requirements",
"label": "Install all (test & production) Requirements",
"type": "shell",
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
"group": {
"kind": "build",
"isDefault": true
+1
View File
@@ -15,6 +15,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
Generated
+4 -6
View File
@@ -945,8 +945,6 @@ CLAUDE.md @home-assistant/core
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
/tests/components/kodi/ @OnFreund
/homeassistant/components/konnected/ @heythisisnate
/tests/components/konnected/ @heythisisnate
/homeassistant/components/kostal_plenticore/ @stegm
/tests/components/kostal_plenticore/ @stegm
/homeassistant/components/kraken/ @eifinger
@@ -1413,8 +1411,8 @@ CLAUDE.md @home-assistant/core
/tests/components/pushover/ @engrbm87
/homeassistant/components/pvoutput/ @frenck
/tests/components/pvoutput/ @frenck
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
/homeassistant/components/pyload/ @tr4nt0r
/tests/components/pyload/ @tr4nt0r
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
@@ -1538,8 +1536,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/saj/ @fredericvl
/homeassistant/components/samsung_infrared/ @lmaertin
/tests/components/samsung_infrared/ @lmaertin
/homeassistant/components/samsungtv/ @chemelli74 @epenet
/tests/components/samsungtv/ @chemelli74 @epenet
/homeassistant/components/samsungtv/ @chemelli74
/tests/components/samsungtv/ @chemelli74
/homeassistant/components/sanix/ @tomaszsluszniak
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/satel_integra/ @Tommatheussen
+3 -1
View File
@@ -134,7 +134,7 @@ class AuthManagerFlowManager(
"""
flow = cast(LoginFlow, flow)
if result["type"] != FlowResultType.CREATE_ENTRY:
if result["type"] is not FlowResultType.CREATE_ENTRY:
return result
# we got final result
@@ -459,6 +459,7 @@ class AuthManager:
token_type: str | None = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
credential: models.Credentials | None = None,
scopes: frozenset[str] | None = None,
) -> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
@@ -514,6 +515,7 @@ class AuthManager:
access_token_expiration,
expire_at,
credential,
scopes,
)
@callback
+7
View File
@@ -211,6 +211,7 @@ class AuthStore:
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
expire_at: float | None = None,
credential: models.Credentials | None = None,
scopes: frozenset[str] | None = None,
) -> models.RefreshToken:
"""Create a new token for a user."""
kwargs: dict[str, Any] = {
@@ -220,6 +221,7 @@ class AuthStore:
"access_token_expiration": access_token_expiration,
"expire_at": expire_at,
"credential": credential,
"scopes": scopes,
}
if client_name:
kwargs["client_name"] = client_name
@@ -475,6 +477,7 @@ class AuthStore:
else:
last_used_at = None
scopes = rt_dict.get("scopes")
token = models.RefreshToken(
id=rt_dict["id"],
user=users[rt_dict["user_id"]],
@@ -493,6 +496,7 @@ class AuthStore:
last_used_ip=rt_dict.get("last_used_ip"),
expire_at=rt_dict.get("expire_at"),
version=rt_dict.get("version"),
scopes=frozenset(scopes) if scopes else None,
)
if "credential_id" in rt_dict:
token.credential = credentials.get(rt_dict["credential_id"])
@@ -581,6 +585,9 @@ class AuthStore:
if refresh_token.credential
else None,
"version": refresh_token.version,
"scopes": sorted(refresh_token.scopes)
if refresh_token.scopes is not None
else None,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
+7
View File
@@ -129,6 +129,13 @@ class RefreshToken:
version: str | None = attr.ib(default=__version__)
# Optional set of websocket-API command scopes. ``None`` means the token
# has no scope restriction (the default for normal user/system tokens).
# When set, the token may only call commands matching an entry in the
# set: a scope ending in ``/`` matches any command whose type starts
# with the prefix; otherwise the scope is an exact ``type`` match.
scopes: frozenset[str] | None = attr.ib(default=None)
@attr.s(slots=True)
class Credentials:
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -39,7 +39,6 @@ from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
CLOUD_NEVER_EXPOSED_ENTITIES,
CONF_DESCRIPTION,
CONF_NAME,
UnitOfTemperature,
@@ -373,9 +372,6 @@ def async_get_entities(
"""Return all entities that are supported by Alexa."""
entities: list[AlexaEntity] = []
for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue
if state.domain not in ENTITY_ADAPTERS:
continue
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
@@ -226,7 +226,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Set initial options."""
# abort if entry is not loaded
if self._get_entry().state != ConfigEntryState.LOADED:
if self._get_entry().state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
hass_apis: list[SelectOptionDict] = [
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.17"]
"requirements": ["py-aosmith==1.0.18"]
}
@@ -89,7 +89,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
def supported_features(self) -> WaterHeaterEntityFeature:
"""Return the list of supported features."""
supports_vacation_mode = any(
supported_mode.mode == AOSmithOperationMode.VACATION
supported_mode.mode is AOSmithOperationMode.VACATION
for supported_mode in self.device.supported_modes
)
@@ -122,7 +122,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
@property
def is_away_mode_on(self) -> bool:
"""Return True if away mode is on."""
return self.device.status.current_mode == AOSmithOperationMode.VACATION
return self.device.status.current_mode is AOSmithOperationMode.VACATION
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new target operation mode."""
@@ -369,7 +369,7 @@ class AppleTVManager(DeviceListener):
attrs[ATTR_MODEL] = (
dev_info.raw_model
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
else model_str(dev_info.model)
)
attrs[ATTR_SW_VERSION] = dev_info.version
@@ -63,7 +63,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
# Listen to keyboard updates
atv.keyboard.listener = self
# Set initial state based on current focus state
self._update_state(atv.keyboard.text_focus_state == KeyboardFocusState.Focused)
self._update_state(atv.keyboard.text_focus_state is KeyboardFocusState.Focused)
@callback
def async_device_disconnected(self) -> None:
@@ -78,7 +78,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
This is a callback function from pyatv.interface.KeyboardListener.
"""
self._update_state(new_state == KeyboardFocusState.Focused)
self._update_state(new_state is KeyboardFocusState.Focused)
def _update_state(self, new_state: bool) -> None:
"""Update and report."""
@@ -354,7 +354,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
"name": self.atv.name,
"type": (
dev_info.raw_model
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
else model_str(dev_info.model)
),
}
@@ -441,12 +441,12 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_password()
# Figure out, depending on protocol, what kind of pairing is needed
if service.pairing == PairingRequirement.Unsupported:
if service.pairing is PairingRequirement.Unsupported:
_LOGGER.debug("%s does not support pairing", self.protocol)
return await self.async_pair_next_protocol()
if service.pairing == PairingRequirement.Disabled:
if service.pairing is PairingRequirement.Disabled:
return await self.async_step_protocol_disabled()
if service.pairing == PairingRequirement.NotNeeded:
if service.pairing is PairingRequirement.NotNeeded:
_LOGGER.debug("%s does not require pairing", self.protocol)
self.credentials[self.protocol.value] = None
return await self.async_pair_next_protocol()
@@ -457,7 +457,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
pair_args: dict[str, Any] = {}
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
pair_args["name"] = "Home Assistant"
if self.protocol == Protocol.DMAP:
if self.protocol is Protocol.DMAP:
pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
# Initiate the pairing process
@@ -139,7 +139,7 @@ class AppleTvMediaPlayer(
all_features = atv.features.all_features()
for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items():
feature_info = all_features.get(feature_name)
if feature_info and feature_info.state != FeatureState.Unsupported:
if feature_info and feature_info.state is not FeatureState.Unsupported:
self._attr_supported_features |= support_flag
# No need to schedule state update here as that will happen when the first
@@ -188,14 +188,14 @@ class AppleTvMediaPlayer(
return MediaPlayerState.OFF
if (
self._is_feature_available(FeatureName.PowerState)
and self.atv.power.power_state == PowerState.Off
and self.atv.power.power_state is PowerState.Off
):
return MediaPlayerState.OFF
if self._playing:
state = self._playing.device_state
if state in (DeviceState.Idle, DeviceState.Loading):
return MediaPlayerState.IDLE
if state == DeviceState.Playing:
if state is DeviceState.Playing:
return MediaPlayerState.PLAYING
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
return MediaPlayerState.PAUSED
@@ -446,7 +446,7 @@ class AppleTvMediaPlayer(
def shuffle(self) -> bool | None:
"""Boolean if shuffle is enabled."""
if self._playing and self._is_feature_available(FeatureName.Shuffle):
return self._playing.shuffle != ShuffleState.Off
return self._playing.shuffle is not ShuffleState.Off
return None
def _is_feature_available(self, feature: FeatureName) -> bool:
@@ -506,7 +506,7 @@ class AppleTvMediaPlayer(
and (self._is_feature_available(FeatureName.TurnOff))
and (
not self._is_feature_available(FeatureName.PowerState)
or self.atv.power.power_state == PowerState.On
or self.atv.power.power_state is PowerState.On
)
):
await self.atv.power.turn_off()
@@ -59,7 +59,7 @@ def _check_keyboard_focus(atv: AppleTVInterface) -> None:
translation_domain=DOMAIN,
translation_key="keyboard_not_available",
) from err
if focus_state != KeyboardFocusState.Focused:
if focus_state is not KeyboardFocusState.Focused:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="keyboard_not_focused",
@@ -1,9 +1,11 @@
"""Config flow to configure the Arcam FMJ component."""
import socket
from typing import Any
from urllib.parse import urlparse
from arcam.fmj.client import Client, ConnectionFailed
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
@@ -14,6 +16,13 @@ from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceIn
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
STEP_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle config flow."""
@@ -29,26 +38,28 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
async def _async_check_and_create(self, host: str, port: int) -> ConfigFlowResult:
async def _async_try_connect(self, host: str, port: int) -> dict[str, str]:
"""Verify the device is reachable; return errors keyed by reason."""
client = Client(host, port)
try:
await client.start()
except ConnectionFailed:
return self.async_abort(reason="cannot_connect")
except socket.gaierror:
return {"base": "invalid_host"}
except TimeoutError:
return {"base": "timeout_connect"}
except ConnectionRefusedError:
return {"base": "connection_refused"}
except ConnectionFailed, OSError:
return {"base": "cannot_connect"}
finally:
await client.stop()
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({host})",
data={CONF_HOST: host, CONF_PORT: port},
)
return {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
uuid = await get_uniqueid_from_host(
async_get_clientsession(self.hass), user_input[CONF_HOST]
@@ -58,17 +69,56 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
user_input[CONF_HOST], user_input[CONF_PORT], uuid
)
return await self._async_check_and_create(
errors = await self._async_try_connect(
user_input[CONF_HOST], user_input[CONF_PORT]
)
if not errors:
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})",
data={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
},
)
fields = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
schema = STEP_DATA_SCHEMA
if user_input is not None:
schema = self.add_suggested_values_to_schema(schema, user_input)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
uuid = await get_uniqueid_from_host(
async_get_clientsession(self.hass), user_input[CONF_HOST]
)
if uuid:
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_mismatch()
errors = await self._async_try_connect(
user_input[CONF_HOST], user_input[CONF_PORT]
)
if not errors:
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
},
)
schema = self.add_suggested_values_to_schema(
STEP_DATA_SCHEMA, user_input or reconfigure_entry.data
)
return self.async_show_form(
step_id="user", data_schema=vol.Schema(fields), errors=errors
step_id="reconfigure", data_schema=schema, errors=errors
)
async def async_step_confirm(
@@ -79,7 +129,10 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = placeholders
if user_input is not None:
return await self._async_check_and_create(self.host, self.port)
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({self.host})",
data={CONF_HOST: self.host, CONF_PORT: self.port},
)
return self.async_show_form(
step_id="confirm", description_placeholders=placeholders
@@ -97,6 +150,9 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
await self._async_set_unique_id_and_update(host, port, uuid)
if await self._async_try_connect(host, port):
return self.async_abort(reason="cannot_connect")
self.host = host
self.port = DEFAULT_PORT
self.port = port
return await self.async_step_confirm()
@@ -263,9 +263,9 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
def media_channel(self) -> str | None:
"""Channel currently playing."""
source = self._state.get_source()
if source == SourceCodes.DAB:
if source is SourceCodes.DAB:
value = self._state.get_dab_station()
elif source == SourceCodes.FM:
elif source is SourceCodes.FM:
value = self._state.get_rds_information()
else:
value = None
@@ -274,7 +274,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
@property
def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
if self._state.get_source() == SourceCodes.DAB:
if self._state.get_source() is SourceCodes.DAB:
value = self._state.get_dls_pdt()
else:
value = None
@@ -3,13 +3,28 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"connection_refused": "Host refused connection",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
},
"flow_title": "{host}",
"step": {
"confirm": {
"description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"description": "[%key:component::arcam_fmj::config::step::user::description%]"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -1355,7 +1355,7 @@ class PipelineRun:
) -> bool:
"""Return true if all targeted entities were in the same area as the device."""
if (
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
intent_response.response_type is not intent.IntentResponseType.ACTION_DONE
or not intent_response.matched_states
):
return False
+4 -4
View File
@@ -251,12 +251,12 @@ class AuthProvidersView(HomeAssistantView):
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
return {
key: val for key, val in result.items() if key not in ("result", "data")
}
if result["type"] != data_entry_flow.FlowResultType.FORM:
if result["type"] is not data_entry_flow.FlowResultType.FORM:
return result # type: ignore[return-value]
data = dict(result)
@@ -289,11 +289,11 @@ class LoginFlowBaseView(HomeAssistantView):
result: AuthFlowResult,
) -> web.Response:
"""Convert the flow result to a response."""
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200.
# We need to manually log failed login attempts.
if (
result["type"] == data_entry_flow.FlowResultType.FORM
result["type"] is data_entry_flow.FlowResultType.FORM
and (errors := result.get("errors"))
and errors.get("base")
in (
@@ -142,9 +142,9 @@ def websocket_depose_mfa(
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
return dict(result)
if result["type"] != data_entry_flow.FlowResultType.FORM:
if result["type"] is not data_entry_flow.FlowResultType.FORM:
return result # type: ignore[return-value]
data = dict(result)
@@ -20,7 +20,7 @@
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.11",
"dbus-fast==5.0.0",
"habluetooth==6.1.0"
"dbus-fast==5.0.3",
"habluetooth==6.4.0"
]
}
@@ -273,7 +273,7 @@ async def ws_subscribe_scanner_details(
def _async_registration_changed(registration: HaScannerRegistration) -> None:
added_event = HaScannerRegistrationEvent.ADDED
event_type = "add" if registration.event == added_event else "remove"
event_type = "add" if registration.event is added_event else "remove"
_async_event_message({event_type: [registration.scanner.details]})
manager = _get_manager(hass)
@@ -185,7 +185,6 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
except BSBLANError as err:
# pylint: disable-next=home-assistant-exception-message-with-translation
raise HomeAssistantError(
"An error occurred while updating the BSBLAN device",
translation_domain=DOMAIN,
translation_key="set_data_error",
) from err
+4 -1
View File
@@ -158,7 +158,10 @@ def process_service_info(
)
# If payload is encrypted and the bindkey is not verified then we need to reauth
if data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified:
if (
data.encryption_scheme is not EncryptionScheme.NONE
and not data.bindkey_verified
):
entry.async_start_reauth(hass, data={"device": data})
return update
@@ -59,7 +59,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = discovery_info
self._discovered_device = device
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
return await self.async_step_bluetooth_confirm()
@@ -125,7 +125,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = discovery.discovery_info
self._discovered_device = discovery.device
if discovery.device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if discovery.device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
return self._async_get_or_create_entry()
@@ -164,7 +164,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = device.last_service_info
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
# Otherwise there wasn't actually encryption so abort
@@ -45,6 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
# on some other unexpected server response.
_LOGGER.warning("Unexpected CalDAV server response: %s", err)
return False
except requests.Timeout as err:
raise ConfigEntryNotReady("Timeout connecting to CalDAV server") from err
except requests.ConnectionError as err:
raise ConfigEntryNotReady("Connection error from CalDAV server") from err
except DAVError as err:
+8 -2
View File
@@ -38,6 +38,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CalDavConfigEntry
from .api import async_get_calendars
from .const import TIMEOUT
from .coordinator import CalDavUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -91,7 +92,12 @@ async def async_setup_platform(
days = config[CONF_DAYS]
client = caldav.DAVClient(
url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL]
url,
None,
username,
password,
ssl_verify_cert=config[CONF_VERIFY_SSL],
timeout=TIMEOUT,
)
calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT)
@@ -231,7 +237,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
await self.hass.async_add_executor_job(
partial(self.coordinator.calendar.add_event, **item_data),
)
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@callback
+5 -5
View File
@@ -138,7 +138,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
async def async_update_todo_item(self, item: TodoItem) -> None:
@@ -150,7 +150,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
except NotFoundError as err:
raise HomeAssistantError(f"Could not find To-do item {uid}") from err
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
vtodo = todo.icalendar_component # type: ignore[attr-defined]
vtodo["SUMMARY"] = item.summary or ""
@@ -174,7 +174,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
async def async_delete_todo_items(self, uids: list[str]) -> None:
@@ -188,14 +188,14 @@ class WebDavTodoListEntity(TodoListEntity):
items = await asyncio.gather(*tasks)
except NotFoundError as err:
raise HomeAssistantError("Could not find To-do item") from err
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
# Run serially as some CalDAV servers do not support concurrent modifications
for item in items:
try:
await self.hass.async_add_executor_job(item.delete)
except (requests.ConnectionError, DAVError) as err:
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV delete error: {err}") from err
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
@@ -1,12 +1,11 @@
"""Config flow for the Cert Expiry platform."""
from collections.abc import Mapping
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DEFAULT_PORT, DOMAIN
@@ -19,8 +18,6 @@ from .errors import (
)
from .helper import get_cert_expiry_timestamp
_LOGGER = logging.getLogger(__name__)
class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -75,9 +72,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
title=title,
data={CONF_HOST: host, CONF_PORT: port},
)
if self.source == SOURCE_IMPORT:
_LOGGER.error("Config import failed for %s", user_input[CONF_HOST])
return self.async_abort(reason="import_failed")
else:
user_input = {}
user_input[CONF_HOST] = ""
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
@@ -32,7 +32,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_should_expose,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import Event, HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, start
@@ -275,9 +274,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
def _should_expose_legacy(self, entity_id: str) -> bool:
"""If an entity should be exposed."""
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
entity_configs = self._prefs.alexa_entity_configs
entity_config = entity_configs.get(entity_id, {})
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
@@ -308,8 +304,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
"""If an entity should be exposed."""
entity_filter: EntityFilter = self._config[CONF_FILTER]
if not entity_filter.empty_filter:
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return entity_filter(entity_id)
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
@@ -22,7 +22,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_should_expose,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import (
CoreState,
Event,
@@ -282,9 +281,6 @@ class CloudGoogleConfig(AbstractConfig):
def _should_expose_legacy(self, entity_id: str) -> bool:
"""If an entity ID should be exposed."""
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
entity_configs = self._prefs.google_entity_configs
entity_config = entity_configs.get(entity_id, {})
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
@@ -316,8 +312,6 @@ class CloudGoogleConfig(AbstractConfig):
"""If an entity should be exposed."""
entity_filter: EntityFilter = self._config[CONF_FILTER]
if not entity_filter.empty_filter:
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return entity_filter(entity_id)
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
+2 -5
View File
@@ -29,7 +29,6 @@ from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.system_health import get_info as get_system_health_info
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -973,7 +972,7 @@ async def google_assistant_get(
return
entity = google_helpers.GoogleEntity(hass, gconf, state)
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported():
if not entity.is_supported():
connection.send_error(
msg["id"],
websocket_api.ERR_NOT_SUPPORTED,
@@ -1075,9 +1074,7 @@ async def alexa_get(
"""Get data for a single alexa entity."""
entity_id: str = msg["entity_id"]
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
hass, entity_id
):
if not entity_supported_by_alexa(hass, entity_id):
connection.send_error(
msg["id"],
websocket_api.ERR_NOT_SUPPORTED,
@@ -110,7 +110,7 @@ class ComelitAlarmEntity(
@property
def available(self) -> bool:
"""Return True if alarm is available."""
if self._area.human_status == AlarmAreaState.UNKNOWN:
if self._area.human_status is AlarmAreaState.UNKNOWN:
return False
return super().available
@@ -124,7 +124,7 @@ class ComelitAlarmEntity(
self._area.human_status,
self._area.armed,
)
if self._area.human_status == AlarmAreaState.ARMED:
if self._area.human_status is AlarmAreaState.ARMED:
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
return AlarmControlPanelState.ARMED_AWAY
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
@@ -43,7 +43,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
device_class=BinarySensorDeviceClass.PROBLEM,
is_on_fn=lambda obj: cast(ComelitVedoAreaObject, obj).anomaly,
available_fn=lambda obj: (
cast(ComelitVedoAreaObject, obj).human_status != AlarmAreaState.UNKNOWN
cast(ComelitVedoAreaObject, obj).human_status is not AlarmAreaState.UNKNOWN
),
),
ComelitBinarySensorEntityDescription(
@@ -67,7 +67,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
object_type=ALARM_ZONE,
device_class=BinarySensorDeviceClass.PROBLEM,
is_on_fn=lambda obj: (
cast(ComelitVedoZoneObject, obj).human_status == AlarmZoneState.FAULTY
cast(ComelitVedoZoneObject, obj).human_status is AlarmZoneState.FAULTY
),
available_fn=lambda obj: (
cast(ComelitVedoZoneObject, obj).human_status
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
translation_placeholders={"error": repr(err)},
) from err
except aiocomelit_exceptions.CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
+2 -2
View File
@@ -166,12 +166,12 @@ class ComelitVedoSensorEntity(
@property
def available(self) -> bool:
"""Sensor availability."""
return self._zone_object.human_status != AlarmZoneState.UNAVAILABLE
return self._zone_object.human_status is not AlarmZoneState.UNAVAILABLE
@property
def native_value(self) -> StateType:
"""Sensor value."""
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
if (status := self._zone_object.human_status) is AlarmZoneState.UNKNOWN:
return None
return cast(str, status.value)
@@ -148,7 +148,7 @@ def _prepare_config_flow_result_json(
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
) -> dict[str, Any]:
"""Convert result to JSON."""
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
return prepare_result_json(result)
data = {key: val for key, val in result.items() if key not in ("data", "context")}
@@ -646,7 +646,7 @@ class DefaultAgent(ConversationEntity):
cache_value = self._intent_cache.get(cache_key)
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.EXPOSED_ENTITIES_ONLY
cache_value.stage is IntentMatchingStage.EXPOSED_ENTITIES_ONLY
):
_LOGGER.debug("Got cached result for exposed entities")
return cache_value.result
@@ -686,7 +686,7 @@ class DefaultAgent(ConversationEntity):
skip_unexposed_entities_match = False
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.UNEXPOSED_ENTITIES
cache_value.stage is IntentMatchingStage.UNEXPOSED_ENTITIES
):
_LOGGER.debug("Got cached result for all entities")
return cache_value.result
@@ -731,7 +731,7 @@ class DefaultAgent(ConversationEntity):
skip_unknown_names = False
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES
cache_value.stage is IntentMatchingStage.UNKNOWN_NAMES
):
_LOGGER.debug("Got cached result for unknown names")
return cache_value.result
@@ -1447,7 +1447,7 @@ class DefaultAgent(ConversationEntity):
response = await self._async_process_intent_result(result, user_input, chat_log)
if (
response.response_type == intent.IntentResponseType.ERROR
response.response_type is intent.IntentResponseType.ERROR
and response.error_code
not in (
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
@@ -1546,7 +1546,7 @@ def _get_match_error_response(
# device_class only
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
if (reason == intent.MatchFailedReason.DOMAIN) and constraints.domains:
if (reason is intent.MatchFailedReason.DOMAIN) and constraints.domains:
domain = next(iter(constraints.domains)) # first domain
if constraints.area_name:
# domain in area
@@ -1565,7 +1565,7 @@ def _get_match_error_response(
# domain only
return ErrorKey.NO_DOMAIN, {"domain": domain}
if reason == intent.MatchFailedReason.DUPLICATE_NAME:
if reason is intent.MatchFailedReason.DUPLICATE_NAME:
if constraints.floor_name:
# duplicate on floor
return ErrorKey.DUPLICATE_ENTITIES_IN_FLOOR, {
@@ -1582,26 +1582,26 @@ def _get_match_error_response(
return ErrorKey.DUPLICATE_ENTITIES, {"entity": result.no_match_name}
if reason == intent.MatchFailedReason.INVALID_AREA:
if reason is intent.MatchFailedReason.INVALID_AREA:
# Invalid area name
return ErrorKey.NO_AREA, {"area": result.no_match_name}
if reason == intent.MatchFailedReason.INVALID_FLOOR:
if reason is intent.MatchFailedReason.INVALID_FLOOR:
# Invalid floor name
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
if reason == intent.MatchFailedReason.FEATURE:
if reason is intent.MatchFailedReason.FEATURE:
# Feature not supported by entity
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
if reason == intent.MatchFailedReason.STATE:
if reason is intent.MatchFailedReason.STATE:
# Entity is not in correct state
assert constraints.states
state = next(iter(constraints.states))
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
if reason == intent.MatchFailedReason.ASSISTANT:
if reason is intent.MatchFailedReason.ASSISTANT:
# Not exposed
if constraints.name:
if constraints.area_name:
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+2 -2
View File
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DeconzConfigEntry
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
from .const import ATTR_OFFSET, ATTR_VALVE
from .entity import DeconzDevice
from .hub import DeconzHub
-2
View File
@@ -43,8 +43,6 @@ PLATFORMS = [
]
ATTR_DARK = "dark"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCKED = "locked"
ATTR_OFFSET = "offset"
ATTR_ON = "on"
ATTR_VALVE = "valve"
@@ -80,7 +80,7 @@ async def async_validate_device_automation_config(
# the checks below which look for a config entry matching the device automation
# domain
if (
automation_type == DeviceAutomationType.ACTION
automation_type is DeviceAutomationType.ACTION
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
):
# Pass the unvalidated config to avoid mutating the raw config twice
@@ -1,11 +1,18 @@
"""Provide functionality to keep track of devices."""
import asyncio
from typing import Any
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from .config_entry import ( # noqa: F401
DATA_COMPONENT,
BaseScannerEntity,
BaseTrackerEntity,
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -33,6 +40,8 @@ from .const import ( # noqa: F401
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
)
@@ -44,7 +53,9 @@ from .legacy import ( # noqa: F401
SOURCE_TYPES,
AsyncSeeCallback,
DeviceScanner,
DeviceTracker,
SeeCallback,
async_create_platform_type,
async_setup_integration as async_setup_legacy_integration,
see,
)
@@ -57,5 +68,43 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
async_setup_legacy_integration(hass, config)
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
component.config = {}
component.register_shutdown()
# The tracker is loaded in the async_setup_legacy_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None:
return
if platform.type != PLATFORM_TYPE_LEGACY:
await component.async_setup_platform(p_type, {}, info)
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
async_setup_legacy_integration(hass, config, tracker_future),
eager_start=True,
)
return True
@@ -37,11 +37,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.event import (
async_track_time_interval,
async_track_utc_time_change,
@@ -204,40 +200,7 @@ def see(
hass.services.call(DOMAIN, SERVICE_SEE, data)
@callback
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
"""Set up the legacy integration."""
# The tracker is loaded in the _async_setup_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
_async_setup_integration(hass, config, tracker_future), eager_start=True
)
async def _async_setup_integration(
async def async_setup_integration(
hass: HomeAssistant,
config: ConfigType,
tracker_future: asyncio.Future[DeviceTracker],
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==3.2.0",
"aiodiscover==3.2.3",
"cached-ipaddress==1.0.1"
]
}
@@ -261,7 +261,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
except KeyError, ValueError:
bootid = None
if change == ssdp.SsdpChange.UPDATE:
if change is ssdp.SsdpChange.UPDATE:
# This is an announcement that bootid is about to change
if self._bootid is not None and self._bootid == bootid:
# Store the new value (because our old value matches) so that we
@@ -281,7 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
await self._device_disconnect()
self._bootid = bootid
if change == ssdp.SsdpChange.BYEBYE:
if change is ssdp.SsdpChange.BYEBYE:
# Device is going away
if self._device:
# Disconnect from gone device
@@ -290,7 +290,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
self._ssdp_connect_failed = False
if (
change == ssdp.SsdpChange.ALIVE
change is ssdp.SsdpChange.ALIVE
and not self._device
and not self._ssdp_connect_failed
):
@@ -718,7 +718,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
# If already playing, or don't want to autoplay, no need to call Play
autoplay = extra.get("autoplay", True)
if self._device.transport_state == TransportState.PLAYING or not autoplay:
if self._device.transport_state is TransportState.PLAYING or not autoplay:
return
# Play it
@@ -748,7 +748,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
if not (play_mode := self._device.play_mode):
return None
if play_mode == PlayMode.VENDOR_DEFINED:
if play_mode is PlayMode.VENDOR_DEFINED:
return None
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
@@ -782,10 +782,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
if not (play_mode := self._device.play_mode):
return None
if play_mode == PlayMode.VENDOR_DEFINED:
if play_mode is PlayMode.VENDOR_DEFINED:
return None
if play_mode == PlayMode.REPEAT_ONE:
if play_mode is PlayMode.REPEAT_ONE:
return RepeatMode.ONE
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
+3 -3
View File
@@ -236,7 +236,7 @@ class DmsDeviceSource:
except KeyError, ValueError:
bootid = None
if change == ssdp.SsdpChange.UPDATE:
if change is ssdp.SsdpChange.UPDATE:
# This is an announcement that bootid is about to change
if self._bootid is not None and self._bootid == bootid:
# Store the new value (because our old value matches) so that we
@@ -258,7 +258,7 @@ class DmsDeviceSource:
await self.device_disconnect()
self._bootid = bootid
if change == ssdp.SsdpChange.BYEBYE:
if change is ssdp.SsdpChange.BYEBYE:
# Device is going away
if self._device:
# Disconnect from gone device
@@ -267,7 +267,7 @@ class DmsDeviceSource:
self._ssdp_connect_failed = False
if (
change == ssdp.SsdpChange.ALIVE
change is ssdp.SsdpChange.ALIVE
and not self._device
and not self._ssdp_connect_failed
):
+1 -6
View File
@@ -6,7 +6,6 @@ import logging
import aiodns
from aiodns.error import DNSError
from pycares import AresError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
@@ -78,11 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
) from err
errors = [
result
for result in results
if isinstance(
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
)
result for result in results if isinstance(result, (TimeoutError, DNSError))
]
if errors and len(errors) == len(results):
await _close_resolvers()
@@ -8,6 +8,12 @@
},
"step": {
"user": {
"data": {
"download_dir": "Download directory"
},
"data_description": {
"download_dir": "The directory where downloaded files will be stored. This can be an absolute path or a path relative to the Home Assistant configuration directory."
},
"description": "Select a location to get to store downloads. The setup will check if the directory exists."
}
}
@@ -15,6 +15,8 @@ _LOGGER = logging.getLogger(__name__)
class EcobeeBaseEntity(Entity):
"""Base methods for Ecobee entities."""
_attr_has_entity_name = True
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
"""Initiate base methods for Ecobee entities."""
self.data = data
@@ -1,4 +1,11 @@
{
"entity": {
"number": {
"fan_min_on_time": {
"default": "mdi:fan-clock"
}
}
},
"services": {
"create_vacation": {
"service": "mdi:umbrella-beach"
@@ -24,7 +24,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
"""Implement the notification entity for the Ecobee thermostat."""
_attr_name = None
_attr_has_entity_name = True
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
"""Initialize the thermostat."""
+40 -2
View File
@@ -74,6 +74,10 @@ async def async_setup_entry(
)
)
entities.extend(
EcobeeFanMinOnTime(data, index) for index in range(len(data.ecobee.thermostats))
)
async_add_entities(entities, True)
@@ -86,7 +90,6 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
_attr_native_max_value = 60
_attr_native_step = 5
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_has_entity_name = True
def __init__(
self,
@@ -130,7 +133,6 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
"""
_attr_device_class = NumberDeviceClass.TEMPERATURE
_attr_has_entity_name = True
_attr_icon = "mdi:thermometer-off"
_attr_mode = NumberMode.BOX
_attr_native_min_value = -25
@@ -165,3 +167,39 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
"""Set new compressor minimum temperature."""
self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value)
self.update_without_throttle = True
class EcobeeFanMinOnTime(EcobeeBaseEntity, NumberEntity):
"""Minimum minutes per hour that the fan must run on an ecobee thermostat."""
_attr_native_min_value = 0
_attr_native_max_value = 60
_attr_native_step = 5
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_translation_key = "fan_min_on_time"
def __init__(
self,
data: EcobeeData,
thermostat_index: int,
) -> None:
"""Initialize ecobee fan minimum on time."""
super().__init__(data, thermostat_index)
self._attr_unique_id = f"{self.base_unique_id}_fan_min_on_time"
self.update_without_throttle = False
async def async_update(self) -> None:
"""Get the latest state from the thermostat."""
if self.update_without_throttle:
await self.data.update(no_throttle=True)
self.update_without_throttle = False
else:
await self.data.update()
self._attr_native_value = self.thermostat["settings"]["fanMinOnTime"]
def set_native_value(self, value: float) -> None:
"""Set new fan minimum on time value."""
step = self._attr_native_step
aligned_value = int(round(value / step) * step)
self.data.ecobee.set_fan_min_on_time(self.thermostat_index, aligned_value)
self.update_without_throttle = True
@@ -37,6 +37,9 @@
"compressor_protection_min_temp": {
"name": "Compressor minimum temperature"
},
"fan_min_on_time": {
"name": "Fan minimum on time"
},
"ventilator_min_type_away": {
"name": "Ventilator minimum time away"
},
@@ -53,7 +53,6 @@ async def async_setup_entry(
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
"""Represent 20 min timer for an ecobee thermostat with ventilator."""
_attr_has_entity_name = True
_attr_name = "Ventilator 20m Timer"
def __init__(
@@ -104,7 +103,6 @@ class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
class EcobeeSwitchAuxHeatOnly(EcobeeBaseEntity, SwitchEntity):
"""Representation of a aux_heat_only ecobee switch."""
_attr_has_entity_name = True
_attr_translation_key = "aux_heat_only"
def __init__(
@@ -31,6 +31,7 @@ class EkeyEvent(EventEntity):
_attr_device_class = EventDeviceClass.BUTTON
_attr_event_types = ["event happened"]
_attr_has_entity_name = True
def __init__(
self,
+3 -3
View File
@@ -150,9 +150,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
raw_pcm_compatible = (
metadata.codec == AudioCodecs.PCM
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
and metadata.channel == AudioChannels.CHANNEL_MONO
and metadata.bit_rate == AudioBitRates.BITRATE_16
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
and metadata.channel is AudioChannels.CHANNEL_MONO
and metadata.bit_rate is AudioBitRates.BITRATE_16
)
if raw_pcm_compatible:
file_format = "pcm_s16le_16"
@@ -50,5 +50,5 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
# Zone in NORMAL state is OFF; any other state is ON
self._attr_is_on = bool(
self._element.logical_status != ZoneLogicalStatus.NORMAL
self._element.logical_status is not ZoneLogicalStatus.NORMAL
)
+2 -2
View File
@@ -104,7 +104,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
ThermostatMode.EMERGENCY_HEAT,
):
return self._element.heat_setpoint
if self._element.mode == ThermostatMode.COOL:
if self._element.mode is ThermostatMode.COOL:
return self._element.cool_setpoint
return None
@@ -162,6 +162,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode]
if (
self._attr_hvac_mode == HVACMode.OFF
and self._element.fan == ThermostatFan.ON
and self._element.fan is ThermostatFan.ON
):
self._attr_hvac_mode = HVACMode.FAN_ONLY
+1 -1
View File
@@ -56,7 +56,7 @@ class ElkNumberSetting(ElkAttachedEntity, NumberEntity):
def __init__(self, element: Setting, elk: Any, elk_data: ELKM1Data) -> None:
"""Initialize the number setting."""
super().__init__(element, elk, elk_data)
if element.value_format == SettingFormat.TIMER:
if element.value_format is SettingFormat.TIMER:
self._attr_device_class = NumberDeviceClass.DURATION
self._attr_native_unit_of_measurement = UnitOfTime.SECONDS
+6 -6
View File
@@ -75,7 +75,7 @@ async def async_setup_entry(
for setting in elk.settings:
setting = cast(Setting, setting)
domain = (
"time" if setting.value_format == SettingFormat.TIME_OF_DAY else "number"
"time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
)
orig_unique_id = generate_unique_id(elk_data.prefix, setting)
@@ -288,7 +288,7 @@ class ElkZone(ElkSensor):
@property
def temperature_unit(self) -> str | None:
"""Return the temperature unit."""
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
return self._temperature_unit
return None
@@ -305,18 +305,18 @@ class ElkZone(ElkSensor):
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
return self._temperature_unit
if self._element.definition == ZoneType.ANALOG_ZONE:
if self._element.definition is ZoneType.ANALOG_ZONE:
return UnitOfElectricPotential.VOLT
return None
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
self._attr_native_value = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPERATURE
)
elif self._element.definition == ZoneType.ANALOG_ZONE:
elif self._element.definition is ZoneType.ANALOG_ZONE:
self._attr_native_value = f"{self._element.voltage}"
else:
self._attr_native_value = pretty_const(self._element.logical_status.name)
+1 -1
View File
@@ -66,7 +66,7 @@ class ElkThermostatEMHeat(ElkEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Get the current emergency heat status."""
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
return self._element.mode is ThermostatMode.EMERGENCY_HEAT
def _elk_set(self, mode: ThermostatMode) -> None:
"""Set the thermostat mode."""
+1 -1
View File
@@ -30,7 +30,7 @@ async def async_setup_entry(
time_settings = [
setting
for setting in cast(list[Setting], elk.settings)
if setting.value_format == SettingFormat.TIME_OF_DAY
if setting.value_format is SettingFormat.TIME_OF_DAY
]
create_elk_entities(
@@ -96,7 +96,7 @@ def __get_coordinator(call: ServiceCall) -> EnergyZeroDataUpdateCoordinator:
"config_entry": entry_id,
},
)
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unloaded_config_entry",
@@ -125,7 +125,7 @@ async def __get_prices(
data: Electricity | Gas
if price_type == PriceType.GAS:
if price_type is PriceType.GAS:
data = await coordinator.energyzero.get_gas_prices_legacy(
start_date=start,
end_date=end,
@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==45.0.4",
"aioesphomeapi==45.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.7.3"
"bleak-esphome==3.8.1"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
async def _async_finish_startup(hass: HomeAssistant) -> None:
"""Run this only when HA has finished its startup."""
if entry.state == ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.LOADED:
await coordinator.async_refresh()
else:
await coordinator.async_config_entry_first_refresh()
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
@@ -284,7 +284,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Manage initial options."""
entry = self._get_entry()
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
self.client = entry.runtime_data
+2 -2
View File
@@ -509,14 +509,12 @@ FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
icon="mdi:battery",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
)
FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
key="devices/battery_level",
translation_key="battery_level",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
)
@@ -654,6 +652,7 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(
self,
@@ -713,6 +712,7 @@ class FitbitBatteryLevelSensor(
"""Implementation of a Fitbit battery level sensor."""
entity_description: FitbitSensorEntityDescription
_attr_has_entity_name = True
_attr_attribution = ATTRIBUTION
def __init__(
-1
View File
@@ -10,7 +10,6 @@ from .coordinator import FloDeviceDataUpdateCoordinator
class FloEntity(Entity):
"""A base class for Flo entities."""
_attr_force_update = False
_attr_has_entity_name = True
_attr_should_poll = False
+1 -1
View File
@@ -109,7 +109,7 @@ def setup_service(hass: HomeAssistant) -> None:
entry: FlumeConfigEntry | None = hass.config_entries.async_get_entry(entry_id)
if not entry:
raise ValueError(f"Invalid config entry: {entry_id}")
if not entry.state == ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
raise ValueError(f"Config entry not loaded: {entry_id}")
return {
"notifications": entry.runtime_data.notifications_coordinator.notifications # type: ignore[dict-item]
@@ -136,7 +136,7 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
ConfigEntryState.SETUP_IN_PROGRESS,
ConfigEntryState.NOT_LOADED,
)
) or entry.state == ConfigEntryState.SETUP_RETRY:
) or entry.state is ConfigEntryState.SETUP_RETRY:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
else:
async_dispatcher_send(
+1 -1
View File
@@ -51,7 +51,7 @@ async def async_setup_entry(
entry.data.get(CONF_NAME, entry.title)
base_unique_id = entry.unique_id or entry.entry_id
if device.device_type == DeviceType.Switch:
if device.device_type is DeviceType.Switch:
entities.append(FluxPowerStateSelect(coordinator.device, entry))
if device.operating_modes:
entities.append(
+1 -1
View File
@@ -32,7 +32,7 @@ async def async_setup_entry(
entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = []
base_unique_id = entry.unique_id or entry.entry_id
if coordinator.device.device_type == DeviceType.Switch:
if coordinator.device.device_type is DeviceType.Switch:
entities.append(FluxSwitch(coordinator, base_unique_id, None))
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
+9 -1
View File
@@ -9,7 +9,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfDataRate,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -45,6 +50,7 @@ CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
translation_key="missed",
native_unit_of_measurement="calls",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -53,6 +59,7 @@ DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
key="partition_free_space",
translation_key="partition_free_space",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -80,6 +87,7 @@ async def async_setup_entry(
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
for sensor_id, sensor_name in router.sensors_temperature_names.items()
@@ -6,7 +6,6 @@ from typing import Any
from freebox_api.exceptions import InsufficientPermissionsError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -19,7 +18,6 @@ SWITCH_DESCRIPTIONS = [
SwitchEntityDescription(
key="wifi",
translation_key="wifi",
entity_category=EntityCategory.CONFIG,
)
]
+2
View File
@@ -438,6 +438,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity):
self._attr_is_on = turn_on
# pylint: disable-next=home-assistant-missing-has-entity-name
class FritzBoxPortSwitch(FritzBoxBaseSwitch):
"""Defines a FRITZ!Box Tools PortForward switch."""
@@ -604,6 +605,7 @@ class FritzBoxProfileSwitch(FritzBoxBaseCoordinatorSwitch):
self.async_write_ha_state()
# pylint: disable-next=home-assistant-missing-has-entity-name
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
"""Defines a FRITZ!Box Tools Wifi switch."""
+3 -3
View File
@@ -212,7 +212,7 @@ class FroniusSolarNet:
inverter_info=_inverter_info,
config_entry=self.config_entry,
)
if self.config_entry.state == ConfigEntryState.LOADED:
if self.config_entry.state is ConfigEntryState.LOADED:
await _coordinator.async_refresh()
else:
await _coordinator.async_config_entry_first_refresh()
@@ -220,7 +220,7 @@ class FroniusSolarNet:
# Only for re-scans. Initial setup adds entities
# through sensor.async_setup_entry
if self.config_entry.state == ConfigEntryState.LOADED:
if self.config_entry.state is ConfigEntryState.LOADED:
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
_LOGGER.debug(
@@ -235,7 +235,7 @@ class FroniusSolarNet:
try:
_inverter_info = await self.fronius.inverter_info()
except FroniusError as err:
if self.config_entry.state == ConfigEntryState.LOADED:
if self.config_entry.state is ConfigEntryState.LOADED:
# During a re-scan we will attempt again as per schedule.
_LOGGER.debug("Re-scan failed for %s", self.host)
return inverter_infos
@@ -42,7 +42,7 @@ async def _collect_coordinators(
raise HomeAssistantError(f"Device '{target}' not found in device registry")
coordinators = list[FullyKioskDataUpdateCoordinator]()
for config_entry in config_entries:
if config_entry.state != ConfigEntryState.LOADED:
if config_entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError(f"{config_entry.title} is not loaded")
coordinators.append(config_entry.runtime_data)
return coordinators
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
@@ -75,7 +75,7 @@ async def async_setup_entry(
mfg_data = await async_get_manufacturer_data({address})
product_type = mfg_data[address].product_type
if product_type == ProductType.UNKNOWN:
if product_type is ProductType.UNKNOWN:
raise ConfigEntryNotReady("Unable to find product type")
client = Client(get_connection(hass, address), product_type)
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioghost"],
"quality_scale": "gold",
"requirements": ["aioghost==0.4.0"]
"requirements": ["aioghost==0.4.16"]
}
+3 -3
View File
@@ -143,7 +143,7 @@ def _get_entity_descriptions(
local_sync = True
if (
search := data.get(CONF_SEARCH)
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
) or calendar_item.access_role is AccessRole.FREE_BUSY_READER:
read_only = True
local_sync = False
entity_description = GoogleCalendarEntityDescription(
@@ -386,14 +386,14 @@ class GoogleCalendarEntity(
"""Return True if the event is visible and not declined."""
if any(
attendee.is_self and attendee.response_status == ResponseStatus.DECLINED
attendee.is_self and attendee.response_status is ResponseStatus.DECLINED
for attendee in event.attendees
):
return False
# Calendar enttiy may be limited to a specific event type
if (
self.entity_description.event_type is not None
and self.entity_description.event_type != event.event_type
and self.entity_description.event_type is not event.event_type
):
return False
# Default calendar entity omits the special types but includes all the others
@@ -247,7 +247,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
user_input: dict[str, Any] | None = None,
) -> SubentryFlowResult:
"""Handle the location step."""
if self._get_entry().state != ConfigEntryState.LOADED:
if self._get_entry().state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
@@ -18,7 +18,6 @@ from homeassistant.components import webhook
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_SUPPORTED_FEATURES,
CLOUD_NEVER_EXPOSED_ENTITIES,
CONF_NAME,
STATE_UNAVAILABLE,
)
@@ -803,8 +802,6 @@ def async_get_entities(
is_supported_cache = config.is_supported_cache
for state in hass.states.async_all():
entity_id = state.entity_id
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue
# Check check inlined for performance to avoid
# function calls for every entity since we enumerate
# the entire state machine here
@@ -12,7 +12,6 @@ import jwt
from homeassistant.components import webhook
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -167,9 +166,6 @@ class GoogleConfig(AbstractConfig):
# Ignore entities that are views
return False
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
entity_registry = er.async_get(self.hass)
registry_entry = entity_registry.async_get(state.entity_id)
if registry_entry:
@@ -225,7 +225,7 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Set conversation options."""
# abort if entry is not loaded
if self._get_entry().state != ConfigEntryState.LOADED:
if self._get_entry().state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
@@ -754,7 +754,7 @@ async def async_prepare_files_for_prompt(
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
)
if uploaded_file.state == FileState.FAILED:
if uploaded_file.state is FileState.FAILED:
raise HomeAssistantError(
f"File `{uploaded_file.name}` processing"
" failed, reason:"
@@ -766,7 +766,7 @@ async def async_prepare_files_for_prompt(
tasks = [
asyncio.create_task(wait_for_file_processing(part))
for part in prompt_parts
if part.state != FileState.ACTIVE
if part.state is not FileState.ACTIVE
]
async with asyncio.timeout(TIMEOUT_MILLIS / 1000):
await asyncio.gather(*tasks)

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