# -*- coding: utf-8 -*- from __future__ import annotations __title__ = "season" __author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)' __purpose__ = "Güneş / tatil / mevsim + tasarruf sezonu bilgisini sağlayan yardımcı katman" __version__ = "0.3.1" __date__ = "2025-11-23" """ ebuild/core/season.py Amaç ----- - SunHolidayInfo kullanarak: * gün doğumu / batımı / öğlen * resmi tatil bilgisi * mevsim ve sezon süresi * tasarruf sezonu (saving) başlangıç/bitiş bilgilerini üretir. - Bu bilgiyi BurnerController ve legacy_syslog gibi modüllere SeasonController.info üzerinden sağlar. Not: - SunHolidayInfo import edilemez veya hata verirse, basit bir fallback mevsim + tasarruf sezonu bilgisi üretir. """ from dataclasses import dataclass from datetime import datetime, time, date, timedelta from typing import Optional, Tuple, Any from .. import config_statics as cfg_s # SunHolidayInfo'yu paket içinden al; yoksa fallback'e düş try: from .sunholiday import SunHolidayInfo # type: ignore except Exception: SunHolidayInfo = None # type: ignore # Tasarruf sezonu parametreleri için runtime config try: from .. import config_runtime as cfg_v # type: ignore except Exception: cfg_v = None # type: ignore # --------------------------------------------------------------------- # Veri yapısı # --------------------------------------------------------------------- @dataclass class SeasonInfo: """SeasonController tarafından sağlanan özet bilgi.""" date: date sunrise: Optional[time] sunset: Optional[time] noon: Optional[time] is_holiday: bool holiday_label: str season: str # "İlkbahar", "Yaz", "Sonbahar", "Kış", ... season_start: str # "YYYY-MM-DD" season_end: str # "YYYY-MM-DD" season_day: int # sezonun toplam gün sayısı season_passed: int # sezon içinde geçen gün season_remaining: int # sezon sonuna kalan gün # Tasarruf sezonu (saving) bilgileri is_season: bool = False # BUGÜN tasarruf sezonu içinde miyiz? saving_start: Optional[date] = None # tasarruf dönemi başlangıç tarihi saving_stop: Optional[date] = None # tasarruf dönemi bitiş tarihi # --------------------------------------------------------------------- # Yardımcı fonksiyonlar # --------------------------------------------------------------------- def _parse_time_str(s: Optional[str]) -> Optional[time]: if not s: return None for fmt in ("%H:%M:%S", "%H:%M"): try: return datetime.strptime(s, fmt).time() except Exception: continue return None def _parse_date_any(v: Any) -> Optional[date]: """ v: datetime / date / str / None → date veya None. """ if v is None: return None if isinstance(v, date) and not isinstance(v, datetime): return v if isinstance(v, datetime): return v.date() if isinstance(v, str): s = v.strip() # "2025-09-23T22:42:51.508546+03:00" => "2025-09-23" if "T" in s: s = s.split("T", 1)[0] # ISO parse dene try: return datetime.fromisoformat(s).date() except Exception: pass # Son çare saf "YYYY-MM-DD" try: return datetime.strptime(s, "%Y-%m-%d").date() except Exception: return None return None def _as_int(x: Any, default: int = 0) -> int: try: return int(x) except Exception: return default def _get_saving_param_for_season(season_name: str) -> Optional[Any]: """ Sezona göre ilgili tasarruf parametresini getirir. - İlkbahar → cfg_v.SEASON_SPRING_SAVE_DAYS - Sonbahar → cfg_v.SEASON_AUTUMN_SAVE_DAYS Yoksa None döner. """ if cfg_v is None: return None if season_name == "İlkbahar": return getattr(cfg_v, "SEASON_SPRING_SAVE_DAYS", None) if season_name == "Sonbahar": return getattr(cfg_v, "SEASON_AUTUMN_SAVE_DAYS", None) return None def tarih_gun_islemi(tarih_str, gun_sayisi): try: tarih = datetime.strptime(tarih_str, "%Y-%m-%d") yeni_tarih = tarih + timedelta(days=gun_sayisi) return yeni_tarih.strftime("%Y-%m-%d") except ValueError: return "Hata: Tarih formatı yanlış olmalı! Örnek: '2025-06-30'" def _compute_saving_window( season_name: str, season_start: Optional[date], season_end: Optional[date], today: date, ) -> Tuple[bool, Optional[date], Optional[date]]: """ Tasarruf sezonu penceresini hesaplar. Buradaki tanım: - SEASON_SPRING_SAVE_DAYS -> Nisan ayı için gün aralığı (nisan) - SEASON_AUTUMN_SAVE_DAYS -> Eylül ayı için gün aralığı (eylül) Örnek: SEASON_SPRING_SAVE_DAYS = (5, 20) -> 5-20 Nisan arası tasarruf SEASON_AUTUMN_SAVE_DAYS = (15, 30) -> 15-30 Eylül arası tasarruf Mantık: - Eğer bugün ay < 6 (Haziran'dan önce) ise -> Nisan aralığını kullan - Eğer bugün ay >= 6 ise -> Eylül aralığını kullan Dönüş: (is_season, saving_start, saving_stop) """ if season_start is None or season_end is None or cfg_v is None: return False, None, None # Bugünün tarihi (sadece date, saat yok) today = date.today() year = today.year # Config'den değerleri güvenli şekilde al (varsayılan değerle) SPRING_SAVE_DAYS = getattr(cfg_v, "SEASON_SPRING_SAVE_DAYS", 20) # örnek: 20 AUTUMN_SAVE_DAYS = getattr(cfg_v, "SEASON_AUTUMN_SAVE_DAYS", 13) # örnek: 13 # İlkbahar mı yoksa Sonbahar mı dönemi aktif? if today.month <= 5: # Ocak-Mayıs arası → ilkbahar dönemi aktif # İlkbaharın son gününden geriye doğru saving_stop = date(year, 5, 31) # 31 Mayıs (dahil) saving_start = saving_stop - timedelta(days=SPRING_SAVE_DAYS - 1) # dahil olduğu için -1 else: # Haziran-Aralık arası → sonbahar dönemi aktif # Sonbaharın ilk gününden ileri doğru saving_start = date(year, 9, 1) # 1 Eylül (dahil) saving_stop = saving_start + timedelta(days=AUTUMN_SAVE_DAYS - 1) # dahil olduğu için -1 # Sezon içinde miyiz? is_season = saving_start <= today <= saving_stop return is_season, saving_start, saving_stop # --------------------------------------------------------------------- # Ana controller # --------------------------------------------------------------------- class SeasonController: """ Tek sorumluluğu: o anki (veya verilen tarihteki) güneş / tatil / mevsim + tasarruf sezonu bilgisini üst katmanlara taşımak. """ def __init__(self, info: SeasonInfo) -> None: self.info = info # --------------------------------------------------------- # Factory metodları # --------------------------------------------------------- @classmethod def from_now(cls) -> "SeasonController": """ Sistem saatine göre SeasonController üretir. GEO_* bilgilerini config_statics'ten alır. """ now = datetime.now() return cls(cls._build_info(now)) @classmethod def from_datetime(cls, dt: datetime) -> "SeasonController": return cls(cls._build_info(dt)) # --------------------------------------------------------- # İç mantık # --------------------------------------------------------- @classmethod def _build_info(cls, now: datetime) -> SeasonInfo: """ SunHolidayInfo varsa onu kullanır; yoksa basit bir fallback sezon + tasarruf sezonu bilgisi üretir. """ geo_city = getattr(cfg_s, "GEO_CITY", "Ankara") geo_country = getattr(cfg_s, "GEO_COUNTRY", "Turkey") geo_tz = getattr(cfg_s, "GEO_TZ", "Europe/Istanbul") geo_lat = float(getattr(cfg_s, "GEO_LAT", 39.92077)) geo_lon = float(getattr(cfg_s, "GEO_LON", 32.85411)) today = now.date() # ------------------------------ # 1) SunHolidayInfo kullanmayı dene # ------------------------------ if SunHolidayInfo is not None: try: tracker = SunHolidayInfo( current_datetime=now, city_name=geo_city, country=geo_country, timezone=geo_tz, latitude=geo_lat, longitude=geo_lon, ) data = tracker.get_info() sunrise_t = _parse_time_str(data.get("sunrise")) sunset_t = _parse_time_str(data.get("sunset")) noon_t = _parse_time_str(data.get("noon")) season_name = data.get("season") or "Bilinmiyor" ss_raw = data.get("season_start") or "" se_raw = data.get("season_end") or "" ds = _parse_date_any(ss_raw) de = _parse_date_any(se_raw) if ds is None: ds = today if de is None or de < ds: de = ds total_days = _as_int(data.get("season_day"), 0) passed_days = _as_int(data.get("season_passed"), 0) remaining_days = _as_int(data.get("season_remaining"), 0) # Eğer harici toplam/remaining güvenilmezse basit hesap if total_days <= 0 or remaining_days < 0: total_days = (de - ds).days + 1 passed_days = (today - ds).days remaining_days = (de - today).days is_season, saving_start, saving_stop = _compute_saving_window( season_name, ds, de, today ) return SeasonInfo( date = today, sunrise = sunrise_t, sunset = sunset_t, noon = noon_t, is_holiday = bool(data.get("is_holiday", False)), holiday_label = str(data.get("holiday_label", "")), season = season_name, season_start = ds.isoformat(), season_end = de.isoformat(), season_day = total_days, season_passed = passed_days, season_remaining = remaining_days, is_season = is_season, saving_start = saving_start, saving_stop = saving_stop, ) except Exception as e: print(f"⚠️ SunHolidayInfo hata, fallback kullanılacak: {e}") # ------------------------------ # 2) Fallback: sadece mevsim tahmini + saving # ------------------------------ month = now.month day = now.day if (month == 12 and day >= 1) or (1 <= month <= 3 and (month < 3 or day <= 20)): season_label = "Kış" ss = f"{now.year}-12-01" se = f"{now.year+1}-03-20" if month == 12 else f"{now.year}-03-20" elif 3 <= month <= 6 and not (month == 6 and day > 20): season_label = "İlkbahar" ss = f"{now.year}-03-21" se = f"{now.year}-06-20" elif 6 <= month <= 9 and not (month == 9 and day > 22): season_label = "Yaz" ss = f"{now.year}-06-21" se = f"{now.year}-09-22" else: season_label = "Sonbahar" ss = f"{now.year}-09-23" se = f"{now.year}-11-30" try: ds = datetime.fromisoformat(ss).date() de = datetime.fromisoformat(se).date() total_days = (de - ds).days + 1 passed_days = (today - ds).days remaining_days = (de - today).days except Exception: ds = today de = today total_days = passed_days = remaining_days = 0 is_season, saving_start, saving_stop = _compute_saving_window( season_label, ds, de, today ) return SeasonInfo( date = today, sunrise = None, sunset = None, noon = None, is_holiday = False, holiday_label = "", season = season_label, season_start = ds.isoformat(), season_end = de.isoformat(), season_day = total_days, season_passed = passed_days, season_remaining = remaining_days, is_season = is_season, saving_start = saving_start, saving_stop = saving_stop, ) # --------------------------------------------------------- # Legacy / debug uyumlu satırlar # --------------------------------------------------------- def to_syslog_lines(self) -> list[str]: """ legacy_syslog.emit_header_season veya debug için özet satırlar üretir. Örnek: "season : İlkbahar 2025-03-21 - 2025-06-20 day:92 passed:10 remaining:82" "saving : True 2025-03-21 - 2025-04-10" """ i = self.info lines: list[str] = [] lines.append( f"season : {i.season} {i.season_start} - {i.season_end} " f"day:{i.season_day} passed:{i.season_passed} remaining:{i.season_remaining}" ) if i.saving_start and i.saving_stop: lines.append( f"saving : {i.is_season} {i.saving_start.isoformat()} - {i.saving_stop.isoformat()}" ) else: lines.append("saving : False - -") return lines