From 66203774ffb347394075306c18030ef07d33c19f Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 20:34:56 +0100 Subject: [PATCH 1/8] Fixed some issues with the new Home Assistant (2024) version --- custom_components/sat/config_flow.py | 21 +++++++++++++++++---- custom_components/sat/manifest.json | 2 +- requirements_test.txt | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index dfc9f1b3..f20f10f9 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -35,6 +35,8 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SAT.""" VERSION = 5 + MINOR_VERSION = 0 + calibration = None overshoot_protection_value = None @@ -58,7 +60,7 @@ async def async_step_user(self, _user_input=None) -> FlowResult: menu_options = [] # Since we rely on the availability logic in 2023.5, we do not support below it. - if MAJOR_VERSION >= 2023 and MINOR_VERSION >= 5: + if MAJOR_VERSION >= 2023 and (MINOR_VERSION >= 5 or MAJOR_VERSION > 2023): menu_options.append("mosquitto") menu_options.append("serial") @@ -382,10 +384,21 @@ async def async_step_finish(self, _user_input=None): return self.async_create_entry(title=self._data[CONF_NAME], data=self._data) async def _create_coordinator(self) -> SatDataUpdateCoordinator: + # Set up the config entry parameters, since they differ per version + config_params = { + "version": self.VERSION, + "domain": DOMAIN, + "title": self._data[CONF_NAME], + "data": self._data, + "source": SOURCE_USER, + } + + # Check Home Assistant version and add parameters accordingly + if MAJOR_VERSION >= 2024: + config_params["minor_version"] = self.MINOR_VERSION + # Create a new config to use - config = ConfigEntry( - version=self.VERSION, domain=DOMAIN, title=self._data[CONF_NAME], data=self._data, source=SOURCE_USER - ) + config = ConfigEntry(**config_params) # Resolve the coordinator by using the factory according to the mode return await SatDataUpdateCoordinatorFactory().resolve( diff --git a/custom_components/sat/manifest.json b/custom_components/sat/manifest.json index 6135a9c8..2f1207e5 100644 --- a/custom_components/sat/manifest.json +++ b/custom_components/sat/manifest.json @@ -22,5 +22,5 @@ "requirements": [ "pyotgw==2.1.3" ], - "version": "2.1.0" + "version": "3.0.1" } diff --git a/requirements_test.txt b/requirements_test.txt index 61b52bad..0cdee997 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pytest pytest-cov pytest-asyncio pytest-homeassistant-custom-component -homeassistant==2023.5.3 +homeassistant==2024.1.2 aiohttp_cors aiodiscover freezegun From fad712d90ac24096461d4f0b642b232de72f5362 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 20:37:11 +0100 Subject: [PATCH 2/8] Upgrade tests to Python 3.11 --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 46d04261..2f6ad77b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10" ] + python-version: [ "3.11" ] steps: - uses: actions/checkout@v2 From 2b3c0f8c8d7aabf8b947b7ab33637db758d962a2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 20:46:31 +0100 Subject: [PATCH 3/8] Test with multiple version of Home Assistant --- .github/workflows/pytest.yml | 2 ++ requirements_test.txt | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2f6ad77b..664a4df4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: python-version: [ "3.11" ] + home-assistant-version: [ "2023.5.4", "2024.1.2" ] steps: - uses: actions/checkout@v2 @@ -23,6 +24,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest + pip install homeassistant==${{ matrix.home-assistant-version }} if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi - name: Test with pytest run: | diff --git a/requirements_test.txt b/requirements_test.txt index 0cdee997..25d7b08a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,6 @@ pytest pytest-cov pytest-asyncio pytest-homeassistant-custom-component -homeassistant==2024.1.2 aiohttp_cors aiodiscover freezegun From 313b7ab364f9a3aaa9f385378403af942f5c70c3 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 21:13:43 +0100 Subject: [PATCH 4/8] Test creating the coordinator in the config flow --- .github/workflows/pytest.yml | 2 +- custom_components/sat/config_flow.py | 90 ++++++++++++++-------------- tests/test_config_flow.py | 13 ++++ 3 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 tests/test_config_flow.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 664a4df4..2f252411 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -24,8 +24,8 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest + pip install -r requirements_test.txt pip install homeassistant==${{ matrix.home-assistant-version }} - if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi - name: Test with pytest run: | pytest \ No newline at end of file diff --git a/custom_components/sat/config_flow.py b/custom_components/sat/config_flow.py index f20f10f9..f6b10ba4 100644 --- a/custom_components/sat/config_flow.py +++ b/custom_components/sat/config_flow.py @@ -42,8 +42,8 @@ class SatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize.""" - self._data = {} - self._errors = {} + self.data = {} + self.errors = {} @staticmethod @callback @@ -74,12 +74,12 @@ async def async_step_user(self, _user_input=None) -> FlowResult: async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" _LOGGER.debug("Discovered OTGW at [socket://%s]", discovery_info.hostname) - self._data[CONF_DEVICE] = f"socket://{discovery_info.hostname}:25238" + self.data[CONF_DEVICE] = f"socket://{discovery_info.hostname}:25238" # abort if we already have exactly this gateway id/host # reload the integration if the host got updated await self.async_set_unique_id(discovery_info.hostname) - self._abort_if_unique_id_configured(updates=self._data, reload_on_update=True) + self._abort_if_unique_id_configured(updates=self.data, reload_on_update=True) return await self.async_step_serial() @@ -90,24 +90,24 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo): ) _LOGGER.debug("Discovered OTGW at [mqtt://%s]", discovery_info.topic) - self._data[CONF_DEVICE] = device.id + self.data[CONF_DEVICE] = device.id # abort if we already have exactly this gateway id/host # reload the integration if the host got updated await self.async_set_unique_id(device.id) - self._abort_if_unique_id_configured(updates=self._data, reload_on_update=True) + self._abort_if_unique_id_configured(updates=self.data, reload_on_update=True) return await self.async_step_mosquitto() async def async_step_mosquitto(self, _user_input=None): - self._errors = {} + self.errors = {} if _user_input is not None: - self._data.update(_user_input) - self._data[CONF_MODE] = MODE_MQTT + self.data.update(_user_input) + self.data[CONF_MODE] = MODE_MQTT if not await mqtt.async_wait_for_mqtt_client(self.hass): - self._errors["base"] = "mqtt_component" + self.errors["base"] = "mqtt_component" return await self.async_step_mosquitto() return await self.async_step_sensors() @@ -115,27 +115,27 @@ async def async_step_mosquitto(self, _user_input=None): return self.async_show_form( step_id="mosquitto", last_step=False, - errors=self._errors, + errors=self.errors, data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_MQTT_TOPIC, default=OPTIONS_DEFAULTS[CONF_MQTT_TOPIC]): str, - vol.Required(CONF_DEVICE, default=self._data.get(CONF_DEVICE)): selector.DeviceSelector( + vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE)): selector.DeviceSelector( selector.DeviceSelectorConfig(model="otgw-nodo") ), }), ) async def async_step_serial(self, _user_input=None): - self._errors = {} + self.errors = {} if _user_input is not None: - self._data.update(_user_input) - self._data[CONF_MODE] = MODE_SERIAL + self.data.update(_user_input) + self.data[CONF_MODE] = MODE_SERIAL gateway = OpenThermGateway() - if not await gateway.connect(port=self._data[CONF_DEVICE], skip_init=True, timeout=5): + if not await gateway.connect(port=self.data[CONF_DEVICE], skip_init=True, timeout=5): await gateway.disconnect() - self._errors["base"] = "connection" + self.errors["base"] = "connection" return await self.async_step_serial() return await self.async_step_sensors() @@ -143,24 +143,24 @@ async def async_step_serial(self, _user_input=None): return self.async_show_form( step_id="serial", last_step=False, - errors=self._errors, + errors=self.errors, data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_DEVICE, default=self._data.get(CONF_DEVICE, "socket://otgw.local:25238")): str, + vol.Required(CONF_DEVICE, default=self.data.get(CONF_DEVICE, "socket://otgw.local:25238")): str, }), ) async def async_step_switch(self, _user_input=None): if _user_input is not None: - self._data.update(_user_input) - self._data[CONF_MODE] = MODE_SWITCH + self.data.update(_user_input) + self.data[CONF_MODE] = MODE_SWITCH return await self.async_step_sensors() return self.async_show_form( step_id="switch", last_step=False, - errors=self._errors, + errors=self.errors, data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_DEVICE): selector.EntitySelector( @@ -174,16 +174,16 @@ async def async_step_switch(self, _user_input=None): async def async_step_simulator(self, _user_input=None): if _user_input is not None: - self._data.update(_user_input) - self._data[CONF_MODE] = MODE_SIMULATOR - self._data[CONF_DEVICE] = f"%s_%s".format(MODE_SIMULATOR, snake_case(_user_input.get(CONF_NAME))) + self.data.update(_user_input) + self.data[CONF_MODE] = MODE_SIMULATOR + self.data[CONF_DEVICE] = f"%s_%s".format(MODE_SIMULATOR, snake_case(_user_input.get(CONF_NAME))) return await self.async_step_sensors() return self.async_show_form( step_id="simulator", last_step=False, - errors=self._errors, + errors=self.errors, data_schema=vol.Schema({ vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_SIMULATED_HEATING, default=OPTIONS_DEFAULTS[CONF_SIMULATED_HEATING]): selector.NumberSelector( @@ -200,13 +200,13 @@ async def async_step_simulator(self, _user_input=None): ) async def async_step_sensors(self, _user_input=None): - await self.async_set_unique_id(self._data[CONF_DEVICE], raise_on_progress=False) + await self.async_set_unique_id(self.data[CONF_DEVICE], raise_on_progress=False) self._abort_if_unique_id_configured() if _user_input is not None: - self._data.update(_user_input) + self.data.update(_user_input) - if self._data[CONF_MODE] in [MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: + if self.data[CONF_MODE] in [MODE_MQTT, MODE_SERIAL, MODE_SIMULATOR]: return await self.async_step_heating_system() return await self.async_step_areas() @@ -238,7 +238,7 @@ async def async_step_sensors(self, _user_input=None): async def async_step_heating_system(self, _user_input=None): if _user_input is not None: - self._data.update(_user_input) + self.data.update(_user_input) return await self.async_step_areas() @@ -258,9 +258,9 @@ async def async_step_heating_system(self, _user_input=None): async def async_step_areas(self, _user_input=None): if _user_input is not None: - self._data.update(_user_input) + self.data.update(_user_input) - if (await self._create_coordinator()).supports_setpoint_management: + if (await self.async_create_coordinator()).supports_setpoint_management: return await self.async_step_calibrate_system() return await self.async_step_automatic_gains() @@ -279,9 +279,9 @@ async def async_step_areas(self, _user_input=None): async def async_step_automatic_gains(self, _user_input=None): if _user_input is not None: - self._data.update(_user_input) + self.data.update(_user_input) - if not self._data[CONF_AUTOMATIC_GAINS]: + if not self.data[CONF_AUTOMATIC_GAINS]: return await self.async_step_pid_controller() return await self.async_step_finish() @@ -299,7 +299,7 @@ async def async_step_calibrate_system(self, _user_input=None): ) async def async_step_calibrate(self, _user_input=None): - coordinator = await self._create_coordinator() + coordinator = await self.async_create_coordinator() async def start_calibration(): try: @@ -343,7 +343,7 @@ async def start_calibration(): async def async_step_calibrated(self, _user_input=None): return self.async_show_menu( step_id="calibrated", - description_placeholders=self._data, + description_placeholders=self.data, menu_options=["calibrate", "finish"], ) @@ -365,10 +365,10 @@ async def async_step_overshoot_protection(self, _user_input=None): ) async def async_step_pid_controller(self, _user_input=None): - self._data[CONF_AUTOMATIC_GAINS] = False + self.data[CONF_AUTOMATIC_GAINS] = False if _user_input is not None: - self._data.update(_user_input) + self.data.update(_user_input) return await self.async_step_finish() return self.async_show_form( @@ -381,15 +381,15 @@ async def async_step_pid_controller(self, _user_input=None): ) async def async_step_finish(self, _user_input=None): - return self.async_create_entry(title=self._data[CONF_NAME], data=self._data) + return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) - async def _create_coordinator(self) -> SatDataUpdateCoordinator: + async def async_create_coordinator(self) -> SatDataUpdateCoordinator: # Set up the config entry parameters, since they differ per version config_params = { "version": self.VERSION, "domain": DOMAIN, - "title": self._data[CONF_NAME], - "data": self._data, + "title": self.data[CONF_NAME], + "data": self.data, "source": SOURCE_USER, } @@ -402,12 +402,12 @@ async def _create_coordinator(self) -> SatDataUpdateCoordinator: # Resolve the coordinator by using the factory according to the mode return await SatDataUpdateCoordinatorFactory().resolve( - hass=self.hass, config_entry=config, mode=self._data[CONF_MODE], device=self._data[CONF_DEVICE] + hass=self.hass, config_entry=config, mode=self.data[CONF_MODE], device=self.data[CONF_DEVICE] ) async def _enable_overshoot_protection(self, overshoot_protection_value: float): - self._data[CONF_OVERSHOOT_PROTECTION] = True - self._data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value + self.data[CONF_OVERSHOOT_PROTECTION] = True + self.data[CONF_MINIMUM_SETPOINT] = overshoot_protection_value class SatOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 00000000..a785ab07 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,13 @@ +from custom_components.sat import MODE_FAKE +from custom_components.sat.config_flow import SatFlowHandler + + +async def test_create_coordinator(hass): + flow_handler = SatFlowHandler() + flow_handler.data = { + "name": "Test", + "mode": MODE_FAKE, + "device": "test_device", + } + + await flow_handler.async_create_coordinator() From e394cb1331459a1d10a2eb3ce027ddcd181fafb2 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 21:16:22 +0100 Subject: [PATCH 5/8] Custom Component for PyTest requires 2023.12.3 --- .github/workflows/pytest.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2f252411..02554fc5 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: python-version: [ "3.11" ] - home-assistant-version: [ "2023.5.4", "2024.1.2" ] + home-assistant-version: [ "2023.12.3", "2024.1.2" ] steps: - uses: actions/checkout@v2 @@ -23,7 +23,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest pip install -r requirements_test.txt pip install homeassistant==${{ matrix.home-assistant-version }} - name: Test with pytest From e267e90ce590e79b1d257c48bcc892736e08ae0a Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 21:24:13 +0100 Subject: [PATCH 6/8] Install a specific version --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 25d7b08a..226505ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pytest pytest-cov pytest-asyncio -pytest-homeassistant-custom-component +pytest-homeassistant-custom-component==0.13.85 aiohttp_cors aiodiscover freezegun From 3e2a359ca6f431aa017f49612b13bd2e632e5fea Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 21:25:13 +0100 Subject: [PATCH 7/8] Install a specific version --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 226505ee..16a10dac 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pytest pytest-cov pytest-asyncio -pytest-homeassistant-custom-component==0.13.85 +pytest-homeassistant-custom-component==0.13.88 aiohttp_cors aiodiscover freezegun From f77d3ff750ca39cefb5203876a925790819e09d5 Mon Sep 17 00:00:00 2001 From: Alex Wijnholds Date: Sat, 6 Jan 2024 21:27:57 +0100 Subject: [PATCH 8/8] Revert multi-step testing for now --- .github/workflows/pytest.yml | 2 -- requirements_test.txt | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 02554fc5..152d9346 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,7 +12,6 @@ jobs: strategy: matrix: python-version: [ "3.11" ] - home-assistant-version: [ "2023.12.3", "2024.1.2" ] steps: - uses: actions/checkout@v2 @@ -24,7 +23,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements_test.txt - pip install homeassistant==${{ matrix.home-assistant-version }} - name: Test with pytest run: | pytest \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 16a10dac..6871af61 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,8 @@ pytest pytest-cov pytest-asyncio -pytest-homeassistant-custom-component==0.13.88 +pytest-homeassistant-custom-component +homeassistant aiohttp_cors aiodiscover freezegun