ebuild_rasp2/ebuild/io/zlegacy_syslog.py

609 lines
20 KiB
Python
Raw Permalink 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__ = "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}"
# configteki 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 .0sı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 = "<CALISIYOR>" if burner_on else "<CALISMIYOR>"
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 : <CALISMIYOR> [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
# Configte 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 = "<CALISIYOR>" if pump_count > 0 else "<CALISMIYOR>"
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: <CALISMIYOR> [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))