Skip to content

Add TrueNAS CE integration - #179103

Draft
kayl-codes wants to merge 35 commits into
home-assistant:devfrom
kayl-codes:truenas_ce
Draft

Add TrueNAS CE integration#179103
kayl-codes wants to merge 35 commits into
home-assistant:devfrom
kayl-codes:truenas_ce

Conversation

@kayl-codes

Copy link
Copy Markdown

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/current WebSocket endpoint via the aiotruenas client 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

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:


🤖 Prepared with Claude Code

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>
Copilot AI balanced review requested due to automatic review settings August 13, 2026 23:57

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown

Check requirements

Checked at commit 010d8cf.

All requirements checks passed. ✅

Package Old New No Advisories Not Yanked Repo Public CI Upload Release Pipeline Security PR Link Async Safe
aiotruenas 1.1.0 ☑️
📦 aiotruenas: 1.1.0
  • No Advisories: ✅ No active advisories reported by PyPI for version 1.1.0.
  • Not Yanked: ✅ Version 1.1.0 is a live (non-yanked) release.
  • Repo Public: ✅ https://github.com/kayl-codes/aiotruenas is publicly accessible.
  • CI Upload: ✅ Trusted Publisher attestation found (GitHub).
  • Release Pipeline: ✅ OIDC via Trusted Publisher attestation (GitHub); automated CI upload verified by PyPI.
  • Security: ☑️ Baseline scan found nothing obvious in pyproject.toml, src/aiotruenas/__init__.py, src/aiotruenas/client.py. Build uses hatchling with no custom install hooks; __init__.py only re-exports symbols; client.py uses websockets.asyncio.client with no exec/eval/network-at-import. This is not a security review — only the cheap checks were run.
  • PR Link: ✅ PR description links to https://github.com/kayl-codes/aiotruenas (source repository for new package).
  • Async Safe: ✅ Async-native library using websockets.asyncio.client; the one synchronous call (_build_ssl_context) is correctly dispatched via asyncio.to_thread. No blocking calls in async def bodies.

Generated by Check requirements (AW) · sonnet46 · 34.7 AIC · ⌖ 7.03 AIC · ⊞ 9.2K ·

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread homeassistant/components/truenas_ce/const.py Outdated
Comment thread homeassistant/components/truenas_ce/config_flow.py Outdated
Comment thread homeassistant/components/truenas_ce/migration.py Outdated
Comment thread homeassistant/components/truenas_ce/migration.py
Comment thread homeassistant/components/truenas_ce/api.py Outdated
Comment thread tests/components/truenas_ce/test_apiparser.py Outdated
Comment thread homeassistant/components/truenas_ce/entity.py Outdated
Comment thread homeassistant/components/truenas_ce/config_flow.py Outdated
Comment thread homeassistant/components/truenas_ce/api.py
Comment thread homeassistant/components/truenas_ce/config_flow.py Outdated
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>
Copilot AI review requested due to automatic review settings August 14, 2026 00:21
@kayl-codes

Copy link
Copy Markdown
Author

Per the review guidance to limit new-integration PRs to a single platform, I've reduced this PR to the sensor platform only. Removed for now (to be resubmitted as separate follow-up PRs once this one is merged):

  • binary_sensor
  • switch
  • button
  • update

This also removed the 12 entity actions that were bound to binary_sensor entities (VM/container/service/app start-stop-restart-reload). All actions bound to sensor entities, plus the domain-level alert/passphrase actions, are unaffected.

hassfest, mypy and the full test suite pass locally with the reduced scope. PR description will follow to match.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_payload includes up to 5,000 characters of every raw API response, including endpoints that can return certificates, account details, addresses, and other fields listed in TO_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-fills trueas.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 calls async_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,
]

Comment thread homeassistant/components/truenas_ce/migration.py Outdated
Comment thread homeassistant/components/truenas_ce/coordinator.py Outdated
Comment thread homeassistant/components/truenas_ce/const.py
- 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>
Copilot AI review requested due to automatic review settings August 14, 2026 06:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RegistryEntry are 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._tcp announcement 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 example airq/manifest.json:11-17, bsblan/manifest.json:12-16, and synology_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 TypeError at a[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 immediate api.error check 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 though human_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>
Copilot AI review requested due to automatic review settings August 14, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.data can contain the API key and dataset passphrases, and this backup intentionally survives config-entry deletion with no normal removal cleanup, leaving secrets in .storage indefinitely.
        "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_interval from 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 sensor is 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,
]

Comment thread homeassistant/components/truenas_ce/api.py
Comment thread homeassistant/components/truenas_ce/migration.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 False result), 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._tcp advertisement 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 example shelly/manifest.json:21-25, synology_dsm/manifest.json:18-24, and tailwind/manifest.json:16-23).
  "zeroconf": ["_http._tcp.local."]

homeassistant/components/truenas_ce/entity.py:95

  • Use the stable system.global.id for 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 storing CONF_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_measurement in registry options is a user customization; this reverses choices such as MB whenever they differ from the integration's computed suggestion, while TrueNASSensor already supplies _attr_suggested_unit_of_measurement for 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_INTERVAL is read directly by TrueNASCoordinator to set update_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. PLATFORMS loads only sensors and SENSOR_SERVICES is 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 brands rule. This integration contains no brand/ 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.

Comment thread homeassistant/components/truenas_ce/migration.py
Comment thread homeassistant/components/truenas_ce/config_flow.py
Comment thread homeassistant/components/truenas_ce/config_flow.py
kayl-codes added a commit to kayl-codes/homeassistant-truenas that referenced this pull request Aug 19, 2026
* 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.
Copilot AI review requested due to automatic review settings August 19, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 their system_id values 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 example synology_dsm/manifest.json:20-23 and shelly/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 changes system_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_by reports 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 PLATFORMS contains only sensor and SENSOR_SERVICES is 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.existing is parked here, then cannot claim the occupied target in the second pass and is permanently left on a temporary sensor.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.
Copilot AI review requested due to automatic review settings August 19, 2026 20:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_INTERVAL from 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:512 confirms that poll_interval is currently retained. Copy only supported non-polling options and use DEFAULT_POLL_INTERVAL in the coordinator.
            self._legacy_options = dict(legacy.options)

homeassistant/components/truenas_ce/manifest.json:14

  • Constrain or remove this generic _http._tcp discovery match. As generated in homeassistant/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_measurement only when the user has not selected a unit (homeassistant/components/sensor/__init__.py:536-541), but this writes directly to the public sensor registry 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 PLATFORMS loads only Platform.SENSOR; this materially overstates the functionality users and reviewers are evaluating.
PLATFORMS = [
    Platform.SENSOR,
]

Comment thread homeassistant/components/truenas_ce/migration.py Outdated
Comment thread homeassistant/components/truenas_ce/coordinator.py
kayl-codes added a commit to kayl-codes/homeassistant-truenas that referenced this pull request Aug 19, 2026
* 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>
Copilot AI review requested due to automatic review settings August 19, 2026 22:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. PLATFORMS loads only sensors and SENSOR_SERVICES is 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 return False when 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 current done status 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>
Copilot AI review requested due to automatic review settings August 20, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 False makes 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_records deliberately 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.id is 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 current done status 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_data replaces a missing UID with {}, but CoordinatorEntity.available remains true while the overall poll succeeds, contradicting the claimed entity-unavailable rule 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,
            )

Comment thread homeassistant/components/truenas_ce/config_flow.py Outdated
kayl-codes and others added 3 commits August 20, 2026 23:03
…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>
kayl-codes added a commit to kayl-codes/homeassistant-truenas that referenced this pull request Aug 20, 2026
…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>
Copilot AI review requested due to automatic review settings August 21, 2026 00:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.id values solely because their hostname (or the TrueNAS fallback) 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_legacy returns False when 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 done explanation claims a local brand/ 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 PLATFORMS loads only sensors and SENSOR_SERVICES is 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 as NAS.local becomes nas.local in 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

Copilot AI review requested due to automatic review settings August 21, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.id for 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 False rollback result as a failure. async_rollback_to_legacy returns False when 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._tcp discovery 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 *.query calls. This parser only merges into cached data, so even a successful non-empty response leaves keys absent from the latest response and removed disks, services, VMs, or tasks remain exposed indefinitely; failed None responses 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_update from 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_host while 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_DISABLED is only saved by the config flow, while the coordinator checks CONF_BEHAVIORS and 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 brands evidence and status. No brand/ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants