From 4049af08576a42ef0b94f1257b87f68784e81164 Mon Sep 17 00:00:00 2001 From: darkrain-nl Date: Sun, 4 Oct 2026 14:21:10 +0200 Subject: [PATCH] Add serial connection support to the Sofar integration (#183850) --- homeassistant/components/sofar/__init__.py | 17 +- homeassistant/components/sofar/config_flow.py | 127 ++++++++-- homeassistant/components/sofar/const.py | 5 + homeassistant/components/sofar/helpers.py | 21 ++ homeassistant/components/sofar/manifest.json | 2 +- .../components/sofar/quality_scale.yaml | 8 +- homeassistant/components/sofar/strings.json | 50 +++- tests/components/sofar/__init__.py | 21 +- tests/components/sofar/conftest.py | 4 +- tests/components/sofar/test_binary_sensor.py | 4 +- tests/components/sofar/test_button.py | 4 +- tests/components/sofar/test_config_flow.py | 233 +++++++++++++----- tests/components/sofar/test_init.py | 94 ++++++- tests/components/sofar/test_select.py | 6 +- tests/components/sofar/test_sensor.py | 10 +- tests/components/sofar/test_services.py | 4 +- tests/components/sofar/test_switch.py | 4 +- 17 files changed, 480 insertions(+), 134 deletions(-) create mode 100644 homeassistant/components/sofar/helpers.py diff --git a/homeassistant/components/sofar/__init__.py b/homeassistant/components/sofar/__init__.py index 9832d9fd28ea..faedb7f26040 100644 --- a/homeassistant/components/sofar/__init__.py +++ b/homeassistant/components/sofar/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import TYPE_CHECKING -from modbus_connection import ModbusError, ModbusTcpParams +from modbus_connection import ModbusError from sofar_modbus.modern.device import ( BATTERY_STRING_COMPONENTS, SofarInverter, @@ -18,7 +18,7 @@ from homeassistant.components.sensor import ( SensorExtraStoredData, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_TYPE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import ( @@ -35,8 +35,10 @@ from .const import ( METER_ENERGY, SCAN_INTERVAL, SETTINGS_SCAN_INTERVAL, + TYPE_TCP, ) from .coordinator import SofarConfigEntry, SofarDataUpdateCoordinator, SofarRuntimeData +from .helpers import create_modbus_params from .sensor import SENSOR_DESCRIPTIONS from .services import async_setup_services @@ -143,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> boo unit = async_get_unit( hass, entry, - ModbusTcpParams(host=entry.data[CONF_HOST], port=entry.data[CONF_PORT]), + create_modbus_params(entry.data), entry.data[CONF_UNIT_ID], ) @@ -233,6 +235,15 @@ async def async_remove_config_entry_device( return True +async def async_migrate_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> bool: + """Migrate an old config entry.""" + if entry.version == 1 and entry.minor_version == 1: + hass.config_entries.async_update_entry( + entry, data={CONF_TYPE: TYPE_TCP, **entry.data}, minor_version=2 + ) + return True + + async def async_unload_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sofar/config_flow.py b/homeassistant/components/sofar/config_flow.py index c67fdebb6c76..8ef0c57b8057 100644 --- a/homeassistant/components/sofar/config_flow.py +++ b/homeassistant/components/sofar/config_flow.py @@ -4,27 +4,47 @@ from collections.abc import Mapping import logging from typing import Any, override -from modbus_connection import ModbusError, ModbusTcpParams +from modbus_connection import ModbusError import probatio from sofar_modbus.modern.device import SofarInverter from homeassistant.components.modbus import async_get_temporary_unit from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, NumberSelectorMode, + SerialPortSelector, TextSelector, ) -from .const import CONF_UNIT_ID, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_UNIT_ID, DOMAIN +from .const import ( + CONF_BAUDRATE, + CONF_UNIT_ID, + DEFAULT_BAUDRATE, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_UNIT_ID, + DOMAIN, + TYPE_SERIAL, + TYPE_TCP, +) +from .helpers import create_modbus_params _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = probatio.Schema( +STEP_RECONFIGURE_SERIAL = "reconfigure_serial" +STEP_RECONFIGURE_TCP = "reconfigure_tcp" + +UNIT_ID_SELECTOR = probatio.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1, max=247)), + probatio.Coerce(int), +) + +STEP_TCP_DATA_SCHEMA = probatio.Schema( { probatio.Required(CONF_HOST): TextSelector(), probatio.Required(CONF_PORT, default=DEFAULT_PORT): probatio.All( @@ -33,19 +53,25 @@ STEP_USER_DATA_SCHEMA = probatio.Schema( ), probatio.Coerce(int), ), - probatio.Required(CONF_UNIT_ID, default=DEFAULT_UNIT_ID): probatio.All( - NumberSelector( - NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1, max=247) - ), + probatio.Required(CONF_UNIT_ID, default=DEFAULT_UNIT_ID): UNIT_ID_SELECTOR, + } +) + +STEP_SERIAL_DATA_SCHEMA = probatio.Schema( + { + probatio.Required(CONF_DEVICE): SerialPortSelector(), + probatio.Required(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): probatio.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1)), probatio.Coerce(int), ), + probatio.Required(CONF_UNIT_ID, default=DEFAULT_UNIT_ID): UNIT_ID_SELECTOR, } ) async def _async_probe(hass: HomeAssistant, data: Mapping[str, Any]) -> SofarInverter: """Connect to the inverter and read its identity, or raise.""" - params = ModbusTcpParams(host=data[CONF_HOST], port=data[CONF_PORT]) + params = create_modbus_params(data) async with async_get_temporary_unit(hass, params, data[CONF_UNIT_ID]) as unit: device = await SofarInverter.async_detect(unit) await device.async_update() @@ -56,28 +82,53 @@ class SofarConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Sofar config flow.""" VERSION = 1 + MINOR_VERSION = 2 @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the initial connection step.""" + """Let the user pick how the inverter is reached.""" + return self.async_show_menu( + step_id="user", menu_options=[TYPE_TCP, TYPE_SERIAL] + ) + + async def async_step_tcp( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle an inverter reached over the network.""" + return await self._async_step_link(TYPE_TCP, STEP_TCP_DATA_SCHEMA, user_input) + + async def async_step_serial( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle an inverter reached over a serial port.""" + return await self._async_step_link( + TYPE_SERIAL, STEP_SERIAL_DATA_SCHEMA, user_input + ) + + async def _async_step_link( + self, + connection_type: str, + schema: probatio.Schema, + user_input: dict[str, Any] | None, + ) -> ConfigFlowResult: + """Ask for the link settings, then probe the inverter behind them.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} if user_input is not None: - device, errors, description_placeholders = await self._async_validate( - user_input - ) + data = {CONF_TYPE: connection_type, **user_input} + device, errors, description_placeholders = await self._async_validate(data) if device is not None: await self.async_set_unique_id(device.serial_number) self._abort_if_unique_id_configured() return self.async_create_entry( - title=device.model or DEFAULT_NAME, data=user_input + title=device.model or DEFAULT_NAME, data=data ) return self.async_show_form( - step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, + step_id=connection_type, + data_schema=schema, errors=errors, description_placeholders=description_placeholders, ) @@ -86,24 +137,50 @@ class SofarConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle updating an existing entry's connection details.""" - reconfigure_entry = self._get_reconfigure_entry() + entry = self._get_reconfigure_entry() + if entry.data.get(CONF_TYPE, TYPE_TCP) == TYPE_SERIAL: + return await self.async_step_reconfigure_serial() + return await self.async_step_reconfigure_tcp() + + async def async_step_reconfigure_tcp( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle updating an inverter reached over the network.""" + return await self._async_step_reconfigure_link( + STEP_RECONFIGURE_TCP, TYPE_TCP, STEP_TCP_DATA_SCHEMA, user_input + ) + + async def async_step_reconfigure_serial( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle updating an inverter reached over a serial port.""" + return await self._async_step_reconfigure_link( + STEP_RECONFIGURE_SERIAL, TYPE_SERIAL, STEP_SERIAL_DATA_SCHEMA, user_input + ) + + async def _async_step_reconfigure_link( + self, + step_id: str, + connection_type: str, + schema: probatio.Schema, + user_input: dict[str, Any] | None, + ) -> ConfigFlowResult: + """Ask for the link settings, then probe the entry's own inverter.""" + entry = self._get_reconfigure_entry() errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} if user_input is not None: - device, errors, description_placeholders = await self._async_validate( - user_input - ) + data = {CONF_TYPE: connection_type, **user_input} + device, errors, description_placeholders = await self._async_validate(data) if device is not None: await self.async_set_unique_id(device.serial_number) self._abort_if_unique_id_mismatch() - return self.async_update_reload_and_abort( - reconfigure_entry, data_updates=user_input - ) + return self.async_update_reload_and_abort(entry, data=data) return self.async_show_form( - step_id="reconfigure", + step_id=step_id, data_schema=self.add_suggested_values_to_schema( - STEP_USER_DATA_SCHEMA, user_input or reconfigure_entry.data + schema, user_input or entry.data ), errors=errors, description_placeholders=description_placeholders, diff --git a/homeassistant/components/sofar/const.py b/homeassistant/components/sofar/const.py index f9a8b6e2501d..5f764ecc645c 100644 --- a/homeassistant/components/sofar/const.py +++ b/homeassistant/components/sofar/const.py @@ -4,11 +4,16 @@ DOMAIN = "sofar" ATTR_MANUFACTURER = "Sofar Solar" DEFAULT_NAME = "Sofar" +DEFAULT_BAUDRATE = 9600 DEFAULT_PORT = 502 DEFAULT_UNIT_ID = 1 SCAN_INTERVAL = 5 SETTINGS_SCAN_INTERVAL = 60 +CONF_BAUDRATE = "baudrate" CONF_UNIT_ID = "unit_id" METER_ENERGY = "meter_energy" + +TYPE_SERIAL = "serial" +TYPE_TCP = "tcp" diff --git a/homeassistant/components/sofar/helpers.py b/homeassistant/components/sofar/helpers.py new file mode 100644 index 000000000000..a16e388a6b47 --- /dev/null +++ b/homeassistant/components/sofar/helpers.py @@ -0,0 +1,21 @@ +"""Helpers for the Sofar integration.""" + +from collections.abc import Mapping +from typing import Any + +from modbus_connection import ModbusSerialParams, ModbusTcpParams + +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE + +from .const import CONF_BAUDRATE, TYPE_SERIAL + + +def create_modbus_params( + data: Mapping[str, Any], +) -> ModbusSerialParams | ModbusTcpParams: + """Build the Modbus link parameters from config entry data.""" + if data[CONF_TYPE] == TYPE_SERIAL: + return ModbusSerialParams( + device=data[CONF_DEVICE], baudrate=data[CONF_BAUDRATE] + ) + return ModbusTcpParams(host=data[CONF_HOST], port=data[CONF_PORT]) diff --git a/homeassistant/components/sofar/manifest.json b/homeassistant/components/sofar/manifest.json index 016f2cf0d4b2..0b6d29c62f43 100644 --- a/homeassistant/components/sofar/manifest.json +++ b/homeassistant/components/sofar/manifest.json @@ -3,7 +3,7 @@ "name": "Sofar", "codeowners": ["@darkrain-nl"], "config_flow": true, - "dependencies": ["modbus"], + "dependencies": ["modbus", "usb"], "documentation": "https://www.home-assistant.io/integrations/sofar", "integration_type": "device", "iot_class": "local_polling", diff --git a/homeassistant/components/sofar/quality_scale.yaml b/homeassistant/components/sofar/quality_scale.yaml index 05ebfeba13d6..73cceed7f3d7 100644 --- a/homeassistant/components/sofar/quality_scale.yaml +++ b/homeassistant/components/sofar/quality_scale.yaml @@ -40,7 +40,7 @@ rules: parallel-updates: done reauthentication-flow: status: exempt - comment: Local Modbus TCP; there is no authentication that can expire or be invalidated. + comment: A local Modbus link has no authentication that can expire or be invalidated. test-coverage: done # Gold @@ -48,10 +48,10 @@ rules: diagnostics: done discovery-update-info: status: exempt - comment: Modbus TCP gateways have no discovery protocol to update network info from. + comment: Modbus devices have no discovery protocol to update connection info from. discovery: status: exempt - comment: Modbus TCP gateways are not announced over any discoverable protocol. + comment: Modbus devices are not announced over any discoverable protocol. docs-data-update: done docs-examples: done docs-known-limitations: done @@ -76,5 +76,5 @@ rules: async-dependency: done inject-websession: status: exempt - comment: This integration communicates over Modbus TCP, not HTTP. + comment: This integration communicates over Modbus, not HTTP. strict-typing: done diff --git a/homeassistant/components/sofar/strings.json b/homeassistant/components/sofar/strings.json index 48113aaa6198..cfa77386f617 100644 --- a/homeassistant/components/sofar/strings.json +++ b/homeassistant/components/sofar/strings.json @@ -9,19 +9,46 @@ "unrecognized_inverter": "The device answered, but its serial number doesn't match a known Sofar model." }, "step": { - "reconfigure": { + "reconfigure_serial": { + "data": { + "baudrate": "[%key:component::sofar::config::step::serial::data::baudrate%]", + "device": "[%key:component::sofar::config::step::serial::data::device%]", + "unit_id": "[%key:component::sofar::config::step::tcp::data::unit_id%]" + }, + "data_description": { + "baudrate": "[%key:component::sofar::config::step::serial::data_description::baudrate%]", + "device": "[%key:component::sofar::config::step::serial::data_description::device%]", + "unit_id": "[%key:component::sofar::config::step::tcp::data_description::unit_id%]" + }, + "description": "Reach this inverter over a serial port wired to its RS485 terminals." + }, + "reconfigure_tcp": { "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", - "unit_id": "Modbus unit ID" + "unit_id": "[%key:component::sofar::config::step::tcp::data::unit_id%]" }, "data_description": { - "host": "[%key:component::sofar::config::step::user::data_description::host%]", - "port": "[%key:component::sofar::config::step::user::data_description::port%]", - "unit_id": "[%key:component::sofar::config::step::user::data_description::unit_id%]" - } + "host": "[%key:component::sofar::config::step::tcp::data_description::host%]", + "port": "[%key:component::sofar::config::step::tcp::data_description::port%]", + "unit_id": "[%key:component::sofar::config::step::tcp::data_description::unit_id%]" + }, + "description": "Reach this inverter over the network, either directly or through a Modbus TCP bridge." }, - "user": { + "serial": { + "data": { + "baudrate": "Baud rate", + "device": "Serial port", + "unit_id": "[%key:component::sofar::config::step::tcp::data::unit_id%]" + }, + "data_description": { + "baudrate": "The speed of the RS485 bus, as set on the inverter. The Sofar default is 9600.", + "device": "The serial port the inverter's RS485 bus is wired to. Ports shared over the network by a serial proxy, such as an ESPHome device, are listed here alongside the local ones.", + "unit_id": "[%key:component::sofar::config::step::tcp::data_description::unit_id%]" + }, + "description": "Set up an inverter wired to a serial port over RS485. The inverter model and register map are detected automatically from its serial number." + }, + "tcp": { "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", @@ -32,7 +59,14 @@ "port": "The Modbus TCP port to connect on.", "unit_id": "The inverter's Modbus unit ID, also called its Modbus device address." }, - "description": "Connect over Modbus TCP. The inverter model and register map are detected automatically from its serial number." + "description": "Set up an inverter reached over the network, either directly or through a Modbus TCP bridge. The inverter model and register map are detected automatically from its serial number." + }, + "user": { + "description": "A Sofar inverter can be set up in Home Assistant in two different ways.\n\nYou can reach it over the network, which covers a Modbus TCP bridge or a stick that speaks Modbus TCP, or over a serial port its RS485 terminals are wired to, which covers ports shared over the network by a serial proxy.", + "menu_options": { + "serial": "Serial port (Modbus RTU)", + "tcp": "Network (Modbus TCP)" + } } } }, diff --git a/tests/components/sofar/__init__.py b/tests/components/sofar/__init__.py index c79663144f74..084a6aec0bd0 100644 --- a/tests/components/sofar/__init__.py +++ b/tests/components/sofar/__init__.py @@ -2,8 +2,14 @@ from modbus_connection.mock import MockModbusUnit -from homeassistant.components.sofar.const import CONF_UNIT_ID -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.sofar.const import ( + CONF_BAUDRATE, + CONF_UNIT_ID, + DEFAULT_BAUDRATE, + TYPE_SERIAL, + TYPE_TCP, +) +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE MOCK_SERIAL = "SS2ES104N5S445" MOCK_MODEL = "4.4 KTLX-G3" @@ -13,12 +19,21 @@ MOCK_HYBRID_MODEL = "HYDxxKTL-3P" MOCK_HW_VERSION = "V100" MOCK_SW_VERSION = "V220" -MOCK_USER_INPUT = { +MOCK_TCP_INPUT = { CONF_HOST: "192.168.1.100", CONF_PORT: 502, CONF_UNIT_ID: 1, } +MOCK_ENTRY_DATA = {CONF_TYPE: TYPE_TCP, **MOCK_TCP_INPUT} + +MOCK_SERIAL_INPUT = { + CONF_DEVICE: "/dev/ttyUSB0", + CONF_BAUDRATE: DEFAULT_BAUDRATE, + CONF_UNIT_ID: 1, +} +MOCK_SERIAL_ENTRY_DATA = {CONF_TYPE: TYPE_SERIAL, **MOCK_SERIAL_INPUT} + def _seed_string(unit: MockModbusUnit, address: int, words: int, text: str) -> None: """Encode ASCII across consecutive registers, two characters each.""" diff --git a/tests/components/sofar/conftest.py b/tests/components/sofar/conftest.py index de0a7b5834f0..e145cbe1f5a3 100644 --- a/tests/components/sofar/conftest.py +++ b/tests/components/sofar/conftest.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components.sofar.const import DOMAIN from homeassistant.core import HomeAssistant -from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter +from . import MOCK_ENTRY_DATA, MOCK_MODEL, MOCK_SERIAL, seed_pv_inverter from tests.common import MockConfigEntry @@ -39,7 +39,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, unique_id=MOCK_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_MODEL, ) diff --git a/tests/components/sofar/test_binary_sensor.py b/tests/components/sofar/test_binary_sensor.py index ea03662699c5..c3d92fcfda28 100644 --- a/tests/components/sofar/test_binary_sensor.py +++ b/tests/components/sofar/test_binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter +from . import MOCK_ENTRY_DATA, MOCK_MODEL, MOCK_SERIAL, seed_pv_inverter from tests.common import MockConfigEntry, snapshot_platform @@ -29,7 +29,7 @@ async def test_all_entities( connection = MockModbusConnection() seed_pv_inverter(connection.for_unit(1)) entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT, title=MOCK_MODEL + domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA, title=MOCK_MODEL ) entry.add_to_hass(hass) with ( diff --git a/tests/components/sofar/test_button.py b/tests/components/sofar/test_button.py index 3a3a7bbf0c11..1e59dc68cd08 100644 --- a/tests/components/sofar/test_button.py +++ b/tests/components/sofar/test_button.py @@ -16,11 +16,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import ( + MOCK_ENTRY_DATA, MOCK_HYBRID_MODEL, MOCK_HYBRID_SERIAL, MOCK_MODEL, MOCK_SERIAL, - MOCK_USER_INPUT, seed_hybrid_inverter, seed_pv_inverter, ) @@ -39,7 +39,7 @@ async def _setup( connection = MockModbusConnection() seed(connection.for_unit(1)) entry = MockConfigEntry( - domain=DOMAIN, unique_id=serial, data=MOCK_USER_INPUT, title=model + domain=DOMAIN, unique_id=serial, data=MOCK_ENTRY_DATA, title=model ) entry.add_to_hass(hass) with ( diff --git a/tests/components/sofar/test_config_flow.py b/tests/components/sofar/test_config_flow.py index 5652543aee52..ded855ec822b 100644 --- a/tests/components/sofar/test_config_flow.py +++ b/tests/components/sofar/test_config_flow.py @@ -2,20 +2,40 @@ from collections.abc import AsyncIterator, Callable from contextlib import asynccontextmanager +from typing import Any from unittest.mock import AsyncMock, _patch, patch -from modbus_connection import ModbusTcpParams, ModbusTimeoutError +from modbus_connection import ModbusSerialParams, ModbusTcpParams, ModbusTimeoutError from modbus_connection.mock import MockModbusConnection, MockModbusUnit import pytest from homeassistant import config_entries -from homeassistant.components.sofar.const import DEFAULT_NAME, DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.components.sofar.config_flow import ( + STEP_RECONFIGURE_SERIAL, + STEP_RECONFIGURE_TCP, +) +from homeassistant.components.sofar.const import ( + CONF_BAUDRATE, + DEFAULT_NAME, + DOMAIN, + TYPE_SERIAL, + TYPE_TCP, +) +from homeassistant.config_entries import ConfigEntryDisabler, ConfigFlowResult +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError -from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter +from . import ( + MOCK_ENTRY_DATA, + MOCK_MODEL, + MOCK_SERIAL, + MOCK_SERIAL_ENTRY_DATA, + MOCK_SERIAL_INPUT, + MOCK_TCP_INPUT, + seed_pv_inverter, +) from tests.common import MockConfigEntry, get_schema_suggested_value @@ -28,7 +48,9 @@ def _patch_temporary_unit(connection: MockModbusConnection) -> _patch: @asynccontextmanager async def _get_temporary_unit( - hass: HomeAssistant, params: ModbusTcpParams, unit_id: int + hass: HomeAssistant, + params: ModbusSerialParams | ModbusTcpParams, + unit_id: int, ) -> AsyncIterator[MockModbusUnit]: yield connection.for_unit(unit_id) @@ -52,34 +74,66 @@ class _RaisingTemporaryUnit: """No cleanup: entry never succeeded.""" -async def test_user_step_shows_form(hass: HomeAssistant) -> None: - """Test the initial form renders with no errors before any input.""" +async def _start_flow(hass: HomeAssistant, connection_type: str) -> str: + """Open the flow, pick a connection type, and return the flow ID.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is FlowResultType.FORM + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "user" + assert result["menu_options"] == [TYPE_TCP, TYPE_SERIAL] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": connection_type} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == connection_type assert result["errors"] == {} + return result["flow_id"] + +@pytest.mark.parametrize( + ("connection_type", "user_input", "expected_params", "expected_data"), + [ + pytest.param( + TYPE_TCP, + {**MOCK_TCP_INPUT, CONF_PORT: 1502}, + ModbusTcpParams(host="192.168.1.100", port=1502), + {**MOCK_ENTRY_DATA, CONF_PORT: 1502}, + id="tcp", + ), + pytest.param( + TYPE_SERIAL, + {**MOCK_SERIAL_INPUT, CONF_BAUDRATE: 19200}, + ModbusSerialParams(device="/dev/ttyUSB0", baudrate=19200), + {**MOCK_SERIAL_ENTRY_DATA, CONF_BAUDRATE: 19200}, + id="serial", + ), + ], +) async def test_user_step_success( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + connection_type: str, + user_input: dict[str, Any], + expected_params: ModbusSerialParams | ModbusTcpParams, + expected_data: dict[str, Any], ) -> None: - """Test successful user flow creating a config entry.""" + """Test each connection type probes the inverter and creates an entry.""" mock_conn = MockModbusConnection() seed_pv_inverter(mock_conn.for_unit(1)) + flow_id = await _start_flow(hass, connection_type) - with _patch_temporary_unit(mock_conn): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_USER_INPUT, - ) + with _patch_temporary_unit(mock_conn) as mock_temporary_unit: + result = await hass.config_entries.flow.async_configure(flow_id, user_input) + mock_temporary_unit.assert_called_once_with(hass, expected_params, 1) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_MODEL - assert result["data"] == MOCK_USER_INPUT + assert result["data"] == expected_data assert result["result"].unique_id == MOCK_SERIAL assert len(mock_setup_entry.mock_calls) == 1 @@ -90,17 +144,14 @@ async def test_user_step_success_without_model( """Test the flow falls back to the default title for an unknown model.""" mock_conn = MockModbusConnection() seed_pv_inverter(mock_conn.for_unit(1), serial=_UNMODELED_SERIAL) + flow_id = await _start_flow(hass, TYPE_TCP) with _patch_temporary_unit(mock_conn): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_USER_INPUT, - ) + result = await hass.config_entries.flow.async_configure(flow_id, MOCK_TCP_INPUT) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME - assert result["data"] == MOCK_USER_INPUT + assert result["data"] == MOCK_ENTRY_DATA assert result["result"].unique_id == _UNMODELED_SERIAL @@ -139,16 +190,13 @@ async def test_user_step_errors( """Test the user step reports the right error and recovers, per failure.""" mock_conn = MockModbusConnection() seed(mock_conn.for_unit(1)) + flow_id = await _start_flow(hass, TYPE_TCP) with _patch_temporary_unit(mock_conn): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_USER_INPUT, - ) + result = await hass.config_entries.flow.async_configure(flow_id, MOCK_TCP_INPUT) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == TYPE_TCP assert result["errors"] == {"base": expected_error} assert result["description_placeholders"] == expected_placeholders @@ -156,9 +204,7 @@ async def test_user_step_errors( seed_pv_inverter(working_conn.for_unit(1)) with _patch_temporary_unit(working_conn): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT - ) + result = await hass.config_entries.flow.async_configure(flow_id, MOCK_TCP_INPUT) assert result["type"] is FlowResultType.CREATE_ENTRY @@ -171,15 +217,13 @@ async def test_user_step_link_settings_conflict( "Modbus device ('192.168.1.100', 502) is already in use with different " "link settings" ) + flow_id = await _start_flow(hass, TYPE_TCP) + with patch( "homeassistant.components.sofar.config_flow.async_get_temporary_unit", return_value=_RaisingTemporaryUnit(error), ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_USER_INPUT, - ) + result = await hass.config_entries.flow.async_configure(flow_id, MOCK_TCP_INPUT) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -188,67 +232,119 @@ async def test_user_step_link_settings_conflict( async def test_user_step_already_configured(hass: HomeAssistant) -> None: """Test aborting when the inverter is already configured.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT) + entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA) entry.add_to_hass(hass) mock_conn = MockModbusConnection() seed_pv_inverter(mock_conn.for_unit(1)) + flow_id = await _start_flow(hass, TYPE_TCP) with _patch_temporary_unit(mock_conn): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_USER_INPUT, - ) + result = await hass.config_entries.flow.async_configure(flow_id, MOCK_TCP_INPUT) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -_NEW_USER_INPUT = {**MOCK_USER_INPUT, CONF_HOST: "192.168.1.200"} +_NEW_TCP_INPUT = {**MOCK_TCP_INPUT, CONF_HOST: "192.168.1.200"} + + +async def _start_reconfigure( + hass: HomeAssistant, entry: MockConfigEntry, step_id: str +) -> ConfigFlowResult: + """Open the reconfigure flow and return the form for the entry's link.""" + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == step_id + + return result async def test_reconfigure_updates_the_entry( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test reconfigure updates the entry and reloads it.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT) + entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA) entry.add_to_hass(hass) - result = await entry.start_reconfigure_flow(hass) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reconfigure" + result = await _start_reconfigure(hass, entry, STEP_RECONFIGURE_TCP) mock_conn = MockModbusConnection() seed_pv_inverter(mock_conn.for_unit(1)) with _patch_temporary_unit(mock_conn): result = await hass.config_entries.flow.async_configure( - result["flow_id"], _NEW_USER_INPUT + result["flow_id"], _NEW_TCP_INPUT ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert entry.data == _NEW_USER_INPUT + assert entry.data == {CONF_TYPE: TYPE_TCP, **_NEW_TCP_INPUT} + + +async def test_reconfigure_offers_the_current_settings( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test the link an entry is on comes back prefilled.""" + entry = _serial_entry() + entry.add_to_hass(hass) + result = await _start_reconfigure(hass, entry, STEP_RECONFIGURE_SERIAL) + + assert ( + get_schema_suggested_value(result["data_schema"].schema, CONF_DEVICE) + == MOCK_SERIAL_INPUT[CONF_DEVICE] + ) + + mock_conn = MockModbusConnection() + seed_pv_inverter(mock_conn.for_unit(1)) + new_input = {**MOCK_SERIAL_INPUT, CONF_DEVICE: "/dev/ttyUSB1"} + + with _patch_temporary_unit(mock_conn): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], new_input + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == {CONF_TYPE: TYPE_SERIAL, **new_input} + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reconfigure_unmigrated_entry(hass: HomeAssistant) -> None: + """Test a disabled entry that skipped migration is treated as TCP.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL, + data=MOCK_TCP_INPUT, + minor_version=1, + disabled_by=ConfigEntryDisabler.USER, + ) + entry.add_to_hass(hass) + result = await _start_reconfigure(hass, entry, STEP_RECONFIGURE_TCP) + + assert ( + get_schema_suggested_value(result["data_schema"].schema, CONF_HOST) + == MOCK_TCP_INPUT[CONF_HOST] + ) async def test_reconfigure_rejects_a_different_serial(hass: HomeAssistant) -> None: """Test reconfigure aborts if the inverter's serial doesn't match.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT) + entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA) entry.add_to_hass(hass) - result = await entry.start_reconfigure_flow(hass) + result = await _start_reconfigure(hass, entry, STEP_RECONFIGURE_TCP) mock_conn = MockModbusConnection() seed_pv_inverter(mock_conn.for_unit(1), serial=_UNMODELED_SERIAL) with _patch_temporary_unit(mock_conn): result = await hass.config_entries.flow.async_configure( - result["flow_id"], _NEW_USER_INPUT + result["flow_id"], _NEW_TCP_INPUT ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unique_id_mismatch" - assert entry.data == MOCK_USER_INPUT + assert entry.data == MOCK_ENTRY_DATA @pytest.mark.parametrize( @@ -276,27 +372,27 @@ async def test_reconfigure_errors( expected_placeholders: dict[str, str], ) -> None: """Test the reconfigure step reports the right error and recovers.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT) + entry = MockConfigEntry(domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA) entry.add_to_hass(hass) - result = await entry.start_reconfigure_flow(hass) + result = await _start_reconfigure(hass, entry, STEP_RECONFIGURE_TCP) mock_conn = MockModbusConnection() seed(mock_conn.for_unit(1)) with _patch_temporary_unit(mock_conn): result = await hass.config_entries.flow.async_configure( - result["flow_id"], _NEW_USER_INPUT + result["flow_id"], _NEW_TCP_INPUT ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reconfigure" + assert result["step_id"] == STEP_RECONFIGURE_TCP assert result["errors"] == {"base": expected_error} assert result["description_placeholders"] == expected_placeholders - assert entry.data == MOCK_USER_INPUT + assert entry.data == MOCK_ENTRY_DATA # The retry starts from what was typed, not from the stored entry. assert ( get_schema_suggested_value(result["data_schema"].schema, CONF_HOST) - == _NEW_USER_INPUT[CONF_HOST] + == _NEW_TCP_INPUT[CONF_HOST] ) working_conn = MockModbusConnection() @@ -304,9 +400,20 @@ async def test_reconfigure_errors( with _patch_temporary_unit(working_conn): result = await hass.config_entries.flow.async_configure( - result["flow_id"], _NEW_USER_INPUT + result["flow_id"], _NEW_TCP_INPUT ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert entry.data == _NEW_USER_INPUT + assert entry.data == {CONF_TYPE: TYPE_TCP, **_NEW_TCP_INPUT} + + +def _serial_entry() -> MockConfigEntry: + """A config entry for an inverter on a serial port.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL, + data=MOCK_SERIAL_ENTRY_DATA, + title=MOCK_MODEL, + minor_version=2, + ) diff --git a/tests/components/sofar/test_init.py b/tests/components/sofar/test_init.py index 8ff9ddb2dcec..85d7f8e07955 100644 --- a/tests/components/sofar/test_init.py +++ b/tests/components/sofar/test_init.py @@ -2,33 +2,44 @@ from collections.abc import Callable from datetime import timedelta +from typing import Any from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory -from modbus_connection import ModbusConnectionError, ModbusError, ModbusTimeoutError +from modbus_connection import ( + ModbusConnectionError, + ModbusError, + ModbusSerialParams, + ModbusTcpParams, + ModbusTimeoutError, +) from modbus_connection.mock import MockModbusConnection, MockModbusUnit import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sofar.const import ( + CONF_BAUDRATE, DOMAIN, SCAN_INTERVAL, SETTINGS_SCAN_INTERVAL, ) from homeassistant.components.sofar.coordinator import SofarRuntimeData from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import CONF_PORT, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import ( + MOCK_ENTRY_DATA, MOCK_HW_VERSION, MOCK_HYBRID_MODEL, MOCK_HYBRID_SERIAL, + MOCK_MODEL, MOCK_SERIAL, + MOCK_SERIAL_ENTRY_DATA, MOCK_SW_VERSION, - MOCK_USER_INPUT, + MOCK_TCP_INPUT, deny_meter_energy, seed_hybrid_inverter, serve_meter_energy, @@ -91,6 +102,45 @@ async def test_setup_and_unload_entry( assert entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.parametrize( + ("data", "expected_params"), + [ + pytest.param( + {**MOCK_ENTRY_DATA, CONF_PORT: 1502}, + ModbusTcpParams(host="192.168.1.100", port=1502), + id="tcp", + ), + pytest.param( + {**MOCK_SERIAL_ENTRY_DATA, CONF_BAUDRATE: 19200}, + ModbusSerialParams(device="/dev/ttyUSB0", baudrate=19200), + id="serial", + ), + ], +) +async def test_setup_connects_over_the_configured_link( + hass: HomeAssistant, + mock_connection: MockModbusConnection, + data: dict[str, Any], + expected_params: ModbusSerialParams | ModbusTcpParams, +) -> None: + """Test setup opens the link the entry is configured for.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_SERIAL, data=data, title=MOCK_MODEL + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( + unit_id + ), + ) as mock_get_unit: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + mock_get_unit.assert_called_once_with(hass, entry, expected_params, 1) + + @pytest.mark.parametrize("key", ["serial_number", "waiting_time"]) async def test_setup_removes_stale_sensor_entities( hass: HomeAssistant, @@ -323,7 +373,7 @@ async def test_setup_entry_unrecognized_inverter_raises_setup_error( # outliving a sofar-modbus library downgrade. Caught before any # Modbus I/O, so no connection needs mocking here. entry = MockConfigEntry( - domain=DOMAIN, unique_id="UNRECOGNIZED_SERIAL_XYZ", data=MOCK_USER_INPUT + domain=DOMAIN, unique_id="UNRECOGNIZED_SERIAL_XYZ", data=MOCK_ENTRY_DATA ) entry.add_to_hass(hass) @@ -403,7 +453,7 @@ async def test_settings_recover_without_a_reload( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -462,7 +512,7 @@ async def test_device_versions_need_a_reload_to_recover( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -665,7 +715,7 @@ async def test_only_wired_battery_packs_become_devices( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -761,7 +811,7 @@ async def test_battery_pack_appears_once_its_block_answers( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -793,7 +843,7 @@ async def _setup_hybrid( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -917,3 +967,29 @@ async def test_a_removed_pack_comes_back_without_a_restart( entity_id = entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id) assert entity_id is not None assert hass.states.get(entity_id).state == "51.5" + + +async def test_migrate_entry_adds_the_connection_type( + hass: HomeAssistant, mock_connection: MockModbusConnection +) -> None: + """Test migration marks a legacy entry as TCP.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL, + data=MOCK_TCP_INPUT, + title=MOCK_MODEL, + minor_version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( + unit_id + ), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entry.minor_version == 2 + assert entry.data == MOCK_ENTRY_DATA diff --git a/tests/components/sofar/test_select.py b/tests/components/sofar/test_select.py index bf15621448a6..1319f2fff599 100644 --- a/tests/components/sofar/test_select.py +++ b/tests/components/sofar/test_select.py @@ -18,11 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import ( + MOCK_ENTRY_DATA, MOCK_HYBRID_MODEL, MOCK_HYBRID_SERIAL, MOCK_MODEL, MOCK_SERIAL, - MOCK_USER_INPUT, seed_hybrid_inverter, seed_pv_inverter, ) @@ -39,7 +39,7 @@ async def _setup_hybrid( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -64,7 +64,7 @@ async def _setup_pv( connection = MockModbusConnection() seed_pv_inverter(connection.for_unit(1)) entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT, title=MOCK_MODEL + domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA, title=MOCK_MODEL ) entry.add_to_hass(hass) with ( diff --git a/tests/components/sofar/test_sensor.py b/tests/components/sofar/test_sensor.py index 5d305a94ed7c..26f29b7c23c6 100644 --- a/tests/components/sofar/test_sensor.py +++ b/tests/components/sofar/test_sensor.py @@ -25,11 +25,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import ( + MOCK_ENTRY_DATA, MOCK_HYBRID_MODEL, MOCK_HYBRID_SERIAL, MOCK_MODEL, MOCK_SERIAL, - MOCK_USER_INPUT, seed_hybrid_inverter, seed_pv_inverter, ) @@ -49,7 +49,7 @@ async def test_all_entities( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -128,7 +128,7 @@ async def test_enabled_by_default_partition( connection = MockModbusConnection() seed(connection.for_unit(1)) entry = MockConfigEntry( - domain=DOMAIN, unique_id=serial, data=MOCK_USER_INPUT, title=model + domain=DOMAIN, unique_id=serial, data=MOCK_ENTRY_DATA, title=model ) entry.add_to_hass(hass) with patch( @@ -172,7 +172,7 @@ async def test_settings_backed_sensor_created_and_state( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) @@ -202,7 +202,7 @@ async def test_enum_option_slugs_are_translation_keys( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) diff --git a/tests/components/sofar/test_services.py b/tests/components/sofar/test_services.py index b7d9b85216aa..b65b4bb9b4c6 100644 --- a/tests/components/sofar/test_services.py +++ b/tests/components/sofar/test_services.py @@ -27,9 +27,9 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from . import ( + MOCK_ENTRY_DATA, MOCK_HYBRID_MODEL, MOCK_HYBRID_SERIAL, - MOCK_USER_INPUT, seed_hybrid_inverter, ) @@ -55,7 +55,7 @@ async def _setup_hybrid( entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_HYBRID_SERIAL, - data=MOCK_USER_INPUT, + data=MOCK_ENTRY_DATA, title=MOCK_HYBRID_MODEL, ) entry.add_to_hass(hass) diff --git a/tests/components/sofar/test_switch.py b/tests/components/sofar/test_switch.py index 8f94ca731228..0794f27ddd29 100644 --- a/tests/components/sofar/test_switch.py +++ b/tests/components/sofar/test_switch.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter +from . import MOCK_ENTRY_DATA, MOCK_MODEL, MOCK_SERIAL, seed_pv_inverter from tests.common import MockConfigEntry, snapshot_platform @@ -34,7 +34,7 @@ async def _setup_pv( if remote_on: connection.for_unit(1).holding[0x1104] = 1 entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT, title=MOCK_MODEL + domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_ENTRY_DATA, title=MOCK_MODEL ) entry.add_to_hass(hass) with (