From c901e712d7844583a07745883087e45178e35007 Mon Sep 17 00:00:00 2001 From: binglua Date: Mon, 22 Jun 2026 02:10:55 +0000 Subject: [PATCH] fix: preserve Mi Fitness local timezone offsets --- .../adapters/mi_fitness_cloud.py | 35 ++++++++----- src/mi_fitness_mcp/storage/__init__.py | 50 ++++++++++++------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/mi_fitness_mcp/adapters/mi_fitness_cloud.py b/src/mi_fitness_mcp/adapters/mi_fitness_cloud.py index e24a02d..0385909 100644 --- a/src/mi_fitness_mcp/adapters/mi_fitness_cloud.py +++ b/src/mi_fitness_mcp/adapters/mi_fitness_cloud.py @@ -6,7 +6,7 @@ import os import struct from collections import defaultdict from collections.abc import AsyncIterator -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta, timezone from typing import Any from urllib.parse import urlencode @@ -184,6 +184,21 @@ class MiFitnessCloudAdapter(DataAdapter): await asyncio.sleep(0.5 * (attempt + 1)) raise RuntimeError(f"Mi Fitness request failed: {last_error}") + + def _request_timezone(self, region: str | None = None) -> timezone: + region_name = self.region if region is None else region + if region_name in ("", "cn"): + return timezone(timedelta(hours=8)) + return UTC + + def _date_range_to_timestamps( + self, start_date: str, end_date: str, region: str | None = None + ) -> tuple[int, int]: + tz = self._request_timezone(region) + start_dt = datetime.fromisoformat(start_date).replace(tzinfo=tz) + end_dt = datetime.fromisoformat(end_date + "T23:59:59").replace(tzinfo=tz) + return int(start_dt.timestamp()), int(end_dt.timestamp()) + async def _fetch_key( self, key: str, start_date: str, end_date: str, region: str | None = None ) -> list[dict]: @@ -193,10 +208,7 @@ class MiFitnessCloudAdapter(DataAdapter): if region_name in ("", "cn") else f"https://{region_name}.hlth.io.mi.com" ) - start_time = int(datetime.fromisoformat(start_date).replace(tzinfo=UTC).timestamp()) - end_time = int( - datetime.fromisoformat(end_date + "T23:59:59").replace(tzinfo=UTC).timestamp() - ) + start_time, end_time = self._date_range_to_timestamps(start_date, end_date, region_name) next_key = None items: list[dict] = [] @@ -259,8 +271,9 @@ class MiFitnessCloudAdapter(DataAdapter): def _record_datetime(self, item: dict) -> datetime: timestamp = int(item.get("time", 0)) - zone_offset = int(item.get("zone_offset", 0)) - return datetime.fromtimestamp(timestamp + zone_offset, tz=UTC).replace(tzinfo=None) + zone_offset = int(item.get("zone_offset", 0) or 0) + tz = timezone(timedelta(seconds=zone_offset)) + return datetime.fromtimestamp(timestamp, tz=tz) def _parse_value(self, item: dict) -> dict[str, Any]: raw = item.get("value", "{}") @@ -269,7 +282,8 @@ class MiFitnessCloudAdapter(DataAdapter): return json.loads(raw) def _timestamp_to_datetime(self, timestamp: Any, zone_offset: int = 0) -> datetime: - return datetime.fromtimestamp(int(timestamp) + int(zone_offset), tz=UTC).replace(tzinfo=None) + tz = timezone(timedelta(seconds=int(zone_offset or 0))) + return datetime.fromtimestamp(int(timestamp), tz=tz) async def _fetch_sport_records_by_time( self, start_date: str, end_date: str, region: str | None = None @@ -280,10 +294,7 @@ class MiFitnessCloudAdapter(DataAdapter): if region_name in ("", "cn") else f"https://{region_name}.hlth.io.mi.com" ) - start_time = int(datetime.fromisoformat(start_date).replace(tzinfo=UTC).timestamp()) - end_time = int( - datetime.fromisoformat(end_date + "T23:59:59").replace(tzinfo=UTC).timestamp() - ) + start_time, end_time = self._date_range_to_timestamps(start_date, end_date, region_name) next_key = None items: list[dict] = [] diff --git a/src/mi_fitness_mcp/storage/__init__.py b/src/mi_fitness_mcp/storage/__init__.py index 91392ab..26bb186 100644 --- a/src/mi_fitness_mcp/storage/__init__.py +++ b/src/mi_fitness_mcp/storage/__init__.py @@ -457,7 +457,11 @@ class Database: timezone, collected_at, timestamp, bpm, sample_type ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET + timezone = excluded.timezone, + collected_at = excluded.collected_at, + timestamp = excluded.timestamp, bpm = excluded.bpm, + sample_type = excluded.sample_type, updated_at = CURRENT_TIMESTAMP """, ( @@ -486,6 +490,9 @@ class Database: timezone, collected_at, timestamp, spo2_pct ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET + timezone = excluded.timezone, + collected_at = excluded.collected_at, + timestamp = excluded.timestamp, spo2_pct = excluded.spo2_pct, updated_at = CURRENT_TIMESTAMP """, @@ -514,6 +521,9 @@ class Database: timezone, collected_at, timestamp, stress_score, level ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET + timezone = excluded.timezone, + collected_at = excluded.collected_at, + timestamp = excluded.timestamp, stress_score = excluded.stress_score, level = excluded.level, updated_at = CURRENT_TIMESTAMP @@ -544,6 +554,8 @@ class Database: timezone, collected_at, event_id, start_at, end_at, duration_seconds ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(user_id, event_id) DO UPDATE SET + timezone = excluded.timezone, + collected_at = excluded.collected_at, start_at = excluded.start_at, end_at = excluded.end_at, duration_seconds = excluded.duration_seconds, @@ -622,7 +634,7 @@ class Database: """ SELECT * FROM sleep_sessions WHERE user_id = ? - AND date(start_at) >= ? AND date(start_at) <= ? + AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ? ORDER BY start_at """, (user_id, start_date, end_date), @@ -641,7 +653,7 @@ class Database: """ SELECT * FROM workouts WHERE user_id = ? - AND date(start_at) >= ? AND date(start_at) <= ? + AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ? ORDER BY start_at """, (user_id, start_date, end_date), @@ -660,7 +672,7 @@ class Database: """ SELECT * FROM body_measurements WHERE user_id = ? - AND date(timestamp) >= ? AND date(timestamp) <= ? + AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ? ORDER BY timestamp """, (user_id, start_date, end_date), @@ -678,7 +690,7 @@ class Database: """ SELECT * FROM heart_rate_samples WHERE user_id = ? - AND date(timestamp) >= ? AND date(timestamp) <= ? + AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ? ORDER BY timestamp """, (user_id, start_date, end_date), @@ -696,7 +708,7 @@ class Database: """ SELECT * FROM spo2_samples WHERE user_id = ? - AND date(timestamp) >= ? AND date(timestamp) <= ? + AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ? ORDER BY timestamp """, (user_id, start_date, end_date), @@ -714,7 +726,7 @@ class Database: """ SELECT * FROM stress_samples WHERE user_id = ? - AND date(timestamp) >= ? AND date(timestamp) <= ? + AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ? ORDER BY timestamp """, (user_id, start_date, end_date), @@ -732,7 +744,7 @@ class Database: """ SELECT * FROM abnormal_heart_beat_events WHERE user_id = ? - AND date(start_at) >= ? AND date(start_at) <= ? + AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ? ORDER BY start_at """, (user_id, start_date, end_date), @@ -768,9 +780,9 @@ class Database: row = conn.execute( """ SELECT - MIN(date(start_at)) as first_date, - MAX(date(start_at)) as last_date, - COUNT(DISTINCT date(start_at)) as days_with_data + MIN(substr(start_at, 1, 10)) as first_date, + MAX(substr(start_at, 1, 10)) as last_date, + COUNT(DISTINCT substr(start_at, 1, 10)) as days_with_data FROM sleep_sessions WHERE user_id = ? """, @@ -788,9 +800,9 @@ class Database: row = conn.execute( """ SELECT - MIN(date(start_at)) as first_date, - MAX(date(start_at)) as last_date, - COUNT(DISTINCT date(start_at)) as days_with_data + MIN(substr(start_at, 1, 10)) as first_date, + MAX(substr(start_at, 1, 10)) as last_date, + COUNT(DISTINCT substr(start_at, 1, 10)) as days_with_data FROM workouts WHERE user_id = ? """, @@ -808,9 +820,9 @@ class Database: row = conn.execute( """ SELECT - MIN(date(timestamp)) as first_date, - MAX(date(timestamp)) as last_date, - COUNT(DISTINCT date(timestamp)) as days_with_data + MIN(substr(timestamp, 1, 10)) as first_date, + MAX(substr(timestamp, 1, 10)) as last_date, + COUNT(DISTINCT substr(timestamp, 1, 10)) as days_with_data FROM body_measurements WHERE user_id = ? """, @@ -827,9 +839,9 @@ class Database: row = conn.execute( """ SELECT - MIN(date(timestamp)) as first_date, - MAX(date(timestamp)) as last_date, - COUNT(DISTINCT date(timestamp)) as days_with_data + MIN(substr(timestamp, 1, 10)) as first_date, + MAX(substr(timestamp, 1, 10)) as last_date, + COUNT(DISTINCT substr(timestamp, 1, 10)) as days_with_data FROM heart_rate_samples WHERE user_id = ? """,