396 lines
14 KiB
Python
396 lines
14 KiB
Python
# -*- 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
|