From bf6cd388cfc5c68cf8236213ae0740ea907199e8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Apr 2022 17:43:06 -0400 Subject: [PATCH 1/5] 0.46.0.dev0 version bump --- zigpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy/__init__.py b/zigpy/__init__.py index 2fa1947b5..532c3ca6e 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 45 +MINOR_VERSION = 46 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 8a9caec8be9ef386b89931ab820413e6f9c67b9a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Apr 2022 22:41:23 -0400 Subject: [PATCH 2/5] Make sure `last_seen` is still a float --- tests/test_appdb.py | 11 +++++------ tests/test_device.py | 2 +- zigpy/appdb.py | 2 +- zigpy/device.py | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_appdb.py b/tests/test_appdb.py index 060e1a3c2..6a59ea214 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -1,5 +1,4 @@ import asyncio -from datetime import datetime import os import sqlite3 import sys @@ -178,8 +177,8 @@ async def test_database(tmpdir): ts = time.time() dev.last_seen = ts dev_last_seen = dev.last_seen - assert isinstance(dev.last_seen, datetime) - assert abs(dev.last_seen.timestamp() - ts) < 0.1 + assert isinstance(dev.last_seen, float) + assert abs(dev.last_seen - ts) < 0.01 # Test a CustomDevice custom_ieee = make_ieee(1) @@ -202,7 +201,7 @@ async def test_database(tmpdir): assert dev.endpoints[1].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.endpoints[99].in_clusters[0x0008]._attr_cache[0x0011] == 17 custom_dev_last_seen = dev.last_seen - assert isinstance(custom_dev_last_seen, datetime) + assert isinstance(custom_dev_last_seen, float) await app.pre_shutdown() @@ -221,7 +220,7 @@ async def test_database(tmpdir): assert dev.endpoints[3].device_type == profiles.zll.DeviceType.COLOR_LIGHT assert dev.relays == relays_1 # The timestamp won't be restored exactly but it is more than close enough - assert abs((dev.last_seen - dev_last_seen).total_seconds()) < 0.01 + assert abs(dev.last_seen - dev_last_seen) < 0.01 dev = app2.get_device(custom_ieee) # This virtual attribute is added by the quirk, there is no corresponding cluster @@ -229,7 +228,7 @@ async def test_database(tmpdir): assert dev.endpoints[1].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.endpoints[99].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.relays == relays_2 - assert abs((dev.last_seen - custom_dev_last_seen).total_seconds()) < 0.01 + assert abs(dev.last_seen - custom_dev_last_seen) < 0.01 dev.relays = None app.handle_leave(99, ieee) diff --git a/tests/test_device.py b/tests/test_device.py index be65adb9d..419f719a3 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -440,7 +440,7 @@ def test_device_last_seen(dev, monkeypatch): dev.last_seen = 0 epoch = datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc) - assert dev.last_seen == epoch + assert dev.last_seen == epoch.timestamp() dev.listener_event.assert_called_once_with("device_last_seen_updated", epoch) dev.listener_event.reset_mock() diff --git a/zigpy/appdb.py b/zigpy/appdb.py index 22f6c3f00..0dcb33b9a 100644 --- a/zigpy/appdb.py +++ b/zigpy/appdb.py @@ -372,7 +372,7 @@ async def _save_device(self, device: zigpy.typing.DeviceType) -> None: status=excluded.status, last_seen=excluded.last_seen""" await self.execute( - q, (device.ieee, device.nwk, device.status, device.last_seen) + q, (device.ieee, device.nwk, device.status, device._last_seen) ) if device.node_desc is not None: diff --git a/zigpy/device.py b/zigpy/device.py index 3067cc81f..6218de836 100644 --- a/zigpy/device.py +++ b/zigpy/device.py @@ -85,8 +85,8 @@ def update_last_seen(self) -> None: self.last_seen = datetime.now(timezone.utc) @property - def last_seen(self) -> datetime | None: - return self._last_seen + def last_seen(self) -> float | None: + return self._last_seen.timestamp() if self._last_seen is not None else None @last_seen.setter def last_seen(self, value: datetime | int | float): From 6a68054682eb30d071ca4921136927fcee34d9d0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Apr 2022 23:40:43 -0400 Subject: [PATCH 3/5] Set `last_seen = None` instead of `0` --- zigpy/appdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zigpy/appdb.py b/zigpy/appdb.py index 0dcb33b9a..627b0b969 100644 --- a/zigpy/appdb.py +++ b/zigpy/appdb.py @@ -565,7 +565,9 @@ async def _load_devices(self) -> None: async for (ieee, nwk, status, last_seen) in cursor: dev = self._application.add_device(ieee, nwk) dev.status = zigpy.device.Status(status) - dev._last_seen = last_seen + + if last_seen > UNIX_EPOCH: + dev._last_seen = last_seen async def _load_node_descriptors(self) -> None: async with self.execute(f"SELECT * FROM node_descriptors{DB_V}") as cursor: From 79990a111f495f6cabfeefe29fbc29717d52468b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Apr 2022 23:41:19 -0400 Subject: [PATCH 4/5] Fix silent SQL error and add more thorough unit tests --- tests/test_appdb.py | 44 +++++++++++++++++++++++++++++++++++ tests/test_appdb_migration.py | 25 ++++++++++++++------ zigpy/appdb.py | 4 ++-- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/tests/test_appdb.py b/tests/test_appdb.py index 6a59ea214..eef700ab6 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -773,6 +773,50 @@ async def test_load_unsupp_attr_wrong_cluster(tmpdir): await app.pre_shutdown() +async def test_last_seen(tmpdir): + db = os.path.join(str(tmpdir), "test.db") + app = await make_app(db) + + ieee = make_ieee() + app.handle_join(99, ieee, 0) + + dev = app.get_device(ieee=ieee) + ep = dev.add_endpoint(3) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = 260 + ep.device_type = profiles.zha.DeviceType.PUMP + clus = ep.add_input_cluster(0) + ep.add_output_cluster(1) + clus._update_attribute(4, "Custom") + clus._update_attribute(5, "Model") + app.device_initialized(dev) + + old_last_seen = dev.last_seen + await app.pre_shutdown() + + # The `last_seen` of a joined device persists + app = await make_app(db) + dev = app.get_device(ieee=ieee) + await app.pre_shutdown() + + next_last_seen = dev.last_seen + assert abs(next_last_seen - old_last_seen) < 0.01 + + await asyncio.sleep(0.1) + + # Now the last_seen will update + app = await make_app(db) + dev = app.get_device(ieee=ieee) + dev.update_last_seen() + await app.pre_shutdown() + + # And it will be updated when the database next loads + app = await make_app(db) + dev = app.get_device(ieee=ieee) + assert dev.last_seen > next_last_seen + 0.1 + await app.pre_shutdown() + + @pytest.mark.parametrize( "stdlib_version,use_sqlite", [ diff --git a/tests/test_appdb_migration.py b/tests/test_appdb_migration.py index b2ebfe398..b880ae271 100644 --- a/tests/test_appdb_migration.py +++ b/tests/test_appdb_migration.py @@ -405,13 +405,6 @@ async def test_v4_to_v5_migration_bad_neighbors(test_db, with_bad_neighbor): assert num_new_neighbors == num_v4_neighbors -async def test_v5_to_v6_migration(test_db): - test_db_v5 = test_db("simple_v5.sql") - - app = await make_app(test_db_v5) - await app.pre_shutdown() - - @pytest.mark.parametrize("with_quirk_attribute", [False, True]) async def test_v4_to_v6_migration_missing_endpoints(test_db, with_quirk_attribute): """V5's schema was too rigid and failed to migrate endpoints created by quirks""" @@ -495,3 +488,21 @@ async def test_migration_missing_tables(): appdb.execute.assert_called_once_with("SELECT * FROM table2_v1") await appdb.shutdown() + + +async def test_last_seen_migration(test_db): + test_db_v5 = test_db("simple_v5.sql") + + # To preserve the old behavior, `0` will not be exposed to ZHA, only `None` + app = await make_app(test_db_v5) + dev = app.get_device(nwk=0xBD4D) + + assert dev.last_seen is None + dev.update_last_seen() + assert isinstance(dev.last_seen, float) + await app.pre_shutdown() + + # But the device's `last_seen` will still update properly when it's actually set + app = await make_app(test_db_v5) + assert isinstance(app.get_device(nwk=0xBD4D).last_seen, float) + await app.pre_shutdown() diff --git a/zigpy/appdb.py b/zigpy/appdb.py index 627b0b969..65e13389e 100644 --- a/zigpy/appdb.py +++ b/zigpy/appdb.py @@ -228,7 +228,7 @@ def device_last_seen_updated( async def _save_device_last_seen(self, ieee: t.EUI64, last_seen: datetime) -> None: await self.execute( - f"UPDATE devices{DB_V} SET last_seen=? WHERE ieee=?", (ieee, last_seen) + f"UPDATE devices{DB_V} SET last_seen=? WHERE ieee=?", (last_seen, ieee) ) await self._db.commit() @@ -372,7 +372,7 @@ async def _save_device(self, device: zigpy.typing.DeviceType) -> None: status=excluded.status, last_seen=excluded.last_seen""" await self.execute( - q, (device.ieee, device.nwk, device.status, device._last_seen) + q, (device.ieee, device.nwk, device.status, device._last_seen or UNIX_EPOCH) ) if device.node_desc is not None: From e6216c45e7046a7a5590df78b3d6bfb819928d41 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Apr 2022 23:02:22 -0400 Subject: [PATCH 5/5] 0.45.1 version bump --- zigpy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zigpy/__init__.py b/zigpy/__init__.py index 532c3ca6e..b7f6a7cb8 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 46 -PATCH_VERSION = "0.dev0" +MINOR_VERSION = 45 +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"