# -*- 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 Rasp2 tabanlı sistemin syslog çıktısını, yeni ebuild mimarisi ile uyumlu ve okunaklı şekilde üretir. Çıktı şu ana bloklardan oluşur: 1) Üst bilgi: - Versiyon ve zaman satırı - Güneş bilgisi (sunrise / sunset, sistem On/Off, lisans id) - Mevsim bilgisi (season, bahar dönemi bilgisi) - Tatil bilgisi (varsa adıyla) 2) Bina ısı bilgisi - Bina Isı : [ min - avg - max ] 3) Hat sensörleri (burner.py içinden doldurulan kısım): - Dış Isı 1 - Çıkış Isı 2 - Dönüş hatları (isim map'inden) 4) Used dış ısı 5) Brülör / devirdaim / özet satırı Not --- Bu modül sadece formatlama ve çıktı üretiminden sorumludur. Gerçek ölçümler ve kontrol kararları üst katmanlardan (HeatEngine, Burner, Building, Environment, SeasonController vb.) alınır. """ # Bu modül gerçekten hangi path'ten import ediliyor, görmek için: # --------------------------------------------------------- def _safe_import(desc, import_func): """ desc: ekranda görünecek ad (örn: 'Building', 'legacy_syslog') import_func: gerçek import'u yapan lambda """ try: obj = import_func() #print(f"legacy_syslog.py [IMPORT OK] {desc} ->", obj) return obj except Exception as e: print(f"legacy_syslog.py [IMPORT FAIL] {desc}: {e}") traceback.print_exc() return None from datetime import datetime, time from typing import Optional import logging import logging.handlers try: # SeasonController ve konfig from ..core.season import SeasonController cfg = _safe_import( "config_statics", lambda: __import__("ebuild.config_statics", fromlist=["*"]),) cfv = _safe_import( "config_runtime", lambda: __import__("ebuild.config_runtime", fromlist=["*"]),) #from .. import config_statics as cfg except ImportError: # test / standalone SeasonController = None # type: ignore cfg = None # type: ignore cfv = None print("SeasonController, config_statics import ERROR") # ---------------------------------------------------------------------- # Logger kurulumu (Syslog + stdout) # ---------------------------------------------------------------------- _LOGGER: Optional[logging.Logger] = None def _get_logger() -> logging.Logger: global _LOGGER if _LOGGER is not None: return _LOGGER #print("logger..1:", stream_fmt) 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) print("logger..2:", stream_fmt) _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ı) """ #print("send_legacy_syslog BRULOR:", message) 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 (header) üreticiler # ---------------------------------------------------------------------- def emit_header_version(line_no: int, now: datetime) -> int: """ 1. satır: versiyon + zaman bilgisi. Örnek: ************** 00.02.01 2025-11-22 18:15:00 ************* """ v_str = _format_version_3part(__version__) body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************" send_legacy_syslog(format_line(line_no, body)) return line_no + 1 def emit_header_sun_and_system( 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 _only_date(s: str) -> str: """ ISO tarih-zaman stringinden sadece YYYY-MM-DD kısmını alır. Ö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. """ # SeasonController.info hem dict hem obje olabilir, ikisini de destekle info = getattr(season_ctrl, "info", season_ctrl) def _get(field: str, default=None): if isinstance(info, dict): return info.get(field, default) return getattr(info, field, default) # ---- 3. satır: season ---- season_name = _get("season", "Unknown") season_start = _only_date(_get("season_start", "")) season_end = _only_date(_get("season_end", "")) season_day = _get("season_day", "") season_passed = _get("season_passed", "") season_remain = _get("season_remaining", "") body = ( f"season : {season_name} {season_start} - {season_end} " f"[{season_day} pass:{season_passed} kalan:{season_remain}]" ) send_legacy_syslog(format_line(line_no, body)) line_no += 1 # ---- 4. satır: bahar dönemi (SADECE aktifse) ---- bahar_tasarruf = bool(_get("bahar_tasarruf", False)) if bahar_tasarruf: bahar_basx = _only_date(_get("bahar_basx", "")) bahar_bitx = _only_date(_get("bahar_bitx", "")) body = f"bahar : {bahar_basx} - {bahar_bitx}" send_legacy_syslog(format_line(line_no, body)) line_no += 1 return line_no def emit_header_holiday( line_no: int, is_holiday: bool, holiday_label: str, ) -> int: """ Tatil satırı (sunrise + season altına). Kurallar: - Tatil yoksa (False) HİÇ satır basma. - Tatil varsa: [ 5 Tatil: True Adı: Cumhuriyet Bayramı] """ 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 system_on = (system_onoff == 1) # 2) Güneş + Sistem / Lisans sunrise = season_ctrl.info.sunrise sunset = season_ctrl.info.sunset line_no = emit_header_sun_and_system( line_no=line_no, sunrise=sunrise, sunset=sunset, system_on=system_on, licence_id=licence_id, ) # 3) Mevsim bilgisi (sunrise ALTINA) line_no = emit_header_season(line_no, season_ctrl) # 4) Tatil bilgisi (sadece True ise) line_no = emit_header_holiday( line_no=line_no, is_holiday=season_ctrl.info.is_holiday, holiday_label=season_ctrl.info.holiday_label, ) # Sonraki satır: bina ısı / dış ısı / F-B detayları için kullanılacak return line_no def _fmt_temp(val: Optional[float]) -> str: return "None" if val is None else f"{val:.2f}" PUMP_SHORT_MAP = { "circulation_a": "A", "circulation_b": "B", "circ_1": "A", "circ_2": "B", } def _short_pump_name(ch: str) -> str: if ch in PUMP_SHORT_MAP: return PUMP_SHORT_MAP[ch] # sonu _a/_b ise yine yakala if ch.endswith("_a"): return "A" if ch.endswith("_b"): return "B" return ch # tanımıyorsak orijinal ismi yaz 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, line_temps: Optional[Dict[str, Optional[float]]] = None, ign_stats=None, circ_stats=None, ) -> None: """ BurnerController'dan tek çağrıyla BRULOR bloğunu basar. - Önce üst blok (versiyon + güneş + mevsim + tatil) - Sonra bina ısı satırı - Dış ısı / used dış ısı - Son satırda brülör ve pompaların durumu """ #print("log_burner_header CALLED", season) # 1) Üst header blok if season is None: # SeasonController yoksa, sadece versiyon ve zaman bas line_no = 1 v_str = _format_version_3part(__version__) body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************" send_legacy_syslog(format_line(line_no, body)) line_no += 1 else: line_no = emit_top_block(now, season) # 2) Bina ısı satırı if building_avg is None: min_s = "None" avg_s = "None" max_s = "None" else: # Şimdilik min=avg=max gibi davranalım; ileride gerçek min/max eklenebilir min_s = f"{building_min:5.2f}" avg_s = f"{building_avg:5.2f}" max_s = f"{building_max:5.2f}" # config’teki mod cfg_mode = getattr(cfg, "BUILD_BURNER", "?") if cfg is not None else "?" body = f"Build [{mode}-{cfg_mode}] Heats[Min:{min_s}°C Avg:{avg_s}°C Max:{max_s}°C]" send_legacy_syslog(format_line(line_no, body)) line_no += 1 # line_temps yoksa, burayı pas geç if line_temps is not None: # CONFIG'TEN ID'LERİ AL outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", None) if cfg is not None else None out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", None) if cfg is not None else None ret_ids = getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) if cfg is not None else [] ret_map = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP", {}) if cfg is not None else {} line_no = 4 # dış ısı satırı numarası # 4: Dis isi if outside_id and outside_id in line_temps: t = line_temps.get(outside_id) namex = getattr(cfg, "OUTSIDE_SENSOR_NAME", "Dis isi") if cfg is not None else "Dis isi" msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {outside_id} " send_legacy_syslog(format_line(line_no, msg)) line_no += 1 # 5: Cikis isi if out_id and out_id in line_temps: t = line_temps.get(out_id) namex = getattr(cfg, "BURNER_OUT_SENSOR_NAME", "Cikis isi") if cfg is not None else "Cıkıs isi" msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {out_id} " send_legacy_syslog(format_line(line_no, msg)) line_no += 1 # 6..N: Donus isi X namex = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP",[]) for sid in ret_ids: if sid not in line_temps: continue t = line_temps.get(sid) try: namexx = ret_map.get(sid) except: namex = '???' msg = f"{namexx:<15.15}: {_fmt_temp(t)}°C - {sid} " send_legacy_syslog(format_line(line_no, msg)) line_no += 1 # 3) Dış ısı / used dış ısı out_str = "--" used_str = "--" if outside_c is not None: out_str = f"{outside_c:5.2f}" if used_out_c is not None: used_str = f"{used_out_c:5.2f}" usedxx = "Sistem Isı" #------------------------------------------------------------------ # 9: Sistem Isı - Used + [WEEKEND_HEAT_BOOST_C, BURNER_COMFORT_OFFSET_C] # ------------------------------------------------------------------ used_val = used_out_c if used_out_c is not None else None used_str = "None" if used_val is None else f"{used_val:.2f}" if cfv is not None: w_val = float(getattr(cfv, "WEEKEND_HEAT_BOOST_C", 0.0) or 0.0) c_val = float(getattr(cfv, "BURNER_COMFORT_OFFSET_C", 0.0) or 0.0) else: w_val = 0.0 c_val = 0.0 # Sayıları [2, 1] gibi, gereksiz .0’sız yazalım def _fmt_num(x: float) -> str: if x == int(x): return str(int(x)) return f"{x:g}" sabitler_str = f"[w:{_fmt_num(w_val)} c:{_fmt_num(c_val)}]" body = f"{usedxx:<15.15}: {used_str}°C {sabitler_str} " send_legacy_syslog(format_line(line_no, body)) line_no += 1 # ------------------------------------------------------------------ # 11: Brülör Motor satırı (MAX_OUTLET_C ile) # ------------------------------------------------------------------ if cfv is not None: max_out = float(getattr(cfv, "MAX_OUTLET_C", 45.0) or 45.0) else: max_out = 45.0 if cfv is not None: min_ret = float(getattr(cfv, "CIRCULATION_MIN_RETURN_C", 25.0) or 25.0) else: min_ret = 25.0 br_status = "" if burner_on else "" br_flag = 1 if burner_on else 0 ign_sw = 0 ign_total = "00:00:00" ign_today = "00:00:00" if ign_stats: ign_sw = ign_stats.get("switch_count", 0) ign_total = ign_stats.get("total_on_str", "00:00:00") ign_today = ign_stats.get("today_on_str", "00:00:00") # Eski stile benzeteceğiz: # [ 11 Brulor Motor : [0] 0 00:00:00 00:00:00 L:45.0 ] body11 = ( f"Brulor Motor : {br_status} " f"[{br_flag}] {ign_sw} {ign_total} {ign_today} L:{max_out:.1f}" ) send_legacy_syslog(format_line(line_no, body11)) line_no += 1 # ------------------------------------------------------------------ # 12: Devirdaim Motor satırı (CIRCULATION_MIN_RETURN_C ile) # ------------------------------------------------------------------ ch_to_logical = {} pumps_on_list = list(pumps_on) if pumps_on else [] # --- circulation mapping: channel -> logical ('circ_1', 'circ_2') --- ch_to_logical = {} cfg_groups = getattr(cfg, "BURNER_GROUPS", {}) # ileride çoklu brülör olursa buraya burner_id parametresi de geçirsin istersen grp = cfg_groups.get(0, {}) circ_cfg = grp.get("circulation", {}) or {} for logical_name, info in circ_cfg.items(): ch = info.get("channel") if ch: ch_to_logical[ch] = logical_name # Config’te default=1 olan pompaları da topla (cfg_default_pumps) cfg_default_pumps = [] for logical_name, info in circ_cfg.items(): ch = info.get("channel") if ch and info.get("default", 0): cfg_default_pumps.append(ch) # Kısa isim A/B istersek: def _logical_to_short(name: str) -> str: if name == "circ_1": return "A" if name == "circ_2": return "B" return name pump_count = len(cfg_default_pumps) dev_status = "" if pump_count > 0 else "" pump_labels = [] for ch in cfg_default_pumps: logical = ch_to_logical.get(ch) if logical is not None: pump_labels.append(_logical_to_short(logical)) else: pump_labels.append(ch) pumps_str = ",".join(pump_labels) if pump_labels else "-" cir_sw = 0 cir_total = "00:00:00" cir_today = "00:00:00" if circ_stats: cir_sw = circ_stats.get("switch_count", 0) cir_total = circ_stats.get("total_on_str", "00:00:00") cir_today = circ_stats.get("today_on_str", "00:00:00") # [ 12 Devirdaim Mot: [0] 0 00:00:00 00:00:00 L:25.0] body12 = ( f"Devirdaim Mot: {dev_status} " f"[{pump_count}] {br_flag}] {cir_sw} {cir_total} {cir_today} L:{pumps_str} {min_ret:.1f}" ) send_legacy_syslog(format_line(line_no, body12)) line_no += 1 return line_no # ---------------------------------------------------------------------- # Ö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))