Add TrueNAS CE integration - #179103
Conversation
Adds a new integration for monitoring and controlling TrueNAS servers via the modern JSON-RPC 2.0 /api/current WebSocket API (aiotruenas). Previously distributed via HACS at kayl-codes/homeassistant-truenas (quality scale: Platinum). Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Hi @kayl-codes
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
When adding new integrations, limit included platforms to a single platform. While we appreciate the effort, reviewing larger than necessary PRs slows down the review process. Please reduce this PR to a single platform. See the review process for more details.
Check requirementsChecked at commit All requirements checks passed. ✅
📦 aiotruenas: 1.1.0
|
There was a problem hiding this comment.
Pull request overview
Adds a comprehensive TrueNAS CE integration using the aiotruenas JSON-RPC client.
Changes:
- Adds configuration, discovery, polling, diagnostics, repairs, and migration support.
- Adds sensor, binary sensor, switch, button, update, and action platforms.
- Adds extensive integration tests and typing configuration.
Reviewed changes
Copilot reviewed 46 out of 48 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
.strict-typing |
Enables strict typing. |
CODEOWNERS |
Assigns integration owners. |
mypy.ini |
Adds strict mypy settings. |
requirements_all.txt |
Adds aiotruenas. |
homeassistant/components/truenas_ce/__init__.py |
Implements setup and domain services. |
homeassistant/components/truenas_ce/api.py |
Wraps the TrueNAS client. |
homeassistant/components/truenas_ce/apiparser.py |
Parses API payloads. |
homeassistant/components/truenas_ce/binary_sensor.py |
Implements binary sensors and actions. |
homeassistant/components/truenas_ce/binary_sensor_types.py |
Defines binary sensors. |
homeassistant/components/truenas_ce/button.py |
Implements buttons. |
homeassistant/components/truenas_ce/button_types.py |
Defines buttons. |
homeassistant/components/truenas_ce/config_flow.py |
Implements configuration and discovery. |
homeassistant/components/truenas_ce/const.py |
Defines integration constants. |
homeassistant/components/truenas_ce/coordinator.py |
Coordinates polling and subscriptions. |
homeassistant/components/truenas_ce/diagnostics.py |
Provides redacted diagnostics. |
homeassistant/components/truenas_ce/entity.py |
Implements shared entity behavior. |
homeassistant/components/truenas_ce/helper.py |
Provides shared helpers. |
homeassistant/components/truenas_ce/icons.json |
Defines entity and action icons. |
homeassistant/components/truenas_ce/manifest.json |
Declares integration metadata. |
homeassistant/components/truenas_ce/migration.py |
Migrates legacy entities and history. |
homeassistant/components/truenas_ce/quality_scale.yaml |
Declares quality-scale compliance. |
homeassistant/components/truenas_ce/repairs.py |
Implements repair flows. |
homeassistant/components/truenas_ce/sensor.py |
Implements sensors and actions. |
homeassistant/components/truenas_ce/sensor_types.py |
Defines sensors. |
homeassistant/components/truenas_ce/services.yaml |
Defines action schemas. |
homeassistant/components/truenas_ce/strings.json |
Adds user-facing translations. |
homeassistant/components/truenas_ce/switch.py |
Implements switches. |
homeassistant/components/truenas_ce/switch_types.py |
Defines switches. |
homeassistant/components/truenas_ce/update.py |
Implements update entities. |
homeassistant/components/truenas_ce/update_types.py |
Defines update entities. |
tests/components/truenas_ce/__init__.py |
Initializes the test package. |
tests/components/truenas_ce/_fakes.py |
Provides shared test doubles. |
tests/components/truenas_ce/test_api.py |
Tests the API wrapper. |
tests/components/truenas_ce/test_apiparser.py |
Tests payload parsing. |
tests/components/truenas_ce/test_binary_sensor.py |
Tests binary sensors. |
tests/components/truenas_ce/test_button.py |
Tests buttons. |
tests/components/truenas_ce/test_config_flow.py |
Tests configuration flows. |
tests/components/truenas_ce/test_coordinator.py |
Tests coordinator behavior. |
tests/components/truenas_ce/test_diagnostics.py |
Tests diagnostics. |
tests/components/truenas_ce/test_entity.py |
Tests shared entities. |
tests/components/truenas_ce/test_entity_description.py |
Tests description validation. |
tests/components/truenas_ce/test_entity_setup.py |
Tests platform setup wiring. |
tests/components/truenas_ce/test_init.py |
Tests integration setup and services. |
tests/components/truenas_ce/test_repairs.py |
Tests repair flows. |
tests/components/truenas_ce/test_sensor.py |
Tests sensors. |
tests/components/truenas_ce/test_services.py |
Tests domain services. |
tests/components/truenas_ce/test_switch.py |
Tests switches. |
tests/components/truenas_ce/test_update.py |
Tests update entities. |
Suppressed comments (4)
homeassistant/components/truenas_ce/switch.py:81
- Raise the recorded API error before refreshing; otherwise a rejected cloud-sync/cron disable request is reported as a successful switch action.
await self.coordinator.api.query(
self._update_method, [self._data["id"], {"enabled": False}]
)
await self.coordinator.async_request_refresh()
homeassistant/components/truenas_ce/switch.py:94
- Raise the recorded API error before refreshing; otherwise a failed service start is silently reported as a successful switch action.
await self.coordinator.api.query("service.start", [self._data["service"]])
await self.coordinator.async_request_refresh()
homeassistant/components/truenas_ce/switch.py:100
- Raise the recorded API error before refreshing; otherwise a failed service stop is silently reported as a successful switch action.
await self.coordinator.api.query("service.stop", [self._data["service"]])
await self.coordinator.async_request_refresh()
tests/components/truenas_ce/test_apiparser.py:102
- Add concrete parameter annotations to this parametrized test function, as required for all test parameters.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Per Core review guidance, new-integration PRs should include only a single platform. binary_sensor, switch, button and update (plus the 12 entity actions bound to binary_sensor entities) will follow as separate PRs once this one is merged. Co-Authored-By: Claude <noreply@anthropic.com>
|
Per the review guidance to limit new-integration PRs to a single platform, I've reduced this PR to the
This also removed the 12 entity actions that were bound to
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated 3 comments.
Suppressed comments (9)
homeassistant/components/truenas_ce/api.py:320
- Remove API parameters from this debug log. Dataset unlock calls pass the plaintext passphrase in
params, so enabling debug logging writes a stored secret into logs that users may share.
_LOGGER.debug("TrueNAS %s query: %s, %s", self._host, service, params)
homeassistant/components/truenas_ce/api.py:344
- Redact response bodies before logging them.
_summarize_payloadincludes up to 5,000 characters of every raw API response, including endpoints that can return certificates, account details, addresses, and other fields listed inTO_REDACT; truncation does not prevent credential or personal-data exposure.
if _LOGGER.isEnabledFor(DEBUG):
_LOGGER.debug(
"TrueNAS %s query (%s) response: %s",
self._host,
service,
_summarize_payload(data),
)
homeassistant/components/truenas_ce/migration.py:158
- Only adopt a legacy entry with a verified match. Falling back to the sole legacy entry when hosts differ can disable one TrueNAS server and attach its entity history to a newly configured, unrelated server.
# Single legacy entry with a differing host (e.g. host was normalized): adopt it.
return candidates[0] if len(candidates) == 1 else None
homeassistant/components/truenas_ce/config_flow.py:210
- Remove the user-configurable polling interval and use the integration's fixed interval. Home Assistant integration polling intervals are not user-configurable, and this currently permits polling the server every five seconds.
return vol.Schema(
{
vol.Required(CONF_POLL_INTERVAL, default=poll): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value="5", label="5 s"),
selector.SelectOptionDict(value="10", label="10 s"),
selector.SelectOptionDict(value="30", label="30 s"),
selector.SelectOptionDict(value="60", label="60 s"),
selector.SelectOptionDict(value="120", label="120 s"),
selector.SelectOptionDict(value="300", label="300 s"),
]
)
),
homeassistant/components/truenas_ce/config_flow.py:92
- Remove the integration-name field from the config flow. Device integration entry titles are generated by Home Assistant and can be customized later; this field also makes entity unique IDs depend on user-entered display text.
vol.Required(
CONF_NAME, default=truenas_config.get(CONF_NAME, DEFAULT_DEVICE_NAME)
): str,
homeassistant/components/truenas_ce/entity.py:404
- Require admin authorization for the destructive entity actions. This loop registers reboot, shutdown, dataset lock/unlock, snapshot, and passphrase mutation as ordinary entity services, allowing non-admin users with entity control permission to change configuration or take the NAS offline.
for service in services:
platform.async_register_entity_service(
service.name, service.schema, service.action
)
homeassistant/components/truenas_ce/const.py:28
- Correct the fallback hostname to
truenas.local. When DNS guessing fails, the current typo pre-fillstrueas.local, causing the initial connection attempt to target the wrong host.
DEFAULT_HOST = "trueas.local"
homeassistant/components/truenas_ce/config_flow.py:734
- Persist an explicit opt-out when the user selects “Set up from scratch.” This branch only returns to the user form, so
async_setup_entry()still callsasync_adopt_legacy_entities()and can disable the legacy entry, remove its entities, and adopt its history despite the user's selection.
async def async_step_migrate_manual(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip the takeover and configure TrueNAS CE from scratch."""
return await self.async_step_user()
homeassistant/components/truenas_ce/const.py:10
- Align the implemented platforms with the PR description. The PR promises binary sensors, switches, buttons, and update entities, but the integration forwards only the sensor platform, so those entity types cannot be created.
PLATFORMS = [
Platform.SENSOR,
]
- Redact passphrases/API responses before debug-logging (api.py) - Require admin for reboot/shutdown/dataset-lock/passphrase-set services - Fix zeroconf probe ignoring a rejected bogus key (false negative) - Fix System device being deleted before its entities are attached - Auto-derive the config-entry name/title from the device instead of asking the user (Core policy: no user-chosen entry names) - Drop the user-facing poll-interval option (Core policy: integration-owned) - Restrict legacy-entry adoption to an exact host match - Retry pending legacy entity-id remaps on every setup, without ever re-touching an already-resolved (or since manually renamed) one - Add tests/components/truenas_ce/test_migration.py (previously untested: forward adoption, pending retries, permutation-safe remaps, rollback) - Fix typo in DEFAULT_HOST, add missing test parameter annotations Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (8)
homeassistant/components/truenas_ce/config_flow.py:753
- Make the “set up from scratch” path explicitly suppress legacy adoption. This step currently only returns to the user form, while
async_adopt_legacy_entities()later adopts any legacy entry with the same host, so choosing this option and entering the existing server still disables the legacy entry and migrates its entities.
async def async_step_migrate_manual(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip the takeover and configure TrueNAS CE from scratch."""
return await self.async_step_user()
homeassistant/components/truenas_ce/migration.py:251
- Preserve all user-owned registry metadata before deleting the legacy entries. The snapshot only records name, icon, area, and disabled state, so aliases, labels, categories, hidden state, and entity options from
RegistryEntryare permanently lost during this supposedly transparent migration and cannot be restored on rollback.
{
_R_UNIQUE_ID: entry.unique_id,
_R_ENTITY_DOMAIN: entry.domain,
_R_ENTITY_ID: entry.entity_id,
_R_NAME: entry.name,
_R_ICON: entry.icon,
_R_AREA: entry.area_id,
_R_DISABLED: entry.disabled_by == er.RegistryEntryDisabler.USER,
}
homeassistant/components/truenas_ce/migration.py:284
- Include the config-entry ID in the backup key. The timestamp has only one-second resolution, so two entries migrated in the same second write the same Store and both persist the same key; the second save overwrites the first entry’s safety backup.
timestamp = dt_util.utcnow().strftime("%Y%m%d_%H%M%S")
store: Store[dict[str, Any]] = Store(
hass, _BACKUP_VERSION, f"{_BACKUP_KEY_PREFIX}_{timestamp}"
)
homeassistant/components/truenas_ce/manifest.json:24
- Scope or remove this generic Zeroconf registration. As written, every
_http._tcpannouncement triggers up to two active WebSocket/login probes, so ordinary printers, routers, and web services can generate substantial connection traffic and delays; other integrations scope this generic type by name or TXT properties (for exampleairq/manifest.json:11-17,bsblan/manifest.json:12-16, andsynology_dsm/manifest.json:18-24).
"zeroconf": [
"_http._tcp.local."
]
homeassistant/components/truenas_ce/const.py:10
- Either implement the advertised platforms or correct the PR scope. The PR description promises binary sensors, switches, buttons, and update entities, but this integration forwards only the sensor platform, so none of those entity types can be created.
PLATFORMS = [
Platform.SENSOR,
]
homeassistant/components/truenas_ce/init.py:433
- Ignore malformed alert-list elements before projecting properties. Checking only the outer list means a non-mapping element can raise
TypeErrorata[k], turning one malformed API item into a failed service call despite this handler’s defensive response handling.
prop_list = [p.strip() for p in props.split(",")]
filtered = [{k: a[k] for k in prop_list if k in a} for a in alerts]
homeassistant/components/truenas_ce/api.py:334
- Clear the shared error state after a successful call completes. The coordinator issues many queries concurrently; while this call awaits, another query can set
_error, causing this successful caller’s immediateapi.errorcheck to report an unrelated failure.
try:
data = await self._client.call(service, params)
except TrueNASCallError as exc:
homeassistant/components/truenas_ce/apiparser.py:403
- Normalize every non-string human-date value to
None. The helper currently leaves integers and other malformed values unchanged, which can feed an invalid native value to timestamp sensors even thoughhuman_date_to_utc()already handles arbitrary inputs safely.
def _convert_human_date(target: dict[str, Any], name: str) -> None:
"""Convert human-readable date string to UTC datetime or None if unparsable."""
value = target.get(name)
if isinstance(value, str):
converted = human_date_to_utc(value)
target[name] = converted
Resolves all 605 prek findings from the Copilot-review follow-up: - TID251, TRY401/300, BLE001, PLC0415, D102/D107/D415, RUF005/059, N806, B007/PERF102, codespell, yamllint (36 findings) - Missing/malformed docstrings (D103/D205) across the entire test suite (569 findings) Also works around the known Ruff py314-target except-tuple formatter bug (except (A, B): -> invalid except A, B:) with # fmt: skip on the 5 affected lines, verified against prek's actual ruff-format. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 37 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
homeassistant/components/truenas_ce/config_flow.py:747
- Persist and honor an explicit migration opt-out. Selecting “Set up from scratch” only returns to the user form; setup later unconditionally adopts any legacy entry with the same host, so configuring that server still disables and strips the legacy entry despite the user's choice.
async def async_step_migrate_manual(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip the takeover and configure TrueNAS CE from scratch."""
return await self.async_step_user()
homeassistant/components/truenas_ce/migration.py:290
- Exclude credentials from the standalone migration backup.
legacy_entry.datacan contain the API key and dataset passphrases, and this backup intentionally survives config-entry deletion with no normal removal cleanup, leaving secrets in.storageindefinitely.
"legacy_config": {
"data": dict(legacy_entry.data),
"options": dict(legacy_entry.options),
},
homeassistant/components/truenas_ce/config_flow.py:705
- Skip legacy entries that have already been migrated. Always returning the first legacy entry means the disabled entry from the first takeover is offered forever, preventing additional legacy TrueNAS instances from being imported.
This issue also appears on line 743 of the same file.
legacy_entries = self.hass.config_entries.async_entries(LEGACY_DOMAIN)
return legacy_entries[0] if legacy_entries else None
homeassistant/components/truenas_ce/config_flow.py:740
- Drop the user-configurable polling interval during migration and use the fixed integration interval. This copies
poll_intervalfrom legacy options, and the coordinator honors it, contrary to Core's requirement that integration polling cadence is not stored as a user option.
self._legacy_options = dict(legacy.options)
homeassistant/components/truenas_ce/manifest.json:24
- Restrict discovery instead of matching every HTTP advertisement. This bare service type triggers bogus-key WSS/WS probes against every HTTP device on the network; add a reliable name/TXT filter or omit zeroconf until TrueNAS advertises a discriminator.
"zeroconf": [
"_http._tcp.local."
]
homeassistant/components/truenas_ce/const.py:11
- Align the implemented platforms with the stated PR scope. Only
sensoris loaded and no binary-sensor, switch, button, or update platform modules exist, so the entity types advertised in the PR description are absent.
PLATFORMS = [
Platform.SENSOR,
]
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated 3 comments.
Suppressed comments (8)
homeassistant/components/truenas_ce/repairs.py:78
- Keep the repair issue open until the background rollback succeeds. The flow currently deletes it immediately while the task helper converts failures into logs (and ignores a
Falseresult), so a failed destructive rollback is presented as complete and the user loses the actionable retry path.
self.hass.async_create_task(
_async_rollback_and_log_errors(self.hass, entry)
)
ir.async_delete_issue(
self.hass, DOMAIN, f"{ISSUE_MIGRATION_ROLLBACK}_{self._entry_id}"
)
homeassistant/components/truenas_ce/manifest.json:14
- Narrow or remove this generic zeroconf registration. Matching every
_http._tcpadvertisement launches this flow for unrelated printers, routers, and web services and actively attempts multiple WebSocket authentication handshakes against each; established core integrations filter this service type by name or properties (for exampleshelly/manifest.json:21-25,synology_dsm/manifest.json:18-24, andtailwind/manifest.json:16-23).
"zeroconf": ["_http._tcp.local."]
homeassistant/components/truenas_ce/entity.py:95
- Use the stable
system.global.idfor the System device identifier. The current identifier includes the live hostname, so renaming the TrueNAS host creates a second System device on the next setup and leaves the old device orphaned despite the config flow already storingCONF_SYSTEM_ID.
inst = coordinator.config_entry.data[CONF_NAME]
system_info = coordinator.data["system_info"]
identifier = format_device_identifier(inst, system_info["hostname"])
homeassistant/components/truenas_ce/init.py:115
- Do not overwrite the entity registry's display-unit option on every startup.
unit_of_measurementin registry options is a user customization; this reverses choices such as MB whenever they differ from the integration's computed suggestion, whileTrueNASSensoralready supplies_attr_suggested_unit_of_measurementfor the default.
unit, _ = scaled_data_unit(value, binary)
entry = ent_reg.async_get(entity_id)
options = dict(entry.options.get("sensor", {})) if entry else {}
if options.get("unit_of_measurement") != unit:
options["unit_of_measurement"] = unit
ent_reg.async_update_entity_options(entity_id, "sensor", options)
homeassistant/components/truenas_ce/config_flow.py:626
- Drop the legacy polling interval instead of copying all options into the new entry.
CONF_POLL_INTERVALis read directly byTrueNASCoordinatorto setupdate_interval, so migrated users retain a user-configurable polling frequency (including 5 seconds), contrary to the integration requirement that polling intervals be integration-owned.
self._legacy_options = dict(legacy.options)
homeassistant/components/truenas_ce/apiparser.py:181
- Move the TrueNAS response parsing and RPC-domain mapping into
aiotruenas. This general parser plus the large integration coordinator makes Core own protocol interpretation and state-shaping logic; integrations are expected to remain thin Home Assistant adapters around their client library.
def parse_api(
data: dict[str, Any] | None = None,
source: dict[str, Any] | list[Any] | str | None = None,
key: str | None = None,
key_secondary: str | None = None,
key_search: str | None = None,
vals: list[ApiValueSpec] | None = None,
val_proc: list[list[dict[str, Any]]] | None = None,
ensure_vals: list[ApiValueSpec] | None = None,
only: list[dict[str, Any]] | None = None,
skip: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
homeassistant/components/truenas_ce/const.py:7
- Update the PR description to match the sensor-only implementation or add the advertised platforms and actions.
PLATFORMSloads only sensors andSENSOR_SERVICESis empty, while the description currently promises binary sensors, switches, buttons, updates, and control actions.
PLATFORMS = [
Platform.SENSOR,
]
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Correct the evidence for the
brandsrule. This integration contains nobrand/directory, so the comment's claim that local icon variants ship here is false; either add those assets or reference the linked Home Assistant Brands contribution instead.
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
repository no longer accepts custom-integration submissions, so this is
the supported path. A core migration would still need a brands-repo PR.
* fix: namespace migration backups per config entry The .storage migration-backup key was global (not scoped per config entry). Two TrueNAS instances migrating around the same time could collide on the same timestamped key, or have _remove_backups prune the other instance's snapshot after writing its own. Namespace the key and the cleanup scan by config entry id (_entry_backup_prefix) instead. Found while porting the Community-Edition migration module to home-assistant/core (home-assistant/core#179103); mirrored back here as a standalone fix, kept separate from #94 which is being reviewed in parallel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: update _remove_backups/_write_migration_backup tests for the new prefix param CI was red on this PR: the per-entry prefix change updated _remove_backups' signature and _write_migration_backup's call site, but the existing unit tests still exercised the old (pre-namespacing) call shape and failed with TypeError / assertion mismatches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: capture utcnow() once in _write_migration_backup, hoist rollback's backup prefix Addresses Sourcery review feedback on PR #95: - _write_migration_backup called dt_util.utcnow() twice (timestamp key and payload "created" field), risking a sub-second mismatch between them; capture it once and reuse. - async_rollback_to_legacy computed the entry's backup prefix inline at the call site instead of via a local variable, unlike _write_migration_backup's pattern; hoist it for consistency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: use _BACKUP_KEY_PREFIX constant instead of hardcoded prefix string Sourcery review round 2 flagged that _remove_backups tests still hardcoded the base backup prefix literal instead of referencing the constant, which would silently drift out of sync if the prefix ever changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… add device-identifier collision test Ports Sourcery-refined fixes from prod PR home-assistant#94 (device-identifier instance prefix + composite-reference-length guard) and PR home-assistant#95 (namespaced backup prefix) into the truenas_ce component mirror.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (8)
homeassistant/components/truenas_ce/config_flow.py:433
- Allow distinct servers to have the same device-derived name and key entities/devices by stable identity instead. Two boxes can legitimately report the same hostname—or both fall back to
TrueNAS—and the user has no editable field to resolve this error, even when theirsystem_idvalues differ.
if not errors and truenas_config[CONF_NAME] in configured_instances(self.hass):
errors["base"] = "name_exists"
homeassistant/components/truenas_ce/apiparser.py:174
- Move JSON-RPC response parsing and normalization into
aiotruenas. This generic parser and the coordinator's many protocol-specific mappings make the integration own TrueNAS API compatibility instead of remaining a thin Home Assistant wrapper.
def parse_api(
data: dict[str, Any] | None = None,
source: dict[str, Any] | list[Any] | str | None = None,
key: str | None = None,
key_secondary: str | None = None,
homeassistant/components/truenas_ce/manifest.json:14
- Constrain or remove this generic zeroconf matcher. This is the only integration registering bare
_http._tcp.local.; established matchers add a name or property filter (for examplesynology_dsm/manifest.json:20-23andshelly/manifest.json:23-24). As written, every HTTP advertisement triggers WSS and WS login probes against unrelated devices.
"zeroconf": ["_http._tcp.local."]
homeassistant/components/truenas_ce/entity.py:61
- Base the System device identifier on the persisted
system_id, not the mutable NAS hostname. Renaming TrueNAS changessystem_info["hostname"], so the next setup registers a different System device and leaves the previous device orphaned despite already having a stable global ID.
def format_device_identifier(inst: str, hostname: str) -> str:
"""Build the main TrueNAS ("System") device identifier value.
Shared so other platforms (e.g. the diagnostic statistics-cleanup button)
associate with the existing device instead of duplicating the format.
"""
return f"{inst}_{hostname}"
homeassistant/components/truenas_ce/migration.py:103
- Abort adoption when disabling the legacy entry returns
False.async_set_disabled_byreports a failed unload, but this path continues removing registry entries while the old coordinator may still be active and able to recreate them.
await hass.config_entries.async_set_disabled_by(
legacy_entry.entry_id, ConfigEntryDisabler.USER
)
homeassistant/components/truenas_ce/const.py:7
- Update the PR description to state that this submission is sensor-only. It currently promises binary sensors, switches, buttons, update entities, and actions, while
PLATFORMScontains onlysensorandSENSOR_SERVICESis empty.
PLATFORMS = [
Platform.SENSOR,
]
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Correct the brands rule evidence to reference the actual brands contribution. No
brand/directory or image assets exist in this integration, so the current explanation for marking the Bronze rule done is factually incorrect.
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
repository no longer accepts custom-integration submissions, so this is
the supported path. A core migration would still need a brands-repo PR.
homeassistant/components/truenas_ce/migration.py:415
- Keep entities on their original IDs when a target is occupied outside the remap set. A pair such as
sensor.new -> sensor.existingis parked here, then cannot claim the occupied target in the second pass and is permanently left on a temporarysensor.truenas_ce_mig_*ID.
temp_id, counter = _temp_entity_id(ent_reg, current_id, counter)
ent_reg.async_update_entity(current_id, new_entity_id=temp_id)
parked.append((temp_id, target_id, record))
DeviceInfo is already imported unquoted and used directly elsewhere in this method, so the forward-reference-style string cast served no purpose.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
homeassistant/components/truenas_ce/config_flow.py:626
- Drop the legacy polling interval instead of copying it into the new entry. The coordinator still reads
CONF_POLL_INTERVALfrom these migrated options, so this preserves a user-configurable 5–300 second interval despite the Core requirement that polling cadence be integration-owned;test_config_flow.py:512confirms thatpoll_intervalis currently retained. Copy only supported non-polling options and useDEFAULT_POLL_INTERVALin the coordinator.
self._legacy_options = dict(legacy.options)
homeassistant/components/truenas_ce/manifest.json:14
- Constrain or remove this generic
_http._tcpdiscovery match. As generated inhomeassistant/generated/zeroconf.py:620-691, every other integration on this service type supplies a name or TXT-property filter; this unfiltered entry starts a flow and makes two WebSocket authentication attempts against every HTTP-advertising printer, router, and appliance on the network. If TrueNAS has no reliable discriminator, manual setup is safer than active probing of all generic HTTP services.
"zeroconf": ["_http._tcp.local."]
homeassistant/components/truenas_ce/init.py:115
- Do not overwrite a user's sensor display-unit option on every startup. Core explicitly applies
suggested_unit_of_measurementonly when the user has not selected a unit (homeassistant/components/sensor/__init__.py:536-541), but this writes directly to the publicsensorregistry options and replaces choices such as MB with GB. Rely on the entity's suggested unit or limit any registry rewrite to a one-time, versioned migration that preserves explicit user customizations.
entry = ent_reg.async_get(entity_id)
options = dict(entry.options.get("sensor", {})) if entry else {}
if options.get("unit_of_measurement") != unit:
options["unit_of_measurement"] = unit
ent_reg.async_update_entity_options(entity_id, "sensor", options)
homeassistant/components/truenas_ce/const.py:7
- Update the PR description to match this sensor-only submission. It currently promises binary sensors, switches, buttons, update entities, and their actions, while
PLATFORMSloads onlyPlatform.SENSOR; this materially overstates the functionality users and reviewers are evaluating.
PLATFORMS = [
Platform.SENSOR,
]
* fix: drop unnecessary string quoting on cast(DeviceInfo, ...) DeviceInfo is already imported unquoted and used directly elsewhere in this method, so the forward-reference-style string cast served no purpose. Mirrors the same Copilot-flagged fix from the home-assistant/core#179103 mirror branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: raise UpdateFailed when system_info is missing after refresh A middleware error leaves ds["system_info"] at its empty initial value (query() returns None on failure without dropping the socket), but the refresh still counted as successful, crashing register_system_device() on the missing "hostname" key instead of retrying setup cleanly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: name the missing field in the system_info UpdateFailed message Sourcery review round 2 flagged the generic wording; naming the field (hostname) makes the retried-setup failure easier to diagnose in logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
- migration.py: re-enable the legacy entry and check its result before removing the CE entry, so a failed legacy setup aborts the rollback with both entries intact instead of leaving neither working. - coordinator.py: raise UpdateFailed when system_info.hostname is missing after refresh, instead of crashing in register_system_device(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (6)
homeassistant/components/truenas_ce/const.py:7
- Make the PR description match the sensor-only scope.
PLATFORMSloads only sensors andSENSOR_SERVICESis empty, so the description currently overstates the shipped binary sensors, switches, buttons, updates, and actions.
PLATFORMS = [
Platform.SENSOR,
]
homeassistant/components/truenas_ce/config_flow.py:633
- Preserve the user's “set up from scratch” choice through entry setup. This branch sets no migration-skip state, so entering the same host still lets
async_adopt_legacy_entities()find, disable, and adopt the legacy entry despite the explicit manual choice.
async def async_step_migrate_manual(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip the takeover and configure TrueNAS CE from scratch."""
return await self.async_step_user()
homeassistant/components/truenas_ce/migration.py:103
- Abort adoption when disabling the legacy entry fails.
async_set_disabled_by()can returnFalsewhen reload/unload fails, but the code then removes registry entries while the legacy coordinator may still be active, violating the stated stop-before-release ordering.
if legacy_entry.disabled_by is None:
await hass.config_entries.async_set_disabled_by(
legacy_entry.entry_id, ConfigEntryDisabler.USER
)
homeassistant/components/truenas_ce/repairs.py:77
- Delete the repair issue only after rollback succeeds. The background rollback can return
False(for example when the legacy entry fails setup) or raise, yet this path immediately reports completion and removes the only issue, leaving the failed rollback visible only in logs.
self.hass.async_create_task(
_async_rollback_and_log_errors(self.hass, entry)
)
ir.async_delete_issue(
self.hass, DOMAIN, f"{ISSUE_MIGRATION_ROLLBACK}_{self._entry_id}"
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Mark branding as pending and reference the linked Brands PR. This integration contains no
brand/directory, and home-assistant/brands#10948 is still open and blocked, so the currentdonestatus and local-assets explanation are inaccurate.
brands:
status: done
comment: |
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
homeassistant/components/truenas_ce/strings.json:43
- Rename this setting to match its actual effect.
get_cronjob()removes disabled jobs from coordinator data (coordinator.py:2596-2610), while this PR registers no manual-run action, so the current label and description promise unrelated behavior.
"cronjob_skip_disabled": "Skip disabled cronjobs on manual run",
…acy disable fails async_step_migrate_manual now sets MIGRATION_DONE so a later coordinator setup with the same host as the legacy entry can never let async_adopt_legacy_entities silently override the user's explicit "from scratch" choice. async_adopt_legacy_entities now aborts (without persisting MIGRATION_DONE) if disabling the legacy entry fails, instead of proceeding to release its entities out from under a still-active legacy coordinator; a later setup retries the adoption once the legacy entry can be disabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated 1 comment.
Suppressed comments (8)
homeassistant/components/truenas_ce/const.py:71
- Default certificate verification to enabled. The API uses WSS and sends an API key, so
Falsemakes first-time setups accept an unauthenticated TLS peer despite the UI text saying verification should only be disabled for exceptional self-signed certificates.
DEFAULT_SSL_VERIFY = False
homeassistant/components/truenas_ce/migration.py:107
- Defer automatic legacy takeover until the replacement platforms have parity. This PR only loads
sensor, while_collect_legacy_recordsdeliberately excludes every other legacy platform; disabling the whole legacy entry here therefore makes its binary sensors, switches, buttons, and update entities unavailable.
disabled = legacy_entry.disabled_by is not None or (
await hass.config_entries.async_set_disabled_by(
legacy_entry.entry_id, ConfigEntryDisabler.USER
)
)
homeassistant/components/truenas_ce/migration.py:585
- Create the rollback repair before directing users to it. No production caller invokes
raise_migration_rollback_issue, so the migration notification currently tells users to open a Repairs issue that is never created.
"To undo this, go to **Settings → Repairs**, open the "
"**TrueNAS CE migration — rollback available** repair, and choose "
"**Roll back to the previous integration**. This works only while you "
homeassistant/components/truenas_ce/config_flow.py:434
- Allow different servers to have the same generated title. TrueNAS commonly uses the same default hostname on multiple boxes, and config-entry titles need not be unique; this check blocks the second box even when its
system.global.idis different, so entity/device uniqueness should use that stable ID instead.
if not errors and truenas_config[CONF_NAME] in configured_instances(self.hass):
errors["base"] = "name_exists"
homeassistant/components/truenas_ce/migration.py:246
- Normalize both hosts before matching the legacy entry. The imported host is sanitized when the form is submitted, but the legacy value is compared verbatim here, so values such as
NAS.local, a URL, or a trailing slash fail adoption and are then permanently marked migrated.
host = config_entry.data.get(CONF_HOST)
return next(
(
entry
for entry in hass.config_entries.async_entries(LEGACY_DOMAIN)
if entry.data.get(CONF_HOST) == host
),
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Mark the brands rule as pending until the assets are available. This checkout has no
brand/assets, and the linked Brands PR #10948 is still open, so the currentdonestatus and explanation that images ship inside the integration are inaccurate.
brands:
status: done
comment: |
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
repository no longer accepts custom-integration submissions, so this is
the supported path. A core migration would still need a brands-repo PR.
homeassistant/components/truenas_ce/sensor.py:402
- Remove forced updates from the disk temperature sensor. Re-recording an unchanged temperature every poll does not add information and creates unnecessary state-change events and recorder rows for every disk.
class TrueNASDiskSensor(TrueNASSensor):
"""Disk temperature sensor.
force_update ensures HA records every poll value even when the temperature
is stable, so the history graph never appears frozen.
"""
_attr_force_update = True
homeassistant/components/truenas_ce/entity.py:452
- Mark referenced entities unavailable when their object data disappears.
_refresh_datareplaces a missing UID with{}, butCoordinatorEntity.availableremains true while the overall poll succeeds, contradicting the claimedentity-unavailablerule and leaving removed disks/apps displayed as available with an unknown state.
self._data: dict[str, Any] = data.get(self._uid, {}) if self._uid else data
if self._uid and not self._data:
_LOGGER.debug(
"Data for UID %s is missing or empty in %s",
self._uid,
self.entity_description.data_path,
)
…red host _async_update_rediscovered_entry tried every configured entry's real API key against a zeroconf-discovered host once it merely answered the tiny bogus-key probe handshake. Any device on the LAN able to mimic that handshake (reject a bogus key as ERR_INVALID_KEY) could harvest every stored TrueNAS credential this way, regardless of whether the box's system_id ultimately matched. Remove the automatic credential probe entirely; a probed host always falls through to the existing user-facing confirm step now. A rediscovered box is folded into its matching entry (host update via _abort_if_unique_id_configured(updates=...)) only once this flow has itself authenticated the box through a real, user-authorized connection and confirmed its system_id -- never via a credential borrowed from an unrelated entry.
Sourcery review feedback on the mirrored Prod PR (kayl-codes/homeassistant-truenas#98): proves the existing entry isn't reused when a rediscovered host turns out to be a different physical device, not just that its host is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the equivalent Prod-repo fix (kayl-codes/homeassistant-truenas PR home-assistant#98): the mismatch test didn't prove the newly created entry's unique_id actually reflects the rediscovered system_id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red host (#98) * fix: never replay a stored API key against an unauthenticated discovered host _async_update_rediscovered_entry tried every configured entry's real API key against a zeroconf-discovered host once it merely answered the tiny bogus-key probe handshake. Any device on the LAN able to mimic that handshake (reject a bogus key as ERR_INVALID_KEY) could harvest every stored TrueNAS credential this way, regardless of whether the box's system_id ultimately matched. Remove the automatic credential probe entirely; a probed host always falls through to the existing user-facing confirm step now. A rediscovered box is folded into its matching entry (host update via _abort_if_unique_id_configured(updates=...)) only once this flow has itself authenticated the box through a real, user-authorized connection and confirmed its system_id -- never via a credential borrowed from an unrelated entry. Mirrors the fix already applied against home-assistant/core#179103. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: avoid name collision in zeroconf host-fold test The submitted user_input and the pre-existing matched entry both defaulted to the _user_input() helper's CONF_NAME ("TrueNAS"), so the flow's name_exists guard fired before the system_id-based unique_id check ever ran, making the flow return FORM instead of the expected ABORT. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: assert new entry gets a distinct unique_id on system_id mismatch Sourcery flagged that the mismatch test didn't prove the newly created entry's unique_id actually reflects the rediscovered system_id (only that the old entry was left untouched). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (10)
homeassistant/components/truenas_ce/const.py:71
- Default certificate verification to enabled. The current false default sends the user's API key over unauthenticated TLS unless they notice and opt in, despite the form text saying verification should only be disabled for an unavoidable self-signed certificate.
DEFAULT_SSL_VERIFY = False
homeassistant/components/truenas_ce/migration.py:585
- Provide a real UI path before directing users to this repair. No code in this PR calls
raise_migration_rollback_issue, and only the sensor platform is loaded, so the referenced repair never exists and users cannot initiate the advertised rollback.
"To undo this, go to **Settings → Repairs**, open the "
"**TrueNAS CE migration — rollback available** repair, and choose "
"**Roll back to the previous integration**. This works only while you "
homeassistant/components/truenas_ce/config_flow.py:433
- Allow distinct servers to share a generated display name. This rejects different hosts and different
system.global.idvalues solely because their hostname (or theTrueNASfallback) collides, and users cannot edit the generated name to proceed; use host/system ID only for uniqueness and permit duplicate entry titles.
if not errors and truenas_config[CONF_NAME] in configured_instances(self.hass):
errors["base"] = "name_exists"
homeassistant/components/truenas_ce/repairs.py:41
- Treat a false rollback result as a failure.
async_rollback_to_legacyreturnsFalsewhen the legacy entry cannot be enabled, but this wrapper currently reports that path as success after the original repair issue has already been deleted, leaving no UI-visible failure.
await async_rollback_to_legacy(hass, entry)
homeassistant/components/truenas_ce/migration.py:427
- Do not park an entity when its target is occupied outside this remap. In that collision case this code renames the entity to
truenas_ce_mig_*, then every later pending-record retry parks it under another temporary ID; leave its current ID unchanged unless the target is free or occupied by another member of the permutation.
temp_id, counter = _temp_entity_id(ent_reg, current_id, counter)
ent_reg.async_update_entity(current_id, new_entity_id=temp_id)
parked.append((temp_id, target_id, record))
homeassistant/components/truenas_ce/config_flow.py:545
- Handle more than one legacy entry instead of always selecting the first. After the first migration that disabled legacy entry remains in
async_entries, so every subsequent setup flow offers it again and there is no way to import another TrueNAS server.
legacy_entries = self.hass.config_entries.async_entries(LEGACY_DOMAIN)
return legacy_entries[0] if legacy_entries else None
homeassistant/components/truenas_ce/sensor.py:402
- Remove forced updates for disk temperatures. Emitting an unchanged state every 60 seconds creates 1,440 recorder rows per disk per day; stable values are expected to produce a flat history graph without duplicate state writes.
force_update ensures HA records every poll value even when the temperature
is stable, so the history graph never appears frozen.
"""
_attr_force_update = True
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Mark the brands rule pending until assets are available. This
doneexplanation claims a localbrand/folder exists, but this PR contains none, and the linked Brands PR #10948 is still open; the official Bronze rule has no exception and requires assets in the Brands repository.
status: done
comment: |
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
repository no longer accepts custom-integration submissions, so this is
homeassistant/components/truenas_ce/const.py:7
- Align the PR description with the sensor-only implementation or restore the advertised platforms and actions. The description promises binary sensors, switches, buttons, updates, and control actions, but
PLATFORMSloads only sensors andSENSOR_SERVICESis empty.
PLATFORMS = [
Platform.SENSOR,
]
homeassistant/components/truenas_ce/migration.py:245
- Normalize both hosts before matching the legacy entry. The import form runs the copied host through
_sanitize_host, so a legacy value such asNAS.localbecomesnas.localin the new entry and this exact comparison silently skips adoption even when the user kept the prefilled host.
if entry.data.get(CONF_HOST) == host
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 37 changed files in this pull request and generated no new comments.
Suppressed comments (11)
homeassistant/components/truenas_ce/const.py:71
- Enable certificate verification by default. The current default disables TLS authentication for every new WSS connection, exposing the API key to network interception and contradicting the form text that says verification should remain enabled.
DEFAULT_SSL_VERIFY = False
homeassistant/components/truenas_ce/config_flow.py:433
- Allow separate servers to share a hostname-derived title. Two TrueNAS systems commonly use the same default hostname, but this rejects the second one; use the already-fetched
system.global.idfor entity/device identity instead of requiring a unique human-readable name.
if not errors and truenas_config[CONF_NAME] in configured_instances(self.hass):
errors["base"] = "name_exists"
homeassistant/components/truenas_ce/repairs.py:42
- Treat a
Falserollback result as a failure.async_rollback_to_legacyreturnsFalsewhen the bridge is gone or legacy setup fails, but this task currently reports success after deleting the original issue and never creates the failure issue.
try:
await async_rollback_to_legacy(hass, entry)
except Exception:
homeassistant/components/truenas_ce/manifest.json:14
- Remove the unqualified
_http._tcpdiscovery matcher. TrueNAS provides no distinguishing TXT property, so this launches WSS/WS probes against every generic HTTP mDNS device on the network; manual setup is safer until a selective discovery signature exists.
"requirements": ["aiotruenas==1.1.0"],
"zeroconf": ["_http._tcp.local."]
homeassistant/components/truenas_ce/const.py:7
- Align the implementation, PR description, and linked documentation before merging. Core loads only sensors and defines no entity services, while the PR and documentation advertise binary sensors, switches, buttons, update entities, diagnostics, actions, options, reauthentication, and Platinum quality.
PLATFORMS = [
Platform.SENSOR,
]
requirements_all.txt:456
- Add the aiotruenas v1.1.0 release or comparison link to the PR description. The dependency checklist is checked, but the description only links the repository root rather than the required dependency diff or changelog.
# homeassistant.components.truenas_ce
aiotruenas==1.1.0
homeassistant/components/truenas_ce/apiparser.py:174
- Add replacement semantics for keyed collection responses and use it for full
*.querycalls. This parser only merges into cacheddata, so even a successful non-empty response leaves keys absent from the latest response and removed disks, services, VMs, or tasks remain exposed indefinitely; failedNoneresponses should be distinguished from authoritative lists before pruning.
def parse_api(
data: dict[str, Any] | None = None,
source: dict[str, Any] | list[Any] | str | None = None,
key: str | None = None,
key_secondary: str | None = None,
homeassistant/components/truenas_ce/sensor.py:402
- Remove
force_updatefrom disk temperature sensors. Identical values already render as a flat history graph; forcing every disk to emit a state event each poll only increases recorder writes and database growth.
_attr_force_update = True
homeassistant/components/truenas_ce/migration.py:245
- Normalize both hosts before matching the legacy entry. The new host is lowercased by
_sanitize_hostwhile legacy data remains raw, so a case-different legacy hostname is missed and migration is permanently marked done without adopting its entities.
if entry.data.get(CONF_HOST) == host
homeassistant/components/truenas_ce/config_flow.py:84
- Remove this field until the corresponding action exists, or wire it into shipped behavior.
CONF_CRONJOB_SKIP_DISABLEDis only saved by the config flow, while the coordinator checksCONF_BEHAVIORSand no manual-run service is registered, so changing this visible setting has no effect.
CONF_CRONJOB_SKIP_DISABLED,
default=truenas_config.get(
CONF_CRONJOB_SKIP_DISABLED, DEFAULT_CRONJOB_SKIP_DISABLED
),
): bool,
homeassistant/components/truenas_ce/quality_scale.yaml:16
- Correct the Bronze
brandsevidence and status. Nobrand/directory exists in this integration, and core integrations use the linked Brands repository PR, which is still open; mark this pending until that asset PR is merged or reference the actual merged asset source.
Brand images ship inside the integration (brand/ folder with icon,
dark_icon and @2x variants), served locally by HA 2026.3+. The brands
repository no longer accepts custom-integration submissions, so this is
the supported path. A core migration would still need a brands-repo PR.
Proposed change
Adds a new integration for monitoring and controlling TrueNAS servers (TrueNAS 25.04+). It communicates exclusively over TrueNAS's modern JSON-RPC 2.0
/api/currentWebSocket endpoint via theaiotruenasclient library (local polling, 60s interval).Provides sensors, binary sensors, switches, buttons and update entities covering system info, disks, pools, datasets, VMs, containers/apps, services, directory services, cloud sync, replication, rsync/snapshot tasks, cron jobs, alerts, UPS and network interfaces, plus a set of actions (start/stop/restart VMs/containers/apps, service control, dataset snapshot/lock/unlock, system reboot/shutdown, alert dismiss/restore, etc.).
This integration has been distributed independently via HACS for some time at kayl-codes/homeassistant-truenas (quality scale: Platinum) and is now being submitted for inclusion in Home Assistant Core.
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests:
🤖 Prepared with Claude Code