"""Hong Kong Observatory data client. Fetches real-time weather observations, warnings, and forecasts from HKO. Uses public RSS/JSON feeds where available. """ import json from datetime import datetime from typing import Optional, List import requests class HKOClient: """Fetch data from Hong Kong Observatory's public data feeds.""" BASE_URL = "https://data.weather.gov.hk" ENDPOINTS = { "current_weather": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en", "9day_forecast": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=fnd&lang=en", "warnings": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=warnsum&lang=en", "local_forecast": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=flw&lang=en", "swt": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=swt&lang=en", "rainfall": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=rfmap", "lightning": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=lmap", "uv_index": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=uvi", "earthquake": f"{BASE_URL}/weatherAPI/opendata/weather.php?dataType=eq", } def __init__(self): self.session = requests.Session() self.session.headers.update({ "User-Agent": "HK-Weather-Market/1.0", "Accept": "application/json", }) def get_current_weather(self) -> Optional[dict]: """Get current weather observations for Hong Kong.""" try: resp = self.session.get(self.ENDPOINTS["current_weather"], timeout=15) resp.raise_for_status() data = resp.json() result = { "update_time": data.get("updateTime", ""), "temperature": [], "humidity": [], "rainfall": [], "warning_message": data.get("warningMessage", ""), "rainstorm_reminder": data.get("rainstormReminder", ""), "mintemp_from00to09": data.get("mintempFrom00To09", ""), } # Parse temperature data for record in data.get("temperature", {}).get("data", []): result["temperature"].append({ "place": record.get("place"), "value": record.get("value"), "unit": record.get("unit", "C"), }) for record in data.get("humidity", {}).get("data", []): result["humidity"].append({ "place": record.get("place"), "value": record.get("value"), "unit": record.get("unit", "%"), }) # Parse rainfall for record in data.get("rainfall", {}).get("data", []): result["rainfall"].append({ "place": record.get("place"), "max": record.get("max"), "min": record.get("min"), "unit": record.get("unit", "mm"), }) return result except Exception as e: print(f"HKO current weather error: {e}") return None def get_forecast(self) -> Optional[List[dict]]: """Get 9-day weather forecast from HKO.""" try: resp = self.session.get(self.ENDPOINTS["9day_forecast"], timeout=15) resp.raise_for_status() data = resp.json() forecasts = [] for day in data.get("weatherForecast", []): forecasts.append({ "date": day.get("forecastDate"), "week": day.get("week"), "forecast_wind": day.get("forecastWind"), "forecast_weather": day.get("forecastWeather"), "forecast_temp_max": day.get("forecastMaxtemp", {}).get("value"), "forecast_temp_min": day.get("forecastMintemp", {}).get("value"), "forecast_humidity_max": day.get("forecastMaxrh", {}).get("value"), "forecast_humidity_min": day.get("forecastMinrh", {}).get("value"), "forecast_rain_probability": self._parse_probability( day.get("forecastRainProbability", {}) ), "psr": day.get("PSR"), }) return forecasts except Exception as e: print(f"HKO forecast error: {e}") return None def get_warnings(self) -> Optional[dict]: """Get current weather warnings and signals.""" try: resp = self.session.get(self.ENDPOINTS["warnings"], timeout=15) resp.raise_for_status() return resp.json() except Exception as e: print(f"HKO warnings error: {e}") return None def get_typhoon_info(self) -> Optional[dict]: """Get tropical cyclone warnings and tracks from HKO.""" try: resp = self.session.get(self.ENDPOINTS["swt"], timeout=15) resp.raise_for_status() return resp.json() except Exception as e: print(f"HKO typhoon info error: {e}") return None def get_rainfall_map(self) -> Optional[dict]: """Get recent rainfall distribution.""" try: resp = self.session.get(self.ENDPOINTS["rainfall"], timeout=15) resp.raise_for_status() return resp.json() except Exception as e: print(f"HKO rainfall map error: {e}") return None def is_t8_active(self) -> bool: """Check if T8 or higher typhoon signal is active.""" warnings = self.get_warnings() if not warnings: return False for detail in warnings.get("details", []): name = detail.get("name", "") if any(signal in name for signal in ["8", "9", "10"]): if "tropical cyclone" in name.lower() or "typhoon" in name.lower(): return True return False def get_current_signal_level(self) -> int: """Get current typhoon signal level (0, 1, 3, 8, 9, 10).""" typhoon = self.get_typhoon_info() if not typhoon: return 0 for signal_text in ["T1", "T3", "T8", "T9", "T10"]: if signal_text in str(typhoon): return int(signal_text[1:]) return 0 @staticmethod def _parse_probability(prob_data: dict) -> Optional[str]: """Parse HKO rainfall probability data.""" values = [] for period in prob_data.get("probability", []): perc = period.get("value") if perc: values.append(perc) return " / ".join(values) if values else None