Repository: blakeblackshear/frigate

Issue:
[Support]: Recording / playback offset in half-hour time zones
### Describe the problem you are having

I'm in South Australia. We think like to think that we're special here, so we have a special time zone. Currently UTC+10:30.

With Frigate 0.11 & earlier I had /etc/localtime bind mounted into Frigate. Recordings were saved to disk in a directory structure using local timestamps & all was well (except for possibly overwriting an hour of recordings when DST ends).

With Frigate 0.12 I noticed that a switch to UTC timestamps was made internally, but segments were saved as 30-59 before wrapping around to 00-29 in the same directory. Removing the /etc/localtime bind mount addressed this. 

Still, when viewing clips "based on the browser's timezone Australia/Adelaide" there is an offset of 30 minutes. When clicking on an event at 18:06 (correct timestamp), the player loads & seeks to 6 minutes but this actually corresponds to 18:36. To view the event from 18:06, I have to load the previous hour & seek to 36 minutes.

I had a lazy solution in mind to add/subtract the minute part of the offset in the frontend. I got as far as determining the offset in JavaScript before my head started hurting too much & concluded that this a questionable approach.
```
# returns 30 in Australia/Adelaide
# should return 15 in Asia/Kathmandu (UTC+5:45)
((new Date().getTimezoneOffset() % 60 % 60) + 60) % 60;
# or
# returns 30 in Australia/Adelaide
# should return 45 in Asia/Kathmandu (UTC+5:45)
((-new Date().getTimezoneOffset() % 60 % 60) + 60) % 60;
```

### Version

0.12.0-CF2466C

### Frigate config file

```yaml
# No time zone relevant config in config.yml
```


### Relevant log output

```shell
# No relevant logs?
```


### FFprobe output from your camera

```shell
# Not a camera specific problem
```


### Frigate stats

_No response_

### Operating system

Debian

### Install method

Docker Compose

### Coral version

PCIe

### Network connection

Wired

### Camera make and model

# Not a camera specific problem

### Any other information that may be helpful

_No response_


Discussion:
Thanks for the detailed report, I know what the problem is here. 

Resolving patch:
diff --git a/frigate/http.py b/frigate/http.py
--- a/frigate/http.py
+++ b/frigate/http.py
@@ -41,6 +41,7 @@
     ffprobe_stream,
     restart_frigate,
     vainfo_hwaccel,
+    get_tz_modifiers,
 )
 from frigate.storage import StorageMaintainer
 from frigate.version import VERSION
@@ -91,7 +92,7 @@ def is_healthy():
 @bp.route("/events/summary")
 def events_summary():
     tz_name = request.args.get("timezone", default="utc", type=str)
-    tz_offset = f"{int(datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()/60/60)} hour"
+    hour_modifier, minute_modifier = get_tz_modifiers(tz_name)
     has_clip = request.args.get("has_clip", type=int)
     has_snapshot = request.args.get("has_snapshot", type=int)
 
@@ -111,7 +112,10 @@ def events_summary():
             Event.camera,
             Event.label,
             fn.strftime(
-                "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", tz_offset)
+                "%Y-%m-%d",
+                fn.datetime(
+                    Event.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ).alias("day"),
             Event.zones,
             fn.COUNT(Event.id).alias("count"),
@@ -121,7 +125,10 @@ def events_summary():
             Event.camera,
             Event.label,
             fn.strftime(
-                "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", tz_offset)
+                "%Y-%m-%d",
+                fn.datetime(
+                    Event.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ),
             Event.zones,
         )
@@ -912,12 +919,14 @@ def get_recordings_storage_usage():
 @bp.route("/<camera_name>/recordings/summary")
 def recordings_summary(camera_name):
     tz_name = request.args.get("timezone", default="utc", type=str)
-    tz_offset = f"{int(datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()/60/60)} hour"
+    hour_modifier, minute_modifier = get_tz_modifiers(tz_name)
     recording_groups = (
         Recordings.select(
             fn.strftime(
                 "%Y-%m-%d %H",
-                fn.datetime(Recordings.start_time, "unixepoch", tz_offset),
+                fn.datetime(
+                    Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ).alias("hour"),
             fn.SUM(Recordings.duration).alias("duration"),
             fn.SUM(Recordings.motion).alias("motion"),
@@ -927,13 +936,17 @@ def recordings_summary(camera_name):
         .group_by(
             fn.strftime(
                 "%Y-%m-%d %H",
-                fn.datetime(Recordings.start_time, "unixepoch", tz_offset),
+                fn.datetime(
+                    Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             )
         )
         .order_by(
             fn.strftime(
                 "%Y-%m-%d H",
-                fn.datetime(Recordings.start_time, "unixepoch", tz_offset),
+                fn.datetime(
+                    Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ).desc()
         )
     )
@@ -942,7 +955,9 @@ def recordings_summary(camera_name):
         Event.select(
             fn.strftime(
                 "%Y-%m-%d %H",
-                fn.datetime(Event.start_time, "unixepoch", tz_offset),
+                fn.datetime(
+                    Event.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ).alias("hour"),
             fn.COUNT(Event.id).alias("count"),
         )
@@ -950,7 +965,9 @@ def recordings_summary(camera_name):
         .group_by(
             fn.strftime(
                 "%Y-%m-%d %H",
-                fn.datetime(Event.start_time, "unixepoch", tz_offset),
+                fn.datetime(
+                    Event.start_time, "unixepoch", hour_modifier, minute_modifier
+                ),
             ),
         )
         .objects()
@@ -1147,17 +1164,11 @@ def vod_hour_no_timezone(year_month, day, hour, camera_name):
 # TODO make this nicer when vod module is removed
 @bp.route("/vod/<year_month>/<day>/<hour>/<camera_name>/<tz_name>")
 def vod_hour(year_month, day, hour, camera_name, tz_name):
-    tz_offset = int(
-        datetime.now(pytz.timezone(tz_name.replace(",", "/")))
-        .utcoffset()
-        .total_seconds()
-        / 60
-        / 60
-    )
     parts = year_month.split("-")
-    start_date = datetime(
-        int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc
-    ) - timedelta(hours=tz_offset)
+    start_date = (
+        datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc)
+        - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
+    )
     end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
     start_ts = start_date.timestamp()
     end_ts = end_date.timestamp()
diff --git a/frigate/util.py b/frigate/util.py
--- a/frigate/util.py
+++ b/frigate/util.py
@@ -14,12 +14,13 @@
 from collections import Counter
 from collections.abc import Mapping
 from multiprocessing import shared_memory
-from typing import Any, AnyStr
+from typing import Any, AnyStr, Tuple
 
 import cv2
 import numpy as np
 import os
 import psutil
+import pytz
 
 from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
 
@@ -1040,3 +1041,14 @@ def delete(self, name):
             self.shm_store[name].close()
             self.shm_store[name].unlink()
             del self.shm_store[name]
+
+
+def get_tz_modifiers(tz_name: str) -> Tuple[str, str]:
+    seconds_offset = (
+        datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
+    )
+    hours_offset = int(seconds_offset / 60 / 60)
+    minutes_offset = int(seconds_offset / 60 - hours_offset * 60)
+    hour_modifier = f"{hours_offset} hour"
+    minute_modifier = f"{minutes_offset} minute"
+    return hour_modifier, minute_modifier


Test patch: