Add Modbus Connection integration - #175407
Conversation
Layer 2 of the shared-Modbus-connection design: a new integration whose only job is to own Modbus connection config entries (one per physical link) and publish a live, backend-neutral connection that consumer integrations borrow units from. - config flow: Network (TCP / RTU-over-TCP) or Serial (RTU, including network serial proxies). A duplicate link aborts before the connection is opened; otherwise the connection is opened to validate it - runtime_data holds the live ModbusConnection (built via the tmodbus-backed modbus_connection.tmodbus connect functions); the entry owns close() and reloads on connection loss - async_get_unit(hass, entry_id, unit_id): the only consumer touchpoint - returns a backend-neutral ModbusUnit, or raises ConnectionNotReady (a ConfigEntryNotReady, so consumers get setup-retry, and a ModbusError) - quality_scale.yaml at bronze with strict typing (unique-config-entry exempt: Modbus endpoints have no hardware unique ID; entity/action rules exempt: the integration has no entities) - tests: setup/unload, connect-failure retry, reload-on-loss, the config flow (network/serial happy paths, cannot_connect, cannot_open_serial_port, and a parametrized duplicate check) and the async_get_unit accessor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Check requirementsChecked at commit
📦 modbus-connection: 3.2.0
|
There was a problem hiding this comment.
Pull request overview
This PR adds a new foundational modbus_connection integration (implementing architecture discussion #1418). Its sole responsibility is to own one config entry per physical Modbus link and expose a live, backend-neutral ModbusConnection that consumer integrations borrow units from via a single accessor. The integration is a thin wrapper over the new modbus-connection[tmodbus] library; connection/protocol logic lives in that library, keeping the integration itself minimal.
Changes:
- New config flow with a menu choosing Network (TCP / RTU-over-TCP) or Serial (RTU), validated by actually opening the connection, and deduplicated by connection parameters.
async_setup_entry/async_unload_entrystore the live connection inruntime_data, schedule an entry reload on connection loss, and close the connection on unload.- Public
async_get_unit(hass, entry_id, unit_id)accessor plus aConnectionNotReady(aConfigEntryNotReady/ModbusError) for consumers; bronze quality scale, strict typing, and tests for setup/unload/config flow.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| homeassistant/components/modbus_connection/init.py | Setup/unload, _async_open, connection-lost reload, and the async_get_unit accessor. |
| homeassistant/components/modbus_connection/config_flow.py | Menu + network/serial steps, param-based dedupe, and connect-to-validate. |
| homeassistant/components/modbus_connection/exceptions.py | ConnectionNotReady combining ConfigEntryNotReady and library ModbusError. |
| homeassistant/components/modbus_connection/const.py | Domain, transport, and serial default constants. |
| homeassistant/components/modbus_connection/manifest.json | New integration manifest (hub, config_flow, requirement). |
| homeassistant/components/modbus_connection/strings.json | Config-flow and exception strings. |
| homeassistant/components/modbus_connection/quality_scale.yaml | Bronze quality-scale rule statuses. |
| .strict-typing | Enables strict typing for the new integration. |
| requirements_all.txt | Adds the modbus-connection[tmodbus]==3.2.0 dependency. |
| tests/components/modbus_connection/* | Tests and fixtures for setup, config flow, and async_get_unit. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
async_setup_entry only caught ModbusConnectionError, while the config flow's validation catches the broader ModbusError. A serial open failing with a different ModbusError would propagate instead of raising ConfigEntryNotReady. Widen the caught type to match and cover serial in the retry test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Check requirementsChecked at commit
📦 modbus-connection: 3.3.0
|
The integration has no conditions or triggers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Guard against a connection_entry_id that resolves to a loaded config entry from another integration: check the entry domain before touching runtime_data, so it raises ConnectionNotReady instead of crashing on a foreign runtime_data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
| from collections.abc import Generator | ||
| from unittest.mock import AsyncMock, patch | ||
|
|
||
| from modbus_connection.mock import MockModbusConnection | ||
| import pytest | ||
|
|
||
| from homeassistant.components.modbus_connection.const import CONNECTION_TCP, DOMAIN | ||
| from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
| from tests.common import MockConfigEntry | ||
|
|
||
|
|
Reject an out-of-range TCP port (must be 1-65535) and a non-positive baud rate instead of only coercing to int. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Use a SelectSelector so parity renders as None/Even/Odd instead of the raw N/E/O codes, drop the option list from the field description, and keep the default at None (the most common). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Replace the manual entry-iteration dedupe with the idiomatic _async_abort_entries_match (a serial link is matched by device path, a TCP link by host and port). This is the hassfest-recognized pattern, so unique-config-entry is now done rather than exempt. Selector translation option keys must be lowercase, so the uppercase N/E/O parity codes failed hassfest. Use explicit option labels (None/Even/Odd) instead and keep the stored N/E/O values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Selector translation keys must be lowercase, so store parity as lowercase n/e/o with a translation_key (None/Even/Odd) and uppercase it in _async_open before passing it to the connection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
Uppercase the selected parity in the config flow so the entry data holds the N/E/O code the connection expects, keeping _async_open a plain pass-through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016huRs96kGdQbNQopXseC4H
| entry = cast( | ||
| "ModbusConnectionConfigEntry | None", | ||
| hass.config_entries.async_get_entry(connection_entry_id), | ||
| ) |
PR Review — Add Modbus Connection integrationClean, well-scoped connection-owner integration; prior Copilot findings are largely addressed, but confirm test fixtures resolve and the RTU-over-TCP claim. Strengths worth calling out:
What needs attention before merge:
🟡 Important
1. Referenced `mock_modbus_connection` / `mock_modbus_unit` fixtures are not defined here
|
bluetoothbot
left a comment
There was a problem hiding this comment.
Blocking issues found.
- Referenced
mock_modbus_connection/mock_modbus_unitfixtures are not defined here
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Breaking change
Proposed change
Implement home-assistant/architecture#1418
A new Modbus Connection integration whose only job is to own Modbus connection config entries (one per physical link) and publish a live, backend-neutral connection that consumer integrations borrow units from.
async_get_unit(hass, connection_entry_id, unit_id): the only consumer touchpoint. Returns a backend-neutral ModbusUnit, or raises ConnectionNotReady (a ConfigEntryNotReady, so consumers get setup-retry)Integration relies on
modbus-connection, a new Python package to abstract modbus connections. Source at https://github.com/home-assistant-libs/modbus-connectionType 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: