# -*- coding: utf-8 -*- from __future__ import annotations """ ebuild/io/legacy_syslog.py Legacy tarzı BRULOR syslog çıktısı üretmek için yardımcı fonksiyonlar. - Syslog (/dev/log) + stdout'a aynı formatta basar. - burner.BurnerController tarafından her tick'te çağrılan log_burner_header() ile eski sistemdeki benzer satırları üretir. """ __title__ = "legacy_syslog" __author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)' __purpose__ = "Legacy tarzı syslog çıktısı üreten köprü" __version__ = "0.3.0" __date__ = "2025-11-23" from datetime import datetime, time, timedelta from typing import Optional, Iterable, Tuple, Dict import logging import logging.handlers try: # Mevsim + güneş bilgileri için from ..core.season import SeasonController # type: ignore from .. import config_statics as cfg # type: ignore except ImportError: # test / standalone SeasonController = None # type: ignore cfg = None # type: ignore try: # Çalışma parametreleri (konfor offset, max çıkış vb.) from .. import config_runtime as cfg_v # type: ignore except ImportError: cfg_v = None # type: ignore try: # Hat sensörlerinden doğrudan okuma için from ..io.ds18b20 import DS18B20Sensor # type: ignore except Exception: DS18B20Sensor = None # type: ignore #print("legacy_syslog IMPORT EDİLDİ:", __file__) # ---------------------------------------------------------------------- # 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) 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 as e: print("legacy_syslog: SysLogHandler eklenemedi:", e) # Ayrıca stdout'a da yaz (debug için) stream_handler = logging.StreamHandler() stream_fmt = logging.Formatter("BRULOR: %(message)s") stream_handler.setFormatter(stream_fmt) logger.addHandler(stream_handler) _LOGGER = logger return logger 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. 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]" """ return f"[{line_no:3d} {body}]" # ---------------------------------------------------------------------- # Header yardımcıları # ---------------------------------------------------------------------- def _format_version_3part(ver: str) -> str: """ "0.2.1" → "00.02.01" formatı. """ 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}" def emit_header_version(line_no: int, now: datetime) -> int: """ 1. satır: versiyon + zaman bilgisi. ************** 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_sunrise( line_no: int, sunrise: time, sunset: time, system_on: bool, licence_id: int, ) -> int: """ 2. satır: Sunrise/Sunset + sistem On/Off + Lisans id satırı. Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094 """ def _fmt_t(t: time) -> str: if not t: return "--:--" return t.strftime("%H:%M") sun_str = f"Sunrise:{_fmt_t(sunrise)} Sunset:{_fmt_t(sunset)} " 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. """ if not s: return "--" s = s.strip() if "T" in s: s = s.split("T", 1)[0] return s def emit_header_season(line_no: int, season_ctrl) -> int: info = getattr(season_ctrl, "info", season_ctrl) # 1) SEASON satırı (bunu zaten yazıyorsun) season_name = getattr(info, "season", "Unknown") s_start = getattr(info, "season_start", "") s_end = getattr(info, "season_end", "") day_total = getattr(info, "season_day", 0) or 0 day_passed = getattr(info, "season_passed", 0) or 0 day_left = getattr(info, "season_remaining", 0) or 0 body = ( f"season : {season_name} {s_start} - {s_end} " f"[{day_total} pass:{day_passed} kalan:{day_left}]" ) send_legacy_syslog(format_line(line_no, body)) line_no += 1 # 2) BAHAR / TASARRUF SATIRI # SeasonInfo'dan tasarruf penceresini alıyoruz saving_start = getattr(info, "saving_start", None) saving_stop = getattr(info, "saving_stop", None) today = getattr(info, "date", None) # saving_start/stop yoksa bahar satırı YOK if saving_start is None or saving_stop is None or today is None: return line_no # Gösterim penceresi: saving_start - 3 gün .. saving_stop + 3 gün window_start = saving_start - timedelta(days=3) window_stop = saving_stop + timedelta(days=3) if window_start <= today <= window_stop: # Syslog'ta görünen tarih aralığı GERÇEK tasarruf aralığı olsun: # saving_start / saving_stop bahar_bas = saving_start.isoformat() bahar_bit = saving_stop.isoformat() body2 = f"bahar : {bahar_bas} - {bahar_bit}" send_legacy_syslog(format_line(line_no, body2)) 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). """ 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 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. Dönüş: bir sonraki satır numarası. """ line_no = 1 # 1: versiyon + zaman line_no = emit_header_version(line_no, now) # 2: güneş bilgisi info = getattr(season_ctrl, "info", season_ctrl) sunrise = getattr(info, "sunrise", None) sunset = getattr(info, "sunset", None) system_on = bool(getattr(info, "system_on", True)) licence_id = int( getattr(info, "licence_id", getattr(cfg, "BUILDING_LICENCEID", 0)) if cfg is not None else 0 ) line_no = emit_header_sunrise( line_no=line_no, sunrise=sunrise, sunset=sunset, system_on=system_on, licence_id=licence_id, ) # 3: mevsim line_no = emit_header_season(line_no, season_ctrl) # 4: tatil (varsa) is_hol = bool(getattr(info, "is_holiday", False)) hol_label = getattr(info, "holiday_label", "") line_no = emit_header_holiday(line_no, is_hol, hol_label) return line_no # ---------------------------------------------------------------------- # Hat sensörleri + brülör header # ---------------------------------------------------------------------- def _fmt_temp(t: Optional[float]) -> str: if t is None: return "None°C" return f"{t:5.2f}°C" def _read_line_temps_from_config() -> Dict[str, Optional[float]]: """ OUTSIDE_SENSOR_ID, BURNER_OUT_SENSOR_ID ve RETURN_LINE_SENSOR_IDS için DS18B20 üzerinden anlık sıcaklıkları okur. """ temps: Dict[str, Optional[float]] = {} if DS18B20Sensor is None or cfg is None: return temps outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", "") or "" out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", "") or "" ret_ids = list(getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) or []) all_ids = [] if outside_id: all_ids.append(outside_id) if out_id: all_ids.append(out_id) all_ids.extend([sid for sid in ret_ids if sid]) for sid in all_ids: try: sensor = DS18B20Sensor(serial=sid) temps[sid] = sensor.read_temperature() except Exception: temps[sid] = None return temps def log_burner_header( now: datetime, mode: str, season: "SeasonController", building_avg: Optional[float], outside_c: Optional[float], used_out_c: Optional[float], fire_sp: float, burner_on: bool, pumps_on: Iterable[str], ) -> None: """ burner.BurnerController.tick() her çağrıldığında; header + bina özet + hat sensörleri + sistem ısı + brülör/devirdaim satırlarını üretir. """ try: # 1) Üst blok if season is not None and SeasonController is not None: line_no = emit_top_block(now, season) else: line_no = emit_header_version(1, now) # 2) Bina ısı satırı mode = (mode or "?").upper() cfg_mode = ( str(getattr(cfg, "BUILD_BURNER", mode)).upper() if cfg is not None else mode ) outside_limit = float(getattr(cfg_v, "OUTSIDE_LIMIT_HEAT_C", 0.0)) min_c = None max_c = None avg_c = building_avg body_build = ( f"Build [{mode}-{cfg_mode}] " f"Heats[Min:{_fmt_temp(min_c)} Avg:{_fmt_temp(avg_c)} Max:{_fmt_temp(max_c)}] L:{outside_limit}" ) send_legacy_syslog(format_line(line_no, body_build)) line_no += 1 # 3) Hat sensörleri line_temps = _read_line_temps_from_config() outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", "") if cfg is not None else "" outside_name = ( getattr(cfg, "OUTSIDE_SENSOR_NAME", "Dış Isı 1") if cfg is not None else "Dış Isı 1" ) out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", "") if cfg is not None else "" out_name = ( getattr(cfg, "BURNER_OUT_SENSOR_NAME", "Çıkış Isı 2") if cfg is not None else "Çıkış Isı 2" ) ret_ids = ( list(getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) or []) if cfg is not None else [] ) name_map = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP", {}) if cfg is not None else {} if outside_id: t = line_temps.get(outside_id, outside_c) body = f"{outside_name:<15}: {_fmt_temp(t)} - {outside_id} " send_legacy_syslog(format_line(line_no, body)) line_no += 1 if out_id: t = line_temps.get(out_id) body = f"{out_name:<15}: {_fmt_temp(t)} - {out_id} " send_legacy_syslog(format_line(line_no, body)) line_no += 1 for sid in ret_ids: if not sid: continue t = line_temps.get(sid) nm = name_map.get(sid, sid) body = f"{nm:<15}: {_fmt_temp(t)} - {sid} " send_legacy_syslog(format_line(line_no, body)) line_no += 1 # 4) Sistem ısı satırı (used_out + weekend/comfort offset) weekend_boost = ( float(getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)) if cfg_v is not None else 0.0 ) comfort_off = ( float(getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)) if cfg_v is not None else 0.0 ) body_sys = ( f"Sistem Isı : {_fmt_temp(used_out_c)} " f"[w:{weekend_boost:g} c:{comfort_off:g}]" ) send_legacy_syslog(format_line(line_no, body_sys)) line_no += 1 # 5) Brülör / devirdaim satırları (istatistikler şimdilik 0) max_out = ( float(getattr(cfg_v, "MAX_OUTLET_C", fire_sp)) if cfg_v is not None else fire_sp ) br_status = "" if burner_on else "" br_flag = 1 if burner_on else 0 body_br = ( f"Brulor Motor : {br_status} [{br_flag}] " f"0 00:00:00 00:00:00 L:{max_out:.1f}" ) send_legacy_syslog(format_line(line_no, body_br)) line_no += 1 pumps_on_list = list(pumps_on or []) pump_count = len(pumps_on_list) dev_status = "" if pump_count > 0 else "" min_ret = ( float(getattr(cfg_v, "CIRCULATION_MIN_RETURN_C", 25.0)) if cfg_v is not None else 25.0 ) pumps_str = ",".join(pumps_on_list) if pumps_on_list else "-" body_dev = ( f"Devirdaim Mot: {dev_status} " f"[{pump_count}] 0 00:00:00 00:00:00 " f"L:{pumps_str} {min_ret:.1f}" ) send_legacy_syslog(format_line(line_no, body_dev)) except Exception as exc: print("BRULOR log_burner_header ERROR:", exc) if __name__ == "__main__": # Basit standalone test now = datetime.now() if SeasonController is None: emit_header_version(1, now) else: sc = SeasonController() log_burner_header( now=now, mode="F", season=sc, building_avg=None, outside_c=None, used_out_c=None, fire_sp=45.0, burner_on=False, pumps_on=(), )