ilk işlem
This commit is contained in:
1
ebuild/io/__init__.py
Normal file
1
ebuild/io/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
BIN
ebuild/io/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/adc_mcp3008.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/adc_mcp3008.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/config_ini.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/config_ini.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/dbtext.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/dbtext.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/ds18b20.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/ds18b20.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/legacy_syslog.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/legacy_syslog.cpython-39.pyc
Normal file
Binary file not shown.
BIN
ebuild/io/__pycache__/relay_driver.cpython-39.pyc
Normal file
BIN
ebuild/io/__pycache__/relay_driver.cpython-39.pyc
Normal file
Binary file not shown.
423
ebuild/io/adc_mcp3008.py
Normal file
423
ebuild/io/adc_mcp3008.py
Normal file
@@ -0,0 +1,423 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "analog_sensors"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "MCP3008 tabanlı basınç, gaz, yağmur ve LDR sensörleri için sınıf bazlı arayüz"
|
||||
__version__ = "0.1.0"
|
||||
__date__ = "2025-11-21"
|
||||
|
||||
"""
|
||||
ebuild/core/analog_sensors.py
|
||||
|
||||
Revision : 2025-11-21
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- MCP3008 ADC üzerinden bağlı analog sensörler için tekil sınıf yapıları
|
||||
sağlamak:
|
||||
* PressureAnalogSensor : su hattı basınç sensörü
|
||||
* GasAnalogSensor : MQ-4 veya benzeri gaz sensörü
|
||||
* RainAnalogSensor : yağmur sensörü (wet/dry)
|
||||
* LDRAnalogSensor : ışık seviyesi sensörü
|
||||
- Her bir sensör:
|
||||
* MCP3008ADC üzerinden ilgili kanalı okur (config_statics.ADC_CHANNELS).
|
||||
* Ham raw (0..1023) ve volt cinsinden değer döndürebilir.
|
||||
* Eşik ve basit state (SAFE/WARN/ALARM vb.) hesabını kendi içinde tutar.
|
||||
- Güvenlik ve uyarı mantığı üst kattaki HeatEngine/Burner/Buzzer/Legacy
|
||||
ile kolay entegre edilebilir.
|
||||
|
||||
Notlar
|
||||
------
|
||||
- Bu modül "karar mantığı" ve analog okuma katmanını birleştirir.
|
||||
- Röle kapama, sistem shutdown, buzzer vb. aksiyonlar yine üst katmanda
|
||||
yapılmalıdır.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict
|
||||
|
||||
try:
|
||||
from ..io.adc_mcp3008 import MCP3008ADC
|
||||
from .. import config_statics as cfg
|
||||
except ImportError:
|
||||
MCP3008ADC = None # type: ignore
|
||||
cfg = None # type: ignore
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Ortak durum enum'u
|
||||
# -------------------------------------------------------------
|
||||
class SafetyState:
|
||||
SAFE = "SAFE"
|
||||
WARN = "WARN"
|
||||
ALARM = "ALARM"
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Ortak base sınıf
|
||||
# -------------------------------------------------------------
|
||||
@dataclass
|
||||
class BaseAnalogSensor:
|
||||
"""
|
||||
MCP3008 üzerinden tek bir analog kanalı temsil eden temel sınıf.
|
||||
|
||||
Özellikler:
|
||||
-----------
|
||||
- adc : MCP3008ADC örneği
|
||||
- channel : int kanal no (0..7)
|
||||
- name : mantıksal isim (örn: "gas", "pressure")
|
||||
- last_raw : son okunan ham değer (0..1023)
|
||||
- last_volt : son okunan volt cinsinden değer
|
||||
"""
|
||||
|
||||
adc: MCP3008ADC
|
||||
name: str
|
||||
channel: Optional[int] = None
|
||||
|
||||
last_raw: Optional[int] = None
|
||||
last_volt: Optional[float] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Eğer kanal configten alınacaksa burada çöz
|
||||
if self.channel is None and cfg is not None:
|
||||
ch_map = getattr(cfg, "ADC_CHANNELS", {})
|
||||
if self.name in ch_map:
|
||||
self.channel = int(ch_map[self.name])
|
||||
if self.channel is None:
|
||||
raise ValueError(f"{self.__class__.__name__}: '{self.name}' için kanal bulunamadı.")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def read_raw(self) -> Optional[int]:
|
||||
"""
|
||||
ADC'den ham değeri okur (0..1023).
|
||||
"""
|
||||
if self.channel is None:
|
||||
return None
|
||||
raw = self.adc.read_raw(self.channel)
|
||||
self.last_raw = raw
|
||||
return raw
|
||||
|
||||
def read_voltage(self) -> Optional[float]:
|
||||
"""
|
||||
ADC'den raw okur ve volt cinsine çevirir.
|
||||
"""
|
||||
if self.channel is None:
|
||||
return None
|
||||
volt = self.adc.read_voltage(self.channel)
|
||||
self.last_volt = volt
|
||||
return volt
|
||||
|
||||
def update(self) -> Optional[int]:
|
||||
"""
|
||||
Varsayılan olarak sadece raw okur; alt sınıflar state hesaplarını
|
||||
kendi override'larında yapar.
|
||||
"""
|
||||
return self.read_raw()
|
||||
|
||||
def summary(self) -> str:
|
||||
return f"{self.__class__.__name__}(name={self.name}, ch={self.channel}, raw={self.last_raw}, V={self.last_volt})"
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Gaz sensörü (MQ-4) – kill switch mantığı ile
|
||||
# -------------------------------------------------------------
|
||||
@dataclass
|
||||
class GasAnalogSensor(BaseAnalogSensor):
|
||||
"""
|
||||
Gaz sensörü (MQ-4) için analog ve güvenlik mantığı.
|
||||
|
||||
State mantığı:
|
||||
- raw >= alarm_threshold → ALARM, latched
|
||||
- raw >= warn_threshold → WARN (latched varsa ALARM)
|
||||
- trend (slope) ile hızlı artış + warn üstü → ALARM
|
||||
- Diğer durumlarda SAFE (latched yoksa).
|
||||
|
||||
latched_alarm True olduğu sürece:
|
||||
- state ALARM olarak kalır
|
||||
- should_shutdown_system() True döner
|
||||
"""
|
||||
|
||||
warn_threshold: int = field(default=150)
|
||||
alarm_threshold: int = field(default=250)
|
||||
slope_min_delta: int = field(default=30)
|
||||
slope_window: int = field(default=5)
|
||||
|
||||
history_len: int = field(default=20)
|
||||
|
||||
state: str = SafetyState.SAFE
|
||||
latched_alarm: bool = False
|
||||
_history: list[int] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
# Konfig override
|
||||
if cfg is not None:
|
||||
self.warn_threshold = int(getattr(cfg, "GAS_WARN_THRESHOLD_RAW", self.warn_threshold))
|
||||
self.alarm_threshold = int(getattr(cfg, "GAS_ALARM_THRESHOLD_RAW", self.alarm_threshold))
|
||||
self.slope_min_delta = int(getattr(cfg, "GAS_SLOPE_MIN_DELTA", self.slope_min_delta))
|
||||
self.slope_window = int(getattr(cfg, "GAS_SLOPE_WINDOW", self.slope_window))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def reset_latch(self) -> None:
|
||||
"""
|
||||
Gaz alarm latch'ini manuel olarak sıfırlar.
|
||||
"""
|
||||
self.latched_alarm = False
|
||||
# state sonraki update ile yeniden değerlendirilecek.
|
||||
|
||||
def update(self) -> Optional[int]:
|
||||
"""
|
||||
Ham değeri okur ve state hesaplar.
|
||||
"""
|
||||
raw = self.read_raw()
|
||||
if raw is None:
|
||||
return None
|
||||
|
||||
# history güncelle
|
||||
self._history.append(raw)
|
||||
if len(self._history) > self.history_len:
|
||||
self._history = self._history[-self.history_len:]
|
||||
|
||||
self._evaluate_state(raw)
|
||||
return raw
|
||||
|
||||
def _evaluate_state(self, raw: int) -> None:
|
||||
# Trend kontrolü
|
||||
slope_alarm = False
|
||||
if len(self._history) >= self.slope_window:
|
||||
first = self._history[-self.slope_window]
|
||||
delta = raw - first
|
||||
if delta >= self.slope_min_delta:
|
||||
slope_alarm = True
|
||||
|
||||
# Eşik mantığı
|
||||
if raw >= self.alarm_threshold or (slope_alarm and raw >= self.warn_threshold):
|
||||
self.state = SafetyState.ALARM
|
||||
self.latched_alarm = True
|
||||
elif raw >= self.warn_threshold:
|
||||
if self.latched_alarm:
|
||||
self.state = SafetyState.ALARM
|
||||
else:
|
||||
self.state = SafetyState.WARN
|
||||
else:
|
||||
if self.latched_alarm:
|
||||
self.state = SafetyState.ALARM
|
||||
else:
|
||||
self.state = SafetyState.SAFE
|
||||
|
||||
def should_shutdown_system(self) -> bool:
|
||||
"""
|
||||
Gaz açısından sistemin tamamen kapatılması gerekip gerekmediğini
|
||||
söyler.
|
||||
"""
|
||||
return self.latched_alarm
|
||||
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"GasAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
|
||||
f"state={self.state}, latched={self.latched_alarm})"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Basınç sensörü – limit kontrolü
|
||||
# -------------------------------------------------------------
|
||||
@dataclass
|
||||
class PressureAnalogSensor(BaseAnalogSensor):
|
||||
"""
|
||||
Su hattı basınç sensörü.
|
||||
|
||||
State mantığı:
|
||||
- raw < (min_raw - warn_hyst) veya raw > (max_raw + warn_hyst) → WARN
|
||||
- min_raw <= raw <= max_raw → SAFE
|
||||
- Aradaki buffer bölgede state korunur.
|
||||
"""
|
||||
|
||||
min_raw: int = field(default=200)
|
||||
max_raw: int = field(default=900)
|
||||
warn_hyst: int = field(default=20)
|
||||
|
||||
state: str = SafetyState.SAFE
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
if cfg is not None:
|
||||
self.min_raw = int(getattr(cfg, "PRESSURE_MIN_RAW", self.min_raw))
|
||||
self.max_raw = int(getattr(cfg, "PRESSURE_MAX_RAW", self.max_raw))
|
||||
self.warn_hyst = int(getattr(cfg, "PRESSURE_WARN_HYST", self.warn_hyst))
|
||||
|
||||
def update(self) -> Optional[int]:
|
||||
raw = self.read_raw()
|
||||
if raw is None:
|
||||
return None
|
||||
self._evaluate_state(raw)
|
||||
return raw
|
||||
|
||||
def _evaluate_state(self, raw: int) -> None:
|
||||
if raw < (self.min_raw - self.warn_hyst) or raw > (self.max_raw + self.warn_hyst):
|
||||
self.state = SafetyState.WARN
|
||||
elif self.min_raw <= raw <= self.max_raw:
|
||||
self.state = SafetyState.SAFE
|
||||
# Buffer bölgede state korunur.
|
||||
|
||||
def is_pressure_ok(self) -> bool:
|
||||
return self.state == SafetyState.SAFE
|
||||
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"PressureAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
|
||||
f"state={self.state}, min={self.min_raw}, max={self.max_raw})"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Yağmur sensörü – basit dry/wet mantığı
|
||||
# -------------------------------------------------------------
|
||||
@dataclass
|
||||
class RainAnalogSensor(BaseAnalogSensor):
|
||||
"""
|
||||
Yağmur sensörü (analog).
|
||||
|
||||
Basit model:
|
||||
- raw <= dry_threshold → DRY
|
||||
- raw >= wet_threshold → WET
|
||||
- arası → MID
|
||||
|
||||
Gerektiğinde bu sınıf geliştirilebilir (ör. şiddetli yağmur vs.).
|
||||
"""
|
||||
|
||||
dry_threshold: int = field(default=100)
|
||||
wet_threshold: int = field(default=400)
|
||||
|
||||
state: str = "UNKNOWN" # "DRY", "MID", "WET"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
# İleride configten override eklenebilir:
|
||||
if cfg is not None:
|
||||
self.dry_threshold = int(getattr(cfg, "RAIN_DRY_THRESHOLD_RAW", self.dry_threshold))
|
||||
self.wet_threshold = int(getattr(cfg, "RAIN_WET_THRESHOLD_RAW", self.wet_threshold))
|
||||
|
||||
def update(self) -> Optional[int]:
|
||||
raw = self.read_raw()
|
||||
if raw is None:
|
||||
return None
|
||||
self._evaluate_state(raw)
|
||||
return raw
|
||||
|
||||
def _evaluate_state(self, raw: int) -> None:
|
||||
if raw <= self.dry_threshold:
|
||||
self.state = "DRY"
|
||||
elif raw >= self.wet_threshold:
|
||||
self.state = "WET"
|
||||
else:
|
||||
self.state = "MID"
|
||||
|
||||
def is_raining(self) -> bool:
|
||||
return self.state == "WET"
|
||||
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"RainAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
|
||||
f"state={self.state}, dry_th={self.dry_threshold}, wet_th={self.wet_threshold})"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# LDR sensörü – ışık seviyesi
|
||||
# -------------------------------------------------------------
|
||||
@dataclass
|
||||
class LDRAnalogSensor(BaseAnalogSensor):
|
||||
"""
|
||||
LDR (ışık) sensörü.
|
||||
|
||||
Basit model:
|
||||
- raw <= dark_threshold → DARK
|
||||
- raw >= bright_threshold → BRIGHT
|
||||
- arası → MID
|
||||
|
||||
Bu bilgi:
|
||||
- dış ortam karanlık/aydınlık
|
||||
- gece/gündüz teyidi
|
||||
- aydınlatma / gösterge kararları
|
||||
için kullanılabilir.
|
||||
"""
|
||||
|
||||
dark_threshold: int = field(default=200)
|
||||
bright_threshold: int = field(default=800)
|
||||
|
||||
state: str = "UNKNOWN" # "DARK", "MID", "BRIGHT"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
if cfg is not None:
|
||||
self.dark_threshold = int(getattr(cfg, "LDR_DARK_THRESHOLD_RAW", self.dark_threshold))
|
||||
self.bright_threshold = int(getattr(cfg, "LDR_BRIGHT_THRESHOLD_RAW", self.bright_threshold))
|
||||
|
||||
def update(self) -> Optional[int]:
|
||||
raw = self.read_raw()
|
||||
if raw is None:
|
||||
return None
|
||||
self._evaluate_state(raw)
|
||||
return raw
|
||||
|
||||
def _evaluate_state(self, raw: int) -> None:
|
||||
if raw <= self.dark_threshold:
|
||||
self.state = "DARK"
|
||||
elif raw >= self.bright_threshold:
|
||||
self.state = "BRIGHT"
|
||||
else:
|
||||
self.state = "MID"
|
||||
|
||||
def is_dark(self) -> bool:
|
||||
return self.state == "DARK"
|
||||
|
||||
def is_bright(self) -> bool:
|
||||
return self.state == "BRIGHT"
|
||||
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"LDRAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
|
||||
f"state={self.state}, dark_th={self.dark_threshold}, bright_th={self.bright_threshold})"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Tümünü toplayan minik hub (opsiyonel)
|
||||
# -------------------------------------------------------------
|
||||
class AnalogSensorsHub:
|
||||
"""
|
||||
MCP3008 üstündeki tüm analog sensörleri yöneten yardımcı sınıf.
|
||||
|
||||
- pressure : PressureAnalogSensor
|
||||
- gas : GasAnalogSensor
|
||||
- rain : RainAnalogSensor
|
||||
- ldr : LDRAnalogSensor
|
||||
"""
|
||||
|
||||
def __init__(self, adc: MCP3008ADC) -> None:
|
||||
self.adc = adc
|
||||
|
||||
self.pressure = PressureAnalogSensor(adc=self.adc, name="pressure")
|
||||
self.gas = GasAnalogSensor(adc=self.adc, name="gas")
|
||||
self.rain = RainAnalogSensor(adc=self.adc, name="rain")
|
||||
self.ldr = LDRAnalogSensor(adc=self.adc, name="ldr")
|
||||
|
||||
def update_all(self) -> Dict[str, Optional[int]]:
|
||||
"""
|
||||
Tüm sensörleri günceller ve ham değerleri döndürür.
|
||||
"""
|
||||
return {
|
||||
"pressure": self.pressure.update(),
|
||||
"gas": self.gas.update(),
|
||||
"rain": self.rain.update(),
|
||||
"ldr": self.ldr.update(),
|
||||
}
|
||||
|
||||
def should_shutdown_system(self) -> bool:
|
||||
"""
|
||||
Gaz sensörü açısından kill-switch gerekip gerekmediğini söyler.
|
||||
"""
|
||||
return self.gas.should_shutdown_system()
|
||||
333
ebuild/io/config_ini.py
Normal file
333
ebuild/io/config_ini.py
Normal file
@@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__title__ = "config_ini"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "INI tabanlı konfigürasyon ve değer depolama yardımcıları"
|
||||
__version__ = "0.2.0"
|
||||
__date__ = "2025-11-20"
|
||||
|
||||
"""
|
||||
ebuild/io/config_ini.py
|
||||
|
||||
Revision : 2025-11-20
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
Eski EdmConfig yapısının modernize edilmiş, hatalardan arındırılmış ama
|
||||
aynı API'yi koruyan sürümü.
|
||||
|
||||
Bu modül:
|
||||
- INI dosyalarını ConfigParser ile yönetir
|
||||
- Eksik section/item gördüğünde otomatik oluşturur
|
||||
- Dosya değişimini (mtime) izleyerek reload imkanı verir
|
||||
- Basit bir kilit (lock) mekanizması ile aynı dosyaya birden fazla
|
||||
yazma girişimini sıraya sokar.
|
||||
|
||||
Kullanım Örnekleri
|
||||
------------------
|
||||
- EdmConfig:
|
||||
cfg = EdmConfig("/home/karatay/ebuild/config.ini")
|
||||
port = cfg.item("serial", "ttyUSB0", "/dev/ttyUSB0")
|
||||
|
||||
- ConfigDict:
|
||||
section = cfg.get_section("serial")
|
||||
tty0 = section.get_item("ttyUSB0")
|
||||
|
||||
- KilitliDosya:
|
||||
log = KilitliDosya("/var/log/ebuild.log")
|
||||
log.yaz("merhaba dünya\\n")
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from configparser import ConfigParser
|
||||
import traceback as tb # eski davranışla uyum için bırakıldı
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ConfigDict – tek bir section'ın sözlük hali
|
||||
# ---------------------------------------------------------------------------
|
||||
class ConfigDict:
|
||||
"""
|
||||
Bir INI dosyasındaki tek bir section'ı sözlük benzeri interface ile
|
||||
kullanmaya yarar.
|
||||
|
||||
- Anahtar yoksa, otomatik olarak section içine eklenir ve boş string
|
||||
ile başlatılır.
|
||||
- Değerler her zaman string olarak saklanır.
|
||||
"""
|
||||
|
||||
def __init__(self, cfgfile: str, section_name: str):
|
||||
self.cfgfile = cfgfile
|
||||
self.section_name = section_name
|
||||
|
||||
self.cfg = ConfigParser()
|
||||
self.cfg.read(cfgfile)
|
||||
|
||||
if not self.cfg.has_section(section_name):
|
||||
self.cfg.add_section(section_name)
|
||||
self._write()
|
||||
|
||||
self.section = dict(self.cfg.items(section_name))
|
||||
|
||||
def _write(self) -> None:
|
||||
"""INI dosyasını diske yazar."""
|
||||
with open(self.cfgfile, "w") as f:
|
||||
self.cfg.write(f)
|
||||
|
||||
def get_item(self, item_name: str) -> str:
|
||||
"""
|
||||
Section içindeki bir item’ı döndürür.
|
||||
Yoksa item'i oluşturur, boş string ile başlatır.
|
||||
"""
|
||||
try:
|
||||
return self.section[item_name]
|
||||
except KeyError:
|
||||
# Yoksa ekle
|
||||
self.cfg.set(self.section_name, item_name, "")
|
||||
self._write()
|
||||
|
||||
# Yeniden oku
|
||||
self.cfg.read(self.cfgfile)
|
||||
self.section = dict(self.cfg.items(self.section_name))
|
||||
return self.section.get(item_name, "")
|
||||
|
||||
def set_item(self, item_name: str, value) -> None:
|
||||
"""
|
||||
Section içindeki bir item’ın değerini günceller (string'e çevirerek).
|
||||
"""
|
||||
value = str(value)
|
||||
self.cfg.set(self.section_name, item_name, value)
|
||||
self._write()
|
||||
self.section[item_name] = value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EdmConfig – bir INI dosyasını yöneten üst sınıf
|
||||
# ---------------------------------------------------------------------------
|
||||
class EdmConfig:
|
||||
"""
|
||||
Tek bir INI dosyası için üst seviye sarmalayıcı.
|
||||
|
||||
Özellikler:
|
||||
- Dosya yoksa otomatik oluşturur.
|
||||
- item(section, name, default) ile değer okur; yoksa default yazar.
|
||||
- reload() ile mtime kontrolü yaparak dosya değişimini algılar.
|
||||
"""
|
||||
|
||||
def __init__(self, cfg_file_name: str):
|
||||
self.fname = cfg_file_name
|
||||
|
||||
# Dosya yoksa oluştur
|
||||
if not os.path.isfile(cfg_file_name):
|
||||
with open(cfg_file_name, "w") as f:
|
||||
f.write("")
|
||||
|
||||
self.cfg = ConfigParser()
|
||||
self.cfg.read(cfg_file_name)
|
||||
|
||||
# İlk yükleme zamanı
|
||||
try:
|
||||
self.originalTime = os.path.getmtime(cfg_file_name)
|
||||
except OSError:
|
||||
self.originalTime = None
|
||||
|
||||
# ---------------------------
|
||||
# Reload mekanizması
|
||||
# ---------------------------
|
||||
def get_loadtime(self) -> float:
|
||||
"""Ini dosyasının son yüklenme zamanını (mtime) döndürür."""
|
||||
return self.originalTime
|
||||
|
||||
def reload(self) -> bool:
|
||||
"""
|
||||
Dosya değişmişse yeniden okur, True döner.
|
||||
Değişmemişse False döner.
|
||||
"""
|
||||
if self.fname is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
current = os.path.getmtime(self.fname)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
if self.originalTime is None or current > self.originalTime:
|
||||
self.cfg.read(self.fname)
|
||||
self.originalTime = current
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ---------------------------
|
||||
# Section & item işlemleri
|
||||
# ---------------------------
|
||||
def add_section(self, section: str) -> None:
|
||||
"""Yeni bir section ekler (yoksa)."""
|
||||
if not self.cfg.has_section(section):
|
||||
self.cfg.add_section(section)
|
||||
self._write()
|
||||
|
||||
def add_item(self, section: str, item: str, value: str = "") -> None:
|
||||
"""
|
||||
İlgili section yoksa oluşturur, item yoksa ekler ve default
|
||||
değerini yazar.
|
||||
"""
|
||||
if not self.cfg.has_section(section):
|
||||
self.add_section(section)
|
||||
|
||||
if not self.cfg.has_option(section, item):
|
||||
self.cfg.set(section, item, str(value))
|
||||
self._write()
|
||||
|
||||
def set_item(self, section: str, name: str, value="") -> None:
|
||||
"""
|
||||
Belirli bir section/key için değeri ayarlar (string'e çevirir).
|
||||
"""
|
||||
self.add_section(section)
|
||||
self.cfg.set(section, name, str(value))
|
||||
self._write()
|
||||
|
||||
def item(self, section: str, name: str, default="") -> str:
|
||||
"""
|
||||
INI'den item okur; yoksa veya boşsa default değeri yazar ve onu döndürür.
|
||||
|
||||
eski davranışla uyumlu: her zaman string döner.
|
||||
"""
|
||||
try:
|
||||
val = self.cfg.get(section, name).strip()
|
||||
if val == "":
|
||||
self.set_item(section, name, default)
|
||||
return str(default)
|
||||
return val
|
||||
except Exception:
|
||||
# Eski koddaki gibi stack trace istersen:
|
||||
# print(tb.format_exc())
|
||||
self.add_item(section, name, default)
|
||||
return str(default)
|
||||
|
||||
def get_items(self, section: str) -> dict:
|
||||
"""Verilen section'daki tüm key/value çiftlerini dict olarak döndürür."""
|
||||
return dict(self.cfg.items(section))
|
||||
|
||||
def get_section(self, section_name: str) -> ConfigDict:
|
||||
"""
|
||||
Verilen section için ConfigDict nesnesi döndürür.
|
||||
Section yoksa oluşturur.
|
||||
"""
|
||||
if not self.cfg.has_section(section_name):
|
||||
self.cfg.add_section(section_name)
|
||||
self._write()
|
||||
return ConfigDict(self.fname, section_name)
|
||||
|
||||
def get_section_names(self):
|
||||
"""INI içindeki tüm section isimlerini döndürür."""
|
||||
return self.cfg.sections()
|
||||
|
||||
def get_key_names(self, section_name: str = ""):
|
||||
"""
|
||||
section_name verilirse o section altındaki key listesi,
|
||||
verilmezse her section için key listelerini döndürür.
|
||||
"""
|
||||
if section_name:
|
||||
if not self.cfg.has_section(section_name):
|
||||
return []
|
||||
return list(self.cfg[section_name].keys())
|
||||
|
||||
return {s: list(self.cfg[s].keys()) for s in self.cfg.sections()}
|
||||
|
||||
def get_section_values(self, section_name: str = ""):
|
||||
"""
|
||||
section_name verilirse o section'ın dict halini,
|
||||
verilmezse tüm section'ların dict halini döndürür.
|
||||
"""
|
||||
if section_name:
|
||||
if not self.cfg.has_section(section_name):
|
||||
return {}
|
||||
return dict(self.cfg.items(section_name))
|
||||
return {s: dict(self.cfg.items(s)) for s in self.cfg.sections()}
|
||||
|
||||
def _write(self) -> None:
|
||||
"""Ini dosyasını diske yazar."""
|
||||
with open(self.fname, "w") as f:
|
||||
self.cfg.write(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KilitliDosya – çok basit file lock mekanizması
|
||||
# ---------------------------------------------------------------------------
|
||||
class KilitliDosya:
|
||||
"""
|
||||
Çok basit bir dosya kilit mekanizması.
|
||||
|
||||
Aynı dosyaya birden fazla sürecin/yazmanın çakışmasını azaltmak için
|
||||
`fname.LCK` dosyasını lock olarak kullanır.
|
||||
"""
|
||||
|
||||
def __init__(self, fname: str):
|
||||
self.fname = fname
|
||||
|
||||
def _lockfile(self) -> str:
|
||||
return f"{self.fname}.LCK"
|
||||
|
||||
def kontrol(self) -> bool:
|
||||
"""Lock dosyası var mı? True/False."""
|
||||
return os.path.exists(self._lockfile())
|
||||
|
||||
def kilitle(self) -> bool:
|
||||
"""
|
||||
Lock almaya çalışır. Lock yoksa oluşturur ve True döner.
|
||||
Varsa False döner.
|
||||
"""
|
||||
if not self.kontrol():
|
||||
with open(self._lockfile(), "w") as f:
|
||||
f.write(" ")
|
||||
return True
|
||||
return False
|
||||
|
||||
def kilit_ac(self) -> None:
|
||||
"""Lock dosyasını kaldırır (yoksa sessizce geçer)."""
|
||||
try:
|
||||
os.remove(self._lockfile())
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def oku(self):
|
||||
"""Ana dosyayı satır satır okur ve liste döndürür."""
|
||||
with open(self.fname, "r") as f:
|
||||
return f.readlines()
|
||||
|
||||
def yaz(self, text: str) -> bool:
|
||||
"""
|
||||
Dosyaya kilitleyerek ekleme yapar.
|
||||
Lock alamazsa 10 deneme yapar, her seferinde 0.2s bekler.
|
||||
"""
|
||||
for _ in range(10):
|
||||
if self.kilitle():
|
||||
try:
|
||||
with open(self.fname, "a") as f:
|
||||
f.write(text)
|
||||
finally:
|
||||
self.kilit_ac()
|
||||
return True
|
||||
time.sleep(0.2)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modül test / örnek kullanım
|
||||
# ---------------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
# Basit smoke-test
|
||||
test_ini = "test_config.ini"
|
||||
cfg = EdmConfig(test_ini)
|
||||
|
||||
# Birkaç değer dene
|
||||
port = cfg.item("serial", "ttyUSB0", "/dev/ttyUSB0")
|
||||
mode = cfg.item("general", "mode", "auto")
|
||||
print("serial.ttyUSB0 =", port)
|
||||
print("general.mode =", mode)
|
||||
|
||||
# KilitliDosya testi
|
||||
log = KilitliDosya("test_log.txt")
|
||||
log.yaz("Config_ini self test OK\n")
|
||||
338
ebuild/io/dbtext.py
Normal file
338
ebuild/io/dbtext.py
Normal file
@@ -0,0 +1,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__title__ = "dbtext"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "Sensör ve röle olaylarını metin tabanlı SQL log olarak saklayan yardımcı sınıf"
|
||||
__version__ = "0.2.0"
|
||||
__date__ = "2025-11-20"
|
||||
|
||||
"""
|
||||
ebuild/io/dbtext.py
|
||||
|
||||
Revision : 2025-11-20
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
Her sensör ve rölenin:
|
||||
- Ne zaman açıldığı / kapandığı
|
||||
- Hangi değeri ürettiği
|
||||
- Hangi kaynaktan geldiği
|
||||
|
||||
bilgisini tarih-saat bazlı olarak düz bir metin dosyasında tutmak.
|
||||
|
||||
Kayıt formatı (satır başına bir olay):
|
||||
INSERT INTO <table> (ts, app, source, event_type, value, unit, extra)
|
||||
VALUES ('YYYY-MM-DD HH:MM:SS', 'APP', 'SOURCE', 'EVENT', VALUE, 'UNIT', 'EXTRA');
|
||||
|
||||
Örnek:
|
||||
INSERT INTO ebrulor_log (ts, app, source, event_type, value, unit, extra)
|
||||
VALUES ('2025-11-20 12:34:56', 'ESYSTEM', 'relay:circulation_a', 'state', 1, 'bool', 'on');
|
||||
|
||||
Böylece:
|
||||
- Dosya istenirse direkt PostgreSQL'e pipe edilip çalıştırılabilir
|
||||
- Aynı zamanda bu modül basit bir parser ile geri okunabilir
|
||||
"""
|
||||
|
||||
#from __future__ import annotations
|
||||
|
||||
import os
|
||||
import datetime as _dt
|
||||
import re
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from .config_ini import KilitliDosya
|
||||
|
||||
|
||||
class DBText:
|
||||
""" Metin tabanlı SQL log dosyası için yardımcı sınıf.
|
||||
|
||||
Parametreler
|
||||
-----------
|
||||
filename : str
|
||||
Log dosyasının yolu (örnek: "ebina_log.sql").
|
||||
table : str
|
||||
SQL INSERT komutlarında kullanılacak tablo adı.
|
||||
app : str
|
||||
Uygulama adı (örn. "ESYSTEM").
|
||||
use_lock : bool
|
||||
True ise yazarken KilitliDosya kullanılır (çoklu süreç için daha güvenli).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename: str,
|
||||
table: str = "ebrulor_log",
|
||||
app: str = "EBUILD",
|
||||
use_lock: bool = True,
|
||||
) -> None:
|
||||
self.filename = filename
|
||||
self.table = table
|
||||
self.app = app
|
||||
self.use_lock = use_lock
|
||||
|
||||
# Dosya yoksa basit bir header ile oluştur
|
||||
if not os.path.isfile(self.filename):
|
||||
with open(self.filename, "w", encoding="utf-8") as f:
|
||||
f.write(f"-- DBText log file for table {self.table}\n")
|
||||
f.write(f"-- created at {_dt.datetime.now().isoformat()}\n\n")
|
||||
|
||||
self._locker = KilitliDosya(self.filename) if use_lock else None
|
||||
|
||||
# SQL satırlarını parse etmek için basit regex
|
||||
self._re_values = re.compile(
|
||||
r"VALUES \('(?P<ts>[^']*)',\s*'(?P<app>[^']*)',\s*'(?P<source>[^']*)',\s*"
|
||||
r"'(?P<etype>[^']*)',\s*(?P<value>NULL|[-0-9.]+),\s*(?P<unit>NULL|'[^']*'),\s*"
|
||||
r"'(?P<extra>[^']*)'\);"
|
||||
)
|
||||
|
||||
# -------------------------------------------------
|
||||
# Yardımcılar
|
||||
# -------------------------------------------------
|
||||
@staticmethod
|
||||
def _escape(value: str) -> str:
|
||||
"""SQL için tek tırnak kaçışı yapar."""
|
||||
return value.replace("'", "''")
|
||||
|
||||
def _write_line(self, line: str) -> None:
|
||||
""" Tek bir satırı log dosyasına yazar.
|
||||
|
||||
use_lock=True ise KilitliDosya üzerinden, değilse doğrudan append.
|
||||
"""
|
||||
if self.use_lock and self._locker is not None:
|
||||
self._locker.yaz(line + "\n")
|
||||
else:
|
||||
with open(self.filename, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
# -------------------------------------------------
|
||||
# Genel amaçlı event yazma
|
||||
# -------------------------------------------------
|
||||
def insert_event(
|
||||
self,
|
||||
source: str,
|
||||
event_type: str,
|
||||
value: Optional[float] = None,
|
||||
unit: Optional[str] = None,
|
||||
timestamp: Optional[_dt.datetime] = None,
|
||||
extra: str = "",
|
||||
) -> None:
|
||||
""" Genel amaçlı bir olay kaydı ekler.
|
||||
|
||||
Örnek kullanım:
|
||||
logger.insert_event(
|
||||
source="Sensor:28-00000660e983",
|
||||
event_type="temperature",
|
||||
value=23.5,
|
||||
unit="°C",
|
||||
timestamp=datetime.now(),
|
||||
extra="Daire 2 Kat 1 Yön 5",
|
||||
)
|
||||
"""
|
||||
ts = timestamp or _dt.datetime.now()
|
||||
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Değerler
|
||||
v_str = "NULL" if value is None else f"{float(value):.3f}"
|
||||
u_str = "NULL" if unit is None else f"'{self._escape(str(unit))}'"
|
||||
extra_str = self._escape(extra or "")
|
||||
|
||||
src_str = self._escape(source)
|
||||
etype_str = self._escape(event_type)
|
||||
app_str = self._escape(self.app)
|
||||
|
||||
line = (
|
||||
f"INSERT INTO {self.table} (ts, app, source, event_type, value, unit, extra) "
|
||||
f"VALUES ('{ts_str}', '{app_str}', '{src_str}', '{etype_str}', {v_str}, {u_str}, '{extra_str}');"
|
||||
)
|
||||
|
||||
self._write_line(line)
|
||||
|
||||
# -------------------------------------------------
|
||||
# Sensör / röle özel kısayol metotları
|
||||
# -------------------------------------------------
|
||||
def log_state_change(
|
||||
self,
|
||||
device_kind: str,
|
||||
name: str,
|
||||
is_on: bool,
|
||||
timestamp: Optional[_dt.datetime] = None,
|
||||
extra: str = "",
|
||||
) -> None:
|
||||
""" Röle / dijital çıkış / giriş gibi ON/OFF durumlarını loglar.
|
||||
|
||||
device_kind : "relay", "sensor", "pump" vb.
|
||||
name : cihaz ismi ("circulation_a", "burner_contactor" vb.)
|
||||
is_on : True → 1 (on), False → 0 (off)
|
||||
|
||||
Event:
|
||||
source = f"{device_kind}:{name}"
|
||||
event_type = "state"
|
||||
value = 1.0 / 0.0
|
||||
unit = "bool"
|
||||
"""
|
||||
source = f"{device_kind}:{name}"
|
||||
val = 1.0 if is_on else 0.0
|
||||
ex = extra or ("on" if is_on else "off")
|
||||
|
||||
self.insert_event(
|
||||
source=source,
|
||||
event_type="state",
|
||||
value=val,
|
||||
unit="bool",
|
||||
timestamp=timestamp,
|
||||
extra=ex,
|
||||
)
|
||||
|
||||
def log_sensor_value(
|
||||
self,
|
||||
name: str,
|
||||
value: float,
|
||||
unit: str = "",
|
||||
timestamp: Optional[_dt.datetime] = None,
|
||||
extra: str = "",
|
||||
) -> None:
|
||||
""" Analog / sayısal sensör değerlerini loglar.
|
||||
|
||||
Örnek:
|
||||
logger.log_sensor_value("outside_temp", 12.3, "°C")
|
||||
"""
|
||||
source = f"sensor:{name}"
|
||||
self.insert_event(
|
||||
source=source,
|
||||
event_type="measurement",
|
||||
value=value,
|
||||
unit=unit,
|
||||
timestamp=timestamp,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
# -------------------------------------------------
|
||||
# Okuma API'si
|
||||
# -------------------------------------------------
|
||||
def _parse_line(self, line: str) -> Optional[Dict[str, Any]]:
|
||||
""" Tek bir INSERT satırını dict'e çevirir.
|
||||
|
||||
Beklenen format:
|
||||
INSERT INTO <table> (...) VALUES ('ts', 'app', 'source', 'etype', value, unit, 'extra');
|
||||
"""
|
||||
line = line.strip()
|
||||
if not line or not line.upper().startswith("INSERT INTO"):
|
||||
return None
|
||||
|
||||
m = self._re_values.search(line)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
gd = m.groupdict()
|
||||
ts_str = gd.get("ts", "")
|
||||
try:
|
||||
ts = _dt.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
ts = None
|
||||
|
||||
# value
|
||||
raw_v = gd.get("value", "NULL")
|
||||
if raw_v == "NULL":
|
||||
value = None
|
||||
else:
|
||||
try:
|
||||
value = float(raw_v)
|
||||
except Exception:
|
||||
value = None
|
||||
|
||||
# unit
|
||||
raw_u = gd.get("unit", "NULL")
|
||||
if raw_u == "NULL":
|
||||
unit = None
|
||||
else:
|
||||
# 'C' şeklindeki stringten tek tırnakları atıyoruz
|
||||
unit = raw_u.strip("'")
|
||||
|
||||
return {
|
||||
"ts": ts,
|
||||
"app": gd.get("app", ""),
|
||||
"source": gd.get("source", ""),
|
||||
"event_type": gd.get("etype", ""),
|
||||
"value": value,
|
||||
"unit": unit,
|
||||
"extra": gd.get("extra", ""),
|
||||
}
|
||||
|
||||
def iter_events(
|
||||
self,
|
||||
source: Optional[str] = None,
|
||||
event_type: Optional[str] = None,
|
||||
since: Optional[_dt.datetime] = None,
|
||||
until: Optional[_dt.datetime] = None,
|
||||
):
|
||||
""" Log dosyasındaki olayları satır satır okur ve filtre uygular.
|
||||
|
||||
Parametreler:
|
||||
source : None veya tam eşleşen source string
|
||||
event_type : None veya tam eşleşen event_type
|
||||
since : None veya bu tarihten SONRAKİ kayıtlar
|
||||
until : None veya bu tarihten ÖNCEKİ kayıtlar
|
||||
|
||||
Yield:
|
||||
dict: {ts, app, source, event_type, value, unit, extra}
|
||||
"""
|
||||
if not os.path.isfile(self.filename):
|
||||
return
|
||||
|
||||
with open(self.filename, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
rec = self._parse_line(line)
|
||||
if not rec:
|
||||
continue
|
||||
|
||||
ts = rec["ts"]
|
||||
if since and ts and ts < since:
|
||||
continue
|
||||
if until and ts and ts > until:
|
||||
continue
|
||||
if source and rec["source"] != source:
|
||||
continue
|
||||
if event_type and rec["event_type"] != event_type:
|
||||
continue
|
||||
|
||||
yield rec
|
||||
|
||||
def get_state_history(
|
||||
self,
|
||||
device_kind: str,
|
||||
name: str,
|
||||
limit: int = 100,
|
||||
since: Optional[_dt.datetime] = None,
|
||||
until: Optional[_dt.datetime] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
""" Belirli bir cihazın (sensör / röle) son durum değişikliklerini döndürür.
|
||||
|
||||
device_kind : "relay", "sensor", "pump" vb.
|
||||
name : cihaz adı
|
||||
limit : maksimum kaç kayıt döneceği (en yeni kayıtlar)
|
||||
"""
|
||||
src = f"{device_kind}:{name}"
|
||||
events = list(self.iter_events(
|
||||
source=src,
|
||||
event_type="state",
|
||||
since=since,
|
||||
until=until,
|
||||
))
|
||||
|
||||
# En yeni kayıtlar sondadır; tersten limit al
|
||||
events.sort(key=lambda r: (r["ts"] or _dt.datetime.min), reverse=True)
|
||||
return events[:limit]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Basit self-test
|
||||
logger = DBText(filename="test_dbtext_log.sql", table="ebrulor_log", app="ESYSTEM")
|
||||
|
||||
now = _dt.datetime.now()
|
||||
logger.log_state_change("relay", "circulation_a", True, timestamp=now, extra="manual test on")
|
||||
logger.log_state_change("relay", "circulation_a", False, timestamp=now + _dt.timedelta(seconds=10), extra="manual test off")
|
||||
|
||||
print("Son durum değişiklikleri:")
|
||||
history = logger.get_state_history("relay", "circulation_a", limit=10)
|
||||
for h in history:
|
||||
print(h)
|
||||
158
ebuild/io/ds18b20.py
Normal file
158
ebuild/io/ds18b20.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "ds18b20"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "DS18B20 1-Wire sıcaklık sensörü sürücüsü"
|
||||
__version__ = "0.1.0"
|
||||
__date__ = "2025-11-21"
|
||||
|
||||
"""
|
||||
ebuild/io/ds18b20.py
|
||||
|
||||
Revision : 2025-11-21
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- DS18B20 sensörlerini 1-Wire üzerinden /sys/bus/w1/devices yolundan okuyarak
|
||||
sıcaklık (°C) bilgisi sağlamak.
|
||||
- Tek sensör için DS18B20Sensor sınıfı,
|
||||
- Otomatik cihaz keşfi için DS18B20Bus yardımcı sınıfı sunar.
|
||||
|
||||
Notlar
|
||||
------
|
||||
- 1-Wire kernel modüllerinin (w1_gpio, w1_therm) yüklü olması gerekir.
|
||||
- Bu sürücü yalnızca dosya sisteminden okuma yapar; filtreleme, smoothing,
|
||||
bina/daire eşlemesi gibi işlemler üst katmanlarda yapılır.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class DS18B20Sensor:
|
||||
"""
|
||||
Tek bir DS18B20 sensörünü temsil eder.
|
||||
|
||||
Özellikler:
|
||||
-----------
|
||||
- serial : 1-Wire cihaz id'si (örn: "28-00000660e983")
|
||||
- base_path : /sys/bus/w1/devices (varsayılan)
|
||||
- read_temperature() : son sıcaklığı °C cinsinden döndürür
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
serial: str,
|
||||
base_path: str = "/sys/bus/w1/devices",
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
self.serial = serial
|
||||
self.base_path = base_path
|
||||
self.device_path = os.path.join(base_path, serial, "w1_slave")
|
||||
self.name = name or serial
|
||||
|
||||
self.is_connected: bool = True
|
||||
self.error_count: int = 0
|
||||
|
||||
self.last_temperature: Optional[float] = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def read_temperature(self) -> Optional[float]:
|
||||
"""
|
||||
Sensörden anlık sıcaklık okur (°C).
|
||||
|
||||
Dönüş:
|
||||
- Başarı: float (örn: 23.437)
|
||||
- Hata: None (error_count artar, is_connected False olur)
|
||||
"""
|
||||
try:
|
||||
with open(self.device_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
if not lines:
|
||||
raise IOError("w1_slave boş okundu")
|
||||
|
||||
# İlk satır CRC ve 'YES/NO' bilgisini içerir.
|
||||
if not lines[0].strip().endswith("YES"):
|
||||
raise IOError("CRC hatalı veya sensör doğrulanamadı")
|
||||
|
||||
# İkinci satırda 't=xxxxx' ifadesini arıyoruz.
|
||||
pos = lines[1].find("t=")
|
||||
if pos == -1:
|
||||
raise ValueError("t= alanı bulunamadı")
|
||||
|
||||
raw = lines[1][pos + 2 :].strip()
|
||||
t_c = float(raw) / 1000.0
|
||||
|
||||
self.is_connected = True
|
||||
self.last_temperature = t_c
|
||||
return t_c
|
||||
|
||||
except Exception:
|
||||
self.error_count += 1
|
||||
self.is_connected = False
|
||||
self.last_temperature = None
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def exists(self) -> bool:
|
||||
"""
|
||||
Cihaz dosyasının mevcut olup olmadığını kontrol eder.
|
||||
"""
|
||||
return os.path.exists(self.device_path)
|
||||
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Sensör hakkında kısa bir özet döndürür.
|
||||
"""
|
||||
status = "OK" if self.is_connected else "ERR"
|
||||
return f"DS18B20Sensor(name={self.name}, serial={self.serial}, status={status}, errors={self.error_count})"
|
||||
|
||||
|
||||
class DS18B20Bus:
|
||||
"""
|
||||
Bir 1-Wire hattı üzerindeki DS18B20 cihazlarını keşfetmek için yardımcı sınıf.
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "/sys/bus/w1/devices") -> None:
|
||||
self.base_path = base_path
|
||||
|
||||
def discover(self) -> List[str]:
|
||||
"""
|
||||
Sistemdeki tüm DS18B20 cihazlarının seri numaralarını listeler.
|
||||
Örnek:
|
||||
["28-00000660e983", "28-0000066144f9", ...]
|
||||
"""
|
||||
pattern = os.path.join(self.base_path, "28-*")
|
||||
devices = glob.glob(pattern)
|
||||
serials = [os.path.basename(p) for p in devices]
|
||||
return serials
|
||||
|
||||
def get_sensors(self) -> Dict[str, DS18B20Sensor]:
|
||||
"""
|
||||
Otomatik keşif yaparak her seri için bir DS18B20Sensor nesnesi döndürür.
|
||||
"""
|
||||
result: Dict[str, DS18B20Sensor] = {}
|
||||
for serial in self.discover():
|
||||
result[serial] = DS18B20Sensor(serial=serial, base_path=self.base_path)
|
||||
return result
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Basit test
|
||||
# ----------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
bus = DS18B20Bus()
|
||||
serials = bus.discover()
|
||||
print("Bulunan DS18B20 cihazları:")
|
||||
for s in serials:
|
||||
print(" -", s)
|
||||
|
||||
sensors = bus.get_sensors()
|
||||
for serial, sensor in sensors.items():
|
||||
t = sensor.read_temperature()
|
||||
print(f"{serial}: {t} °C ({sensor.summary()})")
|
||||
|
||||
454
ebuild/io/edm_db.py
Normal file
454
ebuild/io/edm_db.py
Normal file
@@ -0,0 +1,454 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__title__ = "edm_db"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "PostgreSQL'e EDM tarzı veri yazan yardımcı sınıf"
|
||||
__version__ = "0.2.0"
|
||||
__date__ = "2025-11-20"
|
||||
|
||||
"""
|
||||
edm_db.py
|
||||
|
||||
Revision : 2025-11-20
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
Eski Rasp2 tabanlı sistemin PostgreSQL'e veri yazma işlerini üstlenen
|
||||
yardımcı sınıfın (EdmDB) temizlenmiş, hatalardan arındırılmış ve
|
||||
okunabilirliği artırılmış sürümü.
|
||||
|
||||
Özellikler
|
||||
----------
|
||||
- Veritabanı bağlantı parametrelerini edmConfig.conf içinden okur
|
||||
(section: [database] varsayımıyla).
|
||||
- Bağlantıyı (opsiyonel olarak) açar; bağlantı yoksa fonksiyonlar
|
||||
sessizce False döndürebilir veya sadece log dosyasına SQL basabilir.
|
||||
- Eski koddaki ana fonksiyonlar korunmuştur:
|
||||
- db_exec()
|
||||
- avg_head()
|
||||
- db_write_861(), db_write_861_data(), db_write()
|
||||
- Diğer SELECT/UPDATE fonksiyonları (read_0861_order, write_0861_order, ...)
|
||||
|
||||
Not
|
||||
---
|
||||
Aşağıdaki kodda bazı yerlerde güvenlik açısından tavsiye edilen
|
||||
`parametrized query` kullanımı yerine eski string formatlama
|
||||
kullanılmıştır; bu modül legacy uyumluluk öncelikli olduğu için
|
||||
bu haliyle korunmuştur.
|
||||
"""
|
||||
|
||||
import psycopg2 as psql
|
||||
from datetime import datetime
|
||||
|
||||
import edmConfig # Senin eski EdmConfig modülün (conf içinde EdmConfig örneği bekliyoruz)
|
||||
|
||||
|
||||
class EdmDB:
|
||||
"""
|
||||
EDM veritabanı yardımcı sınıfı.
|
||||
|
||||
- Bağlantı parametrelerini edmConfig.conf üzerinden okur.
|
||||
Örn. config.ini içinde:
|
||||
|
||||
[database]
|
||||
tcpip = 10.10.2.44
|
||||
database = edm_10094
|
||||
user = root
|
||||
password = system
|
||||
port = 5432
|
||||
|
||||
- db_exec() ile self.sql içinde tutulan komutu çalıştırır.
|
||||
"""
|
||||
|
||||
def __init__(self, ini_name: str = "database", auto_connect: bool = False):
|
||||
"""
|
||||
ini_name: config.ini içindeki section ismi (varsayılan: [database])
|
||||
auto_connect: True verilirse __init__ sırasında PostgreSQL bağlantısı açmayı dener.
|
||||
"""
|
||||
self.conf = edmConfig.conf
|
||||
self.sql = ""
|
||||
|
||||
# Bağlantı parametrelerini INI'den okuyoruz
|
||||
self.w_ip = self.conf.item(ini_name, "tcpip") # host
|
||||
self.w_db = self.conf.item(ini_name, "database") # db name
|
||||
self.w_us = self.conf.item(ini_name, "user") # user
|
||||
self.w_pw = self.conf.item(ini_name, "password") # password
|
||||
self.w_pt = self.conf.item(ini_name, "port") # port (string)
|
||||
|
||||
self.con = None
|
||||
|
||||
if auto_connect:
|
||||
self.connect()
|
||||
|
||||
# -------------------------------------------------
|
||||
# Bağlantı yönetimi
|
||||
# -------------------------------------------------
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
PostgreSQL bağlantısını açar.
|
||||
Başarılıysa True, hata olursa False döner.
|
||||
"""
|
||||
try:
|
||||
self.con = psql.connect(
|
||||
host=self.w_ip,
|
||||
user=self.w_us,
|
||||
password=self.w_pw,
|
||||
database=self.w_db,
|
||||
port=int(self.w_pt),
|
||||
)
|
||||
self.con.autocommit = True
|
||||
# print("EdmDB: connection ok") # İstersen açarsın
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("EdmDB: connection error:", ex)
|
||||
self.con = None
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
"""Veritabanı bağlantısını kapatır."""
|
||||
if self.con is not None:
|
||||
try:
|
||||
self.con.close()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.con = None
|
||||
|
||||
# -------------------------------------------------
|
||||
# Temel SQL yürütme
|
||||
# -------------------------------------------------
|
||||
def db_exec(self) -> bool:
|
||||
"""
|
||||
self.sql değişkeninde tutulan komutu çalıştırır.
|
||||
|
||||
Bağlantı yoksa:
|
||||
- Şimdilik sadece True döndürüyoruz (test amaçlı).
|
||||
Bağlantı varsa:
|
||||
- execute + commit, hata varsa False döner.
|
||||
"""
|
||||
if not self.sql:
|
||||
return True
|
||||
|
||||
if self.con is None:
|
||||
# Bağlantı yok; legacy davranışa yakın olması için
|
||||
# burada True döndürüp sadece SQL'i debug amaçlı yazabilirsin.
|
||||
# print("EdmDB: no connection, sql skipped:", self.sql)
|
||||
return True
|
||||
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(self.sql)
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("EdmDB.db_exec ERROR:", ex)
|
||||
return False
|
||||
|
||||
# -------------------------------------------------
|
||||
# Örnek veri okuma fonksiyonu
|
||||
# -------------------------------------------------
|
||||
def avg_head(self):
|
||||
"""
|
||||
AVG_HEAT_OUTSIDE tablosundan örnek bir kayıt okur.
|
||||
|
||||
Dönüş:
|
||||
[avg, max, min, saat] şeklinde liste
|
||||
Eğer okuma yapılamazsa:
|
||||
[-9990.0, -9999.0, -9999.0, -99]
|
||||
"""
|
||||
avg_heat = [-9990.0, -9999.0, -9999.0, -99]
|
||||
|
||||
if self.con is None:
|
||||
return avg_heat
|
||||
|
||||
try:
|
||||
sql = "SELECT avgr, maxr, minr, saatr FROM AVG_HEAT_OUTSIDE WHERE saatr = 2;"
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
row = cr.fetchone()
|
||||
if row:
|
||||
avg_heat[0] = row[0]
|
||||
avg_heat[1] = row[1]
|
||||
avg_heat[2] = row[2]
|
||||
avg_heat[3] = row[3]
|
||||
except Exception as ex:
|
||||
print("EdmDB.avg_head ERROR:", ex)
|
||||
|
||||
return avg_heat
|
||||
|
||||
# -------------------------------------------------
|
||||
# Eski sistem fonksiyonları (istatistik / görev takibi)
|
||||
# -------------------------------------------------
|
||||
def old_datas(self):
|
||||
"""
|
||||
edm_0861_data_brulor_percent tablosundan eski verileri okur.
|
||||
"""
|
||||
if self.con is None:
|
||||
return []
|
||||
|
||||
sql = "SELECT endusuk, enfazla, toplam_harcama, toplam_sure, oran FROM public.edm_0861_data_brulor_percent"
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
return cr.fetchall()
|
||||
except Exception as ex:
|
||||
print("EdmDB.old_datas ERROR:", ex)
|
||||
return []
|
||||
|
||||
def old_values(self):
|
||||
"""
|
||||
edm_0861_data_start_stop_brulor tablosundan, bugüne ait bazı
|
||||
start/stop verilerini okur.
|
||||
"""
|
||||
if self.con is None:
|
||||
return []
|
||||
|
||||
sql = (
|
||||
"SELECT createdate, prev_createdate, elpsetime "
|
||||
"FROM edm_0861_data_start_stop_brulor "
|
||||
"WHERE createdate > current_date "
|
||||
"AND sensor_value = 1 "
|
||||
"ORDER BY 1"
|
||||
)
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
return cr.fetchall()
|
||||
except Exception as ex:
|
||||
print("EdmDB.old_values ERROR:", ex)
|
||||
return []
|
||||
|
||||
def read_0861_order(self, xfunc_group="0", xfunc_sub_item="0"):
|
||||
"""
|
||||
edm_0861_orders tablosundan çalışmaya hazır (exec_status=0) kayıtları okur.
|
||||
"""
|
||||
if self.con is None:
|
||||
return []
|
||||
|
||||
sql = (
|
||||
"SELECT exec_status, uniqueid, func_group, func_sub_item, roleid, "
|
||||
" work_minute, param_count, startdate, stopdate, "
|
||||
" (work_minute * 4) - 0 = param_count as mstatus "
|
||||
"FROM public.edm_0861_orders "
|
||||
"WHERE exec_status = 0 "
|
||||
" AND licenseid = 10094 "
|
||||
" AND activeid = true "
|
||||
" AND func_group = '%s' "
|
||||
" AND current_timestamp < stopdate "
|
||||
" AND startdate < current_timestamp "
|
||||
" AND func_sub_item = '%s' "
|
||||
"ORDER BY startdate;"
|
||||
) % (xfunc_group, xfunc_sub_item)
|
||||
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
return cr.fetchall()
|
||||
except Exception as ex:
|
||||
print("EdmDB.read_0861_order ERROR:", ex)
|
||||
return []
|
||||
|
||||
def write_0861_order(self, uid):
|
||||
"""
|
||||
edm_0861_orders tablosunda param_count değerini 1 artırır.
|
||||
"""
|
||||
if self.con is None:
|
||||
return
|
||||
|
||||
sql = (
|
||||
"UPDATE public.edm_0861_orders "
|
||||
"SET param_count = param_count + 1 "
|
||||
"WHERE exec_status = 0 "
|
||||
" AND licenseid = 10094 "
|
||||
" AND activeid = true "
|
||||
" AND uniqueid = '%s' "
|
||||
" AND param_count < (work_minute * 4) "
|
||||
" AND current_timestamp < stopdate;"
|
||||
) % uid
|
||||
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
except Exception as ex:
|
||||
print("EdmDB.write_0861_order ERROR:", ex)
|
||||
|
||||
def close_0861_order(self, uid):
|
||||
"""
|
||||
Belirli bir order'ı exec_status=5 yaparak kapatır.
|
||||
"""
|
||||
if self.con is None:
|
||||
return
|
||||
|
||||
sql = (
|
||||
"UPDATE public.edm_0861_orders "
|
||||
"SET exec_status = 5, stopdate = current_timestamp "
|
||||
"WHERE exec_status = 0 "
|
||||
" AND licenseid = 10094 "
|
||||
" AND activeid = true "
|
||||
" AND uniqueid = '%s';"
|
||||
) % uid
|
||||
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
except Exception as ex:
|
||||
print("EdmDB.close_0861_order ERROR:", ex)
|
||||
|
||||
def update_0861_order(self, uid):
|
||||
"""
|
||||
Verilen uniqueid için startdate/stopdate'i günceller ve
|
||||
yeni startdate'i döndürür.
|
||||
"""
|
||||
if self.con is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
sql = (
|
||||
"UPDATE public.edm_0861_orders "
|
||||
"SET startdate = current_timestamp, "
|
||||
" stopdate = current_timestamp + INTERVAL '2 day' "
|
||||
"WHERE exec_status = 0 AND uniqueid = %d"
|
||||
) % int(uid)
|
||||
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
|
||||
sql = (
|
||||
"SELECT startdate as mstatus "
|
||||
"FROM public.edm_0861_orders "
|
||||
"WHERE exec_status = 0 AND uniqueid = %d"
|
||||
) % int(uid)
|
||||
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
rows = cr.fetchall()
|
||||
|
||||
for row in rows:
|
||||
return row[0]
|
||||
except Exception as ex:
|
||||
print("EdmDB.update_0861_order ERROR:", ex)
|
||||
|
||||
return None
|
||||
|
||||
# -------------------------------------------------
|
||||
# 0861 / 0861_data yazma fonksiyonları
|
||||
# -------------------------------------------------
|
||||
def db_write_861(self, licenseid, siteid, locationid, device_group, device_code, device_value):
|
||||
"""
|
||||
edm_0861 tablosuna temel bir kayıt ekler.
|
||||
|
||||
NOT: device_value burada sadece varlık için kullanılıyor;
|
||||
asıl anlık değerler 0861_data tablosuna yazılıyor.
|
||||
"""
|
||||
self.sql = (
|
||||
"INSERT INTO public.edm_0861("
|
||||
"licenseid, siteid, locationid, hardware_type, "
|
||||
"hardware_model_code, hardwareuniquecode, "
|
||||
"hardwarejobcode, hardwarecomment, jobcode"
|
||||
") VALUES ('%s','%s','%s','%s','%s','%s','%s','%s','%s')"
|
||||
) % (
|
||||
licenseid,
|
||||
siteid,
|
||||
locationid,
|
||||
"D", # hardware_type
|
||||
device_group, # hardware_model_code
|
||||
device_code, # hardwareuniquecode
|
||||
device_code, # hardwarejobcode
|
||||
device_code, # hardwarecomment
|
||||
device_code, # jobcode
|
||||
)
|
||||
|
||||
if self.db_exec():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_edm_0861(self, licenseid, siteid, locationid, device_code):
|
||||
"""
|
||||
İlgili cihaz için aktif edm_0861 kaydının uniqueid'sini döndürür.
|
||||
Bağlantı yoksa veya kayıt bulunamazsa 0 döner.
|
||||
|
||||
NOT: Eski koddaki "yoksa oluştur sonra tekrar ara" davranışı
|
||||
burada yorum satırı olarak bırakıldı; istersen geri açarsın.
|
||||
"""
|
||||
if self.con is None:
|
||||
return 0
|
||||
|
||||
sql = (
|
||||
"SELECT uniqueid "
|
||||
"FROM public.edm_0861 "
|
||||
"WHERE licenseid = '%s' "
|
||||
" AND siteid = '%s' "
|
||||
" AND locationid = '%s' "
|
||||
" AND NOW() BETWEEN startdate AND stopdate "
|
||||
" AND activeid = True "
|
||||
" AND deleteid = False "
|
||||
" AND hardwarejobcode = '%s'"
|
||||
) % (licenseid, siteid, locationid, device_code)
|
||||
|
||||
try:
|
||||
with self.con.cursor() as cr:
|
||||
cr.execute(sql)
|
||||
rows = cr.fetchall()
|
||||
for row in rows:
|
||||
return row[0]
|
||||
except Exception as ex:
|
||||
print("EdmDB.get_edm_0861 ERROR:", ex)
|
||||
|
||||
# Eski davranış: kayıt yoksa oluşturmayı denerdi.
|
||||
# İstersen buraya geri koyabilirsin.
|
||||
return 0
|
||||
|
||||
def db_write_861_data(self, licenseid, siteid, locationid, device_group, device_code, device_value):
|
||||
"""
|
||||
edm_0861_data tablosuna cihaz verisi (sensor_value) yazar.
|
||||
Bağlantı yoksa SQL'i LOG_device_group.log dosyasına basar.
|
||||
"""
|
||||
xdevice_code = "%s" % (device_code)
|
||||
device_str = ""
|
||||
|
||||
# Değer tipini normalize et
|
||||
if isinstance(device_value, (float, int)):
|
||||
numeric_value = float(device_value)
|
||||
else:
|
||||
device_str = str(device_value)
|
||||
numeric_value = 0.0
|
||||
|
||||
self.sql = (
|
||||
"INSERT INTO public.edm_0861_data("
|
||||
"licenseid, uniqueid, sensor_value, init_value"
|
||||
") VALUES ('%s','%s','%f','%s')"
|
||||
) % (licenseid, xdevice_code, numeric_value, 0)
|
||||
|
||||
# LOG_DEVICEGROUP.log dosyasına da yaz
|
||||
fname = "LOG_%s.log" % (device_group)
|
||||
fsql = "%s:%s\n" % (datetime.now(), self.sql)
|
||||
try:
|
||||
with open(fname, "a") as file_object:
|
||||
file_object.write(fsql)
|
||||
except Exception as ex:
|
||||
print("EdmDB.db_write_861_data LOG ERROR:", ex)
|
||||
|
||||
if self.db_exec():
|
||||
return True
|
||||
|
||||
# DB yazılamadıysa, fallback olarak edm_0861 kaydı oluşturmaya çalış
|
||||
return self.db_write_861(licenseid, siteid, locationid, device_group, device_code, device_value)
|
||||
|
||||
def db_write(self, licenseid, siteid, locationid, device_group, device_code, device_value):
|
||||
"""
|
||||
0861_data'ya yazmayı 3 kez dener.
|
||||
Hata alma durumunda db_write_861_data içindeki fallback devreye girer.
|
||||
"""
|
||||
result = False
|
||||
i = 0
|
||||
while not result and i < 3:
|
||||
i += 1
|
||||
result = self.db_write_861_data(
|
||||
licenseid, siteid, locationid,
|
||||
device_group, device_code, device_value
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Basit bir smoke-test
|
||||
db = EdmDB(auto_connect=False) # Bağlanmadan da oluşturulabilir
|
||||
print("EdmDB instance created. Host:", db.w_ip, "DB:", db.w_db)
|
||||
453
ebuild/io/legacy_syslog.py
Normal file
453
ebuild/io/legacy_syslog.py
Normal file
@@ -0,0 +1,453 @@
|
||||
# -*- 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 /brulor.py'nin syslog formatına yakın satırlar üretmek.
|
||||
- Python logging + SysLogHandler kullanarak:
|
||||
program adı: BRULOR
|
||||
mesaj : "[ 1 .... ]" formatında
|
||||
|
||||
Bu modül:
|
||||
- send_legacy_syslog(message) → tek satır yazar
|
||||
- emit_top_block(now, SeasonController) →
|
||||
1) versiyon satırı
|
||||
2) güneş (sunrise/sunset) + sistem/licence
|
||||
3) mevsim + bahar dönemi + tatil satırları
|
||||
- log_burner_header(...) → BurnerController.tick() için üst blok +
|
||||
sistem ısı + motor bilgilerini basar.
|
||||
"""
|
||||
|
||||
from datetime import datetime, time, timedelta, date
|
||||
from typing import Optional
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
try:
|
||||
# SeasonController ve konfig
|
||||
from ..core.season import SeasonController
|
||||
from .. import config_statics as cfg
|
||||
from .. import config_runtime as cfg_v
|
||||
except ImportError: # test / standalone
|
||||
SeasonController = None # type: ignore
|
||||
cfg = None # type: ignore
|
||||
cfg_v = None # type: ignore
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Logger kurulumu (Syslog + stdout)
|
||||
# ----------------------------------------------------------------------
|
||||
_LOGGER: Optional[logging.Logger] = None
|
||||
|
||||
|
||||
def _get_logger() -> logging.Logger:
|
||||
global _LOGGER
|
||||
if _LOGGER is not None:
|
||||
return _LOGGER
|
||||
|
||||
logger = logging.getLogger("BRULOR")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 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)
|
||||
|
||||
_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ı)
|
||||
"""
|
||||
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: versiyon + güneş + mevsim + tatil
|
||||
# ----------------------------------------------------------------------
|
||||
def emit_header_version(line_no: int, now: datetime) -> int:
|
||||
"""
|
||||
1. satır: Versiyon ve zaman bilgisi.
|
||||
|
||||
Örnek:
|
||||
[ 1 ************** 00.02.01 2025-11-22 22:20:19 *************]
|
||||
"""
|
||||
ver = _format_version_3part(__version__)
|
||||
ts = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
body = f"************** {ver} {ts} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def emit_header_sunrise_sunset(
|
||||
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 _normalize_iso_date(s: Optional[str]) -> str:
|
||||
"""
|
||||
SeasonInfo.season_start / season_end alanlarını sadeleştirir.
|
||||
|
||||
Ö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.
|
||||
"""
|
||||
|
||||
if season_ctrl is None:
|
||||
return line_no
|
||||
|
||||
info = getattr(season_ctrl, "info", None)
|
||||
if info is None:
|
||||
return line_no
|
||||
|
||||
# dataclass benzeri objeden alanları çek
|
||||
season = getattr(info, "season", "Unknown")
|
||||
s_start = _normalize_iso_date(getattr(info, "season_start", ""))
|
||||
s_end = _normalize_iso_date(getattr(info, "season_end", ""))
|
||||
s_day = int(getattr(info, "season_day", 0) or 0)
|
||||
s_pass = int(getattr(info, "season_passed", 0) or 0)
|
||||
s_rem = int(getattr(info, "season_remaining", 0) or 0)
|
||||
|
||||
body = (
|
||||
f"season : {season} {s_start} - {s_end} "
|
||||
f"[{s_day} pass:{s_pass} kalan:{s_rem}]"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# Bahar / tasarruf dönemi bilgileri
|
||||
is_season = bool(getattr(info, "is_season", False))
|
||||
saving_start = getattr(info, "saving_start", None)
|
||||
saving_stop = getattr(info, "saving_stop", None)
|
||||
|
||||
if is_season and isinstance(saving_start, date) and isinstance(saving_stop, date):
|
||||
# Kullanıcı isteği: öncesi/sonrası 3'er gün göster
|
||||
show_start = saving_start - timedelta(days=3)
|
||||
show_stop = saving_stop + timedelta(days=3)
|
||||
body = (
|
||||
f"bahar : {show_start.isoformat()} - {show_stop.isoformat()}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# Eğer resmi tatil bilgisi de varsa opsiyonel satır
|
||||
is_holiday = bool(getattr(info, "is_holiday", False))
|
||||
holiday_label = getattr(info, "holiday_label", "")
|
||||
|
||||
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
|
||||
|
||||
# SeasonController.info'dan sunrise/sunset okumayı dene
|
||||
sunrise = None
|
||||
sunset = None
|
||||
if season_ctrl is not None:
|
||||
info = getattr(season_ctrl, "info", None)
|
||||
if info is not None:
|
||||
sunrise = getattr(info, "sunrise", None)
|
||||
sunset = getattr(info, "sunset", None)
|
||||
|
||||
line_no = emit_header_sunrise_sunset(
|
||||
line_no=line_no,
|
||||
sunrise=sunrise,
|
||||
sunset=sunset,
|
||||
system_on=bool(system_onoff),
|
||||
licence_id=licence_id,
|
||||
)
|
||||
|
||||
# Mevsim + bahar dönemi
|
||||
line_no = emit_header_season(line_no, season_ctrl)
|
||||
|
||||
# Sonraki satır: bina ısı / dış ısı / F-B detayları için kullanılacak
|
||||
return line_no
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# BurnerController entegrasyonu
|
||||
# ----------------------------------------------------------------------
|
||||
def _fmt_c(val: Optional[float]) -> str:
|
||||
"""Dereceyi 'None°C' veya '23.4°C' gibi tek tip formatlar."""
|
||||
if val is None:
|
||||
return "None°C"
|
||||
try:
|
||||
return f"{float(val):.2f}°C"
|
||||
except Exception:
|
||||
return "None°C"
|
||||
|
||||
|
||||
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,
|
||||
) -> None:
|
||||
"""BurnerController.tick() için tek entry-point.
|
||||
|
||||
Buradan:
|
||||
1) Versiyon + güneş + mevsim / bahar / tatil blokları
|
||||
2) Bina / sistem ısı bilgisi
|
||||
3) Brülör ve devirdaim motor satırları
|
||||
syslog'a basılır.
|
||||
"""
|
||||
# 1) Üst blok
|
||||
try:
|
||||
line_no = emit_top_block(now, season)
|
||||
except Exception as exc:
|
||||
# Üst blok patlasa bile alttakileri basalım ki log tamamen kaybolmasın
|
||||
send_legacy_syslog(format_line(1, f"emit_top_block error: {exc}"))
|
||||
line_no = 2
|
||||
|
||||
# 2) Bina/sistem ısı satırı
|
||||
try:
|
||||
# Çalışma modu: F (dış ısı) / B (bina ort)
|
||||
cfg_mode = getattr(cfg, "BUILD_BURNER", "F") if cfg is not None else "F"
|
||||
mode_cfg = str(cfg_mode).upper()
|
||||
|
||||
# Isı limiti (dış ısı limiti)
|
||||
limit = None
|
||||
if cfg_v is not None:
|
||||
try:
|
||||
limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 0.0))
|
||||
except Exception:
|
||||
limit = None
|
||||
|
||||
body = (
|
||||
f"Build [{mode}-{mode_cfg}] "
|
||||
f"Heats[Min:{_fmt_c(None)} Avg:{_fmt_c(building_avg)} Max:{_fmt_c(None)}]"
|
||||
)
|
||||
if limit is not None:
|
||||
# Son köşeli parantezi atmamak için ufak hack
|
||||
body = body[:-1] + f" L:{limit:.1f}]"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# Eski formata yakın "Sistem Isı" satırı
|
||||
w_boost = 0.0
|
||||
c_off = 0.0
|
||||
if cfg_v is not None:
|
||||
try:
|
||||
w_boost = float(getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c_off = float(getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
body = f"Sistem Isı : {_fmt_c(used_out_c)} [w:{int(w_boost)} c:{int(c_off)}]"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# 3) Brülör motor satırı
|
||||
br_state = "<CALISIYOR>" if burner_on else "<CALISMIYOR>"
|
||||
br_flag = 1 if burner_on else 0
|
||||
body = (
|
||||
f"Brulor Motor : {br_state} [{br_flag}] 0 "
|
||||
f"00:00:00 00:00:00 L:{fire_sp:.1f}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# 4) Devirdaim pompa satırı
|
||||
pumps_on = tuple(pumps_on or ())
|
||||
pump_state = "<CALISIYOR>" if pumps_on else "<CALISMIYOR>"
|
||||
pump_flag = 1 if pumps_on else 0
|
||||
pump_label = pumps_on[0] if pumps_on else "-"
|
||||
circ_limit = None
|
||||
if cfg_v is not None:
|
||||
try:
|
||||
circ_limit = float(getattr(cfg_v, "CIRCULATION_MIN_RETURN_C", 25.0))
|
||||
except Exception:
|
||||
circ_limit = None
|
||||
|
||||
if circ_limit is None:
|
||||
body = (
|
||||
f"Devirdaim Mot: {pump_state} [{pump_flag}] 0 "
|
||||
f"00:00:00 00:00:00 L:{pump_label}"
|
||||
)
|
||||
else:
|
||||
body = (
|
||||
f"Devirdaim Mot: {pump_state} [{pump_flag}] 0 "
|
||||
f"00:00:00 00:00:00 L:{pump_label} {circ_limit:.1f}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
except Exception as exc:
|
||||
# Her türlü hata durumunda sessiz kalmak yerine loga yaz
|
||||
send_legacy_syslog(format_line(line_no, f"log_burner_header error: {exc}"))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Ö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))
|
||||
376
ebuild/io/relay_driver.py
Normal file
376
ebuild/io/relay_driver.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "relay_driver"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "GPIO röle sürücüsü + brülör grup soyutlaması"
|
||||
__version__ = "0.4.0"
|
||||
__date__ = "2025-11-22"
|
||||
|
||||
"""
|
||||
ebuild/io/relay_driver.py
|
||||
|
||||
Revision : 2025-11-22
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- Raspberry Pi GPIO üzerinden röle sürmek için basit bir soyutlama.
|
||||
- Soyut kanal isimleri (ör: "igniter", "circulation_a") → BCM pin eşlemesi
|
||||
config_statics.RELAY_GPIO üzerinden gelir.
|
||||
- Brülör grupları için BURNER_GROUPS kullanılır:
|
||||
|
||||
BURNER_GROUPS = {
|
||||
0: {
|
||||
"name": "MainBurner",
|
||||
"location": "Sol binada",
|
||||
"igniter_pin": 16,
|
||||
"circulation": {
|
||||
"circ_1": {"channel": "circulation_a", "pin": 26, "default": 1},
|
||||
"circ_2": {"channel": "circulation_b", "pin": 24, "default": 0},
|
||||
},
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
Bu modül:
|
||||
- Tek tek kanal ON/OFF (set_channel)
|
||||
- Tüm kanalları kapatma (all_off)
|
||||
- Brülör → igniter kanalını ve pompalarını soyutlayan yardımcılar
|
||||
- Kanal bazlı basit istatistik (RelayStats) sağlar.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
try:
|
||||
from .. import config_statics as cfg
|
||||
except ImportError: # test / standalone
|
||||
cfg = None # type: ignore
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# GPIO soyutlama (RPi.GPIO yoksa dummy)
|
||||
# ----------------------------------------------------------------------
|
||||
try:
|
||||
import RPi.GPIO as GPIO # type: ignore
|
||||
|
||||
_HAS_GPIO = True
|
||||
except Exception: # Raspi dışı ortam
|
||||
GPIO = None # type: ignore
|
||||
_HAS_GPIO = False
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# İstatistik yapısı
|
||||
# ----------------------------------------------------------------------
|
||||
@dataclass
|
||||
class RelayStats:
|
||||
"""
|
||||
Tek bir röle kanalı için istatistikler.
|
||||
|
||||
- on_count : kaç defa ON'a çekildi
|
||||
- last_on_ts : en son ON'a çekildiği zaman (epoch saniye)
|
||||
- last_off_ts : en son OFF olduğu zaman (epoch saniye)
|
||||
- last_duration_s : en son ON periyodunun süresi (saniye)
|
||||
- total_on_s : bugüne kadar toplam ON kalma süresi (saniye)
|
||||
"""
|
||||
on_count: int = 0
|
||||
last_on_ts: Optional[float] = None
|
||||
last_off_ts: Optional[float] = None
|
||||
last_duration_s: float = 0.0
|
||||
total_on_s: float = 0.0
|
||||
|
||||
def on(self, now: float) -> None:
|
||||
"""
|
||||
Kanal ON'a çekildiğinde çağrılır.
|
||||
Aynı ON periyodu içinde tekrar çağrılırsa sayaç artmaz.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
self.last_on_ts = now
|
||||
self.on_count += 1
|
||||
|
||||
def off(self, now: float) -> None:
|
||||
"""
|
||||
Kanal OFF'a çekildiğinde çağrılır.
|
||||
Son ON zamanına göre süre hesaplanır, last_duration_s ve total_on_s güncellenir.
|
||||
"""
|
||||
if self.last_on_ts is not None:
|
||||
dur = max(0.0, now - self.last_on_ts)
|
||||
self.last_duration_s = dur
|
||||
self.total_on_s += dur
|
||||
self.last_on_ts = None
|
||||
self.last_off_ts = now
|
||||
|
||||
def current_duration(self, now: Optional[float] = None) -> float:
|
||||
"""
|
||||
Kanal şu anda ON ise, bu ON periyodunun şu ana kadarki süresini döndürür.
|
||||
OFF ise 0.0 döner.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
return 0.0
|
||||
if now is None:
|
||||
now = time.time()
|
||||
return max(0.0, now - self.last_on_ts)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Ana sürücü
|
||||
# ----------------------------------------------------------------------
|
||||
class RelayDriver:
|
||||
"""
|
||||
Basit bir röle sürücüsü.
|
||||
|
||||
- Soyut kanal isimleri: RELAY_GPIO dict'indeki anahtarlar
|
||||
- Brülör grup API'si:
|
||||
* burners() → mevcut brülör id listesi
|
||||
* burner_info(bid) → config_statics.BURNER_GROUPS[bid]
|
||||
* igniter_channel(bid) → ateşleme kanal adı
|
||||
* set_igniter(bid, state)
|
||||
* set_pump(bid, pump_name, state)
|
||||
* enabled_pumps(bid) → default=1 olan isimler (konfig default)
|
||||
* all_pumps(bid) → tüm pompa isimleri
|
||||
* active_pumps(bid) → şu anda ON olan pompa isimleri
|
||||
"""
|
||||
|
||||
def __init__(self, onoff: bool = False) -> None:
|
||||
print("RelayDriver yükleniyor…")
|
||||
|
||||
# Konfigten kanal → GPIO pin map
|
||||
self._pin_map: Dict[str, int] = dict(getattr(cfg, "RELAY_GPIO", {})) if cfg else {}
|
||||
|
||||
# Her kanal için istatistik objesi
|
||||
self._stats: Dict[str, RelayStats] = {
|
||||
ch: RelayStats() for ch in self._pin_map.keys()
|
||||
}
|
||||
|
||||
# Brülör grupları
|
||||
self._burner_groups: Dict[int, dict] = dict(getattr(cfg, "BURNER_GROUPS", {})) if cfg else {}
|
||||
|
||||
# GPIO kurulumu
|
||||
if _HAS_GPIO and self._pin_map:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
for ch, pin in self._pin_map.items():
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
# Güvenli başlangıç: tüm kanallar kapalı
|
||||
GPIO.output(pin, GPIO.LOW)
|
||||
elif not self._pin_map:
|
||||
print("⚠️ RELAY_GPIO konfigürasyonu boş; donanım pin eşlemesi yok.")
|
||||
|
||||
# igniter_pin → kanal adı map'ini BURNER_GROUPS içine enjekte et
|
||||
if self._burner_groups and self._pin_map:
|
||||
pin_to_channel = {pin: ch for ch, pin in self._pin_map.items()}
|
||||
for bid, info in self._burner_groups.items():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
ign_pin = info.get("igniter_pin")
|
||||
if ign_pin is not None:
|
||||
ch = pin_to_channel.get(ign_pin)
|
||||
if ch:
|
||||
info.setdefault("igniter", ch)
|
||||
|
||||
# İstenirse tüm röleleri açılışta kapat
|
||||
if onoff is False:
|
||||
self.all_off()
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Düşük seviye kanal kontrolü
|
||||
# -----------------------------------------------------
|
||||
def set_channel(self, channel: str, state: bool) -> None:
|
||||
"""
|
||||
Verilen kanal adını ON/OFF yapar.
|
||||
"""
|
||||
if channel not in self._pin_map:
|
||||
# Tanımsız kanal – sessiz geç
|
||||
return
|
||||
|
||||
pin = self._pin_map[channel]
|
||||
now = time.time()
|
||||
|
||||
# İstatistik güncelle
|
||||
st = self._stats.get(channel)
|
||||
if st is None:
|
||||
st = RelayStats()
|
||||
self._stats[channel] = st
|
||||
|
||||
if state:
|
||||
st.on(now)
|
||||
else:
|
||||
st.off(now)
|
||||
|
||||
# Donanım
|
||||
if _HAS_GPIO:
|
||||
# Aktif-high röle kartı varsayıyoruz; gerekiyorsa buraya
|
||||
# ACTIVE_LOW/ACTIVE_HIGH gibi bir bayrak eklenebilir.
|
||||
GPIO.output(pin, GPIO.HIGH if state else GPIO.LOW)
|
||||
|
||||
def get_stats(self, channel: str) -> RelayStats:
|
||||
"""
|
||||
Kanal için istatistik objesini döndürür (yoksa yaratır).
|
||||
"""
|
||||
st = self._stats.get(channel)
|
||||
if st is None:
|
||||
st = RelayStats()
|
||||
self._stats[channel] = st
|
||||
return st
|
||||
|
||||
def get_channel_state(self, channel: str) -> bool:
|
||||
"""
|
||||
Kanal şu anda ON mu? (last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
st = self._stats.get(channel)
|
||||
if st is None:
|
||||
return False
|
||||
return st.last_on_ts is not None
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Tüm kanalları güvenli moda çek
|
||||
# -----------------------------------------------------
|
||||
def all_off(self) -> None:
|
||||
"""
|
||||
Tüm kanalları OFF yapar.
|
||||
"""
|
||||
now = time.time()
|
||||
for ch in list(self._pin_map.keys()):
|
||||
st = self._stats.get(ch)
|
||||
if st is not None and st.last_on_ts is not None:
|
||||
st.off(now)
|
||||
if _HAS_GPIO:
|
||||
GPIO.output(self._pin_map[ch], GPIO.LOW)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Brülör grup API'si
|
||||
# -----------------------------------------------------
|
||||
def burners(self) -> List[int]:
|
||||
"""
|
||||
Mevcut brülör id listesini döndürür.
|
||||
"""
|
||||
return sorted(self._burner_groups.keys())
|
||||
|
||||
def burner_info(self, burner_id: int) -> Optional[dict]:
|
||||
"""
|
||||
Verilen brülör id için BURNER_GROUPS kaydını döndürür.
|
||||
"""
|
||||
return self._burner_groups.get(burner_id)
|
||||
|
||||
def igniter_channel(self, burner_id: int) -> Optional[str]:
|
||||
"""
|
||||
Brülörün igniter kanal adını döndürür.
|
||||
|
||||
- Eğer BURNER_GROUPS kaydında 'igniter' alanı varsa doğrudan onu kullanır.
|
||||
- Yoksa 'igniter_pin' alanından pin numarasını alır ve
|
||||
RELAY_GPIO'daki pin → kanal eşlemesini kullanarak kanalı bulur.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return None
|
||||
# BURNER_GROUPS konfiginde igniter_pin veriliyor; bunu kanala çevir.
|
||||
ch = info.get("igniter")
|
||||
if ch:
|
||||
return ch
|
||||
pin = info.get("igniter_pin")
|
||||
if pin is None:
|
||||
return None
|
||||
# pin → channel eşlemesini RELAY_GPIO'dan bul
|
||||
for cname, cpin in self._pin_map.items():
|
||||
if cpin == pin:
|
||||
return cname
|
||||
return None
|
||||
|
||||
def all_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Konfigte tanımlı tüm pompa kanal adlarını döndürür (circulation altı).
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {}) or {}
|
||||
# circ_x → {channel: "circulation_a", pin: ..}
|
||||
for logical_name, entry in circ.items():
|
||||
ch = entry.get("channel")
|
||||
if ch:
|
||||
yield ch
|
||||
|
||||
def enabled_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Varsayılan olarak açık olması gereken pompa kanal adlarını döndürür.
|
||||
(circulation altındaki default=1 kayıtları)
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {}) or {}
|
||||
for logical_name, entry in circ.items():
|
||||
ch = entry.get("channel")
|
||||
default = int(entry.get("default", 0))
|
||||
if ch and default == 1:
|
||||
yield ch
|
||||
|
||||
def active_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Şu anda ON olan pompa kanal adlarını döndürür.
|
||||
"""
|
||||
for ch in self.all_pumps(burner_id):
|
||||
if self.get_channel_state(ch):
|
||||
yield ch
|
||||
|
||||
def set_igniter(self, burner_id: int, state: bool) -> None:
|
||||
"""
|
||||
İlgili brülörün igniter kanalını ON/OFF yapar.
|
||||
"""
|
||||
ch = self.igniter_channel(burner_id)
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
|
||||
def set_pump(self, burner_id: int, pump_name: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen brülörün belirtilen pompasını ON/OFF yapar.
|
||||
pump_name: BURNER_GROUPS[..]["circulation"][pump_name]
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return
|
||||
circ = info.get("circulation", {})
|
||||
if pump_name in circ:
|
||||
ch = circ[pump_name]["channel"]
|
||||
self.set_channel(ch, state)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Yardımcı: özet
|
||||
# -----------------------------------------------------
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Kanallar ve brülör gruplarının kısa bir özetini döndürür (debug amaçlı).
|
||||
"""
|
||||
lines: List[str] = []
|
||||
chans = ", ".join(sorted(self._pin_map.keys()))
|
||||
lines.append(f"Kanallar: {chans}")
|
||||
|
||||
for bid in self.burners():
|
||||
info = self.burner_info(bid) or {}
|
||||
name = info.get("name", "?")
|
||||
loc = info.get("location", "?")
|
||||
ign = self.igniter_channel(bid)
|
||||
pumps = list(self.all_pumps(bid))
|
||||
defaults = list(self.enabled_pumps(bid))
|
||||
lines.append(
|
||||
f" #{bid}: {name} @ {loc} | igniter={ign} | "
|
||||
f"pumps={pumps} | default_on={defaults}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Temizlik
|
||||
# -----------------------------------------------------
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
GPIO pinlerini serbest bırakır.
|
||||
"""
|
||||
if _HAS_GPIO:
|
||||
GPIO.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
drv = RelayDriver()
|
||||
print("\n🧰 RelayDriver Summary")
|
||||
print(drv.summary())
|
||||
141
ebuild/io/sensor_dht11.py
Normal file
141
ebuild/io/sensor_dht11.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "sensor_dht11"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "DHT11 tabanlı dış ortam sıcaklık / nem sensörü sürücüsü"
|
||||
__version__ = "0.1.0"
|
||||
__date__ = "2025-11-21"
|
||||
|
||||
"""
|
||||
ebuild/io/sensor_dht11.py
|
||||
|
||||
Revision : 2025-11-21
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- DHT11 sensöründen sıcaklık (°C) ve bağıl nem (%) okumak.
|
||||
- Varsayılan olarak config_statics.DHT11_OUTSIDE_PIN üzerinde çalışır.
|
||||
- Adafruit_DHT kütüphanesi mevcut değilse veya donanım erişiminde sorun
|
||||
varsa, mock / güvenli modda çalışarak None veya sabit değerler
|
||||
döndürür; böylece sistemin geri kalanı göçmez.
|
||||
|
||||
Notlar
|
||||
------
|
||||
- DHT11, tek data pini üzerinden dijital haberleşme kullanır; ADC gerekmez.
|
||||
- Bu sürücü yalnızca ham okuma yapar. Filtreleme, smoothing, alarm üretimi
|
||||
gibi üst seviye işlemler Environment/HeatEngine katmanına bırakılır.
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
try:
|
||||
import Adafruit_DHT # type: ignore
|
||||
except ImportError:
|
||||
Adafruit_DHT = None # type: ignore
|
||||
|
||||
try:
|
||||
from .. import config_statics as cfg
|
||||
except ImportError:
|
||||
cfg = None # type: ignore
|
||||
|
||||
|
||||
class DHT11Sensor:
|
||||
"""
|
||||
Tek bir DHT11 sensörünü temsil eder.
|
||||
|
||||
Özellikler:
|
||||
-----------
|
||||
- BCM pin numarası (örn: config_statics.DHT11_OUTSIDE_PIN)
|
||||
- read() ile (temperature_c, humidity_percent) döndürür.
|
||||
- Donanım veya kütüphane sorunu durumunda mock moda geçer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bcm_pin: Optional[int] = None,
|
||||
sensor_type: int = 11,
|
||||
name: str = "DHT11_Outside",
|
||||
) -> None:
|
||||
"""
|
||||
Parametreler:
|
||||
bcm_pin : BCM pin numarası. None ise configten okunur.
|
||||
sensor_type: Adafruit_DHT sensör tipi (11 → DHT11).
|
||||
name : Sensörün mantıksal adı (log / debug için).
|
||||
"""
|
||||
if bcm_pin is None and cfg is not None:
|
||||
bcm_pin = int(getattr(cfg, "DHT11_OUTSIDE_PIN", 5))
|
||||
|
||||
if bcm_pin is None:
|
||||
raise ValueError("DHT11Sensor için BCM pin numarası verilmeli veya configte DHT11_OUTSIDE_PIN tanımlı olmalı.")
|
||||
|
||||
self.bcm_pin = int(bcm_pin)
|
||||
self.sensor_type = sensor_type
|
||||
self.name = name
|
||||
|
||||
self.mock_mode = Adafruit_DHT is None
|
||||
|
||||
# Son başarı/başarısızlık bilgisi
|
||||
self.last_temperature: Optional[float] = None
|
||||
self.last_humidity: Optional[float] = None
|
||||
self.last_ok: bool = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Okuma
|
||||
# ------------------------------------------------------------------
|
||||
def read(self, retries: int = 3) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
Sensörden sıcaklık ve nem okur.
|
||||
|
||||
Dönüş:
|
||||
(temperature_c, humidity_percent)
|
||||
|
||||
- Başarı durumunda her iki değer de float (örn: 23.4, 45.0)
|
||||
- Başarısızlıkta (None, None)
|
||||
- Mock modda -> (None, None) veya ileride sabit test değeri
|
||||
"""
|
||||
if self.mock_mode:
|
||||
# Donanım veya Adafruit_DHT yok → güvenli fallback.
|
||||
self.last_temperature = None
|
||||
self.last_humidity = None
|
||||
self.last_ok = False
|
||||
return (None, None)
|
||||
|
||||
# Gerçek okuma
|
||||
temperature: Optional[float] = None
|
||||
humidity: Optional[float] = None
|
||||
|
||||
for _ in range(retries):
|
||||
# Adafruit_DHT.read_retry(DHT11, pin) → (humidity, temperature)
|
||||
hum, temp = Adafruit_DHT.read_retry(self.sensor_type, self.bcm_pin) # type: ignore
|
||||
if hum is not None and temp is not None:
|
||||
humidity = float(hum)
|
||||
temperature = float(temp)
|
||||
break
|
||||
|
||||
self.last_temperature = temperature
|
||||
self.last_humidity = humidity
|
||||
self.last_ok = (temperature is not None and humidity is not None)
|
||||
return (temperature, humidity)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bilgiler
|
||||
# ------------------------------------------------------------------
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Sensör hakkında kısa bir özet döndürür.
|
||||
"""
|
||||
mode = "MOCK" if self.mock_mode else "HW"
|
||||
return f"DHT11Sensor(name={self.name}, pin=BCM{self.bcm_pin}, mode={mode})"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Basit test
|
||||
# ----------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
sensor = DHT11Sensor()
|
||||
print(sensor.summary())
|
||||
t, h = sensor.read()
|
||||
print("Okuma sonucu: T={0}°C, H={1}%".format(t, h))
|
||||
|
||||
608
ebuild/io/z2legacy_syslog.py
Normal file
608
ebuild/io/z2legacy_syslog.py
Normal file
@@ -0,0 +1,608 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "legacy_syslog"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "Legacy tarzı syslog çıktısı üreten köprü"
|
||||
__version__ = "0.2.1"
|
||||
__date__ = "2025-11-22"
|
||||
|
||||
"""
|
||||
ebuild/io/legacy_syslog.py
|
||||
|
||||
Revision : 2025-11-22
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
Eski Rasp2 tabanlı sistemin syslog çıktısını, yeni ebuild mimarisi ile
|
||||
uyumlu ve okunaklı şekilde üretir. Çıktı şu ana bloklardan oluşur:
|
||||
|
||||
1) Üst bilgi:
|
||||
- Versiyon ve zaman satırı
|
||||
- Güneş bilgisi (sunrise / sunset, sistem On/Off, lisans id)
|
||||
- Mevsim bilgisi (season, bahar dönemi bilgisi)
|
||||
- Tatil bilgisi (varsa adıyla)
|
||||
|
||||
2) Bina ısı bilgisi
|
||||
- Bina Isı : [ min - avg - max ]
|
||||
|
||||
3) Hat sensörleri (burner.py içinden doldurulan kısım):
|
||||
- Dış Isı 1
|
||||
- Çıkış Isı 2
|
||||
- Dönüş hatları (isim map'inden)
|
||||
|
||||
4) Used dış ısı
|
||||
5) Brülör / devirdaim / özet satırı
|
||||
|
||||
Not
|
||||
---
|
||||
Bu modül sadece formatlama ve çıktı üretiminden sorumludur. Gerçek
|
||||
ölçümler ve kontrol kararları üst katmanlardan (HeatEngine, Burner,
|
||||
Building, Environment, SeasonController vb.) alınır.
|
||||
"""
|
||||
# Bu modül gerçekten hangi path'ten import ediliyor, görmek için:
|
||||
# ---------------------------------------------------------
|
||||
def _safe_import(desc, import_func):
|
||||
"""
|
||||
desc: ekranda görünecek ad (örn: 'Building', 'legacy_syslog')
|
||||
import_func: gerçek import'u yapan lambda
|
||||
"""
|
||||
try:
|
||||
obj = import_func()
|
||||
#print(f"legacy_syslog.py [IMPORT OK] {desc} ->", obj)
|
||||
return obj
|
||||
except Exception as e:
|
||||
print(f"legacy_syslog.py [IMPORT FAIL] {desc}: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
from datetime import datetime, time
|
||||
from typing import Optional
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
try:
|
||||
# SeasonController ve konfig
|
||||
from ..core.season import SeasonController
|
||||
cfg = _safe_import( "config_statics", lambda: __import__("ebuild.config_statics", fromlist=["*"]),)
|
||||
cfv = _safe_import( "config_runtime", lambda: __import__("ebuild.config_runtime", fromlist=["*"]),)
|
||||
#from .. import config_statics as cfg
|
||||
except ImportError: # test / standalone
|
||||
SeasonController = None # type: ignore
|
||||
cfg = None # type: ignore
|
||||
cfv = None
|
||||
print("SeasonController, config_statics import ERROR")
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Logger kurulumu (Syslog + stdout)
|
||||
# ----------------------------------------------------------------------
|
||||
_LOGGER: Optional[logging.Logger] = None
|
||||
|
||||
|
||||
def _get_logger() -> logging.Logger:
|
||||
global _LOGGER
|
||||
if _LOGGER is not None:
|
||||
return _LOGGER
|
||||
#print("logger..1:", stream_fmt)
|
||||
logger = logging.getLogger("BRULOR")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Aynı handler'ları ikinci kez eklemeyelim
|
||||
if not logger.handlers:
|
||||
# Syslog handler (Linux: /dev/log)
|
||||
try:
|
||||
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
# Syslog mesaj formatı: "BRULOR: [ 1 ... ]"
|
||||
fmt = logging.Formatter("%(name)s: %(message)s")
|
||||
syslog_handler.setFormatter(fmt)
|
||||
logger.addHandler(syslog_handler)
|
||||
except Exception:
|
||||
# /dev/log yoksa sessizce geç; sadece stdout'a yazacağız
|
||||
pass
|
||||
|
||||
# Konsol çıktısı (debug için)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_fmt = logging.Formatter("INFO:BRULOR:%(message)s")
|
||||
stream_handler.setFormatter(stream_fmt)
|
||||
logger.addHandler(stream_handler)
|
||||
print("logger..2:", stream_fmt)
|
||||
_LOGGER = logger
|
||||
return logger
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Temel çıktı fonksiyonları
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def send_legacy_syslog(message: str) -> None:
|
||||
"""
|
||||
Verilen mesajı legacy syslog formatına uygun şekilde ilgili hedefe gönderir.
|
||||
- Syslog (/dev/log) → program adı: BRULOR
|
||||
- Aynı zamanda stdout'a da yazar (DEBUG amaçlı)
|
||||
"""
|
||||
#print("send_legacy_syslog BRULOR:", message)
|
||||
try:
|
||||
logger = _get_logger()
|
||||
logger.info(message)
|
||||
except Exception as e:
|
||||
# Logger bir sebeple çökerse bile BRULOR satırını kaybetmeyelim
|
||||
print("BRULOR:", message, f"(logger error: {e})")
|
||||
|
||||
|
||||
def format_line(line_no: int, body: str) -> str:
|
||||
"""
|
||||
BRULOR satırını klasik formata göre hazırlar.
|
||||
|
||||
Örnek:
|
||||
line_no = 2, body = "Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094"
|
||||
|
||||
"[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]"
|
||||
|
||||
Not:
|
||||
Burada "BRULOR" yazmıyoruz; syslog program adı zaten BRULOR olacak.
|
||||
"""
|
||||
return f"[{line_no:3d} {body}]"
|
||||
|
||||
|
||||
def _format_version_3part(ver: str) -> str:
|
||||
"""
|
||||
__version__ string'ini "00.02.01" formatına çevirir.
|
||||
Örnek:
|
||||
"0.2.1" → "00.02.01"
|
||||
"""
|
||||
parts = (ver or "").split(".")
|
||||
nums = []
|
||||
for p in parts:
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
nums.append(0)
|
||||
while len(nums) < 3:
|
||||
nums.append(0)
|
||||
return f"{nums[0]:02d}.{nums[1]:02d}.{nums[2]:02d}"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Üst blok (header) üreticiler
|
||||
# ----------------------------------------------------------------------
|
||||
def emit_header_version(line_no: int, now: datetime) -> int:
|
||||
"""
|
||||
1. satır: versiyon + zaman bilgisi.
|
||||
Örnek:
|
||||
************** 00.02.01 2025-11-22 18:15:00 *************
|
||||
"""
|
||||
v_str = _format_version_3part(__version__)
|
||||
body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def emit_header_sun_and_system(
|
||||
line_no: int,
|
||||
sunrise: Optional[time],
|
||||
sunset: Optional[time],
|
||||
system_on: bool,
|
||||
licence_id: int,
|
||||
) -> int:
|
||||
"""
|
||||
2. satır: Güneş bilgisi + Sistem On/Off + Lisans id.
|
||||
|
||||
Örnek:
|
||||
[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]
|
||||
"""
|
||||
sun_str = ""
|
||||
if sunrise is not None:
|
||||
sun_str += f"Sunrise:{sunrise.strftime('%H:%M')} "
|
||||
if sunset is not None:
|
||||
sun_str += f"Sunset:{sunset.strftime('%H:%M')} "
|
||||
|
||||
sys_str = "On" if system_on else "Off"
|
||||
body = f"{sun_str}Sistem: {sys_str} Lic:{licence_id}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def _only_date(s: str) -> str:
|
||||
"""
|
||||
ISO tarih-zaman stringinden sadece YYYY-MM-DD kısmını alır.
|
||||
Örn: '2025-09-23T16:33:10.687982+03:00' → '2025-09-23'
|
||||
"""
|
||||
if not s:
|
||||
return "--"
|
||||
s = s.strip()
|
||||
if "T" in s:
|
||||
return s.split("T", 1)[0]
|
||||
return s
|
||||
|
||||
def emit_header_season(
|
||||
line_no: int,
|
||||
season_ctrl: SeasonController,
|
||||
) -> int:
|
||||
"""
|
||||
Sunrise satırının altına mevsim + (varsa) bahar tasarruf dönemi satırını basar.
|
||||
|
||||
Beklenen format:
|
||||
|
||||
BRULOR [ 3 season : Sonbahar 2025-09-23 - 2025-12-20 [89 pass:60 kalan:28] ]
|
||||
BRULOR [ 4 bahar : 2025-09-23 - 2025-10-13 ]
|
||||
|
||||
Notlar:
|
||||
- Bilgiler SeasonController.info içinden okunur (dict veya obje olabilir).
|
||||
- bahar_tasarruf True DEĞİLSE bahar satırı hiç basılmaz.
|
||||
"""
|
||||
|
||||
# SeasonController.info hem dict hem obje olabilir, ikisini de destekle
|
||||
info = getattr(season_ctrl, "info", season_ctrl)
|
||||
|
||||
def _get(field: str, default=None):
|
||||
if isinstance(info, dict):
|
||||
return info.get(field, default)
|
||||
return getattr(info, field, default)
|
||||
|
||||
# ---- 3. satır: season ----
|
||||
season_name = _get("season", "Unknown")
|
||||
season_start = _only_date(_get("season_start", ""))
|
||||
season_end = _only_date(_get("season_end", ""))
|
||||
season_day = _get("season_day", "")
|
||||
season_passed = _get("season_passed", "")
|
||||
season_remain = _get("season_remaining", "")
|
||||
|
||||
body = (
|
||||
f"season : {season_name} {season_start} - {season_end} "
|
||||
f"[{season_day} pass:{season_passed} kalan:{season_remain}]"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# ---- 4. satır: bahar dönemi (SADECE aktifse) ----
|
||||
bahar_tasarruf = bool(_get("bahar_tasarruf", False))
|
||||
if bahar_tasarruf:
|
||||
bahar_basx = _only_date(_get("bahar_basx", ""))
|
||||
bahar_bitx = _only_date(_get("bahar_bitx", ""))
|
||||
body = f"bahar : {bahar_basx} - {bahar_bitx}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
return line_no
|
||||
|
||||
|
||||
def emit_header_holiday(
|
||||
line_no: int,
|
||||
is_holiday: bool,
|
||||
holiday_label: str,
|
||||
) -> int:
|
||||
"""
|
||||
Tatil satırı (sunrise + season altına).
|
||||
|
||||
Kurallar:
|
||||
- Tatil yoksa (False) HİÇ satır basma.
|
||||
- Tatil varsa:
|
||||
[ 5 Tatil: True Adı: Cumhuriyet Bayramı]
|
||||
"""
|
||||
if not is_holiday:
|
||||
return line_no
|
||||
|
||||
label = holiday_label or ""
|
||||
body = f"Tatil: True Adı: {label}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Dışarıdan çağrılacak üst-blok helper
|
||||
# ----------------------------------------------------------------------
|
||||
def emit_top_block(
|
||||
now: datetime,
|
||||
season_ctrl: SeasonController,
|
||||
) -> int:
|
||||
"""
|
||||
F veya B modundan bağımsız olarak, her tick başında üst bilgiyi üretir.
|
||||
|
||||
Sıra:
|
||||
1) Versiyon + zaman
|
||||
2) Sunrise / Sunset / Sistem: On/Off / Lic
|
||||
3) Mevsim bilgisi (SeasonController.to_syslog_lines() → sadeleştirilmiş)
|
||||
4) Tatil bilgisi (sadece tatil varsa)
|
||||
5) Bir sonraki satır numarasını döndürür (bina ısı satırları için).
|
||||
"""
|
||||
line_no = 1
|
||||
|
||||
# 1) Versiyon
|
||||
line_no = emit_header_version(line_no, now)
|
||||
|
||||
# Konfigten sistem ve lisans bilgileri
|
||||
if cfg is not None:
|
||||
licence_id = int(getattr(cfg, "BUILDING_LICENCEID", 0))
|
||||
system_onoff = int(getattr(cfg, "BUILDING_SYSTEMONOFF", 1))
|
||||
else:
|
||||
licence_id = 0
|
||||
system_onoff = 1
|
||||
|
||||
system_on = (system_onoff == 1)
|
||||
|
||||
# 2) Güneş + Sistem / Lisans
|
||||
sunrise = season_ctrl.info.sunrise
|
||||
sunset = season_ctrl.info.sunset
|
||||
|
||||
line_no = emit_header_sun_and_system(
|
||||
line_no=line_no,
|
||||
sunrise=sunrise,
|
||||
sunset=sunset,
|
||||
system_on=system_on,
|
||||
licence_id=licence_id,
|
||||
)
|
||||
|
||||
# 3) Mevsim bilgisi (sunrise ALTINA)
|
||||
line_no = emit_header_season(line_no, season_ctrl)
|
||||
|
||||
# 4) Tatil bilgisi (sadece True ise)
|
||||
line_no = emit_header_holiday(
|
||||
line_no=line_no,
|
||||
is_holiday=season_ctrl.info.is_holiday,
|
||||
holiday_label=season_ctrl.info.holiday_label,
|
||||
)
|
||||
|
||||
# Sonraki satır: bina ısı / dış ısı / F-B detayları için kullanılacak
|
||||
return line_no
|
||||
|
||||
def _fmt_temp(val: Optional[float]) -> str:
|
||||
return "None" if val is None else f"{val:.2f}"
|
||||
PUMP_SHORT_MAP = {
|
||||
"circulation_a": "A",
|
||||
"circulation_b": "B",
|
||||
"circ_1": "A",
|
||||
"circ_2": "B",
|
||||
}
|
||||
|
||||
def _short_pump_name(ch: str) -> str:
|
||||
if ch in PUMP_SHORT_MAP:
|
||||
return PUMP_SHORT_MAP[ch]
|
||||
# sonu _a/_b ise yine yakala
|
||||
if ch.endswith("_a"):
|
||||
return "A"
|
||||
if ch.endswith("_b"):
|
||||
return "B"
|
||||
return ch # tanımıyorsak orijinal ismi yaz
|
||||
|
||||
def log_burner_header(
|
||||
now: datetime,
|
||||
mode: str,
|
||||
season,
|
||||
building_avg: Optional[float],
|
||||
outside_c: Optional[float],
|
||||
used_out_c: Optional[float],
|
||||
fire_sp: float,
|
||||
burner_on: bool,
|
||||
pumps_on,
|
||||
line_temps: Optional[Dict[str, Optional[float]]] = None,
|
||||
ign_stats=None,
|
||||
circ_stats=None,
|
||||
) -> None:
|
||||
"""
|
||||
BurnerController'dan tek çağrıyla BRULOR bloğunu basar.
|
||||
|
||||
- Önce üst blok (versiyon + güneş + mevsim + tatil)
|
||||
- Sonra bina ısı satırı
|
||||
- Dış ısı / used dış ısı
|
||||
- Son satırda brülör ve pompaların durumu
|
||||
"""
|
||||
#print("log_burner_header CALLED", season)
|
||||
# 1) Üst header blok
|
||||
if season is None:
|
||||
# SeasonController yoksa, sadece versiyon ve zaman bas
|
||||
line_no = 1
|
||||
v_str = _format_version_3part(__version__)
|
||||
body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
else:
|
||||
line_no = emit_top_block(now, season)
|
||||
|
||||
# 2) Bina ısı satırı
|
||||
if building_avg is None:
|
||||
min_s = "None"
|
||||
avg_s = "None"
|
||||
max_s = "None"
|
||||
else:
|
||||
# Şimdilik min=avg=max gibi davranalım; ileride gerçek min/max eklenebilir
|
||||
min_s = f"{building_min:5.2f}"
|
||||
avg_s = f"{building_avg:5.2f}"
|
||||
max_s = f"{building_max:5.2f}"
|
||||
|
||||
# config’teki mod
|
||||
cfg_mode = getattr(cfg, "BUILD_BURNER", "?") if cfg is not None else "?"
|
||||
body = f"Build [{mode}-{cfg_mode}] Heats[Min:{min_s}°C Avg:{avg_s}°C Max:{max_s}°C]"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# line_temps yoksa, burayı pas geç
|
||||
if line_temps is not None:
|
||||
# CONFIG'TEN ID'LERİ AL
|
||||
outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", None) if cfg is not None else None
|
||||
out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", None) if cfg is not None else None
|
||||
ret_ids = getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) if cfg is not None else []
|
||||
ret_map = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP", {}) if cfg is not None else {}
|
||||
line_no = 4 # dış ısı satırı numarası
|
||||
|
||||
# 4: Dis isi
|
||||
if outside_id and outside_id in line_temps:
|
||||
t = line_temps.get(outside_id)
|
||||
namex = getattr(cfg, "OUTSIDE_SENSOR_NAME", "Dis isi") if cfg is not None else "Dis isi"
|
||||
msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {outside_id} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 5: Cikis isi
|
||||
if out_id and out_id in line_temps:
|
||||
t = line_temps.get(out_id)
|
||||
namex = getattr(cfg, "BURNER_OUT_SENSOR_NAME", "Cikis isi") if cfg is not None else "Cıkıs isi"
|
||||
msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {out_id} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 6..N: Donus isi X
|
||||
namex = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP",[])
|
||||
for sid in ret_ids:
|
||||
if sid not in line_temps:
|
||||
continue
|
||||
t = line_temps.get(sid)
|
||||
try:
|
||||
namexx = ret_map.get(sid)
|
||||
except:
|
||||
namex = '???'
|
||||
msg = f"{namexx:<15.15}: {_fmt_temp(t)}°C - {sid} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 3) Dış ısı / used dış ısı
|
||||
out_str = "--"
|
||||
used_str = "--"
|
||||
if outside_c is not None:
|
||||
out_str = f"{outside_c:5.2f}"
|
||||
if used_out_c is not None:
|
||||
used_str = f"{used_out_c:5.2f}"
|
||||
usedxx = "Sistem Isı"
|
||||
|
||||
#------------------------------------------------------------------
|
||||
# 9: Sistem Isı - Used + [WEEKEND_HEAT_BOOST_C, BURNER_COMFORT_OFFSET_C]
|
||||
# ------------------------------------------------------------------
|
||||
used_val = used_out_c if used_out_c is not None else None
|
||||
used_str = "None" if used_val is None else f"{used_val:.2f}"
|
||||
|
||||
if cfv is not None:
|
||||
w_val = float(getattr(cfv, "WEEKEND_HEAT_BOOST_C", 0.0) or 0.0)
|
||||
c_val = float(getattr(cfv, "BURNER_COMFORT_OFFSET_C", 0.0) or 0.0)
|
||||
else:
|
||||
w_val = 0.0
|
||||
c_val = 0.0
|
||||
|
||||
# Sayıları [2, 1] gibi, gereksiz .0’sız yazalım
|
||||
def _fmt_num(x: float) -> str:
|
||||
if x == int(x):
|
||||
return str(int(x))
|
||||
return f"{x:g}"
|
||||
|
||||
sabitler_str = f"[w:{_fmt_num(w_val)} c:{_fmt_num(c_val)}]"
|
||||
|
||||
body = f"{usedxx:<15.15}: {used_str}°C {sabitler_str} "
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 11: Brülör Motor satırı (MAX_OUTLET_C ile)
|
||||
# ------------------------------------------------------------------
|
||||
if cfv is not None:
|
||||
max_out = float(getattr(cfv, "MAX_OUTLET_C", 45.0) or 45.0)
|
||||
else:
|
||||
max_out = 45.0
|
||||
|
||||
if cfv is not None:
|
||||
min_ret = float(getattr(cfv, "CIRCULATION_MIN_RETURN_C", 25.0) or 25.0)
|
||||
else:
|
||||
min_ret = 25.0
|
||||
|
||||
br_status = "<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
|
||||
|
||||
# Config’te default=1 olan pompaları da topla (cfg_default_pumps)
|
||||
cfg_default_pumps = []
|
||||
for logical_name, info in circ_cfg.items():
|
||||
ch = info.get("channel")
|
||||
if ch and info.get("default", 0):
|
||||
cfg_default_pumps.append(ch)
|
||||
|
||||
# Kısa isim A/B istersek:
|
||||
def _logical_to_short(name: str) -> str:
|
||||
if name == "circ_1":
|
||||
return "A"
|
||||
if name == "circ_2":
|
||||
return "B"
|
||||
return name
|
||||
|
||||
pump_count = len(cfg_default_pumps)
|
||||
dev_status = "<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))
|
||||
388
ebuild/io/z2relay_driver.py
Normal file
388
ebuild/io/z2relay_driver.py
Normal file
@@ -0,0 +1,388 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "relay_driver"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "GPIO röle sürücüsü + brülör grup soyutlaması"
|
||||
__version__ = "0.4.0"
|
||||
__date__ = "2025-11-22"
|
||||
|
||||
"""
|
||||
ebuild/io/relay_driver.py
|
||||
|
||||
Revision : 2025-11-22
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- Soyut kanal isimleri ile (igniter, circulation_a, ...) GPIO pin sürmek.
|
||||
- config_statics.BURNER_GROUPS üzerinden brülör gruplarını yönetmek.
|
||||
- Her kanal için:
|
||||
* ON/OFF sayacı
|
||||
* Son çalışma süresi
|
||||
* Toplam çalışma süresi
|
||||
* Şu anki çalışma süresi (eğer röle ON ise, anlık akan süre)
|
||||
istatistiklerini tutmak.
|
||||
|
||||
Kullanım
|
||||
--------
|
||||
- Temel kanal API:
|
||||
drv.channels() → ['igniter', 'circulation_a', ...]
|
||||
drv.set_channel("igniter", True/False)
|
||||
drv.get_stats("igniter") → RelayStats
|
||||
drv.get_channel_state("igniter") → bool (şu an ON mu?)
|
||||
|
||||
- Brülör grup API:
|
||||
drv.burners() → [0, 1, ...]
|
||||
drv.burner_info(0) → config_statics.BURNER_GROUPS[0]
|
||||
drv.igniter_channel(0) → "igniter"
|
||||
drv.all_pumps(0) → ['circulation_a', 'circulation_b', ...]
|
||||
drv.enabled_pumps(0) → default=1 olan pompalar
|
||||
drv.active_pumps(0) → şu anda gerçekten ON olan pompalar
|
||||
|
||||
Bu API'ler burner.py ve legacy_syslog.py tarafından kullanılmak üzere tasarlanmıştır.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Iterable, Tuple, List
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
_HAS_GPIO = True
|
||||
except ImportError:
|
||||
_HAS_GPIO = False
|
||||
|
||||
from .. import config_statics as cfg
|
||||
# GPIO aktif seviyesini seç
|
||||
# Birçok Çin röle kartı ACTIVE_LOW çalışır:
|
||||
# - LOW → röle ÇEKER
|
||||
# - HIGH → röle BIRAKIR
|
||||
# Eğer kartın tam tersi ise bunu False yaparsın.
|
||||
ACTIVE_LOW = True
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# İstatistik yapısı
|
||||
# -------------------------------------------------------------------
|
||||
@dataclass
|
||||
class RelayStats:
|
||||
"""
|
||||
Tek bir röle kanalı için istatistikler.
|
||||
|
||||
- on_count : kaç defa ON'a çekildi
|
||||
- last_on_ts : en son ON'a çekildiği zaman (epoch saniye)
|
||||
- last_off_ts : en son OFF olduğu zaman (epoch saniye)
|
||||
- last_duration_s : en son ON periyodunun süresi (saniye)
|
||||
- total_on_s : bugüne kadar toplam ON kalma süresi (saniye)
|
||||
"""
|
||||
on_count: int = 0
|
||||
last_on_ts: Optional[float] = None
|
||||
last_off_ts: Optional[float] = None
|
||||
last_duration_s: float = 0.0
|
||||
total_on_s: float = 0.0
|
||||
|
||||
def on(self, now: float) -> None:
|
||||
"""
|
||||
Kanal ON'a çekildiğinde çağrılır.
|
||||
Aynı ON periyodu içinde tekrar çağrılırsa sayaç artmaz.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
self.last_on_ts = now
|
||||
self.on_count += 1
|
||||
|
||||
def off(self, now: float) -> None:
|
||||
"""
|
||||
Kanal OFF'a çekildiğinde çağrılır.
|
||||
Son ON zamanına göre süre hesaplanır, last_duration_s ve total_on_s güncellenir.
|
||||
"""
|
||||
if self.last_on_ts is not None:
|
||||
dur = max(0.0, now - self.last_on_ts)
|
||||
self.last_duration_s = dur
|
||||
self.total_on_s += dur
|
||||
self.last_on_ts = None
|
||||
self.last_off_ts = now
|
||||
|
||||
def current_duration(self, now: Optional[float] = None) -> float:
|
||||
"""
|
||||
Kanal şu anda ON ise, bu ON periyodunun şu ana kadarki süresini döndürür.
|
||||
OFF ise 0.0 döner.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
return 0.0
|
||||
if now is None:
|
||||
now = time.time()
|
||||
return max(0.0, now - self.last_on_ts)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Ana sürücü
|
||||
# -------------------------------------------------------------------
|
||||
class RelayDriver:
|
||||
"""
|
||||
Basit bir röle sürücüsü.
|
||||
|
||||
- Soyut kanal isimleri: RELAY_GPIO dict'indeki anahtarlar
|
||||
- Brülör grup API'si:
|
||||
* burners() → mevcut brülör id listesi
|
||||
* burner_info(bid) → config_statics.BURNER_GROUPS[bid]
|
||||
* igniter_channel(bid) → ateşleme kanal adı
|
||||
* set_igniter(bid, state)
|
||||
* set_pump(bid, pump_name, state)
|
||||
* enabled_pumps(bid) → default=1 olan isimler (konfig default)
|
||||
* all_pumps(bid) → tüm pompa isimleri
|
||||
* active_pumps(bid) → şu anda ON olan pompa isimleri
|
||||
"""
|
||||
|
||||
def __init__(self, onoff=False) -> None:
|
||||
print("RelayDriver yükleniyor…")
|
||||
|
||||
# Konfigten kanal → GPIO pin map
|
||||
self._pin_map: Dict[str, int] = dict(getattr(cfg, "RELAY_GPIO", {}))
|
||||
|
||||
# Her kanal için istatistik objesi
|
||||
self._stats: Dict[str, RelayStats] = {
|
||||
ch: RelayStats() for ch in self._pin_map.keys()
|
||||
}
|
||||
|
||||
# Brülör grupları
|
||||
self._burner_groups: Dict[int, dict] = dict(getattr(cfg, "BURNER_GROUPS", {}))
|
||||
|
||||
if not self._pin_map:
|
||||
raise RuntimeError("RelayDriver: RELAY_GPIO boş.")
|
||||
|
||||
if _HAS_GPIO:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False) # aynı pini yeniden kullanırken uyarı verme
|
||||
for ch, pin in self._pin_map.items():
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
GPIO.output(pin, GPIO.LOW)
|
||||
else:
|
||||
print("⚠️ GPIO bulunamadı, DRY-RUN modunda çalışıyorum.")
|
||||
|
||||
# Başlangıçta HER ŞEYİ KAPALIYA ÇEK
|
||||
try:
|
||||
if onoff:
|
||||
self.all_off()
|
||||
except Exception:
|
||||
# Çok dert etmeyelim, en kötü GPIO yoktur, vs.
|
||||
pass
|
||||
# -----------------------------------------------------
|
||||
# Temel kanal API
|
||||
# -----------------------------------------------------
|
||||
def channels(self) -> Iterable[str]:
|
||||
"""
|
||||
Mevcut kanal isimlerini döndürür.
|
||||
"""
|
||||
return self._pin_map.keys()
|
||||
|
||||
def channel_pin(self, channel: str) -> Optional[int]:
|
||||
"""
|
||||
Verilen kanalın GPIO pin numarasını döndürür.
|
||||
"""
|
||||
return self._pin_map.get(channel)
|
||||
|
||||
def set_channel(self, channel: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen kanalı ON/OFF yapar, GPIO'yu sürer ve istatistikleri günceller.
|
||||
"""
|
||||
if channel not in self._pin_map:
|
||||
return
|
||||
pin = self._pin_map[channel]
|
||||
now = time.time()
|
||||
|
||||
if _HAS_GPIO:
|
||||
# Aktif-low kartlar için:
|
||||
if ACTIVE_LOW:
|
||||
gpio_state = GPIO.LOW if state else GPIO.HIGH
|
||||
else:
|
||||
gpio_state = GPIO.HIGH if state else GPIO.LOW
|
||||
GPIO.output(pin, gpio_state)
|
||||
|
||||
st = self._stats[channel]
|
||||
if state:
|
||||
st.on(now)
|
||||
else:
|
||||
st.off(now)
|
||||
|
||||
def get_stats(self, channel: str) -> RelayStats:
|
||||
"""
|
||||
Kanalın istatistik objesini döndürür.
|
||||
"""
|
||||
return self._stats[channel]
|
||||
|
||||
def get_channel_state(self, channel: str) -> bool:
|
||||
"""
|
||||
Kanal şu anda ON mu? (last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
st = self._stats.get(channel)
|
||||
if st is None:
|
||||
return False
|
||||
return st.last_on_ts is not None
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Tüm kanalları güvenli moda çek
|
||||
# -----------------------------------------------------
|
||||
def all_off(self) -> None:
|
||||
"""
|
||||
Tüm röle kanallarını KAPALI (LOW) yapar ve istatistikleri günceller.
|
||||
Özellikle:
|
||||
- Uygulama başlatıldığında "her şey kapalı" garantisi
|
||||
- Çıkış/KeyboardInterrupt anında güvenli kapanış için kullanılır.
|
||||
"""
|
||||
now = time.time()
|
||||
for ch, pin in self._pin_map.items():
|
||||
if _HAS_GPIO:
|
||||
GPIO.output(pin, GPIO.LOW)
|
||||
# stats güncelle
|
||||
st = self._stats.get(ch)
|
||||
if st is not None:
|
||||
st.off(now)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Brülör grup API
|
||||
# -----------------------------------------------------
|
||||
def burners(self) -> Iterable[int]:
|
||||
"""
|
||||
Mevcut brülör id'lerini döndürür.
|
||||
"""
|
||||
return self._burner_groups.keys()
|
||||
|
||||
def burner_info(self, burner_id: int) -> Optional[dict]:
|
||||
"""
|
||||
İlgili brülörün BURNER_GROUPS içindeki konfig dict'ini döndürür.
|
||||
"""
|
||||
return self._burner_groups.get(burner_id)
|
||||
|
||||
def igniter_channel(self, burner_id: int) -> Optional[str]:
|
||||
"""
|
||||
Brülörün igniter kanal adını döndürür.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return None
|
||||
return info.get("igniter", None)
|
||||
|
||||
def all_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Konfigte tanımlı tüm pompa kanal adlarını döndürür (circulation altı).
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
# Her pompa için { "channel": "circulation_a", "pin": 26, "default": 1 } beklenir.
|
||||
return [data["channel"] for _, data in circ.items()]
|
||||
|
||||
def enabled_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Konfigte default=1 işaretli pompa kanal adlarını döndürür.
|
||||
Bu, sistem açıldığında / ısıtma başladığında devreye alınacak default pompaları temsil eder.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
return [
|
||||
data["channel"]
|
||||
for _, data in circ.items()
|
||||
if int(data.get("default", 0)) == 1
|
||||
]
|
||||
|
||||
def active_pumps(self, burner_id: int) -> Tuple[str, ...]:
|
||||
"""
|
||||
Şu anda gerçekten ON olan pompa isimlerini döndürür.
|
||||
(GPIO'da HIGH durumda olan kanallar; RelayStats.last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return tuple()
|
||||
circ = info.get("circulation", {})
|
||||
active: List[str] = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel")
|
||||
if ch in self._stats and self._stats[ch].last_on_ts is not None:
|
||||
active.append(pname)
|
||||
return tuple(active)
|
||||
|
||||
def set_igniter(self, burner_id: int, state: bool) -> None:
|
||||
"""
|
||||
İlgili brülörün igniter kanalını ON/OFF yapar.
|
||||
"""
|
||||
ch = self.igniter_channel(burner_id)
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
|
||||
def set_pump(self, burner_id: int, pump_name: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen brülörün belirtilen pompasını ON/OFF yapar.
|
||||
|
||||
pump_name normalde BURNER_GROUPS[..]["circulation"].keys()
|
||||
(örn: "circ_1", "circ_2") olmalıdır; ancak geriye dönük uyumluluk
|
||||
için doğrudan kanal adı ("circulation_a" vb.) verilirse de kabul edilir.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return
|
||||
circ = info.get("circulation", {}) or {}
|
||||
|
||||
# 1) pump_name bir logical ad ise ("circ_1" gibi)
|
||||
if pump_name in circ:
|
||||
ch = circ[pump_name].get("channel")
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
return
|
||||
|
||||
# 2) Geriyedönük: pump_name doğrudan kanal adı ("circulation_a" gibi)
|
||||
for logical_name, pdata in circ.items():
|
||||
ch = pdata.get("channel")
|
||||
if ch == pump_name:
|
||||
self.set_channel(ch, state)
|
||||
return
|
||||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Yardımcı: özet
|
||||
# -----------------------------------------------------
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Kanallar ve brülör gruplarının kısa bir özetini döndürür (debug amaçlı).
|
||||
"""
|
||||
lines: List[str] = []
|
||||
chans = ", ".join(sorted(self._pin_map.keys()))
|
||||
lines.append(f"Kanallar: {chans}")
|
||||
|
||||
lines.append("Brülör grupları:")
|
||||
for bid, info in self._burner_groups.items():
|
||||
name = info.get("name", f"Burner{bid}")
|
||||
loc = info.get("location", "-")
|
||||
ign = info.get("igniter", "igniter")
|
||||
circ = info.get("circulation", {})
|
||||
pumps = []
|
||||
defaults = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel", "?")
|
||||
pumps.append(f"{pname}->{ch}")
|
||||
if int(pdata.get("default", 0)) == 1:
|
||||
defaults.append(pname)
|
||||
lines.append(
|
||||
f" #{bid}: {name} @ {loc} | igniter={ign} | "
|
||||
f"pumps={pumps} | default_on={defaults}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Temizlik
|
||||
# -----------------------------------------------------
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
GPIO pinlerini serbest bırakır.
|
||||
"""
|
||||
if _HAS_GPIO:
|
||||
GPIO.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
drv = RelayDriver()
|
||||
print("\n🧰 RelayDriver Summary")
|
||||
print(drv.summary())
|
||||
463
ebuild/io/z3legacy_syslog.py
Normal file
463
ebuild/io/z3legacy_syslog.py
Normal file
@@ -0,0 +1,463 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
"""
|
||||
ebuild/io/legacy_syslog.py
|
||||
|
||||
Legacy tarzı BRULOR syslog çıktısı üretmek için yardımcı fonksiyonlar.
|
||||
- Syslog (/dev/log) + stdout'a aynı formatta basar.
|
||||
- burner.BurnerController tarafından her tick'te çağrılan log_burner_header()
|
||||
ile eski sistemdeki benzer satırları üretir.
|
||||
"""
|
||||
|
||||
__title__ = "legacy_syslog"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "Legacy tarzı syslog çıktısı üreten köprü"
|
||||
__version__ = "0.3.0"
|
||||
__date__ = "2025-11-23"
|
||||
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Optional, Iterable, Tuple, Dict
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
try:
|
||||
# Mevsim + güneş bilgileri için
|
||||
from ..core.season import SeasonController # type: ignore
|
||||
from .. import config_statics as cfg # type: ignore
|
||||
except ImportError: # test / standalone
|
||||
SeasonController = None # type: ignore
|
||||
cfg = None # type: ignore
|
||||
|
||||
try:
|
||||
# Çalışma parametreleri (konfor offset, max çıkış vb.)
|
||||
from .. import config_runtime as cfg_v # type: ignore
|
||||
except ImportError:
|
||||
cfg_v = None # type: ignore
|
||||
|
||||
try:
|
||||
# Hat sensörlerinden doğrudan okuma için
|
||||
from ..io.ds18b20 import DS18B20Sensor # type: ignore
|
||||
except Exception:
|
||||
DS18B20Sensor = None # type: ignore
|
||||
|
||||
#print("legacy_syslog IMPORT EDİLDİ:", __file__)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Logger kurulumu (Syslog + stdout)
|
||||
# ----------------------------------------------------------------------
|
||||
_LOGGER: Optional[logging.Logger] = None
|
||||
|
||||
|
||||
def _get_logger() -> logging.Logger:
|
||||
global _LOGGER
|
||||
if _LOGGER is not None:
|
||||
return _LOGGER
|
||||
|
||||
logger = logging.getLogger("BRULOR")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if not logger.handlers:
|
||||
# Syslog handler (Linux: /dev/log)
|
||||
try:
|
||||
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
# Syslog mesaj formatı: "BRULOR: [ 1 ... ]"
|
||||
fmt = logging.Formatter("%(name)s: %(message)s")
|
||||
syslog_handler.setFormatter(fmt)
|
||||
logger.addHandler(syslog_handler)
|
||||
except Exception as e:
|
||||
print("legacy_syslog: SysLogHandler eklenemedi:", e)
|
||||
|
||||
# Ayrıca stdout'a da yaz (debug için)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_fmt = logging.Formatter("BRULOR: %(message)s")
|
||||
stream_handler.setFormatter(stream_fmt)
|
||||
logger.addHandler(stream_handler)
|
||||
|
||||
_LOGGER = logger
|
||||
return logger
|
||||
|
||||
|
||||
def send_legacy_syslog(message: str) -> None:
|
||||
"""
|
||||
Verilen mesajı legacy syslog formatına uygun şekilde ilgili hedefe gönderir.
|
||||
- Syslog (/dev/log) → program adı: BRULOR
|
||||
- Aynı zamanda stdout'a da yazar (DEBUG amaçlı)
|
||||
"""
|
||||
try:
|
||||
logger = _get_logger()
|
||||
logger.info(message)
|
||||
except Exception as e:
|
||||
# Logger bir sebeple çökerse bile BRULOR satırını kaybetmeyelim
|
||||
print("BRULOR:", message, f"(logger error: {e})")
|
||||
|
||||
|
||||
def format_line(line_no: int, body: str) -> str:
|
||||
"""
|
||||
BRULOR satırını klasik formata göre hazırlar.
|
||||
|
||||
line_no = 2, body = "Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094"
|
||||
→ "[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]"
|
||||
"""
|
||||
return f"[{line_no:3d} {body}]"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Header yardımcıları
|
||||
# ----------------------------------------------------------------------
|
||||
def _format_version_3part(ver: str) -> str:
|
||||
"""
|
||||
"0.2.1" → "00.02.01" formatı.
|
||||
"""
|
||||
parts = (ver or "").split(".")
|
||||
nums = []
|
||||
for p in parts:
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
nums.append(0)
|
||||
while len(nums) < 3:
|
||||
nums.append(0)
|
||||
return f"{nums[0]:02d}.{nums[1]:02d}.{nums[2]:02d}"
|
||||
|
||||
|
||||
def emit_header_version(line_no: int, now: datetime) -> int:
|
||||
"""
|
||||
1. satır: versiyon + zaman bilgisi.
|
||||
|
||||
************** 00.02.01 2025-11-22 18:15:00 *************
|
||||
"""
|
||||
v_str = _format_version_3part(__version__)
|
||||
body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def emit_header_sunrise(
|
||||
line_no: int,
|
||||
sunrise: time,
|
||||
sunset: time,
|
||||
system_on: bool,
|
||||
licence_id: int,
|
||||
) -> int:
|
||||
"""
|
||||
2. satır: Sunrise/Sunset + sistem On/Off + Lisans id satırı.
|
||||
|
||||
Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094
|
||||
"""
|
||||
def _fmt_t(t: time) -> str:
|
||||
if not t:
|
||||
return "--:--"
|
||||
return t.strftime("%H:%M")
|
||||
|
||||
sun_str = f"Sunrise:{_fmt_t(sunrise)} Sunset:{_fmt_t(sunset)} "
|
||||
sys_str = "On" if system_on else "Off"
|
||||
body = f"{sun_str}Sistem: {sys_str} Lic:{licence_id}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def _only_date(s: str) -> str:
|
||||
"""
|
||||
ISO tarih-zaman stringinden sadece YYYY-MM-DD kısmını alır.
|
||||
"""
|
||||
if not s:
|
||||
return "--"
|
||||
s = s.strip()
|
||||
if "T" in s:
|
||||
s = s.split("T", 1)[0]
|
||||
return s
|
||||
|
||||
def emit_header_season(line_no: int, season_ctrl) -> int:
|
||||
info = getattr(season_ctrl, "info", season_ctrl)
|
||||
|
||||
# 1) SEASON satırı (bunu zaten yazıyorsun)
|
||||
season_name = getattr(info, "season", "Unknown")
|
||||
s_start = getattr(info, "season_start", "")
|
||||
s_end = getattr(info, "season_end", "")
|
||||
day_total = getattr(info, "season_day", 0) or 0
|
||||
day_passed = getattr(info, "season_passed", 0) or 0
|
||||
day_left = getattr(info, "season_remaining", 0) or 0
|
||||
|
||||
body = (
|
||||
f"season : {season_name} {s_start} - {s_end} "
|
||||
f"[{day_total} pass:{day_passed} kalan:{day_left}]"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# 2) BAHAR / TASARRUF SATIRI
|
||||
# SeasonInfo'dan tasarruf penceresini alıyoruz
|
||||
saving_start = getattr(info, "saving_start", None)
|
||||
saving_stop = getattr(info, "saving_stop", None)
|
||||
today = getattr(info, "date", None)
|
||||
|
||||
# saving_start/stop yoksa bahar satırı YOK
|
||||
if saving_start is None or saving_stop is None or today is None:
|
||||
return line_no
|
||||
|
||||
# Gösterim penceresi: saving_start - 3 gün .. saving_stop + 3 gün
|
||||
window_start = saving_start - timedelta(days=3)
|
||||
window_stop = saving_stop + timedelta(days=3)
|
||||
|
||||
if window_start <= today <= window_stop:
|
||||
# Syslog'ta görünen tarih aralığı GERÇEK tasarruf aralığı olsun:
|
||||
# saving_start / saving_stop
|
||||
bahar_bas = saving_start.isoformat()
|
||||
bahar_bit = saving_stop.isoformat()
|
||||
body2 = f"bahar : {bahar_bas} - {bahar_bit}"
|
||||
send_legacy_syslog(format_line(line_no, body2))
|
||||
line_no += 1
|
||||
|
||||
return line_no
|
||||
|
||||
def emit_header_holiday(
|
||||
line_no: int,
|
||||
is_holiday: bool,
|
||||
holiday_label: str,
|
||||
) -> int:
|
||||
"""
|
||||
Tatil satırı (sunrise + season altına).
|
||||
"""
|
||||
if not is_holiday:
|
||||
return line_no
|
||||
label = holiday_label or ""
|
||||
body = f"Tatil: True Adı: {label}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def emit_top_block(
|
||||
now: datetime,
|
||||
season_ctrl: "SeasonController",
|
||||
) -> int:
|
||||
"""
|
||||
F veya B modundan bağımsız olarak, her tick başında üst bilgiyi üretir.
|
||||
Dönüş: bir sonraki satır numarası.
|
||||
"""
|
||||
line_no = 1
|
||||
# 1: versiyon + zaman
|
||||
line_no = emit_header_version(line_no, now)
|
||||
|
||||
# 2: güneş bilgisi
|
||||
info = getattr(season_ctrl, "info", season_ctrl)
|
||||
sunrise = getattr(info, "sunrise", None)
|
||||
sunset = getattr(info, "sunset", None)
|
||||
system_on = bool(getattr(info, "system_on", True))
|
||||
licence_id = int(
|
||||
getattr(info, "licence_id", getattr(cfg, "BUILDING_LICENCEID", 0))
|
||||
if cfg is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
line_no = emit_header_sunrise(
|
||||
line_no=line_no,
|
||||
sunrise=sunrise,
|
||||
sunset=sunset,
|
||||
system_on=system_on,
|
||||
licence_id=licence_id,
|
||||
)
|
||||
|
||||
# 3: mevsim
|
||||
line_no = emit_header_season(line_no, season_ctrl)
|
||||
|
||||
# 4: tatil (varsa)
|
||||
is_hol = bool(getattr(info, "is_holiday", False))
|
||||
hol_label = getattr(info, "holiday_label", "")
|
||||
line_no = emit_header_holiday(line_no, is_hol, hol_label)
|
||||
|
||||
return line_no
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Hat sensörleri + brülör header
|
||||
# ----------------------------------------------------------------------
|
||||
def _fmt_temp(t: Optional[float]) -> str:
|
||||
if t is None:
|
||||
return "None°C"
|
||||
return f"{t:5.2f}°C"
|
||||
|
||||
|
||||
def _read_line_temps_from_config() -> Dict[str, Optional[float]]:
|
||||
"""
|
||||
OUTSIDE_SENSOR_ID, BURNER_OUT_SENSOR_ID ve RETURN_LINE_SENSOR_IDS
|
||||
için DS18B20 üzerinden anlık sıcaklıkları okur.
|
||||
"""
|
||||
temps: Dict[str, Optional[float]] = {}
|
||||
if DS18B20Sensor is None or cfg is None:
|
||||
return temps
|
||||
|
||||
outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", "") or ""
|
||||
out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", "") or ""
|
||||
ret_ids = list(getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) or [])
|
||||
|
||||
all_ids = []
|
||||
if outside_id:
|
||||
all_ids.append(outside_id)
|
||||
if out_id:
|
||||
all_ids.append(out_id)
|
||||
all_ids.extend([sid for sid in ret_ids if sid])
|
||||
|
||||
for sid in all_ids:
|
||||
try:
|
||||
sensor = DS18B20Sensor(serial=sid)
|
||||
temps[sid] = sensor.read_temperature()
|
||||
except Exception:
|
||||
temps[sid] = None
|
||||
|
||||
return temps
|
||||
|
||||
|
||||
def log_burner_header(
|
||||
now: datetime,
|
||||
mode: str,
|
||||
season: "SeasonController",
|
||||
building_avg: Optional[float],
|
||||
outside_c: Optional[float],
|
||||
used_out_c: Optional[float],
|
||||
fire_sp: float,
|
||||
burner_on: bool,
|
||||
pumps_on: Iterable[str],
|
||||
) -> None:
|
||||
"""
|
||||
burner.BurnerController.tick() her çağrıldığında; header + bina özet +
|
||||
hat sensörleri + sistem ısı + brülör/devirdaim satırlarını üretir.
|
||||
"""
|
||||
try:
|
||||
# 1) Üst blok
|
||||
if season is not None and SeasonController is not None:
|
||||
line_no = emit_top_block(now, season)
|
||||
else:
|
||||
line_no = emit_header_version(1, now)
|
||||
|
||||
# 2) Bina ısı satırı
|
||||
mode = (mode or "?").upper()
|
||||
cfg_mode = ( str(getattr(cfg, "BUILD_BURNER", mode)).upper() if cfg is not None else mode )
|
||||
outside_limit = float(getattr(cfg_v, "OUTSIDE_LIMIT_HEAT_C", 0.0))
|
||||
min_c = None
|
||||
max_c = None
|
||||
avg_c = building_avg
|
||||
|
||||
body_build = (
|
||||
f"Build [{mode}-{cfg_mode}] "
|
||||
f"Heats[Min:{_fmt_temp(min_c)} Avg:{_fmt_temp(avg_c)} Max:{_fmt_temp(max_c)}] L:{outside_limit}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body_build))
|
||||
line_no += 1
|
||||
|
||||
# 3) Hat sensörleri
|
||||
line_temps = _read_line_temps_from_config()
|
||||
outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", "") if cfg is not None else ""
|
||||
outside_name = (
|
||||
getattr(cfg, "OUTSIDE_SENSOR_NAME", "Dış Isı 1")
|
||||
if cfg is not None
|
||||
else "Dış Isı 1"
|
||||
)
|
||||
out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", "") if cfg is not None else ""
|
||||
out_name = (
|
||||
getattr(cfg, "BURNER_OUT_SENSOR_NAME", "Çıkış Isı 2")
|
||||
if cfg is not None
|
||||
else "Çıkış Isı 2"
|
||||
)
|
||||
ret_ids = (
|
||||
list(getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) or [])
|
||||
if cfg is not None
|
||||
else []
|
||||
)
|
||||
name_map = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP", {}) if cfg is not None else {}
|
||||
|
||||
if outside_id:
|
||||
t = line_temps.get(outside_id, outside_c)
|
||||
body = f"{outside_name:<15}: {_fmt_temp(t)} - {outside_id} "
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
if out_id:
|
||||
t = line_temps.get(out_id)
|
||||
body = f"{out_name:<15}: {_fmt_temp(t)} - {out_id} "
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
for sid in ret_ids:
|
||||
if not sid:
|
||||
continue
|
||||
t = line_temps.get(sid)
|
||||
nm = name_map.get(sid, sid)
|
||||
body = f"{nm:<15}: {_fmt_temp(t)} - {sid} "
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# 4) Sistem ısı satırı (used_out + weekend/comfort offset)
|
||||
weekend_boost = (
|
||||
float(getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0))
|
||||
if cfg_v is not None
|
||||
else 0.0
|
||||
)
|
||||
comfort_off = (
|
||||
float(getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0))
|
||||
if cfg_v is not None
|
||||
else 0.0
|
||||
)
|
||||
body_sys = (
|
||||
f"Sistem Isı : {_fmt_temp(used_out_c)} "
|
||||
f"[w:{weekend_boost:g} c:{comfort_off:g}]"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body_sys))
|
||||
line_no += 1
|
||||
|
||||
# 5) Brülör / devirdaim satırları (istatistikler şimdilik 0)
|
||||
max_out = (
|
||||
float(getattr(cfg_v, "MAX_OUTLET_C", fire_sp))
|
||||
if cfg_v is not None
|
||||
else fire_sp
|
||||
)
|
||||
br_status = "<CALISIYOR>" if burner_on else "<CALISMIYOR>"
|
||||
br_flag = 1 if burner_on else 0
|
||||
|
||||
body_br = (
|
||||
f"Brulor Motor : {br_status} [{br_flag}] "
|
||||
f"0 00:00:00 00:00:00 L:{max_out:.1f}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body_br))
|
||||
line_no += 1
|
||||
|
||||
pumps_on_list = list(pumps_on or [])
|
||||
pump_count = len(pumps_on_list)
|
||||
dev_status = "<CALISIYOR>" if pump_count > 0 else "<CALISMIYOR>"
|
||||
min_ret = (
|
||||
float(getattr(cfg_v, "CIRCULATION_MIN_RETURN_C", 25.0))
|
||||
if cfg_v is not None
|
||||
else 25.0
|
||||
)
|
||||
pumps_str = ",".join(pumps_on_list) if pumps_on_list else "-"
|
||||
|
||||
body_dev = (
|
||||
f"Devirdaim Mot: {dev_status} "
|
||||
f"[{pump_count}] 0 00:00:00 00:00:00 "
|
||||
f"L:{pumps_str} {min_ret:.1f}"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body_dev))
|
||||
|
||||
except Exception as exc:
|
||||
print("BRULOR log_burner_header ERROR:", exc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Basit standalone test
|
||||
now = datetime.now()
|
||||
if SeasonController is None:
|
||||
emit_header_version(1, now)
|
||||
else:
|
||||
sc = SeasonController()
|
||||
log_burner_header(
|
||||
now=now,
|
||||
mode="F",
|
||||
season=sc,
|
||||
building_avg=None,
|
||||
outside_c=None,
|
||||
used_out_c=None,
|
||||
fire_sp=45.0,
|
||||
burner_on=False,
|
||||
pumps_on=(),
|
||||
)
|
||||
608
ebuild/io/zlegacy_syslog.py
Normal file
608
ebuild/io/zlegacy_syslog.py
Normal file
@@ -0,0 +1,608 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "legacy_syslog"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "Legacy tarzı syslog çıktısı üreten köprü"
|
||||
__version__ = "0.2.1"
|
||||
__date__ = "2025-11-22"
|
||||
|
||||
"""
|
||||
ebuild/io/legacy_syslog.py
|
||||
|
||||
Revision : 2025-11-22
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
Eski Rasp2 tabanlı sistemin syslog çıktısını, yeni ebuild mimarisi ile
|
||||
uyumlu ve okunaklı şekilde üretir. Çıktı şu ana bloklardan oluşur:
|
||||
|
||||
1) Üst bilgi:
|
||||
- Versiyon ve zaman satırı
|
||||
- Güneş bilgisi (sunrise / sunset, sistem On/Off, lisans id)
|
||||
- Mevsim bilgisi (season, bahar dönemi bilgisi)
|
||||
- Tatil bilgisi (varsa adıyla)
|
||||
|
||||
2) Bina ısı bilgisi
|
||||
- Bina Isı : [ min - avg - max ]
|
||||
|
||||
3) Hat sensörleri (burner.py içinden doldurulan kısım):
|
||||
- Dış Isı 1
|
||||
- Çıkış Isı 2
|
||||
- Dönüş hatları (isim map'inden)
|
||||
|
||||
4) Used dış ısı
|
||||
5) Brülör / devirdaim / özet satırı
|
||||
|
||||
Not
|
||||
---
|
||||
Bu modül sadece formatlama ve çıktı üretiminden sorumludur. Gerçek
|
||||
ölçümler ve kontrol kararları üst katmanlardan (HeatEngine, Burner,
|
||||
Building, Environment, SeasonController vb.) alınır.
|
||||
"""
|
||||
# Bu modül gerçekten hangi path'ten import ediliyor, görmek için:
|
||||
# ---------------------------------------------------------
|
||||
def _safe_import(desc, import_func):
|
||||
"""
|
||||
desc: ekranda görünecek ad (örn: 'Building', 'legacy_syslog')
|
||||
import_func: gerçek import'u yapan lambda
|
||||
"""
|
||||
try:
|
||||
obj = import_func()
|
||||
#print(f"legacy_syslog.py [IMPORT OK] {desc} ->", obj)
|
||||
return obj
|
||||
except Exception as e:
|
||||
print(f"legacy_syslog.py [IMPORT FAIL] {desc}: {e}")
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
from datetime import datetime, time
|
||||
from typing import Optional
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
try:
|
||||
# SeasonController ve konfig
|
||||
from ..core.season import SeasonController
|
||||
cfg = _safe_import( "config_statics", lambda: __import__("ebuild.config_statics", fromlist=["*"]),)
|
||||
cfv = _safe_import( "config_runtime", lambda: __import__("ebuild.config_runtime", fromlist=["*"]),)
|
||||
#from .. import config_statics as cfg
|
||||
except ImportError: # test / standalone
|
||||
SeasonController = None # type: ignore
|
||||
cfg = None # type: ignore
|
||||
cfv = None
|
||||
print("SeasonController, config_statics import ERROR")
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Logger kurulumu (Syslog + stdout)
|
||||
# ----------------------------------------------------------------------
|
||||
_LOGGER: Optional[logging.Logger] = None
|
||||
|
||||
|
||||
def _get_logger() -> logging.Logger:
|
||||
global _LOGGER
|
||||
if _LOGGER is not None:
|
||||
return _LOGGER
|
||||
#print("logger..1:", stream_fmt)
|
||||
logger = logging.getLogger("BRULOR")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Aynı handler'ları ikinci kez eklemeyelim
|
||||
if not logger.handlers:
|
||||
# Syslog handler (Linux: /dev/log)
|
||||
try:
|
||||
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
# Syslog mesaj formatı: "BRULOR: [ 1 ... ]"
|
||||
fmt = logging.Formatter("%(name)s: %(message)s")
|
||||
syslog_handler.setFormatter(fmt)
|
||||
logger.addHandler(syslog_handler)
|
||||
except Exception:
|
||||
# /dev/log yoksa sessizce geç; sadece stdout'a yazacağız
|
||||
pass
|
||||
|
||||
# Konsol çıktısı (debug için)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_fmt = logging.Formatter("INFO:BRULOR:%(message)s")
|
||||
stream_handler.setFormatter(stream_fmt)
|
||||
logger.addHandler(stream_handler)
|
||||
print("logger..2:", stream_fmt)
|
||||
_LOGGER = logger
|
||||
return logger
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Temel çıktı fonksiyonları
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def send_legacy_syslog(message: str) -> None:
|
||||
"""
|
||||
Verilen mesajı legacy syslog formatına uygun şekilde ilgili hedefe gönderir.
|
||||
- Syslog (/dev/log) → program adı: BRULOR
|
||||
- Aynı zamanda stdout'a da yazar (DEBUG amaçlı)
|
||||
"""
|
||||
#print("send_legacy_syslog BRULOR:", message)
|
||||
try:
|
||||
logger = _get_logger()
|
||||
logger.info(message)
|
||||
except Exception as e:
|
||||
# Logger bir sebeple çökerse bile BRULOR satırını kaybetmeyelim
|
||||
print("BRULOR:", message, f"(logger error: {e})")
|
||||
|
||||
|
||||
def format_line(line_no: int, body: str) -> str:
|
||||
"""
|
||||
BRULOR satırını klasik formata göre hazırlar.
|
||||
|
||||
Örnek:
|
||||
line_no = 2, body = "Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094"
|
||||
|
||||
"[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]"
|
||||
|
||||
Not:
|
||||
Burada "BRULOR" yazmıyoruz; syslog program adı zaten BRULOR olacak.
|
||||
"""
|
||||
return f"[{line_no:3d} {body}]"
|
||||
|
||||
|
||||
def _format_version_3part(ver: str) -> str:
|
||||
"""
|
||||
__version__ string'ini "00.02.01" formatına çevirir.
|
||||
Örnek:
|
||||
"0.2.1" → "00.02.01"
|
||||
"""
|
||||
parts = (ver or "").split(".")
|
||||
nums = []
|
||||
for p in parts:
|
||||
try:
|
||||
nums.append(int(p))
|
||||
except ValueError:
|
||||
nums.append(0)
|
||||
while len(nums) < 3:
|
||||
nums.append(0)
|
||||
return f"{nums[0]:02d}.{nums[1]:02d}.{nums[2]:02d}"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Üst blok (header) üreticiler
|
||||
# ----------------------------------------------------------------------
|
||||
def emit_header_version(line_no: int, now: datetime) -> int:
|
||||
"""
|
||||
1. satır: versiyon + zaman bilgisi.
|
||||
Örnek:
|
||||
************** 00.02.01 2025-11-22 18:15:00 *************
|
||||
"""
|
||||
v_str = _format_version_3part(__version__)
|
||||
body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def emit_header_sun_and_system(
|
||||
line_no: int,
|
||||
sunrise: Optional[time],
|
||||
sunset: Optional[time],
|
||||
system_on: bool,
|
||||
licence_id: int,
|
||||
) -> int:
|
||||
"""
|
||||
2. satır: Güneş bilgisi + Sistem On/Off + Lisans id.
|
||||
|
||||
Örnek:
|
||||
[ 2 Sunrise:07:39 Sunset:17:29 Sistem: On Lic:10094]
|
||||
"""
|
||||
sun_str = ""
|
||||
if sunrise is not None:
|
||||
sun_str += f"Sunrise:{sunrise.strftime('%H:%M')} "
|
||||
if sunset is not None:
|
||||
sun_str += f"Sunset:{sunset.strftime('%H:%M')} "
|
||||
|
||||
sys_str = "On" if system_on else "Off"
|
||||
body = f"{sun_str}Sistem: {sys_str} Lic:{licence_id}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
def _only_date(s: str) -> str:
|
||||
"""
|
||||
ISO tarih-zaman stringinden sadece YYYY-MM-DD kısmını alır.
|
||||
Örn: '2025-09-23T16:33:10.687982+03:00' → '2025-09-23'
|
||||
"""
|
||||
if not s:
|
||||
return "--"
|
||||
s = s.strip()
|
||||
if "T" in s:
|
||||
return s.split("T", 1)[0]
|
||||
return s
|
||||
|
||||
def emit_header_season(
|
||||
line_no: int,
|
||||
season_ctrl: SeasonController,
|
||||
) -> int:
|
||||
"""
|
||||
Sunrise satırının altına mevsim + (varsa) bahar tasarruf dönemi satırını basar.
|
||||
|
||||
Beklenen format:
|
||||
|
||||
BRULOR [ 3 season : Sonbahar 2025-09-23 - 2025-12-20 [89 pass:60 kalan:28] ]
|
||||
BRULOR [ 4 bahar : 2025-09-23 - 2025-10-13 ]
|
||||
|
||||
Notlar:
|
||||
- Bilgiler SeasonController.info içinden okunur (dict veya obje olabilir).
|
||||
- bahar_tasarruf True DEĞİLSE bahar satırı hiç basılmaz.
|
||||
"""
|
||||
|
||||
# SeasonController.info hem dict hem obje olabilir, ikisini de destekle
|
||||
info = getattr(season_ctrl, "info", season_ctrl)
|
||||
|
||||
def _get(field: str, default=None):
|
||||
if isinstance(info, dict):
|
||||
return info.get(field, default)
|
||||
return getattr(info, field, default)
|
||||
|
||||
# ---- 3. satır: season ----
|
||||
season_name = _get("season", "Unknown")
|
||||
season_start = _only_date(_get("season_start", ""))
|
||||
season_end = _only_date(_get("season_end", ""))
|
||||
season_day = _get("season_day", "")
|
||||
season_passed = _get("season_passed", "")
|
||||
season_remain = _get("season_remaining", "")
|
||||
|
||||
body = (
|
||||
f"season : {season_name} {season_start} - {season_end} "
|
||||
f"[{season_day} pass:{season_passed} kalan:{season_remain}]"
|
||||
)
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# ---- 4. satır: bahar dönemi (SADECE aktifse) ----
|
||||
bahar_tasarruf = bool(_get("bahar_tasarruf", False))
|
||||
if bahar_tasarruf:
|
||||
bahar_basx = _only_date(_get("bahar_basx", ""))
|
||||
bahar_bitx = _only_date(_get("bahar_bitx", ""))
|
||||
body = f"bahar : {bahar_basx} - {bahar_bitx}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
return line_no
|
||||
|
||||
|
||||
def emit_header_holiday(
|
||||
line_no: int,
|
||||
is_holiday: bool,
|
||||
holiday_label: str,
|
||||
) -> int:
|
||||
"""
|
||||
Tatil satırı (sunrise + season altına).
|
||||
|
||||
Kurallar:
|
||||
- Tatil yoksa (False) HİÇ satır basma.
|
||||
- Tatil varsa:
|
||||
[ 5 Tatil: True Adı: Cumhuriyet Bayramı]
|
||||
"""
|
||||
if not is_holiday:
|
||||
return line_no
|
||||
|
||||
label = holiday_label or ""
|
||||
body = f"Tatil: True Adı: {label}"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
return line_no + 1
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Dışarıdan çağrılacak üst-blok helper
|
||||
# ----------------------------------------------------------------------
|
||||
def emit_top_block(
|
||||
now: datetime,
|
||||
season_ctrl: SeasonController,
|
||||
) -> int:
|
||||
"""
|
||||
F veya B modundan bağımsız olarak, her tick başında üst bilgiyi üretir.
|
||||
|
||||
Sıra:
|
||||
1) Versiyon + zaman
|
||||
2) Sunrise / Sunset / Sistem: On/Off / Lic
|
||||
3) Mevsim bilgisi (SeasonController.to_syslog_lines() → sadeleştirilmiş)
|
||||
4) Tatil bilgisi (sadece tatil varsa)
|
||||
5) Bir sonraki satır numarasını döndürür (bina ısı satırları için).
|
||||
"""
|
||||
line_no = 1
|
||||
|
||||
# 1) Versiyon
|
||||
line_no = emit_header_version(line_no, now)
|
||||
|
||||
# Konfigten sistem ve lisans bilgileri
|
||||
if cfg is not None:
|
||||
licence_id = int(getattr(cfg, "BUILDING_LICENCEID", 0))
|
||||
system_onoff = int(getattr(cfg, "BUILDING_SYSTEMONOFF", 1))
|
||||
else:
|
||||
licence_id = 0
|
||||
system_onoff = 1
|
||||
|
||||
system_on = (system_onoff == 1)
|
||||
|
||||
# 2) Güneş + Sistem / Lisans
|
||||
sunrise = season_ctrl.info.sunrise
|
||||
sunset = season_ctrl.info.sunset
|
||||
|
||||
line_no = emit_header_sun_and_system(
|
||||
line_no=line_no,
|
||||
sunrise=sunrise,
|
||||
sunset=sunset,
|
||||
system_on=system_on,
|
||||
licence_id=licence_id,
|
||||
)
|
||||
|
||||
# 3) Mevsim bilgisi (sunrise ALTINA)
|
||||
line_no = emit_header_season(line_no, season_ctrl)
|
||||
|
||||
# 4) Tatil bilgisi (sadece True ise)
|
||||
line_no = emit_header_holiday(
|
||||
line_no=line_no,
|
||||
is_holiday=season_ctrl.info.is_holiday,
|
||||
holiday_label=season_ctrl.info.holiday_label,
|
||||
)
|
||||
|
||||
# Sonraki satır: bina ısı / dış ısı / F-B detayları için kullanılacak
|
||||
return line_no
|
||||
|
||||
def _fmt_temp(val: Optional[float]) -> str:
|
||||
return "None" if val is None else f"{val:.2f}"
|
||||
PUMP_SHORT_MAP = {
|
||||
"circulation_a": "A",
|
||||
"circulation_b": "B",
|
||||
"circ_1": "A",
|
||||
"circ_2": "B",
|
||||
}
|
||||
|
||||
def _short_pump_name(ch: str) -> str:
|
||||
if ch in PUMP_SHORT_MAP:
|
||||
return PUMP_SHORT_MAP[ch]
|
||||
# sonu _a/_b ise yine yakala
|
||||
if ch.endswith("_a"):
|
||||
return "A"
|
||||
if ch.endswith("_b"):
|
||||
return "B"
|
||||
return ch # tanımıyorsak orijinal ismi yaz
|
||||
|
||||
def log_burner_header(
|
||||
now: datetime,
|
||||
mode: str,
|
||||
season,
|
||||
building_avg: Optional[float],
|
||||
outside_c: Optional[float],
|
||||
used_out_c: Optional[float],
|
||||
fire_sp: float,
|
||||
burner_on: bool,
|
||||
pumps_on,
|
||||
line_temps: Optional[Dict[str, Optional[float]]] = None,
|
||||
ign_stats=None,
|
||||
circ_stats=None,
|
||||
) -> None:
|
||||
"""
|
||||
BurnerController'dan tek çağrıyla BRULOR bloğunu basar.
|
||||
|
||||
- Önce üst blok (versiyon + güneş + mevsim + tatil)
|
||||
- Sonra bina ısı satırı
|
||||
- Dış ısı / used dış ısı
|
||||
- Son satırda brülör ve pompaların durumu
|
||||
"""
|
||||
#print("log_burner_header CALLED", season)
|
||||
# 1) Üst header blok
|
||||
if season is None:
|
||||
# SeasonController yoksa, sadece versiyon ve zaman bas
|
||||
line_no = 1
|
||||
v_str = _format_version_3part(__version__)
|
||||
body = f"************** {v_str} {now.strftime('%Y-%m-%d %H:%M:%S')} *************"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
else:
|
||||
line_no = emit_top_block(now, season)
|
||||
|
||||
# 2) Bina ısı satırı
|
||||
if building_avg is None:
|
||||
min_s = "None"
|
||||
avg_s = "None"
|
||||
max_s = "None"
|
||||
else:
|
||||
# Şimdilik min=avg=max gibi davranalım; ileride gerçek min/max eklenebilir
|
||||
min_s = f"{building_min:5.2f}"
|
||||
avg_s = f"{building_avg:5.2f}"
|
||||
max_s = f"{building_max:5.2f}"
|
||||
|
||||
# config’teki mod
|
||||
cfg_mode = getattr(cfg, "BUILD_BURNER", "?") if cfg is not None else "?"
|
||||
body = f"Build [{mode}-{cfg_mode}] Heats[Min:{min_s}°C Avg:{avg_s}°C Max:{max_s}°C]"
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# line_temps yoksa, burayı pas geç
|
||||
if line_temps is not None:
|
||||
# CONFIG'TEN ID'LERİ AL
|
||||
outside_id = getattr(cfg, "OUTSIDE_SENSOR_ID", None) if cfg is not None else None
|
||||
out_id = getattr(cfg, "BURNER_OUT_SENSOR_ID", None) if cfg is not None else None
|
||||
ret_ids = getattr(cfg, "RETURN_LINE_SENSOR_IDS", []) if cfg is not None else []
|
||||
ret_map = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP", {}) if cfg is not None else {}
|
||||
line_no = 4 # dış ısı satırı numarası
|
||||
|
||||
# 4: Dis isi
|
||||
if outside_id and outside_id in line_temps:
|
||||
t = line_temps.get(outside_id)
|
||||
namex = getattr(cfg, "OUTSIDE_SENSOR_NAME", "Dis isi") if cfg is not None else "Dis isi"
|
||||
msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {outside_id} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 5: Cikis isi
|
||||
if out_id and out_id in line_temps:
|
||||
t = line_temps.get(out_id)
|
||||
namex = getattr(cfg, "BURNER_OUT_SENSOR_NAME", "Cikis isi") if cfg is not None else "Cıkıs isi"
|
||||
msg = f"{namex:<15.15}: {_fmt_temp(t)}°C - {out_id} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 6..N: Donus isi X
|
||||
namex = getattr(cfg, "RETURN_LINE_SENSOR_NAME_MAP",[])
|
||||
for sid in ret_ids:
|
||||
if sid not in line_temps:
|
||||
continue
|
||||
t = line_temps.get(sid)
|
||||
try:
|
||||
namexx = ret_map.get(sid)
|
||||
except:
|
||||
namex = '???'
|
||||
msg = f"{namexx:<15.15}: {_fmt_temp(t)}°C - {sid} "
|
||||
send_legacy_syslog(format_line(line_no, msg))
|
||||
line_no += 1
|
||||
|
||||
# 3) Dış ısı / used dış ısı
|
||||
out_str = "--"
|
||||
used_str = "--"
|
||||
if outside_c is not None:
|
||||
out_str = f"{outside_c:5.2f}"
|
||||
if used_out_c is not None:
|
||||
used_str = f"{used_out_c:5.2f}"
|
||||
usedxx = "Sistem Isı"
|
||||
|
||||
#------------------------------------------------------------------
|
||||
# 9: Sistem Isı - Used + [WEEKEND_HEAT_BOOST_C, BURNER_COMFORT_OFFSET_C]
|
||||
# ------------------------------------------------------------------
|
||||
used_val = used_out_c if used_out_c is not None else None
|
||||
used_str = "None" if used_val is None else f"{used_val:.2f}"
|
||||
|
||||
if cfv is not None:
|
||||
w_val = float(getattr(cfv, "WEEKEND_HEAT_BOOST_C", 0.0) or 0.0)
|
||||
c_val = float(getattr(cfv, "BURNER_COMFORT_OFFSET_C", 0.0) or 0.0)
|
||||
else:
|
||||
w_val = 0.0
|
||||
c_val = 0.0
|
||||
|
||||
# Sayıları [2, 1] gibi, gereksiz .0’sız yazalım
|
||||
def _fmt_num(x: float) -> str:
|
||||
if x == int(x):
|
||||
return str(int(x))
|
||||
return f"{x:g}"
|
||||
|
||||
sabitler_str = f"[w:{_fmt_num(w_val)} c:{_fmt_num(c_val)}]"
|
||||
|
||||
body = f"{usedxx:<15.15}: {used_str}°C {sabitler_str} "
|
||||
send_legacy_syslog(format_line(line_no, body))
|
||||
line_no += 1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 11: Brülör Motor satırı (MAX_OUTLET_C ile)
|
||||
# ------------------------------------------------------------------
|
||||
if cfv is not None:
|
||||
max_out = float(getattr(cfv, "MAX_OUTLET_C", 45.0) or 45.0)
|
||||
else:
|
||||
max_out = 45.0
|
||||
|
||||
if cfv is not None:
|
||||
min_ret = float(getattr(cfv, "CIRCULATION_MIN_RETURN_C", 25.0) or 25.0)
|
||||
else:
|
||||
min_ret = 25.0
|
||||
|
||||
br_status = "<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
|
||||
|
||||
# Config’te default=1 olan pompaları da topla (cfg_default_pumps)
|
||||
cfg_default_pumps = []
|
||||
for logical_name, info in circ_cfg.items():
|
||||
ch = info.get("channel")
|
||||
if ch and info.get("default", 0):
|
||||
cfg_default_pumps.append(ch)
|
||||
|
||||
# Kısa isim A/B istersek:
|
||||
def _logical_to_short(name: str) -> str:
|
||||
if name == "circ_1":
|
||||
return "A"
|
||||
if name == "circ_2":
|
||||
return "B"
|
||||
return name
|
||||
|
||||
pump_count = len(cfg_default_pumps)
|
||||
dev_status = "<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))
|
||||
362
ebuild/io/zrelay_driver.py
Normal file
362
ebuild/io/zrelay_driver.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
__title__ = "relay_driver"
|
||||
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
|
||||
__purpose__ = "GPIO röle sürücüsü + brülör grup soyutlaması"
|
||||
__version__ = "0.4.0"
|
||||
__date__ = "2025-11-22"
|
||||
|
||||
"""
|
||||
ebuild/io/relay_driver.py
|
||||
|
||||
Revision : 2025-11-22
|
||||
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
|
||||
|
||||
Amaç
|
||||
-----
|
||||
- Soyut kanal isimleri ile (igniter, circulation_a, ...) GPIO pin sürmek.
|
||||
- config_statics.BURNER_GROUPS üzerinden brülör gruplarını yönetmek.
|
||||
- Her kanal için:
|
||||
* ON/OFF sayacı
|
||||
* Son çalışma süresi
|
||||
* Toplam çalışma süresi
|
||||
* Şu anki çalışma süresi (eğer röle ON ise, anlık akan süre)
|
||||
istatistiklerini tutmak.
|
||||
|
||||
Kullanım
|
||||
--------
|
||||
- Temel kanal API:
|
||||
drv.channels() → ['igniter', 'circulation_a', ...]
|
||||
drv.set_channel("igniter", True/False)
|
||||
drv.get_stats("igniter") → RelayStats
|
||||
drv.get_channel_state("igniter") → bool (şu an ON mu?)
|
||||
|
||||
- Brülör grup API:
|
||||
drv.burners() → [0, 1, ...]
|
||||
drv.burner_info(0) → config_statics.BURNER_GROUPS[0]
|
||||
drv.igniter_channel(0) → "igniter"
|
||||
drv.all_pumps(0) → ['circulation_a', 'circulation_b', ...]
|
||||
drv.enabled_pumps(0) → default=1 olan pompalar
|
||||
drv.active_pumps(0) → şu anda gerçekten ON olan pompalar
|
||||
|
||||
Bu API'ler burner.py ve legacy_syslog.py tarafından kullanılmak üzere tasarlanmıştır.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Iterable, Tuple, List
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
_HAS_GPIO = True
|
||||
except ImportError:
|
||||
_HAS_GPIO = False
|
||||
|
||||
from .. import config_statics as cfg
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# İstatistik yapısı
|
||||
# -------------------------------------------------------------------
|
||||
@dataclass
|
||||
class RelayStats:
|
||||
"""
|
||||
Tek bir röle kanalı için istatistikler.
|
||||
|
||||
- on_count : kaç defa ON'a çekildi
|
||||
- last_on_ts : en son ON'a çekildiği zaman (epoch saniye)
|
||||
- last_off_ts : en son OFF olduğu zaman (epoch saniye)
|
||||
- last_duration_s : en son ON periyodunun süresi (saniye)
|
||||
- total_on_s : bugüne kadar toplam ON kalma süresi (saniye)
|
||||
"""
|
||||
on_count: int = 0
|
||||
last_on_ts: Optional[float] = None
|
||||
last_off_ts: Optional[float] = None
|
||||
last_duration_s: float = 0.0
|
||||
total_on_s: float = 0.0
|
||||
|
||||
def on(self, now: float) -> None:
|
||||
"""
|
||||
Kanal ON'a çekildiğinde çağrılır.
|
||||
Aynı ON periyodu içinde tekrar çağrılırsa sayaç artmaz.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
self.last_on_ts = now
|
||||
self.on_count += 1
|
||||
|
||||
def off(self, now: float) -> None:
|
||||
"""
|
||||
Kanal OFF'a çekildiğinde çağrılır.
|
||||
Son ON zamanına göre süre hesaplanır, last_duration_s ve total_on_s güncellenir.
|
||||
"""
|
||||
if self.last_on_ts is not None:
|
||||
dur = max(0.0, now - self.last_on_ts)
|
||||
self.last_duration_s = dur
|
||||
self.total_on_s += dur
|
||||
self.last_on_ts = None
|
||||
self.last_off_ts = now
|
||||
|
||||
def current_duration(self, now: Optional[float] = None) -> float:
|
||||
"""
|
||||
Kanal şu anda ON ise, bu ON periyodunun şu ana kadarki süresini döndürür.
|
||||
OFF ise 0.0 döner.
|
||||
"""
|
||||
if self.last_on_ts is None:
|
||||
return 0.0
|
||||
if now is None:
|
||||
now = time.time()
|
||||
return max(0.0, now - self.last_on_ts)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Ana sürücü
|
||||
# -------------------------------------------------------------------
|
||||
class RelayDriver:
|
||||
"""
|
||||
Basit bir röle sürücüsü.
|
||||
|
||||
- Soyut kanal isimleri: RELAY_GPIO dict'indeki anahtarlar
|
||||
- Brülör grup API'si:
|
||||
* burners() → mevcut brülör id listesi
|
||||
* burner_info(bid) → config_statics.BURNER_GROUPS[bid]
|
||||
* igniter_channel(bid) → ateşleme kanal adı
|
||||
* set_igniter(bid, state)
|
||||
* set_pump(bid, pump_name, state)
|
||||
* enabled_pumps(bid) → default=1 olan isimler (konfig default)
|
||||
* all_pumps(bid) → tüm pompa isimleri
|
||||
* active_pumps(bid) → şu anda ON olan pompa isimleri
|
||||
"""
|
||||
|
||||
def __init__(self, onoff=False) -> None:
|
||||
print("RelayDriver yükleniyor…")
|
||||
|
||||
# Konfigten kanal → GPIO pin map
|
||||
self._pin_map: Dict[str, int] = dict(getattr(cfg, "RELAY_GPIO", {}))
|
||||
|
||||
# Her kanal için istatistik objesi
|
||||
self._stats: Dict[str, RelayStats] = {
|
||||
ch: RelayStats() for ch in self._pin_map.keys()
|
||||
}
|
||||
|
||||
# Brülör grupları
|
||||
self._burner_groups: Dict[int, dict] = dict(getattr(cfg, "BURNER_GROUPS", {}))
|
||||
|
||||
if not self._pin_map:
|
||||
raise RuntimeError("RelayDriver: RELAY_GPIO boş.")
|
||||
|
||||
if _HAS_GPIO:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False) # aynı pini yeniden kullanırken uyarı verme
|
||||
for ch, pin in self._pin_map.items():
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
GPIO.output(pin, GPIO.LOW)
|
||||
else:
|
||||
print("⚠️ GPIO bulunamadı, DRY-RUN modunda çalışıyorum.")
|
||||
|
||||
# Başlangıçta HER ŞEYİ KAPALIYA ÇEK
|
||||
try:
|
||||
if onoff:
|
||||
self.all_off()
|
||||
except Exception:
|
||||
# Çok dert etmeyelim, en kötü GPIO yoktur, vs.
|
||||
pass
|
||||
# -----------------------------------------------------
|
||||
# Temel kanal API
|
||||
# -----------------------------------------------------
|
||||
def channels(self) -> Iterable[str]:
|
||||
"""
|
||||
Mevcut kanal isimlerini döndürür.
|
||||
"""
|
||||
return self._pin_map.keys()
|
||||
|
||||
def channel_pin(self, channel: str) -> Optional[int]:
|
||||
"""
|
||||
Verilen kanalın GPIO pin numarasını döndürür.
|
||||
"""
|
||||
return self._pin_map.get(channel)
|
||||
|
||||
def set_channel(self, channel: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen kanalı ON/OFF yapar, GPIO'yu sürer ve istatistikleri günceller.
|
||||
"""
|
||||
if channel not in self._pin_map:
|
||||
return
|
||||
pin = self._pin_map[channel]
|
||||
now = time.time()
|
||||
|
||||
if _HAS_GPIO:
|
||||
GPIO.output(pin, GPIO.HIGH if state else GPIO.LOW)
|
||||
|
||||
st = self._stats[channel]
|
||||
if state:
|
||||
st.on(now)
|
||||
else:
|
||||
st.off(now)
|
||||
|
||||
def get_stats(self, channel: str) -> RelayStats:
|
||||
"""
|
||||
Kanalın istatistik objesini döndürür.
|
||||
"""
|
||||
return self._stats[channel]
|
||||
|
||||
def get_channel_state(self, channel: str) -> bool:
|
||||
"""
|
||||
Kanal şu anda ON mu? (last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
st = self._stats.get(channel)
|
||||
if st is None:
|
||||
return False
|
||||
return st.last_on_ts is not None
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Tüm kanalları güvenli moda çek
|
||||
# -----------------------------------------------------
|
||||
def all_off(self) -> None:
|
||||
"""
|
||||
Tüm röle kanallarını KAPALI (LOW) yapar ve istatistikleri günceller.
|
||||
Özellikle:
|
||||
- Uygulama başlatıldığında "her şey kapalı" garantisi
|
||||
- Çıkış/KeyboardInterrupt anında güvenli kapanış için kullanılır.
|
||||
"""
|
||||
now = time.time()
|
||||
for ch, pin in self._pin_map.items():
|
||||
if _HAS_GPIO:
|
||||
GPIO.output(pin, GPIO.LOW)
|
||||
# stats güncelle
|
||||
st = self._stats.get(ch)
|
||||
if st is not None:
|
||||
st.off(now)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Brülör grup API
|
||||
# -----------------------------------------------------
|
||||
def burners(self) -> Iterable[int]:
|
||||
"""
|
||||
Mevcut brülör id'lerini döndürür.
|
||||
"""
|
||||
return self._burner_groups.keys()
|
||||
|
||||
def burner_info(self, burner_id: int) -> Optional[dict]:
|
||||
"""
|
||||
İlgili brülörün BURNER_GROUPS içindeki konfig dict'ini döndürür.
|
||||
"""
|
||||
return self._burner_groups.get(burner_id)
|
||||
|
||||
def igniter_channel(self, burner_id: int) -> Optional[str]:
|
||||
"""
|
||||
Brülörün igniter kanal adını döndürür.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return None
|
||||
return info.get("igniter", None)
|
||||
|
||||
def all_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Konfigte tanımlı tüm pompa kanal adlarını döndürür (circulation altı).
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
# Her pompa için { "channel": "circulation_a", "pin": 26, "default": 1 } beklenir.
|
||||
return [data["channel"] for _, data in circ.items()]
|
||||
|
||||
def enabled_pumps(self, burner_id: int) -> Iterable[str]:
|
||||
"""
|
||||
Konfigte default=1 işaretli pompa kanal adlarını döndürür.
|
||||
Bu, sistem açıldığında / ısıtma başladığında devreye alınacak default pompaları temsil eder.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
return [
|
||||
data["channel"]
|
||||
for _, data in circ.items()
|
||||
if int(data.get("default", 0)) == 1
|
||||
]
|
||||
|
||||
def active_pumps(self, burner_id: int) -> Tuple[str, ...]:
|
||||
"""
|
||||
Şu anda gerçekten ON olan pompa isimlerini döndürür.
|
||||
(GPIO'da HIGH durumda olan kanallar; RelayStats.last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return tuple()
|
||||
circ = info.get("circulation", {})
|
||||
active: List[str] = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel")
|
||||
if ch in self._stats and self._stats[ch].last_on_ts is not None:
|
||||
active.append(pname)
|
||||
return tuple(active)
|
||||
|
||||
def set_igniter(self, burner_id: int, state: bool) -> None:
|
||||
"""
|
||||
İlgili brülörün igniter kanalını ON/OFF yapar.
|
||||
"""
|
||||
ch = self.igniter_channel(burner_id)
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
|
||||
def set_pump(self, burner_id: int, pump_name: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen brülörün belirtilen pompasını ON/OFF yapar.
|
||||
pump_name: BURNER_GROUPS[..]["circulation"][pump_name]
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return
|
||||
circ = info.get("circulation", {})
|
||||
if pump_name in circ:
|
||||
ch = circ[pump_name]["channel"]
|
||||
self.set_channel(ch, state)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Yardımcı: özet
|
||||
# -----------------------------------------------------
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Kanallar ve brülör gruplarının kısa bir özetini döndürür (debug amaçlı).
|
||||
"""
|
||||
lines: List[str] = []
|
||||
chans = ", ".join(sorted(self._pin_map.keys()))
|
||||
lines.append(f"Kanallar: {chans}")
|
||||
|
||||
lines.append("Brülör grupları:")
|
||||
for bid, info in self._burner_groups.items():
|
||||
name = info.get("name", f"Burner{bid}")
|
||||
loc = info.get("location", "-")
|
||||
ign = info.get("igniter", "igniter")
|
||||
circ = info.get("circulation", {})
|
||||
pumps = []
|
||||
defaults = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel", "?")
|
||||
pumps.append(f"{pname}->{ch}")
|
||||
if int(pdata.get("default", 0)) == 1:
|
||||
defaults.append(pname)
|
||||
lines.append(
|
||||
f" #{bid}: {name} @ {loc} | igniter={ign} | "
|
||||
f"pumps={pumps} | default_on={defaults}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Temizlik
|
||||
# -----------------------------------------------------
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
GPIO pinlerini serbest bırakır.
|
||||
"""
|
||||
if _HAS_GPIO:
|
||||
GPIO.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
drv = RelayDriver()
|
||||
print("\n🧰 RelayDriver Summary")
|
||||
print(drv.summary())
|
||||
Reference in New Issue
Block a user