You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
6.6 KiB
165 lines
6.6 KiB
from datetime import datetime
|
|
from typing import List
|
|
from env_canada.ec_weather import ECWeather
|
|
from ..models import LocationInfo, CurrentConditions, WeatherForecast, WeatherAlert, AlertSeverity
|
|
from .base import WeatherProvider
|
|
|
|
class ECProvider(WeatherProvider):
|
|
def __init__(self, **kwargs):
|
|
self.points_cache = {}
|
|
self.extra_zones = kwargs.get('alerts', {}).get('extra_zones', [])
|
|
self.allowed_events = kwargs.get('alerts', {}).get('ca_events', [])
|
|
# Factory passes the full config dict as kwargs
|
|
station_cfg = kwargs.get('station', {})
|
|
self.language = station_cfg.get('announce_language', 'en')
|
|
|
|
# Also support direct language kwarg if passed explicitly
|
|
if 'language' in kwargs:
|
|
self.language = kwargs['language']
|
|
|
|
# Normalize for EC library (accepts 'english' or 'french')
|
|
if self.language in ['fr', 'french']:
|
|
self.language = 'french'
|
|
else:
|
|
self.language = 'english'
|
|
|
|
def _get_ec_data(self, lat, lon):
|
|
# ECWeather auto-selects station based on lat/lon
|
|
import asyncio
|
|
ec = ECWeather(coordinates=(lat, lon), language=self.language)
|
|
asyncio.run(ec.update())
|
|
return ec
|
|
|
|
def get_location_info(self, lat: float, lon: float) -> LocationInfo:
|
|
ec = self._get_ec_data(lat, lon)
|
|
# ECData metadata is nested
|
|
meta = ec.metadata
|
|
# Debug logging
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"DEBUG: ec.metadata type: {type(meta)}")
|
|
logger.info(f"DEBUG: ec.metadata content: {meta}")
|
|
|
|
# Safe access
|
|
if not isinstance(meta, dict):
|
|
meta = {}
|
|
|
|
city = meta.get('location')
|
|
region = meta.get('province')
|
|
|
|
# Fallback to reverse_geocoder if EC fails to name the location
|
|
if not city or city == 'Unknown':
|
|
try:
|
|
import reverse_geocoder as rg
|
|
results = rg.search((lat, lon))
|
|
if results:
|
|
city = results[0].get('name')
|
|
if not region:
|
|
region = results[0].get('admin1')
|
|
except Exception as e:
|
|
logger.warning(f"Reverse geocode failed: {e}")
|
|
|
|
return LocationInfo(
|
|
latitude=lat,
|
|
longitude=lon,
|
|
city=city or 'Unknown',
|
|
region=region or 'Canada',
|
|
country_code="CA",
|
|
timezone="Unknown" # EC lib doesn't trivialy expose TZ
|
|
)
|
|
|
|
def get_conditions(self, lat: float, lon: float) -> CurrentConditions:
|
|
ec = self._get_ec_data(lat, lon)
|
|
cond = ec.conditions
|
|
|
|
return CurrentConditions(
|
|
temperature=float(cond.get('temperature', {}).get('value') or 0),
|
|
humidity=int(cond.get('humidity', {}).get('value') or 0),
|
|
wind_speed=float(cond.get('wind_speed', {}).get('value') or 0),
|
|
wind_direction=cond.get('wind_direction', {}).get('value'),
|
|
description=cond.get('condition', {}).get('value') or 'Unknown'
|
|
)
|
|
|
|
def get_forecast(self, lat: float, lon: float) -> List[WeatherForecast]:
|
|
ec = self._get_ec_data(lat, lon)
|
|
daily = ec.daily_forecasts
|
|
|
|
forecasts = []
|
|
for d in daily:
|
|
forecasts.append(WeatherForecast(
|
|
period_name=d.get('period'),
|
|
high_temp=float(d.get('temperature')) if d.get('temperature') else None,
|
|
low_temp=None, # EC daily structure is period-based (e.g. "Monday", "Monday Night")
|
|
summary=d.get('text_summary', ''),
|
|
short_summary=d.get('text_summary', ''),
|
|
precip_probability=int(d.get('precip_probability') or 0)
|
|
))
|
|
return forecasts
|
|
|
|
def get_alerts(self, lat: float, lon: float) -> List[WeatherAlert]:
|
|
ec_objects = [self._get_ec_data(lat, lon)]
|
|
|
|
# Add extra zones (Station IDs e.g., 'ON/s0000430')
|
|
for zone_id in self.extra_zones:
|
|
if "/" in zone_id: # Basic check if it looks like EC station ID
|
|
try:
|
|
ec_objects.append(ECWeather(station_id=zone_id))
|
|
except Exception:
|
|
pass
|
|
|
|
results = []
|
|
now = datetime.now()
|
|
seen_titles = set()
|
|
|
|
for ec in ec_objects:
|
|
try:
|
|
if not getattr(ec, 'conditions', None): # Ensure updated
|
|
import asyncio
|
|
asyncio.run(ec.update())
|
|
|
|
for a in ec.alerts:
|
|
title = a.get('title', '')
|
|
if title in seen_titles: continue
|
|
seen_titles.add(title)
|
|
|
|
# Filter by allowed list if present
|
|
if self.allowed_events:
|
|
if not any(ae.lower() in title.lower() for ae in self.allowed_events):
|
|
continue
|
|
|
|
# Mapping severity roughly
|
|
severity = AlertSeverity.UNKNOWN
|
|
if "warning" in title.lower(): severity = AlertSeverity.WARNING
|
|
elif "watch" in title.lower(): severity = AlertSeverity.WATCH
|
|
elif "advisory" in title.lower(): severity = AlertSeverity.ADVISORY
|
|
elif "statement" in title.lower(): severity = AlertSeverity.ADVISORY
|
|
|
|
# Using current time as dummy effective/expires if missing
|
|
eff_str = a.get('date')
|
|
eff_dt = now
|
|
if eff_str:
|
|
# EC format often: "2023-10-27T15:00:00-04:00" or similar?
|
|
# actually env_canada lib might return a string.
|
|
# We'll try to parse it if dateutil is available, or use now.
|
|
try:
|
|
from dateutil.parser import parse as parse_date
|
|
eff_dt = parse_date(eff_str)
|
|
except:
|
|
pass
|
|
|
|
results.append(WeatherAlert(
|
|
id=title,
|
|
severity=severity,
|
|
title=title,
|
|
description=a.get('detail', ''),
|
|
area_description=ec.metadata.get('location', 'Local Area'),
|
|
effective=eff_dt,
|
|
expires=now, # EC doesn't explicitly give expires in this view
|
|
issued=eff_dt
|
|
))
|
|
except Exception:
|
|
continue
|
|
|
|
return results
|
|
|