From 6b4d0f2f5be9c460c4e614d65e3fd4c5b75e882e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:12:03 +0000 Subject: [PATCH 01/32] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ff73a7b5..f1175d808 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.1 hooks: - id: ruff name: "Ruff check" From 681bbae8c79cef719951efa7c0307b9549794b76 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 26 Jun 2025 19:34:54 +0200 Subject: [PATCH 02/32] Implement async aiofiles.open() --- pyproject.toml | 1 + tests/test_adam.py | 20 ++++++++++---------- tests/test_anna.py | 34 +++++++++++++++++----------------- tests/test_init.py | 36 +++++++++++++++++++----------------- tests/test_legacy_anna.py | 4 ++-- tests/test_legacy_p1.py | 6 +++--- tests/test_legacy_stretch.py | 8 ++++---- tests/test_p1.py | 6 +++--- 8 files changed, 59 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ede4e456a..ef4299677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ maintainers = [ ] requires-python = ">=3.13" dependencies = [ + "aiofiles", "aiohttp", "defusedxml", "munch", diff --git a/tests/test_adam.py b/tests/test_adam.py index 96b015ce5..1ef41c0de 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -21,7 +21,7 @@ async def test_connect_adam_plus_anna_new(self): """Test extended Adam (firmware 3.7) with Anna and a switch-group setup.""" self.smile_setup = "adam_plus_anna_new" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -139,7 +139,7 @@ async def test_connect_adam_plus_anna_new(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) self.smile_setup = "updated/adam_plus_anna_new" @@ -167,7 +167,7 @@ async def test_connect_adam_plus_anna_new(self): await self.disconnect(server, client) self.smile_setup = "adam_plus_anna_new" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper(raise_timeout=True) await self.device_test( smile, "2023-12-17 00:00:01", testdata, skip_testing=True @@ -190,7 +190,7 @@ async def test_connect_adam_plus_anna_new_regulation_off(self): """Test regultaion_mode off with control_state key missing for Adam.""" self.smile_setup = "adam_plus_anna_new_regulation_off" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -204,7 +204,7 @@ async def test_connect_adam_zone_per_device(self): """Test an extensive setup of Adam with a zone per device.""" self.smile_setup = "adam_zone_per_device" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -285,7 +285,7 @@ async def test_connect_adam_multiple_devices_per_zone(self): """Test an extensive setup of Adam with multiple devices per zone.""" self.smile_setup = "adam_multiple_devices_per_zone" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -333,7 +333,7 @@ async def test_adam_heatpump_cooling(self): """Test Adam with heatpump in cooling mode and idle.""" self.smile_setup = "adam_heatpump_cooling" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() await self.device_test(smile, "2022-01-02 00:00:01", testdata) @@ -359,7 +359,7 @@ async def test_connect_adam_onoff_cooling_fake_firmware(self): """Test an Adam with a fake OnOff cooling device in cooling mode.""" self.smile_setup = "adam_onoff_cooling_fake_firmware" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -382,7 +382,7 @@ async def test_connect_adam_plus_anna(self): """Test Adam (firmware 3.0) with Anna setup.""" self.smile_setup = "adam_plus_anna" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -432,7 +432,7 @@ async def test_adam_plus_jip(self): """Test Adam with Jip setup.""" self.smile_setup = "adam_jip" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() await self.device_test(smile, "2021-06-20 00:00:01", testdata) diff --git a/tests/test_anna.py b/tests/test_anna.py index bce21b1ab..92efae3d9 100644 --- a/tests/test_anna.py +++ b/tests/test_anna.py @@ -17,7 +17,7 @@ async def test_connect_anna_v4(self): """Test an Anna firmware 4 setup.""" self.smile_setup = "anna_v4" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -55,7 +55,7 @@ async def test_connect_anna_v4(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) @@ -97,7 +97,7 @@ async def test_connect_anna_v4_dhw(self): """Test an Anna firmware 4 setup for domestic hot water.""" self.smile_setup = "anna_v4_dhw" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -127,7 +127,7 @@ async def test_connect_anna_v4_no_tag(self): """Test an Anna firmware 4 setup - missing tag (issue).""" self.smile_setup = "anna_v4_no_tag" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -155,7 +155,7 @@ async def test_connect_anna_without_boiler_fw441(self): """Test an Anna with firmware 4.4, without a boiler.""" self.smile_setup = "anna_without_boiler_fw441" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -183,7 +183,7 @@ async def test_connect_anna_heatpump_heating(self): self.smile_setup = "anna_heatpump_heating" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -218,7 +218,7 @@ async def test_connect_anna_heatpump_heating(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval, # set testday to Monday to force an incremental update - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) @@ -240,7 +240,7 @@ async def test_connect_anna_heatpump_cooling(self): """ self.smile_setup = "anna_heatpump_cooling" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -287,7 +287,7 @@ async def test_connect_anna_heatpump_cooling_fake_firmware(self): """ self.smile_setup = "anna_heatpump_cooling_fake_firmware" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -312,7 +312,7 @@ async def test_connect_anna_elga_no_cooling(self): self.smile_setup = "anna_elga_no_cooling" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -337,7 +337,7 @@ async def test_connect_anna_elga_2(self): """Test a 2nd Anna with Elga setup, cooling off, in idle mode (with missing outdoor temperature - solved).""" self.smile_setup = "anna_elga_2" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -366,7 +366,7 @@ async def test_connect_anna_elga_2_schedule_off(self): """Test Anna with Elga setup, cooling off, in idle mode, modified to schedule off.""" self.smile_setup = "anna_elga_2_schedule_off" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -391,7 +391,7 @@ async def test_connect_anna_elga_2_cooling(self): """ self.smile_setup = "anna_elga_2_cooling" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -421,7 +421,7 @@ async def test_connect_anna_elga_2_cooling(self): assert result # Simulate a change of season: from cooling to heating after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) @@ -448,7 +448,7 @@ async def test_connect_anna_loria_heating_idle(self): """Test an Anna with a Loria in heating mode - state idle.""" self.smile_setup = "anna_loria_heating_idle" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -516,7 +516,7 @@ async def test_connect_anna_loria_cooling_active(self): """Test an Anna with a Loria in heating mode - state idle.""" self.smile_setup = "anna_loria_cooling_active" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -540,7 +540,7 @@ async def test_connect_anna_loria_driessens(self): """Test an Anna with a Loria in heating mode - state idle.""" self.smile_setup = "anna_loria_driessens" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" diff --git a/tests/test_init.py b/tests/test_init.py index c80468ec6..ed6be9ece 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -16,6 +16,7 @@ import pytest # Testing +import aiofiles import aiohttp from freezegun import freeze_time from packaging import version @@ -50,7 +51,7 @@ class TestPlugwise: # pylint: disable=attribute-defined-outside-init """Tests for Plugwise Smile.""" - def _write_json(self, call, data): + async def _write_json(self, call, data): """Store JSON data to per-setup files for HA component testing.""" no_fixtures = os.getenv("NO_FIXTURES") == "1" if no_fixtures: @@ -65,8 +66,8 @@ def _write_json(self, call, data): if not os.path.exists(os.path.dirname(datafile)): # pragma: no cover os.mkdir(os.path.dirname(datafile)) - with open(datafile, "w", encoding="utf-8") as fixture_file: - fixture_file.write( + async with aiofiles.open(datafile, "w", encoding="utf-8") as fixture_file: + await fixture_file.write( json.dumps( data, indent=2, @@ -77,15 +78,16 @@ def _write_json(self, call, data): + "\n" ) - def load_testdata( + async def load_testdata( self, smile_type: str = "adam", smile_setup: str = "adam_zone_per_device" ): """Load JSON data from setup, return as object.""" path = os.path.join( os.path.dirname(__file__), f"../tests/data/{smile_type}/{smile_setup}.json" ) - with open(path, encoding="utf-8") as testdata_file: - return json.load(testdata_file) + async with aiofiles.open(path, encoding="utf-8") as testdata_file: + content = await testdata_file.read() + return json.loads(content) async def setup_app( self, @@ -187,8 +189,8 @@ async def smile_appliances(self, request): os.path.dirname(__file__), f"../userdata/{self.smile_setup}/core.appliances.xml", ) - with open(userdata, encoding="utf-8") as filedata: - data = filedata.read() + async with aiofiles.open(userdata, encoding="utf-8") as filedata: + data = await filedata.read() return aiohttp.web.Response(text=data) async def smile_domain_objects(self, request): @@ -197,8 +199,8 @@ async def smile_domain_objects(self, request): os.path.dirname(__file__), f"../userdata/{self.smile_setup}/core.domain_objects.xml", ) - with open(userdata, encoding="utf-8") as filedata: - data = filedata.read() + async with aiofiles.open(userdata, encoding="utf-8") as filedata: + data = await filedata.read() return aiohttp.web.Response(text=data) async def smile_locations(self, request): @@ -207,8 +209,8 @@ async def smile_locations(self, request): os.path.dirname(__file__), f"../userdata/{self.smile_setup}/core.locations.xml", ) - with open(userdata, encoding="utf-8") as filedata: - data = filedata.read() + async with aiofiles.open(userdata, encoding="utf-8") as filedata: + data = await filedata.read() return aiohttp.web.Response(text=data) async def smile_modules(self, request): @@ -217,8 +219,8 @@ async def smile_modules(self, request): os.path.dirname(__file__), f"../userdata/{self.smile_setup}/core.modules.xml", ) - with open(userdata, encoding="utf-8") as filedata: - data = filedata.read() + async with aiofiles.open(userdata, encoding="utf-8") as filedata: + data = await filedata.read() return aiohttp.web.Response(text=data) async def smile_status(self, request): @@ -228,8 +230,8 @@ async def smile_status(self, request): os.path.dirname(__file__), f"../userdata/{self.smile_setup}/system_status_xml.xml", ) - with open(userdata, encoding="utf-8") as filedata: - data = filedata.read() + async with aiofiles.open(userdata, encoding="utf-8") as filedata: + data = await filedata.read() return aiohttp.web.Response(text=data) except OSError as exc: raise aiohttp.web.HTTPNotFound from exc @@ -641,7 +643,7 @@ def test_and_assert(test_dict, data, header): "cooling_state" ] - self._write_json("data", data) + await self._write_json("data", data) if "FIXTURES" in os.environ: _LOGGER.info("Skipping tests: Requested fixtures only") # pragma: no cover diff --git a/tests/test_legacy_anna.py b/tests/test_legacy_anna.py index a3a7b8469..1a2e7e20f 100644 --- a/tests/test_legacy_anna.py +++ b/tests/test_legacy_anna.py @@ -16,7 +16,7 @@ class TestPlugwiseAnna(TestPlugwise): # pylint: disable=attribute-defined-outsi async def test_connect_legacy_anna(self): """Test a legacy Anna device.""" self.smile_setup = "legacy_anna" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper() assert smile.smile_hostname == "smile000000" @@ -53,7 +53,7 @@ async def test_connect_legacy_anna_2(self): """Test another legacy Anna device.""" self.smile_setup = "legacy_anna_2" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper() assert smile.smile_hostname == "smile000000" diff --git a/tests/test_legacy_p1.py b/tests/test_legacy_p1.py index d569ca52b..52f1d8504 100644 --- a/tests/test_legacy_p1.py +++ b/tests/test_legacy_p1.py @@ -15,7 +15,7 @@ async def test_connect_smile_p1_v2(self): """Test a legacy P1 device.""" self.smile_setup = "smile_p1_v2" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper() assert smile.smile_hostname == "smile000000" @@ -39,7 +39,7 @@ async def test_connect_smile_p1_v2_2(self): """Test another legacy P1 device.""" self.smile_setup = "smile_p1_v2_2" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper() assert smile.smile_hostname == "smile000000" @@ -56,7 +56,7 @@ async def test_connect_smile_p1_v2_2(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) self.smile_setup = "updated/smile_p1_v2_2" diff --git a/tests/test_legacy_stretch.py b/tests/test_legacy_stretch.py index 021cc385b..13d079020 100644 --- a/tests/test_legacy_stretch.py +++ b/tests/test_legacy_stretch.py @@ -15,7 +15,7 @@ async def test_connect_stretch_v31(self): """Test a legacy Stretch with firmware 3.1 setup.""" self.smile_setup = "stretch_v31" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper(stretch=True) assert smile.smile_hostname == "stretch000000" @@ -39,7 +39,7 @@ async def test_connect_stretch_v31(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) self.smile_setup = "updated/stretch_v31" @@ -55,7 +55,7 @@ async def test_connect_stretch_v23(self): """Test a legacy Stretch with firmware 2.3 setup.""" self.smile_setup = "stretch_v23" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper(stretch=True) assert smile.smile_hostname == "stretch000000" @@ -94,7 +94,7 @@ async def test_connect_stretch_v27_no_domain(self): # testdata dictionary with key ctrl_id_dev_id => keys:values self.smile_setup = "stretch_v27_no_domain" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_legacy_wrapper(stretch=True) assert smile.smile_hostname == "stretch000000" diff --git a/tests/test_p1.py b/tests/test_p1.py index 562ebc4fd..e379573e6 100644 --- a/tests/test_p1.py +++ b/tests/test_p1.py @@ -15,7 +15,7 @@ async def test_connect_p1v4_442_single(self): """Test a P1 firmware 4.4 single-phase setup.""" self.smile_setup = "p1v4_442_single" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" @@ -33,7 +33,7 @@ async def test_connect_p1v4_442_single(self): # Now change some data and change directory reading xml from # emulating reading newer dataset after an update_interval - testdata_updated = self.load_testdata( + testdata_updated = await self.load_testdata( SMILE_TYPE, f"{self.smile_setup}_UPDATED_DATA" ) self.smile_setup = "updated/p1v4_442_single" @@ -65,7 +65,7 @@ async def test_connect_p1v4_442_triple(self): """Test a P1 firmware 4 3-phase setup.""" self.smile_setup = "p1v4_442_triple" - testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) + testdata = await self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper() assert smile.smile_hostname == "smile000000" From b9d2eb6d5eb0d746e27864bb566c313efab63738 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:00:03 +0200 Subject: [PATCH 03/32] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d47c3971..c5df40835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- Implement async file-open in tests + ## v1.7.6 - Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: #264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: #263](https://github.com/plugwise/python-plugwise-usb/pull/263) From 66eb458dfe0bd2c12a64abd9d94bfd88590343f0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:13:35 +0200 Subject: [PATCH 04/32] Remove unneeded async function designation --- tests/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index ed6be9ece..8c67da028 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -89,7 +89,7 @@ async def load_testdata( content = await testdata_file.read() return json.loads(content) - async def setup_app( + def setup_app( self, broken=False, timeout=False, @@ -132,7 +132,7 @@ async def setup_app( return app - async def setup_legacy_app( + def setup_legacy_app( self, broken=False, timeout=False, @@ -290,7 +290,7 @@ async def connect( ) # Happy flow - app = await self.setup_app(broken, timeout, raise_timeout, fail_auth, stretch) + app = self.setup_app(broken, timeout, raise_timeout, fail_auth, stretch) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -374,7 +374,7 @@ async def connect_legacy( ) # Happy flow - app = await self.setup_legacy_app( + app = self.setup_legacy_app( broken, timeout, raise_timeout, fail_auth, stretch ) From 4a2f7509e60810cc77b7f4a54b34e7cbb74418bd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:14:50 +0200 Subject: [PATCH 05/32] Ruff fix --- tests/test_init.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 8c67da028..d2384add6 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -374,9 +374,7 @@ async def connect_legacy( ) # Happy flow - app = self.setup_legacy_app( - broken, timeout, raise_timeout, fail_auth, stretch - ) + app = self.setup_legacy_app(broken, timeout, raise_timeout, fail_auth, stretch) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" From e99725bd693afaa05e4cdc7b1b4877ed6e202ea8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:16:29 +0200 Subject: [PATCH 06/32] Remove unneeded list(dict()) constructs --- plugwise/data.py | 2 +- plugwise/helper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise/data.py b/plugwise/data.py index 97ec5b51a..45cbe38db 100644 --- a/plugwise/data.py +++ b/plugwise/data.py @@ -86,7 +86,7 @@ def _detect_low_batteries(self) -> list[str]: mac_pattern = re.compile(r"(?:[0-9A-F]{2}){8}") matches = ["Battery", "below"] if self._notifications: - for msg_id, notification in list(self._notifications.items()): + for msg_id, notification in self._notifications.items(): mac_address: str | None = None message: str | None = notification.get("message") warning: str | None = notification.get("warning") diff --git a/plugwise/helper.py b/plugwise/helper.py index 87c4e986e..edd680747 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -723,7 +723,7 @@ def _scan_thermostats(self) -> None: for entity_id, entity in self.gw_entities.items(): self._rank_thermostat(thermo_matching, loc_id, entity_id, entity) - for loc_id, loc_data in list(self._thermo_locs.items()): + for loc_id, loc_data in self._thermo_locs.items(): if loc_data["primary_prio"] != 0: self._zones[loc_id] = { "dev_class": "climate", From af46088f10c3f96a67b012f4e24048f8da7ebf0e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:18:27 +0200 Subject: [PATCH 07/32] Use dict.copy() instead of list(dict) --- plugwise/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/data.py b/plugwise/data.py index 45cbe38db..29b8e0922 100644 --- a/plugwise/data.py +++ b/plugwise/data.py @@ -86,7 +86,7 @@ def _detect_low_batteries(self) -> list[str]: mac_pattern = re.compile(r"(?:[0-9A-F]{2}){8}") matches = ["Battery", "below"] if self._notifications: - for msg_id, notification in self._notifications.items(): + for msg_id, notification in self._notifications.copy().items(): mac_address: str | None = None message: str | None = notification.get("message") warning: str | None = notification.get("warning") From 548d445abf0aa57f3ced2a5cb0aae26026ec41c1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 27 Jun 2025 20:23:16 +0200 Subject: [PATCH 08/32] Combine if's --- plugwise/data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugwise/data.py b/plugwise/data.py index 29b8e0922..cd36c9c4c 100644 --- a/plugwise/data.py +++ b/plugwise/data.py @@ -232,12 +232,12 @@ def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None: if self._on_off_device and isinstance(self._heating_valves(), int): data["binary_sensors"]["heating_state"] = self._heating_valves() != 0 # Add cooling_enabled binary_sensor - if "binary_sensors" in data: - if ( - "cooling_enabled" not in data["binary_sensors"] - and self._cooling_present - ): - data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled + if ( + "binary_sensors" in data + and "cooling_enabled" not in data["binary_sensors"] + and self._cooling_present + ): + data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled # Show the allowed regulation_modes and gateway_modes if entity["dev_class"] == "gateway": From 92ef5a6ca2e1b6da52675a8781cde6e2c9f3b3d4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 28 Jun 2025 09:08:29 +0200 Subject: [PATCH 09/32] Change boolean name to timeout_happened --- tests/test_init.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index d2384add6..cacd86d4f 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -92,7 +92,7 @@ async def load_testdata( def setup_app( self, broken=False, - timeout=False, + timeout_happened=False, raise_timeout=False, fail_auth=False, stretch=False, @@ -108,7 +108,7 @@ def setup_app( if broken: app.router.add_get(CORE_DOMAIN_OBJECTS, self.smile_broken) - elif timeout: + elif timeout_happened: app.router.add_get(CORE_DOMAIN_OBJECTS, self.smile_timeout) else: app.router.add_get(CORE_DOMAIN_OBJECTS, self.smile_domain_objects) @@ -135,7 +135,7 @@ def setup_app( def setup_legacy_app( self, broken=False, - timeout=False, + timeout_happened=False, raise_timeout=False, fail_auth=False, stretch=False, @@ -151,7 +151,7 @@ def setup_legacy_app( if broken: app.router.add_get(CORE_LOCATIONS, self.smile_broken) - elif timeout: + elif timeout_happened: app.router.add_get(CORE_LOCATIONS, self.smile_timeout) else: app.router.add_get(CORE_LOCATIONS, self.smile_locations) @@ -264,12 +264,12 @@ async def smile_fail_auth(cls, request): raise aiohttp.web.HTTPUnauthorized() @staticmethod - def connect_status(broken, timeout, fail_auth): + def connect_status(broken, timeout_happened, fail_auth): """Determine assumed status from settings.""" assumed_status = 200 if broken: assumed_status = 500 - if timeout: + if timeout_happened: assumed_status = 504 if fail_auth: assumed_status = 401 @@ -278,7 +278,7 @@ def connect_status(broken, timeout, fail_auth): async def connect( self, broken=False, - timeout=False, + timeout_happened=False, raise_timeout=False, fail_auth=False, stretch=False, @@ -290,7 +290,7 @@ async def connect( ) # Happy flow - app = self.setup_app(broken, timeout, raise_timeout, fail_auth, stretch) + app = self.setup_app(broken, timeout_happened, raise_timeout, fail_auth, stretch) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -313,7 +313,7 @@ async def connect( timeoutpass_result = True assert timeoutpass_result - if not broken and not timeout and not fail_auth: + if not broken and not timeout_happened and not fail_auth: text = await resp.text() assert "xml" in text @@ -340,7 +340,7 @@ async def connect( websession=websession, ) - if not timeout: + if not timeout_happened: assert smile._timeout == 30 # Connect to the smile @@ -362,7 +362,7 @@ async def connect( async def connect_legacy( self, broken=False, - timeout=False, + timeout_happened=False, raise_timeout=False, fail_auth=False, stretch=False, @@ -374,7 +374,7 @@ async def connect_legacy( ) # Happy flow - app = self.setup_legacy_app(broken, timeout, raise_timeout, fail_auth, stretch) + app = self.setup_legacy_app(broken, timeout_happened, raise_timeout, fail_auth, stretch) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -389,7 +389,7 @@ async def connect_legacy( # Try/exceptpass to accommodate for Timeout of aoihttp try: resp = await websession.get(url) - assumed_status = self.connect_status(broken, timeout, fail_auth) + assumed_status = self.connect_status(broken, timeout_happened, fail_auth) assert resp.status == assumed_status timeoutpass_result = False assert timeoutpass_result @@ -397,7 +397,7 @@ async def connect_legacy( timeoutpass_result = True assert timeoutpass_result - if not broken and not timeout and not fail_auth: + if not broken and not timeout_happened and not fail_auth: text = await resp.text() assert "xml" in text @@ -424,7 +424,7 @@ async def connect_legacy( websession=websession, ) - if not timeout: + if not timeout_happened: assert smile._timeout == 30 # Connect to the smile @@ -464,7 +464,7 @@ async def connect_wrapper( try: _LOGGER.warning("Connecting to device exceeding timeout in response:") - await self.connect(timeout=True) + await self.connect(timeout_happened=True) _LOGGER.error(" - timeout not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.ConnectionFailedError: @@ -491,7 +491,7 @@ async def connect_legacy_wrapper( try: _LOGGER.warning("Connecting to device exceeding timeout in response:") - await self.connect_legacy(timeout=True) + await self.connect_legacy(timeout_happened=True) _LOGGER.error(" - timeout not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.ConnectionFailedError: From 25918eeb55798b562f8231d042b5b7c5002612d7 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sat, 28 Jun 2025 07:09:27 +0000 Subject: [PATCH 10/32] fixup: sc-fixes-2 Python code fixed using ruff --- tests/test_init.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index cacd86d4f..d1e30fb98 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -290,7 +290,9 @@ async def connect( ) # Happy flow - app = self.setup_app(broken, timeout_happened, raise_timeout, fail_auth, stretch) + app = self.setup_app( + broken, timeout_happened, raise_timeout, fail_auth, stretch + ) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -374,7 +376,9 @@ async def connect_legacy( ) # Happy flow - app = self.setup_legacy_app(broken, timeout_happened, raise_timeout, fail_auth, stretch) + app = self.setup_legacy_app( + broken, timeout_happened, raise_timeout, fail_auth, stretch + ) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" From 71cd4203dba6d28a86e1102cf1f40207fb766670 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 28 Jun 2025 09:11:53 +0200 Subject: [PATCH 11/32] Fix missed --- tests/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index d1e30fb98..aae4f9adc 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -307,7 +307,7 @@ async def connect( # Try/exceptpass to accommodate for Timeout of aoihttp try: resp = await websession.get(url) - assumed_status = self.connect_status(broken, timeout, fail_auth) + assumed_status = self.connect_status(broken, timeout_happened, fail_auth) assert resp.status == assumed_status timeoutpass_result = False assert timeoutpass_result From d70f24396a29661cd56d3c9702758429b3ff40f3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 28 Jun 2025 09:23:33 +0200 Subject: [PATCH 12/32] Test: combine connect-functions --- tests/test_init.py | 135 ++++++++++++--------------------------------- 1 file changed, 35 insertions(+), 100 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index aae4f9adc..edfcbfac0 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -277,11 +277,14 @@ def connect_status(broken, timeout_happened, fail_auth): async def connect( self, + function, broken=False, timeout_happened=False, raise_timeout=False, + real_timeout_value=10, fail_auth=False, stretch=False, + url_part=CORE_DOMAIN_OBJECTS, ): """Connect to a smile environment and perform basic asserts.""" port = aiohttp.test_utils.unused_port() @@ -290,9 +293,7 @@ async def connect( ) # Happy flow - app = self.setup_app( - broken, timeout_happened, raise_timeout, fail_auth, stretch - ) + app = function(broken, timeout_happened, raise_timeout, fail_auth, stretch) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -302,7 +303,7 @@ async def connect( client = aiohttp.test_utils.TestClient(server) websession = client.session - url = f"{server.scheme}://{server.host}:{server.port}{CORE_DOMAIN_OBJECTS}" + url = f"{server.scheme}://{server.host}:{server.port}{url_part}" # Try/exceptpass to accommodate for Timeout of aoihttp try: @@ -350,93 +351,7 @@ async def connect( try: smile_version = await smile.connect() assert smile_version is not None - assert smile._timeout == 10 - return server, smile, client - except ( - pw_exceptions.ConnectionFailedError, - pw_exceptions.InvalidXMLError, - pw_exceptions.InvalidAuthentication, - ) as exception: - assert smile_version is None - await self.disconnect(server, client) - raise exception - - async def connect_legacy( - self, - broken=False, - timeout_happened=False, - raise_timeout=False, - fail_auth=False, - stretch=False, - ): - """Connect to a smile environment and perform basic asserts.""" - port = aiohttp.test_utils.unused_port() - test_password = "".join( - secrets.choice(string.ascii_lowercase) for _ in range(8) - ) - - # Happy flow - app = self.setup_legacy_app( - broken, timeout_happened, raise_timeout, fail_auth, stretch - ) - - server = aiohttp.test_utils.TestServer( - app, port=port, scheme="http", host="127.0.0.1" - ) - await server.start_server() - - client = aiohttp.test_utils.TestClient(server) - websession = client.session - - url = f"{server.scheme}://{server.host}:{server.port}{CORE_LOCATIONS}" - - # Try/exceptpass to accommodate for Timeout of aoihttp - try: - resp = await websession.get(url) - assumed_status = self.connect_status(broken, timeout_happened, fail_auth) - assert resp.status == assumed_status - timeoutpass_result = False - assert timeoutpass_result - except Exception: # pylint: disable=broad-except - timeoutpass_result = True - assert timeoutpass_result - - if not broken and not timeout_happened and not fail_auth: - text = await resp.text() - assert "xml" in text - - # Test lack of websession - try: - smile = pw_smile.Smile( - host=server.host, - username=pw_constants.DEFAULT_USERNAME, - password=test_password, - port=server.port, - websession=None, - ) - lack_of_websession = False - assert lack_of_websession - except Exception: # pylint: disable=broad-except - lack_of_websession = True - assert lack_of_websession - - smile = pw_smile.Smile( - host=server.host, - username=pw_constants.DEFAULT_USERNAME, - password=test_password, - port=server.port, - websession=websession, - ) - - if not timeout_happened: - assert smile._timeout == 30 - - # Connect to the smile - smile_version = None - try: - smile_version = await smile.connect() - assert smile_version is not None - assert smile._timeout == 30 + assert smile._timeout == real_timeout_value return server, smile, client except ( pw_exceptions.ConnectionFailedError, @@ -455,7 +370,7 @@ async def connect_wrapper( if fail_auth: try: _LOGGER.warning("Connecting to device with invalid credentials:") - await self.connect(fail_auth=fail_auth) + await self.connect(self.setup_app, fail_auth=fail_auth) _LOGGER.error(" - invalid credentials not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.InvalidAuthentication as exc: @@ -464,11 +379,11 @@ async def connect_wrapper( if raise_timeout: _LOGGER.warning("Connecting to device exceeding timeout in handling:") - return await self.connect(raise_timeout=True) + return await self.connect(self.setup_app, raise_timeout=True) try: _LOGGER.warning("Connecting to device exceeding timeout in response:") - await self.connect(timeout_happened=True) + await self.connect(self.setup_app, timeout_happened=True) _LOGGER.error(" - timeout not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.ConnectionFailedError: @@ -476,14 +391,14 @@ async def connect_wrapper( try: _LOGGER.warning("Connecting to device with missing data:") - await self.connect(broken=True) + await self.connect(self.setup_app, broken=True) _LOGGER.error(" - broken information not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.InvalidXMLError: _LOGGER.info(" + successfully passed XML issue handling.") _LOGGER.info("Connecting to functioning device:") - return await self.connect(stretch=stretch) + return await self.connect(self.setup_app, stretch=stretch) async def connect_legacy_wrapper( self, raise_timeout=False, fail_auth=False, stretch=False @@ -491,11 +406,21 @@ async def connect_legacy_wrapper( """Wrap connect to try negative testing before positive testing.""" if raise_timeout: _LOGGER.warning("Connecting to device exceeding timeout in handling:") - return await self.connect_legacy(raise_timeout=True) + return await self.connect( + self.setup_legacy_app, + raise_timeout=True, + real_timeout_value=30, + url_part=CORE_LOCATIONS, + ) try: _LOGGER.warning("Connecting to device exceeding timeout in response:") - await self.connect_legacy(timeout_happened=True) + await self.connect( + self.setup_legacy_app, + real_timeout_value=30, + timeout_happened=True, + url_part=CORE_LOCATIONS, + ) _LOGGER.error(" - timeout not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.ConnectionFailedError: @@ -503,14 +428,24 @@ async def connect_legacy_wrapper( try: _LOGGER.warning("Connecting to device with missing data:") - await self.connect_legacy(broken=True) + await self.connect( + self.setup_legacy_app, + broken=True, + real_timeout_value=30, + url_part=CORE_LOCATIONS, + ) _LOGGER.error(" - broken information not handled") # pragma: no cover raise self.ConnectError # pragma: no cover except pw_exceptions.InvalidXMLError: _LOGGER.info(" + successfully passed XML issue handling.") _LOGGER.info("Connecting to functioning device:") - return await self.connect_legacy(stretch=stretch) + return await self.connect( + self.setup_legacy_app, + real_timeout_value=30, + stretch=stretch, + url_part=CORE_LOCATIONS, + ) # Generic disconnect @classmethod From b18aa6b882759d825741c98944007f42cd189329 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 28 Jun 2025 10:45:12 +0200 Subject: [PATCH 13/32] Define smile-Munch --- plugwise/__init__.py | 90 +++++++++++++++++---------------------- plugwise/common.py | 9 ++-- plugwise/helper.py | 12 +++--- plugwise/legacy/helper.py | 34 +++++++-------- plugwise/legacy/smile.py | 20 ++------- plugwise/smile.py | 24 +++-------- 6 files changed, 74 insertions(+), 115 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 9c56faa6a..fac6a65bf 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -36,6 +36,7 @@ import aiohttp from defusedxml import ElementTree as etree +from munch import Munch from packaging.version import Version, parse @@ -72,16 +73,17 @@ def __init__( self._smile_api: SmileAPI | SmileLegacyAPI self._stretch_v2 = False self._target_smile: str = NONE - self.smile_hostname: str = NONE - self.smile_hw_version: str | None = None - self.smile_legacy = False - self.smile_mac_address: str | None = None - self.smile_model: str = NONE - self.smile_model_id: str | None = None - self.smile_name: str = NONE - self.smile_type: str = NONE - self.smile_version: Version = Version("0.0.0") - self.smile_zigbee_mac_address: str | None = None + self.smile: Munch = Munch() + self.smile.hostname: str = NONE + self.smile.hw_version: str | None = None + self.smile.legacy = False + self.smile.mac_address: str | None = None + self.smile.model: str = NONE + self.smile.model_id: str | None = None + self.smile.name: str = NONE + self.smile.type: str = NONE + self.smile.version: Version = Version("0.0.0") + self.smile.zigbee_mac_address: str | None = None @property def cooling_present(self) -> bool: @@ -109,7 +111,7 @@ def reboot(self) -> bool: All non-legacy devices support gateway-rebooting. """ - return not self.smile_legacy + return not self.smile.legacy async def connect(self) -> Version: """Connect to the Plugwise Gateway and determine its name, type, version, and other data.""" @@ -158,16 +160,9 @@ async def connect(self) -> Version: self._opentherm_device, self._request, self._schedule_old_states, - self.smile_hostname, - self.smile_hw_version, - self.smile_mac_address, - self.smile_model, - self.smile_model_id, - self.smile_name, - self.smile_type, - self.smile_version, + self.smile, ) - if not self.smile_legacy + if not self.smile.legacy else SmileLegacyAPI( self._is_thermostat, self._loc_data, @@ -176,21 +171,14 @@ async def connect(self) -> Version: self._request, self._stretch_v2, self._target_smile, - self.smile_hostname, - self.smile_hw_version, - self.smile_mac_address, - self.smile_model, - self.smile_name, - self.smile_type, - self.smile_version, - self.smile_zigbee_mac_address, + self.smile, ) ) # Update all endpoints on first connect await self._smile_api.full_xml_update() - return self.smile_version + return self.smile.version async def _smile_detect( self, result: etree.Element, dsmrmain: etree.Element @@ -203,15 +191,15 @@ async def _smile_detect( if (gateway := result.find("./gateway")) is not None: if (v_model := gateway.find("vendor_model")) is not None: model = v_model.text - self.smile_version = parse(gateway.find("firmware_version").text) - self.smile_hw_version = gateway.find("hardware_version").text - self.smile_hostname = gateway.find("hostname").text - self.smile_mac_address = gateway.find("mac_address").text - self.smile_model_id = gateway.find("vendor_model").text + self.smile.version = parse(gateway.find("firmware_version").text) + self.smile.hw_version = gateway.find("hardware_version").text + self.smile.hostname = gateway.find("hostname").text + self.smile.mac_address = gateway.find("mac_address").text + self.smile._model_id = gateway.find("vendor_model").text else: model = await self._smile_detect_legacy(result, dsmrmain, model) - if model == "Unknown" or self.smile_version is None: # pragma: no cover + if model == "Unknown" or self.smile.version is None: # pragma: no cover # Corner case check LOGGER.error( "Unable to find model or version information, please create" @@ -219,7 +207,7 @@ async def _smile_detect( ) raise UnsupportedDeviceError - version_major = str(self.smile_version.major) + version_major = str(self.smile.version.major) self._target_smile = f"{model}_v{version_major}" LOGGER.debug("Plugwise identified as %s", self._target_smile) if self._target_smile not in SMILES: @@ -230,7 +218,7 @@ async def _smile_detect( ) raise UnsupportedDeviceError - if not self.smile_legacy: + if not self.smile.legacy: self._timeout = DEFAULT_TIMEOUT if self._target_smile in ("smile_open_therm_v2", "smile_thermo_v3"): @@ -240,14 +228,14 @@ async def _smile_detect( ) # pragma: no cover raise UnsupportedDeviceError # pragma: no cover - self.smile_model = "Gateway" - self.smile_name = SMILES[self._target_smile].smile_name - self.smile_type = SMILES[self._target_smile].smile_type + self.smile.model = "Gateway" + self.smile.name = SMILES[self._target_smile].smile_name + self.smile.type = SMILES[self._target_smile].smile_type - if self.smile_type == "stretch": + if self.smile.type == "stretch": self._stretch_v2 = int(version_major) == 2 - if self.smile_type == "thermostat": + if self.smile.type == "thermostat": self._is_thermostat = True # For Adam, Anna, determine the system capabilities: # Find the connected heating/cooling device (heater_central), @@ -275,13 +263,13 @@ async def _smile_detect_legacy( return_model = model # Stretch: find the MAC of the zigbee master_controller (= Stick) if (network := result.find("./module/protocols/master_controller")) is not None: - self.smile_zigbee_mac_address = network.find("mac_address").text + self.smile.zigbee_mac_address = network.find("mac_address").text # Find the active MAC in case there is an orphaned Stick if zb_networks := result.findall("./network"): for zb_network in zb_networks: if zb_network.find("./nodes/network_router") is not None: network = zb_network.find("./master_controller") - self.smile_zigbee_mac_address = network.find("mac_address").text + self.smile.zigbee_mac_address = network.find("mac_address").text # Legacy Anna or Stretch: if ( @@ -289,22 +277,22 @@ async def _smile_detect_legacy( or network is not None ): system = await self._request(SYSTEM) - self.smile_version = parse(system.find("./gateway/firmware").text) + self.smile.version = parse(system.find("./gateway/firmware").text) return_model = str(system.find("./gateway/product").text) - self.smile_hostname = system.find("./gateway/hostname").text + self.smile.hostname = system.find("./gateway/hostname").text # If wlan0 contains data it's active, eth0 should be checked last as is preferred for network in ("wlan0", "eth0"): locator = f"./{network}/mac" if (net_locator := system.find(locator)) is not None: - self.smile_mac_address = net_locator.text + self.smile.mac_address = net_locator.text # P1 legacy: elif dsmrmain is not None: status = await self._request(STATUS) - self.smile_version = parse(status.find("./system/version").text) + self.smile.version = parse(status.find("./system/version").text) return_model = str(status.find("./system/product").text) - self.smile_hostname = status.find("./network/hostname").text - self.smile_mac_address = status.find("./network/mac_address").text + self.smile.hostname = status.find("./network/hostname").text + self.smile.mac_address = status.find("./network/mac_address").text else: # pragma: no cover # No cornercase, just end of the line LOGGER.error( @@ -313,7 +301,7 @@ async def _smile_detect_legacy( ) raise ResponseError - self.smile_legacy = True + self.smile.legacy = True return return_model async def async_update(self) -> dict[str, GwEntityData]: diff --git a/plugwise/common.py b/plugwise/common.py index c261eeb4b..be57505e1 100644 --- a/plugwise/common.py +++ b/plugwise/common.py @@ -55,17 +55,16 @@ def __init__(self) -> None: self._heater_id: str = NONE self._on_off_device: bool self.gw_entities: dict[str, GwEntityData] = {} - self.smile_name: str - self.smile_type: str + self.smile: Munch @property def heater_id(self) -> str: """Return the heater-id.""" return self._heater_id - def smile(self, name: str) -> bool: + def check_name(self, name: str) -> bool: """Helper-function checking the smile-name.""" - return self.smile_name == name + return self.smile.name == name def _appl_heater_central_info( self, @@ -173,7 +172,7 @@ def _get_group_switches(self) -> dict[str, GwEntityData]: """ switch_groups: dict[str, GwEntityData] = {} # P1 and Anna don't have switchgroups - if self.smile_type == "power" or self.smile(ANNA): + if self.smile.type == "power" or self.check_name(ANNA): return switch_groups for group in self._domain_objects.findall("./group"): diff --git a/plugwise/helper.py b/plugwise/helper.py index edd680747..b370b9316 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -277,7 +277,7 @@ def _appl_gateway_info(self, appl: Munch, appliance: etree.Element) -> Munch: appl.vendor_name = "Plugwise" # Adam: collect the ZigBee MAC address of the Smile - if self.smile(ADAM): + if self.check_name(ADAM): if ( found := self._domain_objects.find(".//protocols/zig_bee_coordinator") ) is not None: @@ -383,7 +383,7 @@ def _get_measurement_data(self, entity_id: str) -> GwEntityData: data.pop("c_heating_state") self._count -= 1 - if self._is_thermostat and self.smile(ANNA): + if self._is_thermostat and self.check_name(ANNA): self._update_anna_cooling(entity_id, data) self._cleanup_data(data) @@ -484,7 +484,7 @@ def _get_actuator_functionalities( item == "thermostat" and ( entity["dev_class"] != "climate" - if self.smile(ADAM) + if self.check_name(ADAM) else entity["dev_class"] != "thermostat" ) ): @@ -539,7 +539,7 @@ def _get_actuator_mode( Collect the requested gateway mode. """ - if not (self.smile(ADAM) and entity_id == self._gateway_id): + if not (self.check_name(ADAM) and entity_id == self._gateway_id): return None if (search := search_actuator_functionalities(appliance, key)) is not None: @@ -605,10 +605,10 @@ def _process_on_off_device_c_heating_state(self, data: GwEntityData) -> None: Solution for Core issue #81839. """ - if self.smile(ANNA): + if self.check_name(ANNA): data["binary_sensors"]["heating_state"] = data["c_heating_state"] - if self.smile(ADAM): + if self.check_name(ADAM): # First count when not present, then create and init to False. # When present init to False if "heating_state" not in data["binary_sensors"]: diff --git a/plugwise/legacy/helper.py b/plugwise/legacy/helper.py index e22b489e5..37cf3992d 100644 --- a/plugwise/legacy/helper.py +++ b/plugwise/legacy/helper.py @@ -73,10 +73,10 @@ def __init__(self) -> None: self._modules: etree.Element self._stretch_v2: bool self.gw_entities: dict[str, GwEntityData] = {} - self.smile_mac_address: str | None - self.smile_model: str - self.smile_version: Version - self.smile_zigbee_mac_address: str | None + self.smile.mac_address: str | None + self.smile.model: str + self.smile.version: Version + self.smile.zigbee_mac_address: str | None @property def gateway_id(self) -> str: @@ -95,7 +95,7 @@ def _all_appliances(self) -> None: self._create_legacy_gateway() # For legacy P1 collect the connected SmartMeter info - if self.smile_type == "power": + if self.smile.type == "power": appl = Munch() self._p1_smartmeter_info_finder(appl) # Legacy P1 has no more devices @@ -167,13 +167,13 @@ def _all_locations(self) -> None: loc.loc_id = location.attrib["id"] # Filter the valid single location for P1 legacy: services not empty locator = "./services" - if self.smile_type == "power" and len(location.find(locator)) == 0: + if self.smile.type == "power" and len(location.find(locator)) == 0: continue if loc.name == "Home": self._home_loc_id = loc.loc_id # Replace location-name for P1 legacy, can contain privacy-related info - if self.smile_type == "power": + if self.smile.type == "power": loc.name = "Home" self._home_loc_id = loc.loc_id @@ -185,18 +185,18 @@ def _create_legacy_gateway(self) -> None: Use the home_location or FAKE_APPL as entity id. """ self._gateway_id = self._home_loc_id - if self.smile_type == "power": + if self.smile.type == "power": self._gateway_id = FAKE_APPL self.gw_entities[self._gateway_id] = {"dev_class": "gateway"} self._count += 1 for key, value in { - "firmware": str(self.smile_version), + "firmware": str(self.smile.version), "location": self._home_loc_id, - "mac_address": self.smile_mac_address, - "model": self.smile_model, - "name": self.smile_name, - "zigbee_mac_address": self.smile_zigbee_mac_address, + "mac_address": self.smile.mac_address, + "model": self.smile.model, + "name": self.smile.name, + "zigbee_mac_address": self.smile.zigbee_mac_address, "vendor": "Plugwise", }.items(): if value is not None: @@ -224,14 +224,14 @@ def _energy_entity_info_finder(self, appliance: etree, appl: Munch) -> Munch: Collect energy entity info (Smartmeter, Circle, Stealth, etc.): firmware, model and vendor name. """ - if self.smile_type in ("power", "stretch"): + if self.smile.type in ("power", "stretch"): locator = "./services/electricity_point_meter" module_data = self._get_module_data( appliance, locator, self._modules, legacy=True ) appl.zigbee_mac = module_data["zigbee_mac_address"] # Filter appliance without zigbee_mac, it's an orphaned device - if appl.zigbee_mac is None and self.smile_type != "power": + if appl.zigbee_mac is None and self.smile.type != "power": return None appl.hardware = module_data["hardware_version"] @@ -253,7 +253,7 @@ def _p1_smartmeter_info_finder(self, appl: Munch) -> None: appl.entity_id = loc_id appl.location = loc_id appl.mac = None - appl.model = self.smile_model + appl.model = self.smile.model appl.model_id = None appl.name = "P1" appl.pwclass = "smartmeter" @@ -272,7 +272,7 @@ def _get_measurement_data(self, entity_id: str) -> GwEntityData: # Get P1 smartmeter data from MODULES entity = self.gw_entities[entity_id] # !! DON'T CHANGE below two if-lines, will break stuff !! - if self.smile_type == "power": + if self.smile.type == "power": if entity["dev_class"] == "smartmeter": data.update(self._power_data_from_modules()) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index fc4f3a2c5..68108163a 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -44,14 +44,7 @@ def __init__( _request: Callable[..., Awaitable[Any]], _stretch_v2: bool, _target_smile: str, - smile_hostname: str, - smile_hw_version: str | None, - smile_mac_address: str | None, - smile_model: str, - smile_name: str, - smile_type: str, - smile_version: Version, - smile_zigbee_mac_address: str | None, + smile: Munch, ) -> None: """Set the constructor for this class.""" super().__init__() @@ -63,14 +56,7 @@ def __init__( self._request = _request self._stretch_v2 = _stretch_v2 self._target_smile = _target_smile - self.smile_hostname = smile_hostname - self.smile_hw_version = smile_hw_version - self.smile_mac_address = smile_mac_address - self.smile_model = smile_model - self.smile_name = smile_name - self.smile_type = smile_type - self.smile_version = smile_version - self.smile_zigbee_mac_address = smile_zigbee_mac_address + self.smile = smile self._first_update = True self._previous_day_number: str = "0" @@ -86,7 +72,7 @@ async def full_xml_update(self) -> None: self._locations = await self._request(LOCATIONS) self._modules = await self._request(MODULES) # P1 legacy has no appliances - if self.smile_type != "power": + if self.smile.type != "power": self._appliances = await self._request(APPLIANCES) def get_all_gateway_entities(self) -> None: diff --git a/plugwise/smile.py b/plugwise/smile.py index 0947401d4..1095b34f4 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -54,14 +54,7 @@ def __init__( _opentherm_device: bool, _request: Callable[..., Awaitable[Any]], _schedule_old_states: dict[str, dict[str, str]], - smile_hostname: str | None, - smile_hw_version: str | None, - smile_mac_address: str | None, - smile_model: str, - smile_model_id: str | None, - smile_name: str, - smile_type: str, - smile_version: Version, + smile: Munch, ) -> None: """Set the constructor for this class.""" super().__init__() @@ -74,14 +67,7 @@ def __init__( self._opentherm_device = _opentherm_device self._request = _request self._schedule_old_states = _schedule_old_states - self.smile_hostname = smile_hostname - self.smile_hw_version = smile_hw_version - self.smile_mac_address = smile_mac_address - self.smile_model = smile_model - self.smile_model_id = smile_model_id - self.smile_name = smile_name - self.smile_type = smile_type - self.smile_version = smile_version + self.smile = smile self.therms_with_offset_func: list[str] = [] @property @@ -107,7 +93,7 @@ def get_all_gateway_entities(self) -> None: self.therms_with_offset_func = ( self._get_appliances_with_offset_functionality() ) - if self.smile(ADAM): + if self.check_name(ADAM): self._scan_thermostats() if group_data := self._get_group_switches(): @@ -340,7 +326,7 @@ async def set_schedule_state( template = ( '