|
@@ -53,18 +53,21 @@ def _payload_from_result(result: Any) -> dict[str, Any]:
|
|
|
return {}
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _normalize_datetime(dt_str: str | None, tz_name: str | None = None) -> str | None:
|
|
|
|
|
|
|
+def _normalize_datetime(dt_str: str | None, tz_name: str | None = None, lon: float = 0.0) -> str | None:
|
|
|
"""Normalize a datetime string to UTC ISO format without timezone offset.
|
|
"""Normalize a datetime string to UTC ISO format without timezone offset.
|
|
|
|
|
|
|
|
The ephemeris server expects UTC datetimes as plain ISO strings
|
|
The ephemeris server expects UTC datetimes as plain ISO strings
|
|
|
(e.g. '1965-07-02T22:05:00') without timezone info.
|
|
(e.g. '1965-07-02T22:05:00') without timezone info.
|
|
|
|
|
|
|
|
- Two modes:
|
|
|
|
|
|
|
+ Three modes:
|
|
|
- If dt_str has a timezone offset (e.g. '+01:00', '-04:00', 'Z'), it is
|
|
- If dt_str has a timezone offset (e.g. '+01:00', '-04:00', 'Z'), it is
|
|
|
- converted to UTC directly. tz_name is ignored.
|
|
|
|
|
- - If dt_str is naive (no offset) and tz_name is provided, the IANA timezone
|
|
|
|
|
- is used for the conversion (handles historical DST correctly).
|
|
|
|
|
- - If dt_str is naive and tz_name is None, it is assumed to already be UTC.
|
|
|
|
|
|
|
+ converted to UTC directly. tz_name and lon are ignored.
|
|
|
|
|
+ - If dt_str is naive and tz_name is provided, the IANA timezone is used.
|
|
|
|
|
+ For historical dates before standardized timezones (~1890), the IANA
|
|
|
|
|
+ timezone may apply modern rules retroactively, giving wrong offsets.
|
|
|
|
|
+ In that case, we fall back to LMT (Local Mean Time) computed from
|
|
|
|
|
+ longitude: offset_hours = lon / 15.0 (east positive).
|
|
|
|
|
+ - If dt_str is naive and tz_name is None, lon is used for LMT conversion.
|
|
|
"""
|
|
"""
|
|
|
if dt_str is None:
|
|
if dt_str is None:
|
|
|
return None
|
|
return None
|
|
@@ -74,14 +77,18 @@ def _normalize_datetime(dt_str: str | None, tz_name: str | None = None) -> str |
|
|
|
# Offset-aware: convert directly to UTC
|
|
# Offset-aware: convert directly to UTC
|
|
|
dt = dt.astimezone(timezone.utc)
|
|
dt = dt.astimezone(timezone.utc)
|
|
|
elif tz_name:
|
|
elif tz_name:
|
|
|
- # Naive + IANA timezone: attach zone, then convert to UTC
|
|
|
|
|
from zoneinfo import ZoneInfo
|
|
from zoneinfo import ZoneInfo
|
|
|
- dt = dt.replace(tzinfo=ZoneInfo(tz_name))
|
|
|
|
|
- dt = dt.astimezone(timezone.utc)
|
|
|
|
|
- # else: naive, no tz_name — assume already UTC
|
|
|
|
|
|
|
+ dt_tz = dt.replace(tzinfo=ZoneInfo(tz_name))
|
|
|
|
|
+ dt = dt_tz.astimezone(timezone.utc)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Naive, no tz_name — use LMT from longitude
|
|
|
|
|
+ from datetime import timedelta
|
|
|
|
|
+ lmt_offset_hours = lon / 15.0
|
|
|
|
|
+ lmt_tz = timezone(timedelta(hours=lmt_offset_hours))
|
|
|
|
|
+ dt = dt.replace(tzinfo=lmt_tz).astimezone(timezone.utc)
|
|
|
return dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
return dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- logger.warning(f"failed to normalize datetime: {dt_str} tz={tz_name}")
|
|
|
|
|
|
|
+ logger.warning(f"failed to normalize datetime: {dt_str} tz={tz_name} lon={lon}")
|
|
|
return dt_str
|
|
return dt_str
|
|
|
|
|
|
|
|
|
|
|
|
@@ -112,7 +119,7 @@ async def call_sky_state(
|
|
|
datetime is naive. Ignored when datetime has an offset.
|
|
datetime is naive. Ignored when datetime has an offset.
|
|
|
"""
|
|
"""
|
|
|
# Normalize datetime to UTC ISO string
|
|
# Normalize datetime to UTC ISO string
|
|
|
- dt_str = _normalize_datetime(datetime, tz_name=tz)
|
|
|
|
|
|
|
+ dt_str = _normalize_datetime(datetime, tz_name=tz, lon=lon)
|
|
|
url = EPHEMERIS_MCP_URL.strip()
|
|
url = EPHEMERIS_MCP_URL.strip()
|
|
|
if not url.endswith("/mcp/sse"):
|
|
if not url.endswith("/mcp/sse"):
|
|
|
url = url.rstrip("/") + "/mcp/sse"
|
|
url = url.rstrip("/") + "/mcp/sse"
|