# -*- 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())