From ebc92b5428f106eb569d6661bb09830e9b5634ad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 13:22:33 +0100 Subject: [PATCH 01/36] Add reconfigure flow --- custom_components/plugwise_usb/config_flow.py | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 654c1863..78cec388 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import usb -from homeassistant.config_entries import SOURCE_USER, ConfigFlow +from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_BASE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -28,7 +28,7 @@ def plugwise_stick_entries(hass): ] -async def validate_usb_connection(self, device_path=None) -> tuple[dict[str, str], str]: +async def validate_usb_connection(self, device_path=None) -> tuple[dict[str, str], str | None]: """Test if device_path is a real Plugwise USB-Stick.""" errors = {} @@ -123,3 +123,49 @@ async def async_step_manual_path( ), errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconfigure_entry = self._get_reconfigure_entry() + + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + list_of_ports = [ + f"{p}, s/n: {p.serial_number or 'n/a'}" + + (f" - {p.manufacturer}" if p.manufacturer else "") + for p in ports + ] + list_of_ports.append(CONF_MANUAL_PATH) + + if user_input is not None: + user_selection = user_input[CONF_USB_PATH] + + if user_selection == CONF_MANUAL_PATH: + return await self.async_step_manual_path() + + port = ports[list_of_ports.index(user_selection)] + device_path = await self.hass.async_add_executor_job( + usb.get_serial_by_id, port.device + ) + errors, mac_stick = await validate_usb_connection(self.hass, device_path) + if not errors: + await self.async_set_unique_id(mac_stick) + self._abort_if_unique_id_mismatch(reason="not_the_same_stick") + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={CONF_USB_PATH: device_path}, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=vol.Schema( + {vol.Required(CONF_USB_PATH): vol.In(list_of_ports)} + ), + suggested_values=reconfigure_entry.data, + ), + description_placeholders={"title": reconfigure_entry.title}, + errors=errors, + ) From f84bd99f1db0317b8f6522627fa9bef4c2396aaf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 14:24:32 +0100 Subject: [PATCH 02/36] Update strings.json and related --- custom_components/plugwise_usb/strings.json | 8 ++++++-- custom_components/plugwise_usb/translations/en.json | 8 ++++++-- custom_components/plugwise_usb/translations/nl.json | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/custom_components/plugwise_usb/strings.json b/custom_components/plugwise_usb/strings.json index bb1ecefc..855b50ea 100644 --- a/custom_components/plugwise_usb/strings.json +++ b/custom_components/plugwise_usb/strings.json @@ -14,6 +14,10 @@ } } }, + "abort": { + "not_the_same_stick": "The configured Stick does not match with the Stick on the entered port", + "reconfigure_successful": "Reconfiguration successful" + }, "error": { "already_configured": "This device is already configured", "cannot_connect": "Failed to connect", @@ -29,7 +33,7 @@ "fields": { "mac": { "name": "MAC address", - "description": "The full 16 character MAC address of the plugwise device." + "description": "The full 16 character MAC address of the plugwise device" } } }, @@ -39,7 +43,7 @@ "fields": { "mac": { "name": "MAC address", - "description": "The full 16 character MAC address of the plugwise device." + "description": "The full 16 character MAC address of the plugwise device" } } } diff --git a/custom_components/plugwise_usb/translations/en.json b/custom_components/plugwise_usb/translations/en.json index c6517192..0004dc0f 100644 --- a/custom_components/plugwise_usb/translations/en.json +++ b/custom_components/plugwise_usb/translations/en.json @@ -14,6 +14,10 @@ } } }, + "abort": { + "not_the_same_stick": "The configured Stick does not match with the Stick on the entered port", + "reconfigure_successful": "Reconfiguration successful" + }, "error": { "already_configured": "This device is already configured", "cannot_connect": "Failed to connect", @@ -29,7 +33,7 @@ "fields": { "mac": { "name": "MAC address", - "description": "The full 16 character MAC address of the plugwise device." + "description": "The full 16 character MAC address of the plugwise device" } } }, @@ -39,7 +43,7 @@ "fields": { "mac": { "name": "MAC address", - "description": "The full 16 character MAC address of the plugwise device." + "description": "The full 16 character MAC address of the plugwise device" } } } diff --git a/custom_components/plugwise_usb/translations/nl.json b/custom_components/plugwise_usb/translations/nl.json index 92436dcb..f60e18a4 100644 --- a/custom_components/plugwise_usb/translations/nl.json +++ b/custom_components/plugwise_usb/translations/nl.json @@ -14,6 +14,10 @@ } } }, + "abort": { + "not_the_same_stick": "De geconfigureerde Stick matcht niet met de Stick van de ingegeven poort", + "reconfigure_successful": "Herconfiguratie succesvol" + }, "error": { "already_configured": "Dit apparaat is al geconfigureerd", "cannot_connect": "Verbinden is mislukt", @@ -29,7 +33,7 @@ "fields": { "mac": { "name": "MAC adres", - "description": "Het volledige MAC address (16 karakters) van het plugwise apparaat." + "description": "Het volledige MAC address (16 karakters) van het plugwise apparaat" } } }, @@ -39,7 +43,7 @@ "fields": { "mac": { "name": "MAC adres", - "description": "Het volledige MAC address (16 karakters) van het plugwise apparaat." + "description": "Het volledige MAC address (16 karakters) van het plugwise apparaat" } } } From 1149093a54dcb5856acc1c040ffc0be79190b9cb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 15:42:52 +0100 Subject: [PATCH 03/36] Add reconfigure testcases --- tests/test_config_flow.py | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 0a4d4e3e..27063054 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -6,6 +6,7 @@ from custom_components.plugwise_usb.config_flow import CONF_MANUAL_PATH from custom_components.plugwise_usb.const import CONF_USB_PATH, DOMAIN +from plugwise_usb.exceptions import StickError from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import FlowResultType, InvalidData @@ -183,3 +184,75 @@ async def test_failed_initialization(hass, mock_usb_stick_init_error: MagicMock) await hass.async_block_till_done() assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "stick_init"} + + +async def _start_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + port: str, +) -> ConfigFlowResult: + """Initialize a reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + + return await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], {CONF_USB_PATH: port} + ) + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry + assert entry.data.get(CONF_HOST) == TEST_USBPORT + + +async def test_reconfigure_flow_other_stick( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_usb_stick: AsyncMock, +) -> None: + """Test reconfigure flow aborts on other Smile ID.""" + mock_usb_stick.mac_stick = TEST_MAC + + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_the_same_stick" + + +@pytest.mark.parametrize( + ("side_effect", "reason"), + [ + (None, "already_configured"), + (StickError, "cannot_connect"), + (StickError, "stick_init"), + ], +) +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + reason: str, +) -> None: + """Test we handle each reconfigure exception error.""" + + mock_smile_adam.connect.side_effect = side_effect + + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": reason} + assert result.get("step_id") == "reconfigure" \ No newline at end of file From 16a7912de4e0cb77b098e7a893ce4494ad6cc3a7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 19:30:37 +0100 Subject: [PATCH 04/36] Test: correct mock-device --- tests/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 27063054..64c3629b 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -244,12 +244,13 @@ async def test_reconfigure_flow_other_stick( async def test_reconfigure_flow_errors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_usb_stick: AsyncMock, side_effect: Exception, reason: str, ) -> None: """Test we handle each reconfigure exception error.""" - mock_smile_adam.connect.side_effect = side_effect + mock_usb_stick.connect.side_effect = side_effect result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) From ee2b0a649bfe35ac0fb219bc319d42852fcb4f2e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 19:34:16 +0100 Subject: [PATCH 05/36] Add missing constant --- tests/test_config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 64c3629b..d147bfa5 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Plugwise config flow.""" +from typing import Final from unittest.mock import MagicMock, patch import pytest @@ -13,8 +14,9 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry import serial.tools.list_ports -TEST_USBPORT = "/dev/ttyUSB1" -TEST_USBPORT2 = "/dev/ttyUSB2" +TEST_MAC: Final[str] = "01:23:45:67:AB" +TEST_USBPORT: Final[str] = "/dev/ttyUSB1" +TEST_USBPORT2: Final[str] = "/dev/ttyUSB2" def com_port(): From 13bf68adb8c5c67ae0a7caea0b611c3d9fe4dc5d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 19:47:22 +0100 Subject: [PATCH 06/36] PORT -> PATH --- tests/test_config_flow.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index d147bfa5..1addde0e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -15,17 +15,17 @@ import serial.tools.list_ports TEST_MAC: Final[str] = "01:23:45:67:AB" -TEST_USBPORT: Final[str] = "/dev/ttyUSB1" -TEST_USBPORT2: Final[str] = "/dev/ttyUSB2" +TEST_PORT_PATH: Final[str] = "/dev/ttyUSB1" +TEST_PORT2_PATH: Final[str] = "/dev/ttyUSB2" def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo(TEST_USBPORT) + port = serial.tools.list_ports_common.ListPortInfo(TEST_PORT_PATH) port.serial_number = "1234" port.manufacturer = "Virtual serial port" - port.device = TEST_USBPORT + port.device = TEST_PORT_PATH port.description = "Some serial port" return port @@ -50,7 +50,7 @@ async def test_user_flow_select(hass, mock_usb_stick: MagicMock): ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_USB_PATH: TEST_USBPORT} + assert result.get("data") == {CONF_USB_PATH: TEST_PORT_PATH} # Retry to ensure configuring the same port is not allowed result = await hass.config_entries.flow.async_init( @@ -95,11 +95,11 @@ async def test_user_flow_manual( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_USB_PATH: TEST_USBPORT2}, + user_input={CONF_USB_PATH: TEST_PORT2_PATH}, ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_USB_PATH: TEST_USBPORT2} + assert result.get("data") == {CONF_USB_PATH: TEST_PORT2_PATH} async def test_invalid_connection(hass): @@ -191,7 +191,7 @@ async def test_failed_initialization(hass, mock_usb_stick_init_error: MagicMock) async def _start_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - port: str, + device_path: str, ) -> ConfigFlowResult: """Initialize a reconfigure flow.""" mock_config_entry.add_to_hass(hass) @@ -202,7 +202,7 @@ async def _start_reconfigure_flow( assert reconfigure_result["step_id"] == "reconfigure" return await hass.config_entries.flow.async_configure( - reconfigure_result["flow_id"], {CONF_USB_PATH: port} + reconfigure_result["flow_id"], {CONF_USB_PATH: device_path} ) @@ -211,14 +211,14 @@ async def test_reconfigure_flow( mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow.""" - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) assert entry - assert entry.data.get(CONF_HOST) == TEST_USBPORT + assert entry.data.get(CONF_HOST) == TEST_PORT_PATH async def test_reconfigure_flow_other_stick( @@ -229,7 +229,7 @@ async def test_reconfigure_flow_other_stick( """Test reconfigure flow aborts on other Smile ID.""" mock_usb_stick.mac_stick = TEST_MAC - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_the_same_stick" @@ -254,7 +254,7 @@ async def test_reconfigure_flow_errors( mock_usb_stick.connect.side_effect = side_effect - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USBPORT) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": reason} From eb477e0964ed54aeaa253689ceda716e2f135318 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 25 Feb 2026 19:53:39 +0100 Subject: [PATCH 07/36] Improve async_step_reconfigure() --- custom_components/plugwise_usb/config_flow.py | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 78cec388..136dbfdd 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -112,7 +112,9 @@ async def async_step_manual_path( ) errors, mac_stick = await validate_usb_connection(self.hass, device_path) if not errors: - await self.async_set_unique_id(mac_stick) + await self.async_set_unique_id( + unique_id=mac_stick, raise_on_progress=False + ) return self.async_create_entry( title="Stick", data={CONF_USB_PATH: device_path} ) @@ -131,40 +133,34 @@ async def async_step_reconfigure( errors: dict[str, str] = {} reconfigure_entry = self._get_reconfigure_entry() - ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - list_of_ports = [ - f"{p}, s/n: {p.serial_number or 'n/a'}" - + (f" - {p.manufacturer}" if p.manufacturer else "") - for p in ports - ] - list_of_ports.append(CONF_MANUAL_PATH) + # ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + # list_of_ports = [ + # f"{p}, s/n: {p.serial_number or 'n/a'}" + # + (f" - {p.manufacturer}" if p.manufacturer else "") + # for p in ports + # ] + # list_of_ports.append(CONF_MANUAL_PATH) if user_input is not None: - user_selection = user_input[CONF_USB_PATH] - - if user_selection == CONF_MANUAL_PATH: - return await self.async_step_manual_path() - - port = ports[list_of_ports.index(user_selection)] - device_path = await self.hass.async_add_executor_job( - usb.get_serial_by_id, port.device - ) - errors, mac_stick = await validate_usb_connection(self.hass, device_path) + full_input = user_input.get(CONF_USB_PATH) + # user_selection = user_input[CONF_USB_PATH] + # port = ports[list_of_ports.index(user_selection)] + # device_path = await self.hass.async_add_executor_job( + # usb.get_serial_by_id, port.device + # ) + errors, mac_stick = await validate_usb_connection(self.hass, full_input) if not errors: - await self.async_set_unique_id(mac_stick) + await self.async_set_unique_id(mac_stick, raise_on_progress=False) self._abort_if_unique_id_mismatch(reason="not_the_same_stick") return self.async_update_reload_and_abort( reconfigure_entry, - data_updates={CONF_USB_PATH: device_path}, + data_updates=full_input, ) return self.async_show_form( step_id="reconfigure", - data_schema=self.add_suggested_values_to_schema( - data_schema=vol.Schema( - {vol.Required(CONF_USB_PATH): vol.In(list_of_ports)} - ), - suggested_values=reconfigure_entry.data, + data_schema=vol.Schema( + {vol.Required(CONF_USB_PATH): str} #vol.In(list_of_ports)} ), description_placeholders={"title": reconfigure_entry.title}, errors=errors, From cfbca3a0074aff204edd74d7d006e3a86ed8d0c2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 08:13:50 +0100 Subject: [PATCH 08/36] Fix test --- tests/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 1addde0e..e6bfbd61 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -213,7 +213,7 @@ async def test_reconfigure_flow( """Test reconfigure flow.""" result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) - assert result["type"] is FlowResultType.ABORT + assert result["type"] is FlowResultType.FORM assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) @@ -231,7 +231,7 @@ async def test_reconfigure_flow_other_stick( result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) - assert result["type"] is FlowResultType.ABORT + assert result["type"] is FlowResultType.FORM assert result["reason"] == "not_the_same_stick" From cb68799e33cf289b1e7de2cb68bbfb6defbf43ae Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 08:27:39 +0100 Subject: [PATCH 09/36] Improve conftest.py --- tests/conftest.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4bd5335b..9cdb66b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry -TEST_MAC: Final[str] = "01:23:45:67:AB" STICK_IMPORT_MOCK: Final[str] = "custom_components.plugwise_usb.config_flow.Stick" - +TEST_MAC: Final[str] = "01:23:45:67:AB" +TEST_USB_PATH: Final[str] = "/dev/ttyUSB1" @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -27,17 +27,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup -TEST_USBPORT = "/dev/ttyUSB1" - - @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return a mocked v1.2 config entry.""" # pw-beta only return MockConfigEntry( domain=DOMAIN, - data={CONF_USB_PATH: TEST_USBPORT}, + data={CONF_USB_PATH: TEST_USB_PATH}, title="plugwise_usb", - unique_id="TEST_USBPORT", + unique_id="TEST_USB_PATH", ) From 16b81dfe7b91577c6ea2c7f499952c0d8caff2d4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 08:29:46 +0100 Subject: [PATCH 10/36] Line up constants --- tests/test_config_flow.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index e6bfbd61..787945b0 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -15,17 +15,17 @@ import serial.tools.list_ports TEST_MAC: Final[str] = "01:23:45:67:AB" -TEST_PORT_PATH: Final[str] = "/dev/ttyUSB1" -TEST_PORT2_PATH: Final[str] = "/dev/ttyUSB2" +TEST_USB_PATH: Final[str] = "/dev/ttyUSB1" +TEST_USB2_PATH: Final[str] = "/dev/ttyUSB2" def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo(TEST_PORT_PATH) + port = serial.tools.list_ports_common.ListPortInfo(TEST_USB_PATH) port.serial_number = "1234" port.manufacturer = "Virtual serial port" - port.device = TEST_PORT_PATH + port.device = TEST_USB_PATH port.description = "Some serial port" return port @@ -50,7 +50,7 @@ async def test_user_flow_select(hass, mock_usb_stick: MagicMock): ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_USB_PATH: TEST_PORT_PATH} + assert result.get("data") == {CONF_USB_PATH: TEST_USB_PATH} # Retry to ensure configuring the same port is not allowed result = await hass.config_entries.flow.async_init( @@ -95,11 +95,11 @@ async def test_user_flow_manual( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_USB_PATH: TEST_PORT2_PATH}, + user_input={CONF_USB_PATH: TEST_USB2_PATH}, ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_USB_PATH: TEST_PORT2_PATH} + assert result.get("data") == {CONF_USB_PATH: TEST_USB2_PATH} async def test_invalid_connection(hass): @@ -211,14 +211,14 @@ async def test_reconfigure_flow( mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow.""" - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) assert result["type"] is FlowResultType.FORM assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) assert entry - assert entry.data.get(CONF_HOST) == TEST_PORT_PATH + assert entry.data.get(CONF_HOST) == TEST_USB_PATH async def test_reconfigure_flow_other_stick( @@ -229,7 +229,7 @@ async def test_reconfigure_flow_other_stick( """Test reconfigure flow aborts on other Smile ID.""" mock_usb_stick.mac_stick = TEST_MAC - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) assert result["type"] is FlowResultType.FORM assert result["reason"] == "not_the_same_stick" @@ -254,7 +254,7 @@ async def test_reconfigure_flow_errors( mock_usb_stick.connect.side_effect = side_effect - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_PORT_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": reason} From 78f3781b3ee19434852b5370250bd3c98e05fb0b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 08:33:23 +0100 Subject: [PATCH 11/36] Try ABORT, add mock_setup_entry, mock_usb_stick --- tests/test_config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 787945b0..781bfa95 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -198,7 +198,7 @@ async def _start_reconfigure_flow( reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) - assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["type"] is FlowResultType.ABORT assert reconfigure_result["step_id"] == "reconfigure" return await hass.config_entries.flow.async_configure( @@ -209,6 +209,8 @@ async def _start_reconfigure_flow( async def test_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, + mock_usb_stick: AsyncMock, ) -> None: """Test reconfigure flow.""" result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) From b8c4519734bb816f6c67cbc91162a39c72846963 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 16:56:06 +0100 Subject: [PATCH 12/36] Add missing imports --- tests/test_config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 781bfa95..240c99d9 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,15 +1,16 @@ """Test the Plugwise config flow.""" from typing import Final -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from custom_components.plugwise_usb.config_flow import CONF_MANUAL_PATH from custom_components.plugwise_usb.const import CONF_USB_PATH, DOMAIN from plugwise_usb.exceptions import StickError -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType, InvalidData from pytest_homeassistant_custom_component.common import MockConfigEntry import serial.tools.list_ports From 73f54c3939f16c371ce6d932c56de03da687189d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 17:01:04 +0100 Subject: [PATCH 13/36] Revert FLOW to ABORT change --- tests/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 240c99d9..12b67be4 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -199,7 +199,7 @@ async def _start_reconfigure_flow( reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) - assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["type"] is FlowResultType.FORM assert reconfigure_result["step_id"] == "reconfigure" return await hass.config_entries.flow.async_configure( From 752488a0bf2772e5c787a01dd68b5c20806e76f6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 17:05:27 +0100 Subject: [PATCH 14/36] Various improvements --- custom_components/plugwise_usb/config_flow.py | 27 +++++++++++++------ tests/conftest.py | 12 ++++++--- tests/test_config_flow.py | 6 ++--- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 136dbfdd..3511d27f 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -17,6 +17,12 @@ from .const import CONF_MANUAL_PATH, CONF_USB_PATH, DOMAIN, MANUAL_PATH +STICK_RECONF_SCHEMA = vol.Schema( + { + vol.Required(CONF_USB_PATH): str, + } +) + @callback def plugwise_stick_entries(hass): @@ -141,26 +147,31 @@ async def async_step_reconfigure( # ] # list_of_ports.append(CONF_MANUAL_PATH) - if user_input is not None: - full_input = user_input.get(CONF_USB_PATH) + if user_input: + device_path = await self.hass.async_add_executor_job( + usb.get_serial_by_id, user_input.get(CONF_USB_PATH) + ) # user_selection = user_input[CONF_USB_PATH] # port = ports[list_of_ports.index(user_selection)] # device_path = await self.hass.async_add_executor_job( # usb.get_serial_by_id, port.device # ) - errors, mac_stick = await validate_usb_connection(self.hass, full_input) - if not errors: - await self.async_set_unique_id(mac_stick, raise_on_progress=False) + errors, mac_stick = await validate_usb_connection(self.hass, device_path) + if mac_stick: + await self.async_set_unique_id( + unique_id=mac_stick, raise_on_progress=False + ) self._abort_if_unique_id_mismatch(reason="not_the_same_stick") return self.async_update_reload_and_abort( reconfigure_entry, - data_updates=full_input, + data_updates={CONF_USB_PATH: device_path} ) return self.async_show_form( step_id="reconfigure", - data_schema=vol.Schema( - {vol.Required(CONF_USB_PATH): str} #vol.In(list_of_ports)} + data_schema=self.add_suggested_values_to_schema( + data_schema=STICK_RECONF_SCHEMA, + suggested_values=reconfigure_entry.data, ), description_placeholders={"title": reconfigure_entry.title}, errors=errors, diff --git a/tests/conftest.py b/tests/conftest.py index 9cdb66b3..928e3749 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ TEST_MAC: Final[str] = "01:23:45:67:AB" TEST_USB_PATH: Final[str] = "/dev/ttyUSB1" + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" @@ -33,8 +34,9 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, data={CONF_USB_PATH: TEST_USB_PATH}, - title="plugwise_usb", - unique_id="TEST_USB_PATH", + minor_version=1, + version=1, + unique_id=TEST_USB_PATH, ) @@ -110,12 +112,16 @@ def mock_usb_stick_init_error() -> Generator[MagicMock]: yield usb -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +async def setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: """Set up the usb integration.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + return config_entry + @pytest.fixture(autouse=True) def auto_enable_custom_integrations(enable_custom_integrations): diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 12b67be4..4f821a4f 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -209,14 +209,14 @@ async def _start_reconfigure_flow( async def test_reconfigure_flow( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_setup_entry: AsyncMock, mock_usb_stick: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow.""" result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) - assert result["type"] is FlowResultType.FORM + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) From 643febeddcdf12da44f300908564f0ef4fadce8a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 17:54:52 +0100 Subject: [PATCH 15/36] Clean up --- custom_components/plugwise_usb/config_flow.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 3511d27f..e3bb6a6a 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -139,23 +139,10 @@ async def async_step_reconfigure( errors: dict[str, str] = {} reconfigure_entry = self._get_reconfigure_entry() - # ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - # list_of_ports = [ - # f"{p}, s/n: {p.serial_number or 'n/a'}" - # + (f" - {p.manufacturer}" if p.manufacturer else "") - # for p in ports - # ] - # list_of_ports.append(CONF_MANUAL_PATH) - if user_input: device_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, user_input.get(CONF_USB_PATH) ) - # user_selection = user_input[CONF_USB_PATH] - # port = ports[list_of_ports.index(user_selection)] - # device_path = await self.hass.async_add_executor_job( - # usb.get_serial_by_id, port.device - # ) errors, mac_stick = await validate_usb_connection(self.hass, device_path) if mac_stick: await self.async_set_unique_id( From 53f9a6af219fc5d35b401b4ac862d2f9ba989fe4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 17:56:18 +0100 Subject: [PATCH 16/36] Fix unique_id, test with different MAC --- tests/conftest.py | 2 +- tests/test_config_flow.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 928e3749..8872e568 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ def mock_config_entry() -> MockConfigEntry: data={CONF_USB_PATH: TEST_USB_PATH}, minor_version=1, version=1, - unique_id=TEST_USB_PATH, + unique_id=TEST_MAC, ) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 4f821a4f..23021a16 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -16,6 +16,7 @@ import serial.tools.list_ports TEST_MAC: Final[str] = "01:23:45:67:AB" +TEST_MAC2: Final[str] = "02:23:45:67:AB" TEST_USB_PATH: Final[str] = "/dev/ttyUSB1" TEST_USB2_PATH: Final[str] = "/dev/ttyUSB2" @@ -214,14 +215,14 @@ async def test_reconfigure_flow( mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow.""" - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB2_PATH) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) assert entry - assert entry.data.get(CONF_HOST) == TEST_USB_PATH + assert entry.data.get(CONF_USB_PATH) == TEST_USB2_PATH async def test_reconfigure_flow_other_stick( @@ -230,11 +231,11 @@ async def test_reconfigure_flow_other_stick( mock_usb_stick: AsyncMock, ) -> None: """Test reconfigure flow aborts on other Smile ID.""" - mock_usb_stick.mac_stick = TEST_MAC + mock_usb_stick.mac_stick = TEST_MAC2 - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB2_PATH) - assert result["type"] is FlowResultType.FORM + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_the_same_stick" @@ -257,7 +258,7 @@ async def test_reconfigure_flow_errors( mock_usb_stick.connect.side_effect = side_effect - result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB2_PATH) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": reason} From 61daf1cd4376f52f942e5d7d12725d29deb0d0cc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 18:23:18 +0100 Subject: [PATCH 17/36] Guard config_entry.runtime_data --- custom_components/plugwise_usb/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/custom_components/plugwise_usb/__init__.py b/custom_components/plugwise_usb/__init__.py index 8a35c6b5..b7e34480 100644 --- a/custom_components/plugwise_usb/__init__.py +++ b/custom_components/plugwise_usb/__init__.py @@ -162,13 +162,15 @@ async def async_unload_entry( hass: HomeAssistant, config_entry: PlugwiseUSBConfigEntry ) -> bool: """Unload the Plugwise USB stick connection.""" - config_entry.runtime_data[UNSUBSCRIBE_DISCOVERY]() - for coordinator in config_entry.runtime_data[NODES].values(): - await coordinator.unsubscribe_all_nodefeatures() + if hasattr(config_entry, "runtime_data"): + config_entry.runtime_data[UNSUBSCRIBE_DISCOVERY]() + for coordinator in config_entry.runtime_data[NODES].values(): + await coordinator.unsubscribe_all_nodefeatures() + await config_entry.runtime_data[STICK].disconnect() + unload = await hass.config_entries.async_unload_platforms( config_entry, PLUGWISE_USB_PLATFORMS ) - await config_entry.runtime_data[STICK].disconnect() return unload From 77c73d5a4eb5066be78347cfb38cc375924aa615 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 20:19:53 +0100 Subject: [PATCH 18/36] Correct logic --- custom_components/plugwise_usb/config_flow.py | 5 ++++- tests/test_config_flow.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index e3bb6a6a..712b82ad 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -99,6 +99,7 @@ async def async_step_user( return self.async_create_entry( title="Stick", data={CONF_USB_PATH: device_path} ) + return self.async_show_form( step_id=SOURCE_USER, data_schema=vol.Schema( @@ -144,7 +145,7 @@ async def async_step_reconfigure( usb.get_serial_by_id, user_input.get(CONF_USB_PATH) ) errors, mac_stick = await validate_usb_connection(self.hass, device_path) - if mac_stick: + if not errors: await self.async_set_unique_id( unique_id=mac_stick, raise_on_progress=False ) @@ -154,6 +155,8 @@ async def async_step_reconfigure( data_updates={CONF_USB_PATH: device_path} ) + self.async_abort(reason="already configured") + return self.async_show_form( step_id="reconfigure", data_schema=self.add_suggested_values_to_schema( diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 23021a16..44e4584f 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -225,6 +225,20 @@ async def test_reconfigure_flow( assert entry.data.get(CONF_USB_PATH) == TEST_USB2_PATH + +async def test_reconfigure_flow_same_path( + hass: HomeAssistant, + mock_usb_stick: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_USB_PATH) + + assert result["type"] is FlowResultType.FORM + assert result.get("errors") == {"base": "already_configured"} + + async def test_reconfigure_flow_other_stick( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -242,9 +256,7 @@ async def test_reconfigure_flow_other_stick( @pytest.mark.parametrize( ("side_effect", "reason"), [ - (None, "already_configured"), (StickError, "cannot_connect"), - (StickError, "stick_init"), ], ) async def test_reconfigure_flow_errors( From 8ac4f135f035e36df5262c6ab043aa275d9a2024 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 20:48:04 +0100 Subject: [PATCH 19/36] Fixes --- custom_components/plugwise_usb/__init__.py | 3 +-- tests/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise_usb/__init__.py b/custom_components/plugwise_usb/__init__.py index b7e34480..9cdb7783 100644 --- a/custom_components/plugwise_usb/__init__.py +++ b/custom_components/plugwise_usb/__init__.py @@ -168,10 +168,9 @@ async def async_unload_entry( await coordinator.unsubscribe_all_nodefeatures() await config_entry.runtime_data[STICK].disconnect() - unload = await hass.config_entries.async_unload_platforms( + return await hass.config_entries.async_unload_platforms( config_entry, PLUGWISE_USB_PLATFORMS ) - return unload async def async_remove_config_entry_device( diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 44e4584f..c5ba56c9 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -274,4 +274,4 @@ async def test_reconfigure_flow_errors( assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": reason} - assert result.get("step_id") == "reconfigure" \ No newline at end of file + assert result.get("step_id") == "reconfigure" From 3c3e0808842f963b801f28a0ca9ca4d2a5ffb514 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Feb 2026 20:53:44 +0100 Subject: [PATCH 20/36] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5588bdbd..404cb37e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- Implement config_flow reconfigure + ## v0.58.2 - 2026-01-29 - Update plugwise_usb to [v0.47.2](https://github.com/plugwise/python-plugwise-usb/releases/tag/v0.47.2) fixing a bug. From 4b856dc4b157f24755c4b2f3bf97892036658b00 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:03:41 +0000 Subject: [PATCH 21/36] Update custom_components/plugwise_usb/__init__.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- custom_components/plugwise_usb/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/custom_components/plugwise_usb/__init__.py b/custom_components/plugwise_usb/__init__.py index 9cdb7783..295aac03 100644 --- a/custom_components/plugwise_usb/__init__.py +++ b/custom_components/plugwise_usb/__init__.py @@ -162,11 +162,16 @@ async def async_unload_entry( hass: HomeAssistant, config_entry: PlugwiseUSBConfigEntry ) -> bool: """Unload the Plugwise USB stick connection.""" - if hasattr(config_entry, "runtime_data"): - config_entry.runtime_data[UNSUBSCRIBE_DISCOVERY]() - for coordinator in config_entry.runtime_data[NODES].values(): + runtime_data = getattr(config_entry, "runtime_data", None) or {} + if runtime_data: + unsubscribe_discovery = runtime_data.get(UNSUBSCRIBE_DISCOVERY) + if callable(unsubscribe_discovery): + unsubscribe_discovery() + for coordinator in runtime_data.get(NODES, {}).values(): await coordinator.unsubscribe_all_nodefeatures() - await config_entry.runtime_data[STICK].disconnect() + stick = runtime_data.get(STICK) + if stick is not None: + await stick.disconnect() return await hass.config_entries.async_unload_platforms( config_entry, PLUGWISE_USB_PLATFORMS From 986511aeca24b9cff49e6a969ab206f9a63ee31b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:07:21 +0000 Subject: [PATCH 22/36] Add missing _abort_if_unique_id_configured() in manual path Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- custom_components/plugwise_usb/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 712b82ad..71679796 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -122,6 +122,7 @@ async def async_step_manual_path( await self.async_set_unique_id( unique_id=mac_stick, raise_on_progress=False ) + self._abort_if_unique_id_configured() return self.async_create_entry( title="Stick", data={CONF_USB_PATH: device_path} ) From a51b2d1f81399a64e7cac2098db1402f60184fd8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:11:53 +0100 Subject: [PATCH 23/36] Add mock_usb_stick_not_setup() fixture --- tests/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 8872e568..0f95da90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,6 +67,21 @@ async def init_integration( # yield [port] +@pytest.fixture +def mock_usb_stick_not_setup() -> Generator[MagicMock]: + """Return a mocked usb_mock.""" + + with patch(STICK_IMPORT_MOCK, autospec=True) as mock_usb: + usb = mock_usb.return_value + + usb.connect = AsyncMock(return_value=None) + usb.initialize = AsyncMock(return_value=None) + usb.disconnect = AsyncMock(return_value=None) + usb.mac_stick = None + + yield usb + + @pytest.fixture def mock_usb_stick() -> Generator[MagicMock]: """Return a mocked usb_mock.""" From 622776c27279996f9155f02c098317ab505c44e0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:12:22 +0100 Subject: [PATCH 24/36] And implement to correct related testcase --- tests/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index c5ba56c9..e76ac312 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -82,7 +82,7 @@ async def test_user_flow_manual_selected_show_form(hass): async def test_user_flow_manual( - hass, mock_usb_stick: MagicMock, init_integration: MockConfigEntry + hass, mock_usb_stick_not_setup: MagicMock, init_integration: MockConfigEntry ): """Test user flow when USB-stick is manually entered.""" From 8f3f04ce17f86b67d3937c58c2d40b1d2788d98c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:16:15 +0100 Subject: [PATCH 25/36] Make one line --- tests/test_config_flow.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index e76ac312..2824ed2e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -254,10 +254,7 @@ async def test_reconfigure_flow_other_stick( @pytest.mark.parametrize( - ("side_effect", "reason"), - [ - (StickError, "cannot_connect"), - ], + ("side_effect", "reason"),[(StickError, "cannot_connect")], ) async def test_reconfigure_flow_errors( hass: HomeAssistant, From 35bd2f18a6ae9da2df6a1ba3cc3595c8eed7e326 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:26:21 +0100 Subject: [PATCH 26/36] Bump to v0.59.0b0 --- custom_components/plugwise_usb/manifest.json | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise_usb/manifest.json b/custom_components/plugwise_usb/manifest.json index d822359d..7e7cc561 100644 --- a/custom_components/plugwise_usb/manifest.json +++ b/custom_components/plugwise_usb/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/plugwise/python-plugwise-usb/issues", "loggers": ["plugwise_usb"], "requirements": ["plugwise-usb==0.47.2"], - "version": "0.58.2" + "version": "0.59.0b0" } diff --git a/pyproject.toml b/pyproject.toml index 0d1e869e..50137c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plugwise_usb-beta" -version = "0.58.2" +version = "0.59.0b0" description = "Plugwise USB custom_component (BETA)" readme = "README.md" requires-python = ">=3.13" From c17b81f78dd560812f3e81c1f3e96eec041cd0c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:29:35 +0100 Subject: [PATCH 27/36] Remove unneeded line --- custom_components/plugwise_usb/config_flow.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 71679796..0685ba28 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -156,8 +156,6 @@ async def async_step_reconfigure( data_updates={CONF_USB_PATH: device_path} ) - self.async_abort(reason="already configured") - return self.async_show_form( step_id="reconfigure", data_schema=self.add_suggested_values_to_schema( From 9caa76cf6bbfcd3dde1819027a2e73bc6299cea0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 08:37:17 +0100 Subject: [PATCH 28/36] Update strings.json and related --- custom_components/plugwise_usb/strings.json | 6 ++++++ custom_components/plugwise_usb/translations/en.json | 6 ++++++ custom_components/plugwise_usb/translations/nl.json | 6 ++++++ tests/test_config_flow.py | 2 +- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/custom_components/plugwise_usb/strings.json b/custom_components/plugwise_usb/strings.json index 855b50ea..61a1a788 100644 --- a/custom_components/plugwise_usb/strings.json +++ b/custom_components/plugwise_usb/strings.json @@ -12,6 +12,12 @@ "data": { "usb_path": "USB-path" } + }, + "reconfigure": { + "data": { + "usb_path": "USB-path" + }, + "description": "Update device-path for {title}." } }, "abort": { diff --git a/custom_components/plugwise_usb/translations/en.json b/custom_components/plugwise_usb/translations/en.json index 0004dc0f..24bf1fa5 100644 --- a/custom_components/plugwise_usb/translations/en.json +++ b/custom_components/plugwise_usb/translations/en.json @@ -12,6 +12,12 @@ "data": { "usb_path": "USB-path" } + }, + "reconfigure": { + "data": { + "usb_path": "USB-path" + }, + "description": "Update device-path for {title}." } }, "abort": { diff --git a/custom_components/plugwise_usb/translations/nl.json b/custom_components/plugwise_usb/translations/nl.json index f60e18a4..a7e80610 100644 --- a/custom_components/plugwise_usb/translations/nl.json +++ b/custom_components/plugwise_usb/translations/nl.json @@ -12,6 +12,12 @@ "data": { "usb_path": "USB-pad" } + }, + "reconfigure": { + "data": { + "usb_path": "USB-pad" + }, + "description": "USB-pad bijwerken voor {title}." } }, "abort": { diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 2824ed2e..14a8b93f 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -3,11 +3,11 @@ from typing import Final from unittest.mock import AsyncMock, MagicMock, patch +from plugwise_usb.exceptions import StickError import pytest from custom_components.plugwise_usb.config_flow import CONF_MANUAL_PATH from custom_components.plugwise_usb.const import CONF_USB_PATH, DOMAIN -from plugwise_usb.exceptions import StickError from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant From 0426c911602daaa622f6dcc4af785732022f27da Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 11:30:44 +0100 Subject: [PATCH 29/36] Improve reconfigure string --- custom_components/plugwise_usb/strings.json | 2 +- custom_components/plugwise_usb/translations/en.json | 2 +- custom_components/plugwise_usb/translations/nl.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise_usb/strings.json b/custom_components/plugwise_usb/strings.json index 61a1a788..3f5dde0a 100644 --- a/custom_components/plugwise_usb/strings.json +++ b/custom_components/plugwise_usb/strings.json @@ -17,7 +17,7 @@ "data": { "usb_path": "USB-path" }, - "description": "Update device-path for {title}." + "description": "Update USB-{title} device-path" } }, "abort": { diff --git a/custom_components/plugwise_usb/translations/en.json b/custom_components/plugwise_usb/translations/en.json index 24bf1fa5..c46635f9 100644 --- a/custom_components/plugwise_usb/translations/en.json +++ b/custom_components/plugwise_usb/translations/en.json @@ -17,7 +17,7 @@ "data": { "usb_path": "USB-path" }, - "description": "Update device-path for {title}." + "description": "Update USB-{title} device-path" } }, "abort": { diff --git a/custom_components/plugwise_usb/translations/nl.json b/custom_components/plugwise_usb/translations/nl.json index a7e80610..12148eb0 100644 --- a/custom_components/plugwise_usb/translations/nl.json +++ b/custom_components/plugwise_usb/translations/nl.json @@ -17,7 +17,7 @@ "data": { "usb_path": "USB-pad" }, - "description": "USB-pad bijwerken voor {title}." + "description": "USB-{title} pad bijwerken" } }, "abort": { From 730ca6165f59b80bed6a15501c8ab240dd8f7ac9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Feb 2026 20:47:00 +0100 Subject: [PATCH 30/36] Use the same format as elsewhere --- custom_components/plugwise_usb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/plugwise_usb/__init__.py b/custom_components/plugwise_usb/__init__.py index 295aac03..f80e882a 100644 --- a/custom_components/plugwise_usb/__init__.py +++ b/custom_components/plugwise_usb/__init__.py @@ -55,7 +55,7 @@ def _async_migrate_entity_entry( api_stick.cache_folder = hass.config.path( STORAGE_DIR, f"plugwisecache-{config_entry.entry_id}" ) - config_entry.runtime_data = {STICK: api_stick} + config_entry.runtime_data[STICK] = api_stick _LOGGER.info("Connect & initialize Plugwise USB-Stick...") try: From be5e070a0e7ddab914babf115a7f3d78f84a39bb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 14:22:56 +0100 Subject: [PATCH 31/36] Set to v0.59.0 release-version, update CHANGELOG --- CHANGELOG.md | 4 ++-- custom_components/plugwise_usb/manifest.json | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 404cb37e..a0c7a8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## Ongoing +## v0.59.0 -- Implement config_flow reconfigure +- New Feature: implement reconfiguring of the Stick path via PR [407](https://github.com/plugwise/plugwise_usb-beta/pull/407) ## v0.58.2 - 2026-01-29 diff --git a/custom_components/plugwise_usb/manifest.json b/custom_components/plugwise_usb/manifest.json index 7e7cc561..50b1fe8b 100644 --- a/custom_components/plugwise_usb/manifest.json +++ b/custom_components/plugwise_usb/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/plugwise/python-plugwise-usb/issues", "loggers": ["plugwise_usb"], "requirements": ["plugwise-usb==0.47.2"], - "version": "0.59.0b0" + "version": "0.59.0" } diff --git a/pyproject.toml b/pyproject.toml index 50137c9c..3605b7b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plugwise_usb-beta" -version = "0.59.0b0" +version = "0.59.0" description = "Plugwise USB custom_component (BETA)" readme = "README.md" requires-python = ">=3.13" From 1af73a58dda4969470ac69465a18756a5ed568bd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 14:33:02 +0100 Subject: [PATCH 32/36] Ruff --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 0f95da90..5400290d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from custom_components.plugwise_usb.const import CONF_USB_PATH, DOMAIN from homeassistant.core import HomeAssistant + from pytest_homeassistant_custom_component.common import MockConfigEntry STICK_IMPORT_MOCK: Final[str] = "custom_components.plugwise_usb.config_flow.Stick" From e51ba4dfee2f727392af8b1f095e271cdc84e01f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 14:50:48 +0100 Subject: [PATCH 33/36] Revert "Use the same format as elsewhere" This reverts commit 730ca6165f59b80bed6a15501c8ab240dd8f7ac9. --- custom_components/plugwise_usb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/plugwise_usb/__init__.py b/custom_components/plugwise_usb/__init__.py index f80e882a..295aac03 100644 --- a/custom_components/plugwise_usb/__init__.py +++ b/custom_components/plugwise_usb/__init__.py @@ -55,7 +55,7 @@ def _async_migrate_entity_entry( api_stick.cache_folder = hass.config.path( STORAGE_DIR, f"plugwisecache-{config_entry.entry_id}" ) - config_entry.runtime_data[STICK] = api_stick + config_entry.runtime_data = {STICK: api_stick} _LOGGER.info("Connect & initialize Plugwise USB-Stick...") try: From bf0fc5c0177d231e83e2c67c588dea8a190d1d10 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 14:51:38 +0100 Subject: [PATCH 34/36] Fix ident, as suggested --- custom_components/plugwise_usb/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 0685ba28..240c797f 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -148,7 +148,7 @@ async def async_step_reconfigure( errors, mac_stick = await validate_usb_connection(self.hass, device_path) if not errors: await self.async_set_unique_id( - unique_id=mac_stick, raise_on_progress=False + unique_id=mac_stick, raise_on_progress=False ) self._abort_if_unique_id_mismatch(reason="not_the_same_stick") return self.async_update_reload_and_abort( From 7a25f5a421d015c45ee618187853e3c3f7bb5142 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 14:52:48 +0100 Subject: [PATCH 35/36] Fix minor_version discrepancy --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5400290d..0ee28b20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, data={CONF_USB_PATH: TEST_USB_PATH}, - minor_version=1, + minor_version=0, version=1, unique_id=TEST_MAC, ) From c433f447f60fed1f072c46071f652df4b3f9cccb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 1 Mar 2026 15:01:29 +0100 Subject: [PATCH 36/36] Line up format with similar lines --- custom_components/plugwise_usb/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/plugwise_usb/config_flow.py b/custom_components/plugwise_usb/config_flow.py index 240c797f..a1647b90 100644 --- a/custom_components/plugwise_usb/config_flow.py +++ b/custom_components/plugwise_usb/config_flow.py @@ -141,7 +141,7 @@ async def async_step_reconfigure( errors: dict[str, str] = {} reconfigure_entry = self._get_reconfigure_entry() - if user_input: + if user_input is not None: device_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, user_input.get(CONF_USB_PATH) )