ebuild_rasp2/ebuild/core/season.py

396 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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