From 28c92e217bfdeea5be4c2471d915ad9f2e317147 Mon Sep 17 00:00:00 2001 From: axxonite Date: Tue, 22 Sep 2026 19:53:35 -0400 Subject: [PATCH] feat: add `battery_replaced_timestamp` property to `Tile` The Tile API records when a Tile's battery was last replaced, in `result.metadata.battery_replaced_at` as epoch milliseconds, but `Tile` exposes no property for it. Return it the same way `last_timestamp` and `lost_timestamp` are returned: a naive UTC `datetime`, or `None` when the key is absent. Only Tiles with a user-replaceable battery that has actually been replaced carry the key, so absence is the common case rather than an error. The neighbouring `metadata.battery_state` is deliberately left out. It is a modelled estimate rather than a reading: a Tile silent for over a year with a flat cell still reported `"40"`, and Tiles with no battery data report a placeholder `"10"`, which this repo's own fixture also carries. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Qui3EQaoxiVRCBVVe9waLw --- README.md | 1 + pytile/tile.py | 17 +++++++++++++++++ tests/fixtures/tile_details_response.json | 3 ++- tests/test_tile.py | 3 +++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ba5f7..f630ea8 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ The Tile object comes with several properties: - `accuracy`: the location accuracy of the Tile - `altitude`: the altitude of the Tile - `archetype`: the internal reference string that describes the Tile's "family" +- `battery_replaced_timestamp`: the timestamp at which the Tile's battery was last replaced - `battery_status`: the Tile's battery status (e.g., `NONE`, `LEVEL1`, `LEVEL2`) - `dead`: whether the Tile is inactive - `firmware_version`: the Tile's firmware version diff --git a/pytile/tile.py b/pytile/tile.py index 1520ad4..b51abe7 100644 --- a/pytile/tile.py +++ b/pytile/tile.py @@ -69,6 +69,22 @@ def archetype(self) -> str: """ return cast(str, self._tile_data["result"]["archetype"]) + @property + def battery_replaced_timestamp(self) -> datetime | None: + """Return the timestamp at which the Tile's battery was last replaced. + + Only Tiles with a user-replaceable battery that has been replaced carry this. + + Returns: + The timestamp (if it exists). + """ + metadata = self._tile_data["result"].get("metadata") or {} + if (replaced_at := metadata.get("battery_replaced_at")) is None: + return None + return datetime.fromtimestamp(int(replaced_at) / 1000, tz=timezone.utc).replace( + tzinfo=None + ) + @property def battery_status(self) -> str | None: """Return the battery status. @@ -250,6 +266,7 @@ def as_dict(self) -> dict[str, Any]: "accuracy": self.accuracy, "altitude": self.altitude, "archetype": self.archetype, + "battery_replaced_timestamp": self.battery_replaced_timestamp, "battery_status": self.battery_status, "dead": self.dead, "firmware_version": self.firmware_version, diff --git a/tests/fixtures/tile_details_response.json b/tests/fixtures/tile_details_response.json index 6123158..9789909 100644 --- a/tests/fixtures/tile_details_response.json +++ b/tests/fixtures/tile_details_response.json @@ -76,7 +76,8 @@ "auth_key": "xxxxxxxxxxxxxxxxxxxxxxxx", "renewal_status": "NONE", "metadata": { - "battery_state": "10" + "battery_state": "10", + "battery_replaced_at": "1590969600000" }, "battery_status": "NONE", "serial_number": null, diff --git a/tests/test_tile.py b/tests/test_tile.py index 3408229..0676306 100644 --- a/tests/test_tile.py +++ b/tests/test_tile.py @@ -125,6 +125,7 @@ async def test_get_tiles( assert tile.accuracy == 13.496111 assert tile.altitude == 0.4076319168123 assert tile.archetype == "WALLET" + assert tile.battery_replaced_timestamp == datetime(2020, 6, 1, 0, 0) assert tile.battery_status == "NONE" assert not tile.dead assert not tile.lost @@ -237,6 +238,7 @@ async def test_missing_last_tile_state( tile = tiles[TILE_TILE_UUID] assert not tile.accuracy assert not tile.altitude + assert not tile.battery_replaced_timestamp assert not tile.last_timestamp assert not tile.latitude assert not tile.longitude @@ -296,6 +298,7 @@ async def test_tile_as_dict( "accuracy": 13.496111, "altitude": 0.4076319168123, "archetype": "WALLET", + "battery_replaced_timestamp": datetime(2020, 6, 1, 0, 0), "battery_status": "NONE", "dead": False, "firmware_version": "01.12.14.0",