ilk işlem

This commit is contained in:
root
2025-11-24 14:25:02 +03:00
commit 1d458ca9f6
72 changed files with 9732 additions and 0 deletions

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

Binary file not shown.

View File

@@ -0,0 +1,679 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "burner"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina ve/veya dış ısıya göre brülör ve sirkülasyon kontrol çekirdeği"
__version__ = "0.4.3"
__date__ = "2025-11-22"
"""
ebuild/core/systems/burner.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- BUILD_BURNER moduna göre (F/B) brülör ve sirkülasyon pompalarını yönetmek
- Bina ortalaması (B mod) veya dış ısı (F mod) üzerinden ısıtma isteği üretmek
- used_out_heat mantığı ile dış ısıya hafta sonu / konfor offset uygulamak
Bağımlılıklar
--------------
- building.Building
- environment.BuildingEnvironment
- season.SeasonController
- io.relay_driver.RelayDriver
- io.dbtext.DBText
- io.legacy_syslog (syslog/console çıktıları için)
- config_statics (cfg_s)
- config_runtime (cfg_v)
Notlar
------
- Brülör, igniter ve pompalar relay_driver içinde isimlendirilmiş kanallarla
temsil edilir.
- Bu dosya, eski sistemle uyum için mümkün olduğunca log formatını korumaya
çalışır.
"""
import datetime
import time as _time
from dataclasses import dataclass
from typing import Optional, Dict, Any, List, Tuple
from ..building import Building
from ..season import SeasonController
from ..environment import BuildingEnvironment
from ...io.relay_driver import RelayDriver
from ...io.dbtext import DBText
from ...io import legacy_syslog as lsys
from ... import config_statics as cfg_s
from ... import config_runtime as cfg_v
# -------------------------------------------------------------
# Yardımcı: DS18B20 okuma (hat sensörleri için)
# -------------------------------------------------------------
@dataclass
class BurnerState:
burner_on: bool
pumps_on: Tuple[str, ...]
fire_setpoint_c: float
last_change_ts: datetime.datetime
reason: str
last_building_avg: Optional[float]
last_outside_c: Optional[float]
last_used_out_c: Optional[float]
last_mode: str
# ----------------------------- Isı eğrisi --------------------
# Dış ısı → kazan çıkış setpoint haritası
# Örnek bir eğri; config_runtime ile override edilebilir.
BURNER_FIRE_SETPOINT_MAP: Dict[float, Dict[str, float]] = getattr(
cfg_v,
"BURNER_FIRE_SETPOINT_MAP",
{
-10.0: {"fire": 50.0},
-5.0: {"fire": 48.0},
0.0: {"fire": 46.0},
5.0: {"fire": 44.0},
10.0: {"fire": 40.0},
15.0: {"fire": 35.0},
20.0: {"fire": 30.0},
25.0: {"fire": 26.0},
},
)
class BurnerConfig:
"""
Brülör çalışma parametreleri (runtime config'ten override edilebilir).
"""
min_run_sec: int = 60 # brülör en az bu kadar saniye çalışsın
min_stop_sec: int = 60 # brülör en az bu kadar saniye duruşta kalsın
hysteresis_c: float = 0.5 # bina ortalaması için histerezis
# ---------------------------------------------------------
# Yardımcı fonksiyon: bina istatistikleri
# ---------------------------------------------------------
def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]:
try:
if value is None:
return default
return float(value)
except Exception:
return default
def _merge_stats(old: Optional[Dict[str, Any]], new: Dict[str, Any]) -> Dict[str, Any]:
"""
Bina istatistiği için min/avg/max birleştirme.
"""
if old is None:
return dict(new)
def _pick(key: str, func):
a = old.get(key)
b = new.get(key)
if a is None:
return b
if b is None:
return a
return func(a, b)
return {
"min": _pick("min", min),
"avg": new.get("avg"),
"max": _pick("max", max),
}
# ---------------------------------------------------------
# used_out_heat hesabı
# ---------------------------------------------------------
def _apply_weekend_and_comfort(
used_out: Optional[float],
now: datetime.datetime,
weekend_boost_c: float,
comfort_offset_c: float,
) -> Optional[float]:
"""
Haftasonu ve konfor offset'ini used_out üzerine uygular.
"""
if used_out is None:
return None
result = float(used_out)
# Haftasonu boost: Cumartesi / Pazar
if now.weekday() >= 5 and weekend_boost_c != 0.0:
result -= weekend_boost_c
# Konfor offset'i
if comfort_offset_c != 0.0:
result -= comfort_offset_c
return result
def pick_fire_setpoint(outside_c: Optional[float]) -> float:
"""
Dış ısı (used_out_heat) için en yakın fire setpoint'i döndürür.
Eğer outside_c None ise, MAX_OUTLET_C kullanılır.
"""
if outside_c is None:
return float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
keys = sorted(BURNER_FIRE_SETPOINT_MAP.keys())
nearest_key = min(keys, key=lambda k: abs(k - outside_c))
mapping = BURNER_FIRE_SETPOINT_MAP.get(nearest_key, {})
return float(mapping.get("fire", getattr(cfg_v, "MAX_OUTLET_C", 45.0)))
# ---------------------------------------------------------
# Ana sınıf: BurnerController
# ---------------------------------------------------------
class BurnerController:
"""
F/B moduna göre brülör kontrolü yapan sınıf.
BUILD_BURNER = "B"
→ bina ortalama sıcaklığına göre kontrol
BUILD_BURNER = "F"
→ dış ısıya göre (OUTSIDE_LIMIT_HEAT_C) karar veren mod
(burada dış ısı olarak *used_out_heat* kullanılır).
"""
def __init__(
self,
building: Building,
relay_driver: RelayDriver,
logger: Optional[DBText] = None,
config: Optional[BurnerConfig] = None,
burner_id: Optional[int] = None,
environment: Optional[BuildingEnvironment] = None,
) -> None:
self.building = building
self.relays = relay_driver
# Runtime konfig: varsayılan BurnerConfig + config_runtime override
self.cfg = config or BurnerConfig()
try:
self.cfg.min_run_sec = int(getattr(cfg_v, "BURNER_MIN_RUN_SEC", self.cfg.min_run_sec))
self.cfg.min_stop_sec = int(getattr(cfg_v, "BURNER_MIN_STOP_SEC", self.cfg.min_stop_sec))
self.cfg.hysteresis_c = float(getattr(cfg_v, "BURNER_HYSTERESIS_C", self.cfg.hysteresis_c))
except Exception as e:
print("BurnerConfig override error:", e)
# Hangi brülör? → config_statics.BURNER_DEFAULT_ID veya parametre
default_id = int(getattr(cfg_s, "BURNER_DEFAULT_ID", 0))
self.burner_id = int(burner_id) if burner_id is not None else default_id
# DBText logger
log_file = getattr(cfg_s, "BURNER_LOG_FILE", "ebuild_burner_log.sql")
log_table = getattr(cfg_s, "BURNER_LOG_TABLE", "eburner_log")
self.logger = logger or DBText(
filename=log_file,
table=log_table,
app="EBURNER",
)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
# Röle kanal isimleri (eski yapı ile uyum için fallback)
self.igniter_ch: str = getattr(cfg_s, "BURNER_IGNITER_CH", "igniter")
self.pump_channels: List[str] = list(
getattr(cfg_s, "BURNER_PUMPS", ["circulation_a", "circulation_b"])
)
self.default_pumps: List[str] = list(
getattr(cfg_s, "BURNER_DEFAULT_PUMPS", ["circulation_a"])
)
# Bina okuma periyodu (BUILDING_READ_PERIOD_S)
self._building_last_read_ts: Optional[datetime.datetime] = None
self._building_read_period: float = float(
getattr(cfg_v, "BUILDING_READ_PERIOD_S", 60.0)
)
self._building_last_stats: Optional[Dict[str, Any]] = None
# used_out_heat için parametreler
self.used_out_c: Optional[float] = None
self._last_used_update_ts: Optional[datetime.datetime] = None
self.outside_smooth_sec: float = float(
getattr(cfg_v, "OUTSIDE_SMOOTH_SECONDS", 900.0)
)
self.weekend_boost_c: float = float(
getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)
)
self.comfort_offset_c: float = float(
getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)
)
# Ortam nesnesi (opsiyonel)
self.environment = environment
# Ortamdan başlangıç dış ısı alınabiliyorsa used_out'u hemen doldur
if self.environment is not None:
try:
first_out = self.environment.get_outside_temp_cached()
except Exception:
first_out = None
if first_out is not None:
self.used_out_c = first_out
self._last_used_update_ts = datetime.datetime.now()
# Çalışma modu
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
initial_mode = cfg_mode if cfg_mode in ("F", "B") else "F"
# Başlangıç state
self.state = BurnerState(
burner_on=False,
pumps_on=tuple(),
fire_setpoint_c=max_out,
last_change_ts=datetime.datetime.now(),
reason="init",
last_building_avg=None,
last_outside_c=None,
last_used_out_c=None,
last_mode=initial_mode,
)
# Mevsim / güneş bilgisi (syslog üst block için)
try:
self.season = SeasonController.from_now()
except Exception as e:
print("SeasonController.from_now() hata:", e)
self.season = None
# ---------------------------------------------------------
# Bina istatistikleri
# ---------------------------------------------------------
def _get_building_stats(self, now: datetime.datetime) -> Optional[Dict[str, Any]]:
"""
Bina ortalaması / min / max gibi istatistikleri periyodik olarak okur.
BUILDING_READ_PERIOD_S içinde cache kullanır.
"""
if self._building_last_read_ts is None:
need_read = True
else:
delta = (now - self._building_last_read_ts).total_seconds()
need_read = delta >= self._building_read_period
if not need_read:
return self._building_last_stats
self._building_last_read_ts = now
return None
try:
stats = self.building.get_stats()
except Exception as e:
print("Building.get_stats() hata:", e)
return self._building_last_stats
self._building_last_stats = stats
return stats
# ---------------------------------------------------------
# used_out_heat güncelleme
# ---------------------------------------------------------
def _update_used_out(self, now: datetime.datetime, outside_c: Optional[float]) -> Optional[float]:
"""
Dış ısı okumasına göre used_out_heat günceller.
- OUTSIDE_SMOOTH_SECONDS süresince eksponansiyel smoothing
- Haftasonu ve konfor offset'i eklenir.
"""
raw = outside_c
if raw is None:
return self.used_out_c
# Smooth
if self.used_out_c is None or self._last_used_update_ts is None:
smoothed = raw
else:
dt = (now - self._last_used_update_ts).total_seconds()
if dt <= 0:
smoothed = self.used_out_c
else:
tau = max(1.0, self.outside_smooth_sec)
alpha = min(1.0, dt / tau)
smoothed = (1.0 - alpha) * self.used_out_c + alpha * raw
self.used_out_c = smoothed
self._last_used_update_ts = now
# Haftasonu / konfor offset'i uygula
final_used = _apply_weekend_and_comfort(
smoothed,
now,
self.weekend_boost_c,
self.comfort_offset_c,
)
return final_used
# ---------------------------------------------------------
# Isı ihtiyacı kararları
# ---------------------------------------------------------
def _should_heat_by_outside(self, used_out: Optional[float]) -> bool:
"""
F modunda (dış ısıya göre) ısıtma isteği.
"""
limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 17.0))
if used_out is None:
return False
want = used_out < limit
print(f"should_heat_by_outside: used={used_out:.3f}C limit={limit:.1f}C")
return want
def _should_heat_by_building(self, building_avg: Optional[float], now: datetime.datetime) -> bool:
"""
B modunda bina ortalaması + konfor setpoint'e göre ısıtma isteği.
"""
comfort = float(getattr(cfg_v, "COMFORT_SETPOINT_C", 23.0))
h = self.cfg.hysteresis_c
if building_avg is None:
return False
if building_avg < (comfort - h):
return True
if building_avg > (comfort + h):
return False
# Histerezis bandında önceki state'i koru
return self.state.burner_on
# ---------------------------------------------------------
# Min çalışma / durma süreleri
# ---------------------------------------------------------
def _respect_min_times(self, now: datetime.datetime, want_on: bool) -> bool:
"""
min_run_sec / min_stop_sec kurallarını uygular.
- İlk açılışta (state.reason == 'init') kısıtlama uygulanmaz.
"""
# İlk tick: min_run/min_stop uygulama
try:
if getattr(self.state, "reason", "") == "init":
return want_on
except Exception:
pass
elapsed = (now - self.state.last_change_ts).total_seconds()
if self.state.burner_on:
# Çalışırken min_run dolmadan kapatma
if not want_on and elapsed < self.cfg.min_run_sec:
return True
else:
# Kapalıyken min_stop dolmadan açma
if want_on and elapsed < self.cfg.min_stop_sec:
return False
return want_on
# ---------------------------------------------------------
# Çıkışları rölelere uygulama
# ---------------------------------------------------------
def _apply_outputs(
self,
now: datetime.datetime,
mode: str,
burner_on: bool,
pumps_on: Tuple[str, ...],
fire_setpoint_c: float,
reason: str,
) -> None:
"""
Röleleri sürer, state'i günceller, log ve syslog üretir.
"""
# 1) Röle sürücüsü (igniter + pompalar)
try:
# Yeni API: RelayDriver brülör-aware ise
if hasattr(self.relays, "set_igniter"):
# Brülör ateşleme
self.relays.set_igniter(self.burner_id, burner_on)
# Pompalar: her zaman kanal isimleri üzerinden sür
if hasattr(self.relays, "all_pumps"):
all_pumps = list(self.relays.all_pumps(self.burner_id)) # ['circulation_a', ...]
for ch in all_pumps:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# all_pumps yoksa, config_statics'ten gelen pump_channels ile sür
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# Eski/çok basit API: doğrudan kanal adları
self.relays.set_channel(self.igniter_ch, burner_on)
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
except Exception as exc:
# legacy_syslog.log_error YOK, bu yüzden ya loga yaz ya da print et
try:
msg = f"[relay_error] igniter_ch={self.igniter_ch} burner_on={burner_on} pumps_on={pumps_on} exc={exc}"
lsys.send_legacy_syslog(lsys.format_line(98, msg))
except Exception:
print("Relay error in _apply_outputs:", exc)
# 2) State güncelle
if burner_on != self.state.burner_on or tuple(pumps_on) != self.state.pumps_on:
self.state.last_change_ts = now
self.state.burner_on = burner_on
self.state.pumps_on = tuple(pumps_on)
self.state.fire_setpoint_c = fire_setpoint_c
self.state.reason = reason
self.state.last_mode = mode
# 3) DBText logger'a yaz
try:
self.logger.insert(
{
"ts": now,
"mode": mode,
"burner_on": int(burner_on),
"pumps": ",".join(pumps_on),
"fire_sp": fire_setpoint_c,
"reason": reason,
"bavg": _safe_float(self.state.last_building_avg),
"out": _safe_float(self.state.last_outside_c),
"used": _safe_float(self.state.last_used_out_c),
}
)
except Exception:
pass
# 4) Syslog / console üst blok
try:
lsys.log_burner_header(
now=now,
mode=mode,
season=self.season,
building_avg=self.state.last_building_avg,
outside_c=self.state.last_outside_c,
used_out_c=self.state.last_used_out_c,
fire_sp=fire_setpoint_c,
burner_on=burner_on,
pumps_on=pumps_on,
)
except Exception as exc:
# Burayı tamamen sessize almayalım, hatayı konsola basalım
print("BRULOR lsys.log_burner_header error:", exc, "burner.py _apply_outputs()")
# ---------------------------------------------------------
# Ana tick fonksiyonu
# ---------------------------------------------------------
def tick(self, outside_c: Optional[float] = None) -> BurnerState:
"""
Tek bir kontrol adımı.
- Bina istatistiği BUILDING_READ_PERIOD_S periyodunda bir kez okunur,
aradaki tick'lerde cache kullanılır.
- F modunda kararlar *used_out_heat* üzerinden verilir.
"""
now = datetime.datetime.now()
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
mode = cfg_mode
print("tick outside_c:", outside_c)
# 0) dış ısı: parametre yoksa ortamdan al
if outside_c is None and getattr(self, "environment", None) is not None:
try:
outside_c = self.environment.get_outside_temp_cached()
except Exception:
outside_c = None
print("env:", getattr(self, "environment", None))
print("tick outside_c 2:", outside_c)
# 1) bina istatistiği (periyodik)
stats = self._get_building_stats(now)
building_avg = stats.get("avg") if stats else None
# 2) used_out_heat güncelle
used_out = self._update_used_out(now, outside_c)
self.state.last_building_avg = building_avg
self.state.last_outside_c = outside_c
self.state.last_used_out_c = used_out
# 3) ısıtma ihtiyacı
if mode == "F":
want_heat = self._should_heat_by_outside(used_out)
else:
mode = "B" # saçma değer gelirse B moduna zorla
want_heat = self._should_heat_by_building(building_avg, now)
want_heat = self._respect_min_times(now, want_heat)
# 4) fire setpoint F modunda da used_out üzerinden okunur
fire_sp = pick_fire_setpoint(used_out)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
fire_sp = min(fire_sp, max_out)
# 5) pompalar
if want_heat:
if hasattr(self.relays, "enabled_pumps"):
try:
pumps_list = list(self.relays.enabled_pumps(self.burner_id))
pumps = tuple(pumps_list)
except Exception:
pumps = tuple(self.default_pumps)
else:
pumps = tuple(self.default_pumps)
else:
pumps = tuple()
reason = (
f"avg={building_avg}C "
f"outside_raw={outside_c}C "
f"used={used_out}C "
f"want_heat={want_heat}"
)
print("tick reason", reason)
# 7) Rölelere uygula
self._apply_outputs(
now=now,
mode=mode,
burner_on=bool(want_heat),
pumps_on=pumps,
fire_setpoint_c=fire_sp,
reason=reason,
)
print("state", self.state)
return self.state
# -------------------------------------------------------------
# CLI / demo
# -------------------------------------------------------------
def _demo() -> None:
"""
Basit demo: Building + RelayDriver + BuildingEnvironment ile
BurnerController'ı ayağa kaldır, tick() döngüsü yap.
"""
# 1) Bina
try:
building = Building()
print("✅ Building: statics yüklendi\n")
print(building.pretty_summary())
except Exception as e:
print("❌ Building oluşturulamadı:", e)
raise SystemExit(1)
# 2) Ortam (dış ısı, ADC vs.)
try:
env = BuildingEnvironment()
except Exception as e:
print("⚠️ BuildingEnvironment oluşturulamadı:", e)
env = None
# 3) Röle sürücüsü
rel = RelayDriver(onoff=False)
# 4) Denetleyici
ctrl = BurnerController(building, rel, environment=env)
print("🔥 BurnerController başlatıldı")
print(f" Burner ID : {ctrl.burner_id}")
print(f" Çalışma modu (BUILD_BURNER): {getattr(cfg_s, 'BUILD_BURNER', 'F')} (F=dış ısı, B=bina ort)")
print(f" Igniter kanalı : {ctrl.igniter_ch}")
print(f" Pompa kanalları : {ctrl.pump_channels}")
print(f" Varsayılan pompalar : {ctrl.default_pumps}")
print(f" Konfor setpoint (°C) : {getattr(cfg_v, 'COMFORT_SETPOINT_C', 23.0)}")
print(f" Histerezis (°C) : {ctrl.cfg.hysteresis_c}")
print(f" Dış ısı limiti (°C) : {getattr(cfg_v, 'OUTSIDE_HEAT_LIMIT_C', 17.0)}")
print(f" Max kazan çıkış (°C) : {getattr(cfg_v, 'MAX_OUTLET_C', 45.0)}")
print(f" Bina okuma periyodu (s) : {ctrl._building_read_period}")
print(f" OUTSIDE_SMOOTH_SECONDS : {ctrl.outside_smooth_sec}")
print(f" WEEKEND_HEAT_BOOST_C : {ctrl.weekend_boost_c}")
print(f" BURNER_COMFORT_OFFSET_C : {ctrl.comfort_offset_c}")
print("----------------------------------------------------")
print("BurnerController demo (Ctrl+C ile çık)…")
try:
while True:
ctrl.tick()
_time.sleep(5)
except KeyboardInterrupt:
print("\nCtrl+C alındı, çıkış hazırlanıyor…")
finally:
try:
rel.all_off()
print("🔌 Tüm röleler kapatıldı.")
except Exception as e:
print(f"⚠️ Röleleri kapatırken hata: {e}")
finally:
try:
rel.cleanup()
except Exception:
pass
if __name__ == "__main__":
_demo()

View File

@@ -0,0 +1,678 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "burner"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina ve/veya dış ısıya göre brülör ve sirkülasyon kontrol çekirdeği"
__version__ = "0.4.3"
__date__ = "2025-11-22"
"""
ebuild/core/systems/burner.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- BUILD_BURNER moduna göre (F/B) brülör ve sirkülasyon pompalarını yönetmek
- Bina ortalaması (B mod) veya dış ısı (F mod) üzerinden ısıtma isteği üretmek
- used_out_heat mantığı ile dış ısıya hafta sonu / konfor offset uygulamak
Bağımlılıklar
--------------
- building.Building
- environment.BuildingEnvironment
- season.SeasonController
- io.relay_driver.RelayDriver
- io.dbtext.DBText
- io.legacy_syslog (syslog/console çıktıları için)
- config_statics (cfg_s)
- config_runtime (cfg_v)
Notlar
------
- Brülör, igniter ve pompalar relay_driver içinde isimlendirilmiş kanallarla
temsil edilir.
- Bu dosya, eski sistemle uyum için mümkün olduğunca log formatını korumaya
çalışır.
"""
import datetime
import time as _time
from dataclasses import dataclass
from typing import Optional, Dict, Any, List, Tuple
from ..building import Building
from ..season import SeasonController
from ..environment import BuildingEnvironment
from ...io.relay_driver import RelayDriver
from ...io.dbtext import DBText
from ...io import legacy_syslog as lsys
from ... import config_statics as cfg_s
from ... import config_runtime as cfg_v
# -------------------------------------------------------------
# Yardımcı: DS18B20 okuma (hat sensörleri için)
# -------------------------------------------------------------
@dataclass
class BurnerState:
burner_on: bool
pumps_on: Tuple[str, ...]
fire_setpoint_c: float
last_change_ts: datetime.datetime
reason: str
last_building_avg: Optional[float]
last_outside_c: Optional[float]
last_used_out_c: Optional[float]
last_mode: str
# ----------------------------- Isı eğrisi --------------------
# Dış ısı → kazan çıkış setpoint haritası
# Örnek bir eğri; config_runtime ile override edilebilir.
BURNER_FIRE_SETPOINT_MAP: Dict[float, Dict[str, float]] = getattr(
cfg_v,
"BURNER_FIRE_SETPOINT_MAP",
{
-10.0: {"fire": 50.0},
-5.0: {"fire": 48.0},
0.0: {"fire": 46.0},
5.0: {"fire": 44.0},
10.0: {"fire": 40.0},
15.0: {"fire": 35.0},
20.0: {"fire": 30.0},
25.0: {"fire": 26.0},
},
)
class BurnerConfig:
"""
Brülör çalışma parametreleri (runtime config'ten override edilebilir).
"""
min_run_sec: int = 60 # brülör en az bu kadar saniye çalışsın
min_stop_sec: int = 60 # brülör en az bu kadar saniye duruşta kalsın
hysteresis_c: float = 0.5 # bina ortalaması için histerezis
# ---------------------------------------------------------
# Yardımcı fonksiyon: bina istatistikleri
# ---------------------------------------------------------
def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]:
try:
if value is None:
return default
return float(value)
except Exception:
return default
def _merge_stats(old: Optional[Dict[str, Any]], new: Dict[str, Any]) -> Dict[str, Any]:
"""
Bina istatistiği için min/avg/max birleştirme.
"""
if old is None:
return dict(new)
def _pick(key: str, func):
a = old.get(key)
b = new.get(key)
if a is None:
return b
if b is None:
return a
return func(a, b)
return {
"min": _pick("min", min),
"avg": new.get("avg"),
"max": _pick("max", max),
}
# ---------------------------------------------------------
# used_out_heat hesabı
# ---------------------------------------------------------
def _apply_weekend_and_comfort(
used_out: Optional[float],
now: datetime.datetime,
weekend_boost_c: float,
comfort_offset_c: float,
) -> Optional[float]:
"""
Haftasonu ve konfor offset'ini used_out üzerine uygular.
"""
if used_out is None:
return None
result = float(used_out)
# Haftasonu boost: Cumartesi / Pazar
if now.weekday() >= 5 and weekend_boost_c != 0.0:
result -= weekend_boost_c
# Konfor offset'i
if comfort_offset_c != 0.0:
result -= comfort_offset_c
return result
def pick_fire_setpoint(outside_c: Optional[float]) -> float:
"""
Dış ısı (used_out_heat) için en yakın fire setpoint'i döndürür.
Eğer outside_c None ise, MAX_OUTLET_C kullanılır.
"""
if outside_c is None:
return float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
keys = sorted(BURNER_FIRE_SETPOINT_MAP.keys())
nearest_key = min(keys, key=lambda k: abs(k - outside_c))
mapping = BURNER_FIRE_SETPOINT_MAP.get(nearest_key, {})
return float(mapping.get("fire", getattr(cfg_v, "MAX_OUTLET_C", 45.0)))
# ---------------------------------------------------------
# Ana sınıf: BurnerController
# ---------------------------------------------------------
class BurnerController:
"""
F/B moduna göre brülör kontrolü yapan sınıf.
BUILD_BURNER = "B"
→ bina ortalama sıcaklığına göre kontrol
BUILD_BURNER = "F"
→ dış ısıya göre (OUTSIDE_LIMIT_HEAT_C) karar veren mod
(burada dış ısı olarak *used_out_heat* kullanılır).
"""
def __init__(
self,
building: Building,
relay_driver: RelayDriver,
logger: Optional[DBText] = None,
config: Optional[BurnerConfig] = None,
burner_id: Optional[int] = None,
environment: Optional[BuildingEnvironment] = None,
) -> None:
self.building = building
self.relays = relay_driver
# Runtime konfig: varsayılan BurnerConfig + config_runtime override
self.cfg = config or BurnerConfig()
try:
self.cfg.min_run_sec = int(getattr(cfg_v, "BURNER_MIN_RUN_SEC", self.cfg.min_run_sec))
self.cfg.min_stop_sec = int(getattr(cfg_v, "BURNER_MIN_STOP_SEC", self.cfg.min_stop_sec))
self.cfg.hysteresis_c = float(getattr(cfg_v, "BURNER_HYSTERESIS_C", self.cfg.hysteresis_c))
except Exception as e:
print("BurnerConfig override error:", e)
# Hangi brülör? → config_statics.BURNER_DEFAULT_ID veya parametre
default_id = int(getattr(cfg_s, "BURNER_DEFAULT_ID", 0))
self.burner_id = int(burner_id) if burner_id is not None else default_id
# DBText logger
log_file = getattr(cfg_s, "BURNER_LOG_FILE", "ebuild_burner_log.sql")
log_table = getattr(cfg_s, "BURNER_LOG_TABLE", "eburner_log")
self.logger = logger or DBText(
filename=log_file,
table=log_table,
app="EBURNER",
)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
# Röle kanal isimleri (eski yapı ile uyum için fallback)
self.igniter_ch: str = getattr(cfg_s, "BURNER_IGNITER_CH", "igniter")
self.pump_channels: List[str] = list(
getattr(cfg_s, "BURNER_PUMPS", ["circulation_a", "circulation_b"])
)
self.default_pumps: List[str] = list(
getattr(cfg_s, "BURNER_DEFAULT_PUMPS", ["circulation_a"])
)
# Bina okuma periyodu (BUILDING_READ_PERIOD_S)
self._building_last_read_ts: Optional[datetime.datetime] = None
self._building_read_period: float = float(
getattr(cfg_v, "BUILDING_READ_PERIOD_S", 60.0)
)
self._building_last_stats: Optional[Dict[str, Any]] = None
# used_out_heat için parametreler
self.used_out_c: Optional[float] = None
self._last_used_update_ts: Optional[datetime.datetime] = None
self.outside_smooth_sec: float = float(
getattr(cfg_v, "OUTSIDE_SMOOTH_SECONDS", 900.0)
)
self.weekend_boost_c: float = float(
getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)
)
self.comfort_offset_c: float = float(
getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)
)
# Ortam nesnesi (opsiyonel)
self.environment = environment
# Ortamdan başlangıç dış ısı alınabiliyorsa used_out'u hemen doldur
if self.environment is not None:
try:
first_out = self.environment.get_outside_temp_cached()
except Exception:
first_out = None
if first_out is not None:
self.used_out_c = first_out
self._last_used_update_ts = datetime.datetime.now()
# Çalışma modu
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
initial_mode = cfg_mode if cfg_mode in ("F", "B") else "F"
# Başlangıç state
self.state = BurnerState(
burner_on=False,
pumps_on=tuple(),
fire_setpoint_c=max_out,
last_change_ts=datetime.datetime.now(),
reason="init",
last_building_avg=None,
last_outside_c=None,
last_used_out_c=None,
last_mode=initial_mode,
)
# Mevsim / güneş bilgisi (syslog üst block için)
try:
self.season = SeasonController.from_now()
except Exception as e:
print("SeasonController.from_now() hata:", e)
self.season = None
# ---------------------------------------------------------
# Bina istatistikleri
# ---------------------------------------------------------
def _get_building_stats(self, now: datetime.datetime) -> Optional[Dict[str, Any]]:
"""
Bina ortalaması / min / max gibi istatistikleri periyodik olarak okur.
BUILDING_READ_PERIOD_S içinde cache kullanır.
"""
if self._building_last_read_ts is None:
need_read = True
else:
delta = (now - self._building_last_read_ts).total_seconds()
need_read = delta >= self._building_read_period
if not need_read:
return self._building_last_stats
try:
stats = self.building.get_stats()
except Exception as e:
print("Building.get_stats() hata:", e)
return self._building_last_stats
self._building_last_read_ts = now
self._building_last_stats = stats
return stats
# ---------------------------------------------------------
# used_out_heat güncelleme
# ---------------------------------------------------------
def _update_used_out(self, now: datetime.datetime, outside_c: Optional[float]) -> Optional[float]:
"""
Dış ısı okumasına göre used_out_heat günceller.
- OUTSIDE_SMOOTH_SECONDS süresince eksponansiyel smoothing
- Haftasonu ve konfor offset'i eklenir.
"""
raw = outside_c
if raw is None:
return self.used_out_c
# Smooth
if self.used_out_c is None or self._last_used_update_ts is None:
smoothed = raw
else:
dt = (now - self._last_used_update_ts).total_seconds()
if dt <= 0:
smoothed = self.used_out_c
else:
tau = max(1.0, self.outside_smooth_sec)
alpha = min(1.0, dt / tau)
smoothed = (1.0 - alpha) * self.used_out_c + alpha * raw
self.used_out_c = smoothed
self._last_used_update_ts = now
# Haftasonu / konfor offset'i uygula
final_used = _apply_weekend_and_comfort(
smoothed,
now,
self.weekend_boost_c,
self.comfort_offset_c,
)
return final_used
# ---------------------------------------------------------
# Isı ihtiyacı kararları
# ---------------------------------------------------------
def _should_heat_by_outside(self, used_out: Optional[float]) -> bool:
"""
F modunda (dış ısıya göre) ısıtma isteği.
"""
limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 17.0))
if used_out is None:
return False
want = used_out < limit
print(f"should_heat_by_outside: used={used_out:.3f}C limit={limit:.1f}C")
return want
def _should_heat_by_building(self, building_avg: Optional[float], now: datetime.datetime) -> bool:
"""
B modunda bina ortalaması + konfor setpoint'e göre ısıtma isteği.
"""
comfort = float(getattr(cfg_v, "COMFORT_SETPOINT_C", 23.0))
h = self.cfg.hysteresis_c
if building_avg is None:
return False
if building_avg < (comfort - h):
return True
if building_avg > (comfort + h):
return False
# Histerezis bandında önceki state'i koru
return self.state.burner_on
# ---------------------------------------------------------
# Min çalışma / durma süreleri
# ---------------------------------------------------------
def _respect_min_times(self, now: datetime.datetime, want_on: bool) -> bool:
"""
min_run_sec / min_stop_sec kurallarını uygular.
- İlk açılışta (state.reason == 'init') kısıtlama uygulanmaz.
"""
# İlk tick: min_run/min_stop uygulama
try:
if getattr(self.state, "reason", "") == "init":
return want_on
except Exception:
pass
elapsed = (now - self.state.last_change_ts).total_seconds()
if self.state.burner_on:
# Çalışırken min_run dolmadan kapatma
if not want_on and elapsed < self.cfg.min_run_sec:
return True
else:
# Kapalıyken min_stop dolmadan açma
if want_on and elapsed < self.cfg.min_stop_sec:
return False
return want_on
# ---------------------------------------------------------
# Çıkışları rölelere uygulama
# ---------------------------------------------------------
def _apply_outputs(
self,
now: datetime.datetime,
mode: str,
burner_on: bool,
pumps_on: Tuple[str, ...],
fire_setpoint_c: float,
reason: str,
) -> None:
"""
Röleleri sürer, state'i günceller, log ve syslog üretir.
"""
# 1) Röle sürücüsü (igniter + pompalar)
try:
# Yeni API: RelayDriver brülör-aware ise
if hasattr(self.relays, "set_igniter"):
# Brülör ateşleme
self.relays.set_igniter(self.burner_id, burner_on)
# Pompalar: her zaman kanal isimleri üzerinden sür
if hasattr(self.relays, "all_pumps"):
all_pumps = list(self.relays.all_pumps(self.burner_id)) # ['circulation_a', ...]
for ch in all_pumps:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# all_pumps yoksa, config_statics'ten gelen pump_channels ile sür
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# Eski/çok basit API: doğrudan kanal adları
self.relays.set_channel(self.igniter_ch, burner_on)
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
except Exception as exc:
# legacy_syslog.log_error YOK, bu yüzden ya loga yaz ya da print et
try:
msg = f"[relay_error] igniter_ch={self.igniter_ch} burner_on={burner_on} pumps_on={pumps_on} exc={exc}"
lsys.send_legacy_syslog(lsys.format_line(98, msg))
except Exception:
print("Relay error in _apply_outputs:", exc)
# 2) State güncelle
if burner_on != self.state.burner_on or tuple(pumps_on) != self.state.pumps_on:
self.state.last_change_ts = now
self.state.burner_on = burner_on
self.state.pumps_on = tuple(pumps_on)
self.state.fire_setpoint_c = fire_setpoint_c
self.state.reason = reason
self.state.last_mode = mode
# 3) DBText logger'a yaz
try:
self.logger.insert(
{
"ts": now,
"mode": mode,
"burner_on": int(burner_on),
"pumps": ",".join(pumps_on),
"fire_sp": fire_setpoint_c,
"reason": reason,
"bavg": _safe_float(self.state.last_building_avg),
"out": _safe_float(self.state.last_outside_c),
"used": _safe_float(self.state.last_used_out_c),
}
)
except Exception:
pass
# 4) Syslog / console üst blok
try:
lsys.log_burner_header(
now=now,
mode=mode,
season=self.season,
building_avg=self.state.last_building_avg,
outside_c=self.state.last_outside_c,
used_out_c=self.state.last_used_out_c,
fire_sp=fire_setpoint_c,
burner_on=burner_on,
pumps_on=pumps_on,
)
except Exception as exc:
# Burayı tamamen sessize almayalım, hatayı konsola basalım
print("BRULOR lsys.log_burner_header error:", exc, "burner.py _apply_outputs()")
# ---------------------------------------------------------
# Ana tick fonksiyonu
# ---------------------------------------------------------
def tick(self, outside_c: Optional[float] = None) -> BurnerState:
"""
Tek bir kontrol adımı.
- Bina istatistiği BUILDING_READ_PERIOD_S periyodunda bir kez okunur,
aradaki tick'lerde cache kullanılır.
- F modunda kararlar *used_out_heat* üzerinden verilir.
"""
now = datetime.datetime.now()
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
mode = cfg_mode
print("tick outside_c:", outside_c)
# 0) dış ısı: parametre yoksa ortamdan al
if outside_c is None and getattr(self, "environment", None) is not None:
try:
outside_c = self.environment.get_outside_temp_cached()
except Exception:
outside_c = None
print("env:", getattr(self, "environment", None))
print("tick outside_c 2:", outside_c)
# 1) bina istatistiği (periyodik)
stats = self._get_building_stats(now)
building_avg = stats.get("avg") if stats else None
# 2) used_out_heat güncelle
used_out = self._update_used_out(now, outside_c)
self.state.last_building_avg = building_avg
self.state.last_outside_c = outside_c
self.state.last_used_out_c = used_out
# 3) ısıtma ihtiyacı
if mode == "F":
want_heat = self._should_heat_by_outside(used_out)
else:
mode = "B" # saçma değer gelirse B moduna zorla
want_heat = self._should_heat_by_building(building_avg, now)
want_heat = self._respect_min_times(now, want_heat)
# 4) fire setpoint F modunda da used_out üzerinden okunur
fire_sp = pick_fire_setpoint(used_out)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
fire_sp = min(fire_sp, max_out)
# 5) pompalar
if want_heat:
if hasattr(self.relays, "enabled_pumps"):
try:
pumps_list = list(self.relays.enabled_pumps(self.burner_id))
pumps = tuple(pumps_list)
except Exception:
pumps = tuple(self.default_pumps)
else:
pumps = tuple(self.default_pumps)
else:
pumps = tuple()
reason = (
f"avg={building_avg}C "
f"outside_raw={outside_c}C "
f"used={used_out}C "
f"want_heat={want_heat}"
)
print("tick reason", reason)
# 7) Rölelere uygula
self._apply_outputs(
now=now,
mode=mode,
burner_on=bool(want_heat),
pumps_on=pumps,
fire_setpoint_c=fire_sp,
reason=reason,
)
print("state", self.state)
return self.state
# -------------------------------------------------------------
# CLI / demo
# -------------------------------------------------------------
def _demo() -> None:
"""
Basit demo: Building + RelayDriver + BuildingEnvironment ile
BurnerController'ı ayağa kaldır, tick() döngüsü yap.
"""
# 1) Bina
try:
building = Building()
print("✅ Building: statics yüklendi\n")
print(building.pretty_summary())
except Exception as e:
print("❌ Building oluşturulamadı:", e)
raise SystemExit(1)
# 2) Ortam (dış ısı, ADC vs.)
try:
env = BuildingEnvironment()
except Exception as e:
print("⚠️ BuildingEnvironment oluşturulamadı:", e)
env = None
# 3) Röle sürücüsü
rel = RelayDriver(onoff=False)
# 4) Denetleyici
ctrl = BurnerController(building, rel, environment=env)
print("🔥 BurnerController başlatıldı")
print(f" Burner ID : {ctrl.burner_id}")
print(f" Çalışma modu (BUILD_BURNER): {getattr(cfg_s, 'BUILD_BURNER', 'F')} (F=dış ısı, B=bina ort)")
print(f" Igniter kanalı : {ctrl.igniter_ch}")
print(f" Pompa kanalları : {ctrl.pump_channels}")
print(f" Varsayılan pompalar : {ctrl.default_pumps}")
print(f" Konfor setpoint (°C) : {getattr(cfg_v, 'COMFORT_SETPOINT_C', 23.0)}")
print(f" Histerezis (°C) : {ctrl.cfg.hysteresis_c}")
print(f" Dış ısı limiti (°C) : {getattr(cfg_v, 'OUTSIDE_HEAT_LIMIT_C', 17.0)}")
print(f" Max kazan çıkış (°C) : {getattr(cfg_v, 'MAX_OUTLET_C', 45.0)}")
print(f" Bina okuma periyodu (s) : {ctrl._building_read_period}")
print(f" OUTSIDE_SMOOTH_SECONDS : {ctrl.outside_smooth_sec}")
print(f" WEEKEND_HEAT_BOOST_C : {ctrl.weekend_boost_c}")
print(f" BURNER_COMFORT_OFFSET_C : {ctrl.comfort_offset_c}")
print("----------------------------------------------------")
print("BurnerController demo (Ctrl+C ile çık)…")
try:
while True:
ctrl.tick()
_time.sleep(5)
except KeyboardInterrupt:
print("\nCtrl+C alındı, çıkış hazırlanıyor…")
finally:
try:
rel.all_off()
print("🔌 Tüm röleler kapatıldı.")
except Exception as e:
print(f"⚠️ Röleleri kapatırken hata: {e}")
finally:
try:
rel.cleanup()
except Exception:
pass
if __name__ == "__main__":
_demo()

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Yangın alarm sistemi iskeleti."""

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Hidrofor sistemi iskeleti."""

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Sulama sistemi iskeleti."""