ebuild_rasp2/ebuild/io/zrelay_driver.py

363 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "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())