ebuild_rasp2/ebuild/core/building.py

591 lines
21 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__ = "building"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina / daire / sensör ve brülör topolojisi sürücüsü"
__version__ = "0.4.3"
__date__ = "2025-11-22"
"""
ebuild/core/building.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- Statik konfigürasyondan (config_statics) bina, geo ve sensör topolojisini yükler.
- FLAT_AREA içindeki DS18B20 sensörlerini daire/oda bazında yönetir.
- Bina ortalama sıcaklık istatistiklerini üretir.
- Brülör grupları (BURNER_GROUPS) ve hat sensörleri (dış, çıkış, dönüş) için
başlangıçta okunabilir bir özet verir.
- SeasonController ile mevsim / güneş / tatil bilgisi sağlar.
Not
---
Bu modül, hem bağımsız test aracı olarak `python3 -m ebuild.core.building`
şeklinde çalışabilir, hem de BurnerController gibi üst seviye kontrol
modülleri tarafından kullanılabilir.
"""
import os
import json
import time
import datetime
import statistics
from collections import defaultdict
from typing import Dict, List, Optional, Any
from .season import SeasonController
from ..io.dbtext import DBText
from .. import config_statics as cfg_s
from .. import config_runtime as cfg_v
# ------------------------------------------------------------------
# Yardımcı sabitler
# ------------------------------------------------------------------
FlatDirection = {
0: "Kuzey-Kuzey",
1: "Kuzey-Doğu",
2: "Kuzey-Batı",
3: "Güney-Güney",
4: "Güney-Doğu",
5: "Güney-Batı",
6: "Doğu-Doğu",
7: "Batı-Batı",
}
ValveType = {
0: "Yok",
1: "Dijital",
2: "PWM",
3: "Analog",
4: "Enkoder",
5: "Modbus",
6: "CANbus",
}
# ------------------------------------------------------------------
# DS18B20 sensör sınıfı
# ------------------------------------------------------------------
class EDSensor:
"""Tek bir DS18B20 sensörünü temsil eder."""
def __init__(self, serial: str, flat_no: int, flat_direction: int, floor: int, room_no: int = 0) -> None:
self.serial = serial
self.flat_no = flat_no
self.floor = floor
self.room_no = room_no
self.flat_direction = flat_direction
self.device_path = f"/sys/bus/w1/devices/{serial}/w1_slave"
self.flat_check = 0
self.is_connected = True
def read_temperature(self) -> Optional[float]:
try:
with open(self.device_path, "r") as f:
lines = f.readlines()
if lines[0].strip().endswith("YES"):
pos = lines[1].find("t=")
if pos != -1:
raw = lines[1][pos + 2 :]
t_c = float(raw) / 1000.0
self.is_connected = True
return t_c
except Exception:
self.flat_check += 1
self.is_connected = False
return None
# ------------------------------------------------------------------
# Daire nesnesi
# ------------------------------------------------------------------
class Flat:
"""Daire nesnesi: yön, kat ve sensör ilişkisi."""
def __init__(self, flat_no: int, flat_direction: int, sensor: EDSensor, floor: int, room_no: int = 0) -> None:
self.flat_no = flat_no
self.flat_direction = flat_direction
self.sensor = sensor
self.floor = floor
self.room_no = room_no
self.last_heat: Optional[float] = None
self.last_read_time: Optional[datetime.datetime] = None
# ------------------------------------------------------------------
# Bina sınıfı
# ------------------------------------------------------------------
class Building:
"""
Bina seviyesi sürücü.
config_statics üzerinden:
BUILDING_NAME
BUILDING_LOCATION
BUILDING_LABEL
GEO_CITY
GEO_COUNTRY
GEO_TZ
GEO_LAT
GEO_LON
FLAT_AREA
BURNER_GROUPS
OUTSIDE_SENSOR_ID / NAME
BURNER_OUT_SENSOR_ID / NAME
RETURN_LINE_SENSOR_IDS / NAME_MAP
"""
def __init__(
self,
name: Optional[str] = None,
location: Optional[str] = None,
bina_adi: Optional[str] = None,
logtime: bool = False,
logger: Optional[DBText] = None,
) -> None:
# Bina bilgileri config_statics'ten
self.name = name or getattr(cfg_s, "BUILDING_NAME", "Bina")
self.location = location or getattr(cfg_s, "BUILDING_LOCATION", "Ankara")
self.bina_adi = bina_adi or getattr(cfg_s, "BUILDING_LABEL", "A_Binasi")
# Coğrafi bilgiler
self.geo_city = getattr(cfg_s, "GEO_CITY", "Ankara")
self.geo_country = getattr(cfg_s, "GEO_COUNTRY", "Turkey")
self.geo_tz = getattr(cfg_s, "GEO_TZ", "Europe/Istanbul")
self.geo_lat = float(getattr(cfg_s, "GEO_LAT", 39.92077))
self.geo_lon = float(getattr(cfg_s, "GEO_LON", 32.85411))
# Logger
self.logger = logger or DBText(
filename=getattr(cfg_s, "BUILDING_LOG_FILE", "ebina_log.sql"),
table=getattr(cfg_s, "BUILDING_LOG_TABLE", "ebrulor_log"),
app="ESYSTEM",
)
# Çalışma aralığı: 3 dakika (eski koddaki gibi)
interval_min = getattr(cfg_v, "BUILDING_SAMPLE_MINUTES", 3)
self.interval = datetime.timedelta(seconds=60 * interval_min)
self.next_run = datetime.datetime.now()
self.flats: Dict[int, Flat] = {}
self.last_max_floor: Optional[int] = None
self.logtime = logtime
# Season / tatil bilgisi
try:
self.season = SeasonController.from_now()
except Exception as e:
print(f"⚠️ SeasonController.from_now() hata: {e}")
self.season = None
# Daireleri FLAT_AREA'dan oluştur
self._load_statics_from_flat_area()
# Bina genel istatistiği (ilk deneme)
try:
self.get_building_temperature_stats()
except Exception:
pass
def pretty_summary(self) -> str:
"""
Bina durumunu okunabilir bir metin olarak döndür.
Konsola bastığımız bilgilerin string versiyonu.
"""
import io
import contextlib
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
# İstersen istatistiği de özetin içine kat:
self.get_building_temperature_stats()
# Zaten mevcut olan konsol fonksiyonlarını kullanıyoruz
self._print_header()
self._print_sun_and_season()
self._print_burner_topology()
self._print_line_sensors()
self.print_config()
return buf.getvalue()
def get_hat_sensor_by_id(self, sensor_id: str):
"""
Verilen DS18B20 ID'sine göre hat sensör nesnesini döndür.
Yoksa None döndür.
"""
return self.hat_sensors.get(sensor_id) # yapıya göre uyarlarsın
# ------------------------------------------------------------------
# Statik konfigürasyondan daire/sensör yükleme
# ------------------------------------------------------------------
def _load_statics_from_flat_area(self) -> None:
"""config_statics.FLAT_AREA içinden daire/sensörleri yükler."""
self.flats = {}
flat_area = getattr(cfg_s, "FLAT_AREA", [])
for item in flat_area:
serial = item.get("serial")
flat_no = int(item.get("flat_no", 0))
room_no = int(item.get("room_no", 0))
floor = int(item.get("floor", 0))
direction = int(item.get("direction", 0))
if not serial:
continue
sensor = EDSensor(serial, flat_no, direction, floor, room_no=room_no)
# bağlantı testi
sensor.read_temperature()
flat = Flat(flat_no, direction, sensor, floor, room_no=room_no)
self.flats[flat_no] = flat
print("✅ Building: statics yüklendi")
# ------------------------------------------------------------------
# Başlangıç çıktıları
# ------------------------------------------------------------------
def _print_header(self) -> None:
print(f"🏢 Bina adı : {self.bina_adi}")
print(f"📍 Konum : {self.name} / {self.location}")
print(
f"🌍 Geo : {self.geo_city}, {self.geo_country} "
f"({self.geo_lat}, {self.geo_lon}) tz={self.geo_tz}"
)
print(f"🏠 Daire/oda sayısı: {len(self.flats)}\n")
def _print_sun_and_season(self) -> None:
"""SeasonController üzerinden sunrise / sunset ve mevsim / tatil bilgisini yazar."""
if not self.season or not getattr(self.season, "info", None):
print("☀️ Sunrise/Sunset : --:-- / --:--")
print("🧭 Mevsim / Tatil : (bilgi yok)\n")
return
info = self.season.info
sunrise = info.sunrise.strftime("%H:%M") if info.sunrise else "--:--"
sunset = info.sunset.strftime("%H:%M") if info.sunset else "--:--"
print(f"☀️ Sunrise/Sunset : {sunrise} / {sunset}")
if info.season_start and info.season_end:
print(
f"🧭 Mevsim / Tatil : {info.season} "
f"({info.season_start}{info.season_end}), "
f"Tatil={info.is_holiday} {info.holiday_label}"
)
else:
print(
f"🧭 Mevsim / Tatil : {info.season}, "
f"Tatil={info.is_holiday} {info.holiday_label}"
)
print()
def _print_burner_topology(self) -> None:
"""config_statics.BURNER_GROUPS bilgisini okunaklı yazar."""
groups = getattr(cfg_s, "BURNER_GROUPS", {})
if not groups:
print("🔥 Brülör topolojisi: tanımlı değil (BURNER_GROUPS boş).\n")
return
print("🔥 Brülör Topolojisi (config_statics.BURNER_GROUPS):")
for bid, info in groups.items():
name = info.get("name", f"Burner{bid}")
loc = info.get("location", "-")
ign = info.get("igniter_pin", None)
print(f" #{bid}: {name} @ {loc}")
print(f" Igniter pin : {ign}")
circ = info.get("circulation", {})
if not circ:
print(" Pompalar : (tanımlı değil)")
else:
print(" Pompalar :")
for cname, cinfo in circ.items():
pin = cinfo.get("pin")
default = cinfo.get("default", 0)
print(f" - {cname:<7} → pin={pin:<3} default={default}")
print()
# ------------------------------------------------------------------
# Hat sensörleri: dış / çıkış / dönüş
# ------------------------------------------------------------------
def _read_ds18b20_raw(self, serial: str) -> Optional[float]:
"""Seri numarası verilen DS18B20'den tek seferlik okuma."""
path = f"/sys/bus/w1/devices/{serial}/w1_slave"
try:
with open(path, "r") as f:
lines = f.readlines()
if not lines or not lines[0].strip().endswith("YES"):
return None
pos = lines[1].find("t=")
if pos == -1:
return None
raw = lines[1][pos + 2 :]
return float(raw) / 1000.0
except Exception:
return None
def _print_line_sensors(self) -> None:
"""
Dış ısı, kazan çıkış ısısı ve dönüş hat sensörlerini (varsa) ekrana yazar.
config_statics içindeki:
OUTSIDE_SENSOR_ID / OUTSIDE_SENSOR_NAME
BURNER_OUT_SENSOR_ID / BURNER_OUT_SENSOR_NAME
RETURN_LINE_SENSOR_IDS
RETURN_LINE_SENSOR_NAME_MAP
kullanılır.
"""
outside_id = getattr(cfg_s, "OUTSIDE_SENSOR_ID", None)
outside_nm = getattr(cfg_s, "OUTSIDE_SENSOR_NAME", "Dış Isı 1")
burner_id = getattr(cfg_s, "BURNER_OUT_SENSOR_ID", None)
burner_nm = getattr(cfg_s, "BURNER_OUT_SENSOR_NAME", "Çıkış Isı 1")
return_ids = list(getattr(cfg_s, "RETURN_LINE_SENSOR_IDS", []))
name_map = dict(getattr(cfg_s, "RETURN_LINE_SENSOR_NAME_MAP", {}))
if not (outside_id or burner_id or return_ids):
# Hiç tanım yoksa sessizce geç
return
print("🌡️ Hat sensörleri:")
# Dış ısı
if outside_id:
t = self._read_ds18b20_raw(outside_id)
if t is None:
print(f" - {outside_nm:<15}: ❌ Okunamadı ({outside_id})")
else:
print(f" - {outside_nm:<15}: {t:5.2f}°C ({outside_id})")
# Kazan çıkış
if burner_id:
t = self._read_ds18b20_raw(burner_id)
if t is None:
print(f" - {burner_nm:<15}: ❌ Okunamadı ({burner_id})")
else:
print(f" - {burner_nm:<15}: {t:5.2f}°C ({burner_id})")
# Dönüş hatları
for sid in return_ids:
nm = name_map.get(sid, sid)
t = self._read_ds18b20_raw(sid)
if t is None:
print(f" - Dönüş {nm:<10}: ❌ Okunamadı ({sid})")
else:
print(f" - Dönüş {nm:<10}: {t:5.2f}°C ({sid})")
print()
# ------------------------------------------------------------------
# İstatistik fonksiyonları
# ------------------------------------------------------------------
def _summarize(self, values: List[float]) -> dict:
if not values:
return {"min": None, "max": None, "avg": None, "count": 0}
return {
"min": min(values),
"max": max(values),
"avg": round(statistics.mean(values), 2),
"count": len(values),
}
def get_temperature_stats(self) -> str:
stats = {
"floors": defaultdict(list),
"directions": defaultdict(list),
"top_floor": [],
"building": [],
}
max_floor = self.last_max_floor
for flat in self.flats.values():
temp = flat.sensor.read_temperature()
if temp is not None:
flat.last_heat = temp
flat.last_read_time = datetime.datetime.now()
stats["floors"][flat.floor].append(temp)
stats["directions"][flat.flat_direction].append(temp)
stats["building"].append(temp)
if max_floor is not None and flat.floor == max_floor:
stats["top_floor"].append(temp)
result = {
"floors": {f: self._summarize(v) for f, v in stats["floors"].items()},
"directions": {
d: self._summarize(v) for d, v in stats["directions"].items()
},
"top_floor": self._summarize(stats["top_floor"]),
"building": self._summarize(stats["building"]),
}
return json.dumps(result, indent=4, ensure_ascii=False)
def get_last_heat_report(self) -> str:
report = []
for flat in self.flats.values():
temp = flat.sensor.read_temperature()
now = datetime.datetime.now()
if temp is not None:
flat.last_heat = temp
flat.last_read_time = now
report.append(
{
"flat_no": flat.flat_no,
"room_no": flat.room_no,
"direction": flat.flat_direction,
"floor": flat.floor,
"serial": flat.sensor.serial,
"last_heat": flat.last_heat,
"last_read_time": flat.last_read_time.strftime(
"%Y-%m-%d %H:%M:%S"
)
if flat.last_read_time
else None,
"is_connected": flat.sensor.is_connected,
"flat_check": flat.sensor.flat_check,
}
)
return json.dumps(report, indent=4, ensure_ascii=False)
def get_building_temperature_stats(self) -> Optional[dict]:
print("\n🌡️ Bina Genel Isı Bilgisi:")
all_temps: List[float] = []
floor_temps: Dict[int, List[float]] = {}
now = datetime.datetime.now()
# Log zamanı mı?
if now >= self.next_run:
self.logtime = True
self.next_run = now + self.interval
else:
self.logtime = False
self.last_max_floor = None
for flat in self.flats.values():
temp = flat.sensor.read_temperature()
floor = flat.floor
if temp is not None:
flat.last_heat = temp
flat.last_read_time = now
if self.last_max_floor is None or floor > self.last_max_floor:
self.last_max_floor = floor
else:
flat.last_heat = None
flat.last_read_time = None
floor_temps.setdefault(floor, [])
if temp is not None:
all_temps.append(temp)
floor_temps[floor].append(temp)
if self.logtime:
self.logger.insert_event(
source=f"Sensor:{flat.sensor.serial}",
event_type="temperature",
value=temp,
unit="°C",
timestamp=now,
extra=f"Daire {flat.flat_no} Oda {flat.room_no} Kat {flat.floor}, Yön {flat.flat_direction}",
)
if not all_temps or not floor_temps:
print("❌ Hiç sıcaklık verisi alınamadı.\n")
return None
min_temp = min(all_temps)
max_temp = max(all_temps)
avg_temp = sum(all_temps) / len(all_temps)
min_floor = min(floor_temps.keys())
max_floor = max(floor_temps.keys())
avg_min_floor = sum(floor_temps[min_floor]) / max(len(floor_temps[min_floor]), 1)
avg_max_floor = sum(floor_temps[max_floor]) / max(len(floor_temps[max_floor]), 1)
delta_t = abs(avg_max_floor - avg_min_floor)
print(
" Min: {:.2f}°C | Max: {:.2f}°C | Avg: {:.2f}°C | Count: {}".format(
min_temp, max_temp, avg_temp, len(all_temps)
)
)
print(f" En Alt Kat ({min_floor}): {avg_min_floor:.2f}°C")
print(f" En Üst Kat ({max_floor}): {avg_max_floor:.2f}°C")
print(f" 🔺 Delta Isı (Üst - Alt): {delta_t:.2f}°C")
if self.logtime:
self.logger.insert_event(
source="Sensor:min",
event_type="temperature",
value=min_temp,
unit="°C",
timestamp=now,
extra="",
)
self.logger.insert_event(
source="Sensor:max",
event_type="temperature",
value=max_temp,
unit="°C",
timestamp=now,
extra="",
)
self.logger.insert_event(
source="Sensor:avg",
event_type="temperature",
value=avg_temp,
unit="°C",
timestamp=now,
extra="",
)
return {
"min": min_temp,
"max": max_temp,
"avg": avg_temp,
"count": len(all_temps),
"min_floor": min_floor,
"max_floor": max_floor,
"avg_min_floor": avg_min_floor,
"avg_max_floor": avg_max_floor,
"delta_t": delta_t,
}
# ------------------------------------------------------------------
# Yardımcı
# ------------------------------------------------------------------
def print_config(self) -> None:
if not self.flats:
print(f"📦 {self.bina_adi} için tanımlı daire yok.")
return
print(f"\n📦 {self.bina_adi} içeriği:")
for flat_no, flat in self.flats.items():
temp = flat.sensor.read_temperature()
durum = "✅ Bağlı" if temp is not None else "❌ Yok"
print(
" Daire {:<3} Oda {:<1} | Kat: {:<2} | Yön: {:<12} | Sensör: {:<18} | Durum: {} | AKTIF".format(
flat.flat_no,
flat.room_no,
flat.floor,
FlatDirection.get(flat.flat_direction, "Bilinmiyor"),
flat.sensor.serial,
durum,
)
)
def run_forever(self, sleep_s: int = 5) -> None:
"""Eski test aracı gibi: sürekli bina istatistiği üretir."""
while True:
self.get_building_temperature_stats()
time.sleep(sleep_s)
# ----------------------------------------------------------------------
# CLI
# ----------------------------------------------------------------------
if __name__ == "__main__":
bina = Building() # Config'ten her şeyi alır
bina.run_forever()