ilk işlem
This commit is contained in:
1
ebuild/core/systems/__init__.py
Normal file
1
ebuild/core/systems/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
BIN
ebuild/core/systems/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
ebuild/core/systems/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/core/systems/__pycache__/burner.cpython-39.pyc
Normal file
BIN
ebuild/core/systems/__pycache__/burner.cpython-39.pyc
Normal file
Binary file not shown.
679
ebuild/core/systems/burner.py
Normal file
679
ebuild/core/systems/burner.py
Normal 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()
|
||||
678
ebuild/core/systems/burner.py~
Normal file
678
ebuild/core/systems/burner.py~
Normal 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()
|
||||
2
ebuild/core/systems/firealarm.py
Normal file
2
ebuild/core/systems/firealarm.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Yangın alarm sistemi iskeleti."""
|
||||
2
ebuild/core/systems/hydrophore.py
Normal file
2
ebuild/core/systems/hydrophore.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Hidrofor sistemi iskeleti."""
|
||||
2
ebuild/core/systems/irrigation.py
Normal file
2
ebuild/core/systems/irrigation.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Sulama sistemi iskeleti."""
|
||||
Reference in New Issue
Block a user