# -*- coding: utf-8 -*- from __future__ import annotations __title__ = "legacy_syslog" __author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)' __purpose__ = "Legacy tarzı syslog çıktısı üreten köprü" __version__ = "0.2.1" __date__ = "2025-11-22" """ ebuild/io/legacy_syslog.py Revision : 2025-11-22 Authors : Mehmet Karatay & "Saraswati" (ChatGPT) Amaç ----- - Eski /brulor.py'nin syslog formatına yakın satırlar üretmek. - Python logging + SysLogHandler kullanarak: program adı: BRULOR mesaj : "[ 1 .... ]" formatında Bu modül: - send_legacy_syslog(message) → tek satır yazar - emit_top_block(now, SeasonController) → 1) versiyon satırı 2) güneş (sunrise/sunset) + sistem/licence 3) mevsim + bahar dönemi + tatil satırları - log_burner_header(...) → BurnerController.tick() için üst blok + sistem ısı + motor bilgilerini basar. """ from datetime import datetime, time, timedelta, date from typing import Optional import logging import logging.handlers try: # SeasonController ve konfig from ..core.season import SeasonController from .. import config_statics as cfg from .. import config_runtime as cfg_v except ImportError: # test / standalone SeasonController = None # type: ignore cfg = None # type: ignore cfg_v = None # type: ignore # ---------------------------------------------------------------------- # Logger kurulumu (Syslog + stdout) # ---------------------------------------------------------------------- _LOGGER: Optional[logging.Logger] = None def _get_logger() -> logging.Logger: global _LOGGER if _LOGGER is not None: return _LOGGER logger = logging.getLogger("BRULOR") logger.setLevel(logging.INFO) # Aynı handler'ları ikinci kez eklemeyelim if not logger.handlers: # Syslog handler (Linux: /dev/log) try: syslog_handler = logging.handlers.SysLogHandler(address="/dev/log") # Syslog mesaj formatı: "BRULOR: [ 1 ... ]" fmt = logging.Formatter("%(name)s: %(message)s") syslog_handler.setFormatter(fmt) logger.addHandler(syslog_handler) except Exception: # /dev/log yoksa sessizce geç; sadece stdout'a yazacağız pass # Konsol çıktısı (debug için) stream_handler = logging.StreamHandler() stream_fmt = logging.Formatter("INFO:BRULOR:%(message)s") stream_handler.setFormatter(stream_fmt) logger.addHandler(stream_handler) _LOGGER = logger return logger # ---------------------------------------------------------------------- # Temel çıktı fonksiyonları # ---------------------------------------------------------------------- def send_legacy_syslog(message: str) -> None: """ Verilen mesajı legacy syslog formatına uygun şekilde ilgili hedefe gönderir. - Syslog (/dev/log) → program adı: BRULOR - Aynı zamanda stdout'a da yazar (DEBUG amaçlı) """ try: logger = _get_logger() logger.info(message) except Exception as e: # Logger bir sebeple çökerse bile BRULOR satırını kaybetmeyelim print("BRULOR:", message, f"(logger error: {e})") def format_line(line_no: int, body: str) -> str: """ BRULOR satırını klasik formata göre hazırlar. Örnek: line_no = 2, body = "Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094" "[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]" Not: Burada "BRULOR" yazmıyoruz; syslog program adı zaten BRULOR olacak. """ return f"[{line_no:3d} {body}]" def _format_version_3part(ver: str) -> str: """ __version__ string'ini "00.02.01" formatına çevirir. Örnek: "0.2.1" → "00.02.01" """ parts = (ver or "").split(".") nums = [] for p in parts: try: nums.append(int(p)) except ValueError: nums.append(0) while len(nums) < 3: nums.append(0) return f"{nums[0]:02d}.{nums[1]:02d}.{nums[2]:02d}" # ---------------------------------------------------------------------- # Üst blok: versiyon + güneş + mevsim + tatil # ---------------------------------------------------------------------- def emit_header_version(line_no: int, now: datetime) -> int: """ 1. satır: Versiyon ve zaman bilgisi. Örnek: [ 1 ************** 00.02.01 2025-11-22 22:20:19 *************] """ ver = _format_version_3part(__version__) ts = now.strftime("%Y-%m-%d %H:%M:%S") body = f"************** {ver} {ts} *************" send_legacy_syslog(format_line(line_no, body)) return line_no + 1 def emit_header_sunrise_sunset( line_no: int, sunrise: Optional[time], sunset: Optional[time], system_on: bool, licence_id: int, ) -> int: """ 2. satır: Güneş bilgisi + Sistem On/Off + Lisans id. Örnek: [ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094] """ sun_str = "" if sunrise is not None: sun_str += f"Sunrise:{sunrise.strftime('%H:%M')} " if sunset is not None: sun_str += f"Sunset:{sunset.strftime('%H:%M')} " sys_str = "On" if system_on else "Off" body = f"{sun_str}Sistem: {sys_str} Lic:{licence_id}" send_legacy_syslog(format_line(line_no, body)) return line_no + 1 def _normalize_iso_date(s: Optional[str]) -> str: """ SeasonInfo.season_start / season_end alanlarını sadeleştirir. Örn: '2025-09-23T16:33:10.687982+03:00' → '2025-09-23' """ if not s: return "--" s = s.strip() if "T" in s: return s.split("T", 1)[0] return s def emit_header_season( line_no: int, season_ctrl: SeasonController, ) -> int: """ Sunrise satırının altına mevsim + (varsa) bahar tasarruf dönemi satırını basar. Beklenen format: BRULOR [ 3 season : Sonbahar 2025-09-23 - 2025-12-20 [89 pass:60 kalan:28] ] BRULOR [ 4 bahar : 2025-09-23 - 2025-10-13 ] Notlar: - Bilgiler SeasonController.info içinden okunur (dict veya obje olabilir). - bahar_tasarruf True DEĞİLSE bahar satırı hiç basılmaz. """ if season_ctrl is None: return line_no info = getattr(season_ctrl, "info", None) if info is None: return line_no # dataclass benzeri objeden alanları çek season = getattr(info, "season", "Unknown") s_start = _normalize_iso_date(getattr(info, "season_start", "")) s_end = _normalize_iso_date(getattr(info, "season_end", "")) s_day = int(getattr(info, "season_day", 0) or 0) s_pass = int(getattr(info, "season_passed", 0) or 0) s_rem = int(getattr(info, "season_remaining", 0) or 0) body = ( f"season : {season} {s_start} - {s_end} " f"[{s_day} pass:{s_pass} kalan:{s_rem}]" ) send_legacy_syslog(format_line(line_no, body)) line_no += 1 # Bahar / tasarruf dönemi bilgileri is_season = bool(getattr(info, "is_season", False)) saving_start = getattr(info, "saving_start", None) saving_stop = getattr(info, "saving_stop", None) if is_season and isinstance(saving_start, date) and isinstance(saving_stop, date): # Kullanıcı isteği: öncesi/sonrası 3'er gün göster show_start = saving_start - timedelta(days=3) show_stop = saving_stop + timedelta(days=3) body = ( f"bahar : {show_start.isoformat()} - {show_stop.isoformat()}" ) send_legacy_syslog(format_line(line_no, body)) line_no += 1 # Eğer resmi tatil bilgisi de varsa opsiyonel satır is_holiday = bool(getattr(info, "is_holiday", False)) holiday_label = getattr(info, "holiday_label", "") if not is_holiday: return line_no label = holiday_label or "" body = f"Tatil: True Adı: {label}" send_legacy_syslog(format_line(line_no, body)) return line_no + 1 # ---------------------------------------------------------------------- # Dışarıdan çağrılacak üst-blok helper # ---------------------------------------------------------------------- def emit_top_block( now: datetime, season_ctrl: SeasonController, ) -> int: """ F veya B modundan bağımsız olarak, her tick başında üst bilgiyi üretir. Sıra: 1) Versiyon + zaman 2) Sunrise / Sunset / Sistem: On/Off / Lic 3) Mevsim bilgisi (SeasonController.to_syslog_lines() → sadeleştirilmiş) 4) Tatil bilgisi (sadece tatil varsa) 5) Bir sonraki satır numarasını döndürür (bina ısı satırları için). """ line_no = 1 # 1) Versiyon line_no = emit_header_version(line_no, now) # Konfigten sistem ve lisans bilgileri if cfg is not None: licence_id = int(getattr(cfg, "BUILDING_LICENCEID", 0)) system_onoff = int(getattr(cfg, "BUILDING_SYSTEMONOFF", 1)) else: licence_id = 0 system_onoff = 1 # SeasonController.info'dan sunrise/sunset okumayı dene sunrise = None sunset = None if season_ctrl is not None: info = getattr(season_ctrl, "info", None) if info is not None: sunrise = getattr(info, "sunrise", None) sunset = getattr(info, "sunset", None) line_no = emit_header_sunrise_sunset( line_no=line_no, sunrise=sunrise, sunset=sunset, system_on=bool(system_onoff), licence_id=licence_id, ) # Mevsim + bahar dönemi line_no = emit_header_season(line_no, season_ctrl) # Sonraki satır: bina ısı / dış ısı / F-B detayları için kullanılacak return line_no # ---------------------------------------------------------------------- # BurnerController entegrasyonu # ---------------------------------------------------------------------- def _fmt_c(val: Optional[float]) -> str: """Dereceyi 'None°C' veya '23.4°C' gibi tek tip formatlar.""" if val is None: return "None°C" try: return f"{float(val):.2f}°C" except Exception: return "None°C" def log_burner_header( now: datetime, mode: str, season, building_avg: Optional[float], outside_c: Optional[float], used_out_c: Optional[float], fire_sp: float, burner_on: bool, pumps_on, ) -> None: """BurnerController.tick() için tek entry-point. Buradan: 1) Versiyon + güneş + mevsim / bahar / tatil blokları 2) Bina / sistem ısı bilgisi 3) Brülör ve devirdaim motor satırları syslog'a basılır. """ # 1) Üst blok try: line_no = emit_top_block(now, season) except Exception as exc: # Üst blok patlasa bile alttakileri basalım ki log tamamen kaybolmasın send_legacy_syslog(format_line(1, f"emit_top_block error: {exc}")) line_no = 2 # 2) Bina/sistem ısı satırı try: # Çalışma modu: F (dış ısı) / B (bina ort) cfg_mode = getattr(cfg, "BUILD_BURNER", "F") if cfg is not None else "F" mode_cfg = str(cfg_mode).upper() # Isı limiti (dış ısı limiti) limit = None if cfg_v is not None: try: limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 0.0)) except Exception: limit = None body = ( f"Build [{mode}-{mode_cfg}] " f"Heats[Min:{_fmt_c(None)} Avg:{_fmt_c(building_avg)} Max:{_fmt_c(None)}]" ) if limit is not None: # Son köşeli parantezi atmamak için ufak hack body = body[:-1] + f" L:{limit:.1f}]" send_legacy_syslog(format_line(line_no, body)) line_no += 1 # Eski formata yakın "Sistem Isı" satırı w_boost = 0.0 c_off = 0.0 if cfg_v is not None: try: w_boost = float(getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)) except Exception: pass try: c_off = float(getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)) except Exception: pass body = f"Sistem Isı : {_fmt_c(used_out_c)} [w:{int(w_boost)} c:{int(c_off)}]" send_legacy_syslog(format_line(line_no, body)) line_no += 1 # 3) Brülör motor satırı br_state = "" if burner_on else "" br_flag = 1 if burner_on else 0 body = ( f"Brulor Motor : {br_state} [{br_flag}] 0 " f"00:00:00 00:00:00 L:{fire_sp:.1f}" ) send_legacy_syslog(format_line(line_no, body)) line_no += 1 # 4) Devirdaim pompa satırı pumps_on = tuple(pumps_on or ()) pump_state = "" if pumps_on else "" pump_flag = 1 if pumps_on else 0 pump_label = pumps_on[0] if pumps_on else "-" circ_limit = None if cfg_v is not None: try: circ_limit = float(getattr(cfg_v, "CIRCULATION_MIN_RETURN_C", 25.0)) except Exception: circ_limit = None if circ_limit is None: body = ( f"Devirdaim Mot: {pump_state} [{pump_flag}] 0 " f"00:00:00 00:00:00 L:{pump_label}" ) else: body = ( f"Devirdaim Mot: {pump_state} [{pump_flag}] 0 " f"00:00:00 00:00:00 L:{pump_label} {circ_limit:.1f}" ) send_legacy_syslog(format_line(line_no, body)) except Exception as exc: # Her türlü hata durumunda sessiz kalmak yerine loga yaz send_legacy_syslog(format_line(line_no, f"log_burner_header error: {exc}")) # ---------------------------------------------------------------------- # Örnek kullanım (standalone test) # ---------------------------------------------------------------------- if __name__ == "__main__": # Bu blok sadece modülü tek başına test etmek için: # python3 -m ebuild.io.legacy_syslog if SeasonController is None: raise SystemExit("SeasonController import edilemedi (test ortamı).") now = datetime.now() # SeasonController.from_now() kullanıyorsan: try: season = SeasonController.from_now() except Exception as e: raise SystemExit(f"SeasonController.from_now() hata: {e}") next_line = emit_top_block(now, season) # Test için bina ısısını dummy bas: body = "Bina Isı : [ 20.10 - 22.30 - 24.50 ]" send_legacy_syslog(format_line(next_line, body))