363 lines
12 KiB
Python
363 lines
12 KiB
Python
# -*- 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())
|