Skip to content

Commit dbdae12

Browse files
committed
improve timezone handling, fix lat/lon swapped bug
1 parent bfa7053 commit dbdae12

4 files changed

Lines changed: 518 additions & 486 deletions

File tree

findmydad/geofences.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import os
3+
import zoneinfo
34
from typing import Literal, TypedDict
45

56
import duckdb
@@ -11,6 +12,7 @@ class Geofence(TypedDict):
1112
schedule_start: datetime.time | None
1213
schedule_end: datetime.time | None
1314
description: str
15+
timezone: zoneinfo.ZoneInfo
1416

1517

1618
class GeofenceManager:
@@ -30,8 +32,34 @@ def refresh_geofences(self):
3032
schedule_stop::TIME as schedule_stop,
3133
description,
3234
ST_GeomFromGeoJSON(geojson) as geometry,
35+
timezone::VARCHAR as timezone,
3336
FROM read_csv('{self.url}')
34-
)"""
37+
);
38+
-- convert a timestamp to a time in the given timezone
39+
CREATE OR REPLACE MACRO time_at(timestamp, timezone) AS (
40+
strftime((timestamp AT TIME ZONE timezone), '%H:%M:%S')::TIME
41+
);
42+
CREATE MACRO violations(lat, lon, timestamp) AS TABLE
43+
SELECT *, time_at(timestamp, timezone) AS time_at
44+
FROM geofences
45+
WHERE NOT ST_Contains(geometry, ST_Point(lon, lat))
46+
AND (
47+
status = 'on'
48+
OR
49+
(
50+
status = 'schedule'
51+
AND
52+
CASE
53+
WHEN schedule_start IS NULL THEN TRUE
54+
WHEN schedule_stop IS NULL THEN TRUE
55+
WHEN schedule_start <= schedule_stop THEN
56+
time_at(timestamp, timezone) >= schedule_start AND time_at(timestamp, timezone) <= schedule_stop
57+
ELSE
58+
time_at(timestamp, timezone) >= schedule_start OR time_at(timestamp, timezone) <= schedule_stop
59+
END
60+
)
61+
);
62+
"""
3563
)
3664

3765
def get_geofences(self) -> list[Geofence]:
@@ -43,31 +71,24 @@ def violations(
4371
) -> list[Geofence]:
4472
"""if the point is outside any geofence"""
4573
relation = self.conn.sql(
46-
"""
47-
FROM geofences
48-
WHERE NOT ST_Contains(geometry, ST_Point($lat, $lon))
49-
AND (
50-
status = 'on'
51-
OR
52-
(
53-
status = 'schedule'
54-
AND
55-
CASE
56-
WHEN schedule_start IS NULL THEN TRUE
57-
WHEN schedule_stop IS NULL THEN TRUE
58-
WHEN schedule_start <= schedule_stop THEN
59-
schedule_start <= $localtime AND $localtime <= schedule_stop
60-
ELSE
61-
schedule_start <= $localtime OR $localtime <= schedule_stop
62-
END
63-
)
64-
)
65-
""",
66-
params={"lat": lat, "lon": lon, "localtime": timestamp.time()},
74+
"FROM violations($lat, $lon, $timestamp)",
75+
params={"lat": lat, "lon": lon, "timestamp": timestamp},
6776
)
6877
return self._to_dicts(relation)
6978

70-
def _to_dicts(self, relation: duckdb.DuckDBPyRelation) -> list[Geofence]:
71-
cols = ["id", "status", "schedule_start", "schedule_stop", "description"]
72-
selection = relation.select(*cols)
73-
return [Geofence(zip(cols, row)) for row in selection.fetchall()]
79+
@staticmethod
80+
def _to_dicts(relation: duckdb.DuckDBPyRelation) -> list[Geofence]:
81+
cols = [
82+
"id",
83+
"status",
84+
"schedule_start",
85+
"schedule_stop",
86+
"description",
87+
"timezone",
88+
]
89+
extra = [c for c in relation.columns if c not in cols]
90+
cols = [*cols, *extra]
91+
raw = [dict(zip(cols, row)) for row in relation.select(*cols).fetchall()]
92+
for row in raw:
93+
row["timezone"] = zoneinfo.ZoneInfo(row["timezone"])
94+
return [Geofence(**row) for row in raw]

findmydad/main.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ def _google_maps_info(lat: float, lon: float) -> str:
4545
def summarize_violations(violations: list[Violation]) -> str:
4646
# there could be multiple violations that tie as the latest,
4747
# but here it doesn't matter which one we pick
48-
violation = max(violations, key=lambda x: x["timestamp"])
49-
return f"At {violation['timestamp']}, Dad was at {_google_maps_info(violation['lat'], violation['lon'])} "
48+
latest_violation = max(violations, key=lambda x: x["timestamp"])
49+
geofence_timezone = latest_violation["geofence"]["timezone"]
50+
# print the timestamp without the timezone info
51+
violation_timestamp_in_localtime = (
52+
latest_violation["timestamp"]
53+
.astimezone(geofence_timezone)
54+
.strftime("%Y-%m-%d %H:%M:%S")
55+
)
56+
return f"At {violation_timestamp_in_localtime} ({geofence_timezone}), Dad was at {_google_maps_info(latest_violation['lat'], latest_violation['lon'])} "
5057

5158

5259
def main():

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ dev-dependencies = [
1717
"ruff",
1818
"dotenv",
1919
]
20+
21+
[build-system]
22+
requires = ["pdm-backend>=1.0.0"]
23+
build-backend = "pdm.backend"

0 commit comments

Comments
 (0)