ilk işlem

This commit is contained in:
root 2025-11-24 14:25:02 +03:00
commit 1d458ca9f6
72 changed files with 9732 additions and 0 deletions

100
README.md Normal file
View File

@ -0,0 +1,100 @@
# ebuild
`ebuild`, Raspberry Pi 2 üzerinde çalışacak şekilde tasarlanmış,
**bina ısıtma / soğutma / sulama ve genel otomasyon** projesinin çekirdek Python kodunu içerir.
Proje yapısı:
-------------
```text
ebuild/
├─ README.md
├─ requirements.txt
├─ document/ # Tasarım PDF'leri, notlar, şemalar
├─ scripts/
│ └─ run_ebuild_main.py # Ana döngüyü çalıştırmak için yardımcı script
├─ tests/
│ └─ test_building.py
└─ ebuild/
├─ __init__.py
├─ config_statics.py # Bina topolojisi, FLAT_AREA, sensör/pin map'leri
├─ config_variables.py # Setpoint, haritalar, çalışma modu (hot-reload)
├─ reloader.py # Statik/dinamik config için ConfigReloader
├─ core/
│ ├─ building.py # Building/Flat/EDSensor iskeleti
│ ├─ environment.py # BuildingEnvironment dış ısı/nem/yağmur/ışık
│ ├─ devices.py # Sensör/aktüatör soyutlamaları
│ └─ systems/
│ ├─ burner.py # Brülör sistemi ve BurnerController
│ ├─ irrigation.py # Sulama sistemi
│ ├─ firealarm.py # Yangın alarm sistemi
│ └─ hydrophore.py # Hidrofor sistemi
├─ io/
│ ├─ legacy_syslog.py # Eski Rasp2 syslog formatı köprüsü
│ ├─ dbtext.py # Metin tabanlı log/DB iskeleti
│ ├─ relay_driver.py # GPIO röle sürücüsü
│ ├─ sensor_ds18b20.py # DS18B20 sensör sürücü iskeleti
│ ├─ sensor_dht11.py # DHT11 sensör sürücü iskeleti
│ └─ adc_mcp3008.py # MCP3008 ADC (yağmur / LDR) iskeleti
└─ runtime/
├─ main.py # Ana çalışma döngüsü iskeleti
└─ __init__.py
```
Konfigürasyon tasarımı:
-----------------------
* `config_statics.py`
- Bina topolojisi (FLAT_AREA: `serial`, `flat_no`, `room_no`, `floor`, `direction`, `size_m2`)
- Donanım pin eşlemeleri (`RELAY_GPIO`, `OUTPUT_GPIO`, `INPUT_GPIO` vb.)
- Ortak sensör ID'leri (dış ısı, dönüş hattı, kazan çıkışı vb.)
* `config_variables.py`
- Konfor sıcaklığı, aralık ve tolerans (`BUILDING_SETPOINT_C`, `BUILDING_RANGE_C`, `BUILD_HEAT_TOLERANCE`)
- Brülör setpoint haritaları (`BURNER_FIRE_SETPOINT_MAP`)
- Saat bazlı delta-T ve tasarruf haritaları
- Zaman bazlı override listesi (`HEAT_TIME_OVERRIDES`)
- Runtime sensör/dairesel aktif/pasif durumları (`FLAT_STATUS`)
`reloader.ConfigReloader`, bu iki dosyanın değişimini ayrı ayrı izler:
- `statics_changed=True` → bina/sistem topolojisi değişmiş (genelde restart / re-init gerekir)
- `variables_changed=True` → parametreler değişmiş (hot-reload ile devam edilebilir)
`document/` klasörü:
--------------------
Proje ile ilgili **tüm dokümantasyon ve tasarım çalışmaları** bu klasörde
tutulmalıdır. Örneğin:
- Mimari ve akış diyagramları (PDF)
- Donanım bağlantı şemaları
- Test raporları
- Eski sistemden yeni ebuild'e geçiş notları
Her yeni tasarım veya önemli revizyon için, ilgili notları PDF olarak
`document/` klasörüne koymak, ileride bakım ve geri izleme açısından
işleri kolaylaştırır.
Raspberry Pi 2 üzerinde kurulum:
--------------------------------
1. Python 3 ve `pip` paket yöneticisinin kurulu olduğundan emin ol.
2. Proje klasörüne geç:
```bash
cd ebuild
```
3. (İsteğe bağlı) Sanal ortam oluştur:
```bash
python3 -m venv .venv
source .venv/bin/activate
```
4. Gerekli Python paketlerini yükle:
```bash
pip install -r requirements.txt
```
5. Test amaçlı ana döngüyü çalıştır:
```bash
python3 scripts/run_ebuild_main.py
```
Notlar:
-------
* GPIO ve sensör kütüphaneleri gerçek Raspberry Pi üzerinde test edilmelidir.
* Bu repo şu anda **iskelet** durumundadır; `core/` ve `io/` altındaki dosyalar
senin gerçek sürücü ve kontrol kodlarınla yavaş yavaş doldurulacaktır.

1
deltilda.sh Normal file
View File

@ -0,0 +1 @@
find . -type f -name '*~' -print -delete

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,118 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /ZapfDingbats /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/PageMode /UseNone /Pages 10 0 R /Type /Catalog
>>
endobj
9 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20251117152250+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251117152250+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
10 0 obj
<<
/Count 3 /Kids [ 5 0 R 6 0 R 7 0 R ] /Type /Pages
>>
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2144
>>
stream
Gau0ED,91]&H9tYQqp_ldLbSqE(Kp-Kqg#/\O'KaUe&l[h@!QBleHfWPOn,;m)F<j%Ymg+.8iuYms'$<R:i*o!c$[-q!Ud@F>]`?%fliN)LS6Og[*AHSo+a&?]Tg0ao4<ng\p:1q>567^^Vh$elLL;1e@Q8qk^!P1Xtt;1Sr+Qf]7$ek[Sd0K7ED^]6n/"UnPuT61qNX"?ia.b!aX]m9Ut]XWk>OULC(X9uM&m%GjtBa^$Y#ncf!@C:7j/#l;dgBXV.4hXGd]jCb_/qL\^.c\]#N-]G:(TV(!Y3`#O9I1oZ?7i)qiHPZ>Ke"Bs+eHj<)pf%>/o[\TH\TQK'N>XA:NEOW(>@eKr@"*H(3JHeoSBrSeA;*n,8r#/g3'5s1Lhht$U]];kUsQaol;1;#Aumkpcm3)D>t/m,<lPbd/`Z!QTaDN-*pf+_@)aV^icK@PO;>U")[&<W+sNRI<lDkZ9ODUpr'HIiGUsBY.Ph=BVo]\/,*k9P)'[G;H+^]&r(K?(UPqW]D:DgZiUP%`<j(OhR8Bi8Ana"65M!MV,?p=_p,*eGGjOa=GYD%kqmk4]Ea\C+OCK"U6dGgrSkA'E`GT#tpRd<o:8bfS=M@29!ofr.)jnhVPaeq_i8L7"1iEbA_?>M7gHt2)dep+n+g\3f4-8o]1Gja.WbBKZ%XaCX`[I".K<4X<I6q+8,`l$KI"r*c2pKB;LN_/j4HL.,"N\=&Od\O>&ZKb->.:0<M2\P)&1FY&a@Tp%e=iU8p^t[ergGTB)%XEgiNIO`GBQBT/e<*j8@_hQc]0bETVjM[+,mVc'dhW]\([;JFA;4PXfgp]K0!9OFSG*iMEX>36Qcjnoti<qF!KsPY;S+CN'ns'2h`GTJRJ'%#sCrQ/3l3P7\bc-U!__l1gFWt64>ERRf?Y3,*sP^?9Z&>K/U7g;Qjuj.a$m3#G"2(KY.c^:NGoP?';h_[Gpk\]g'X+AguO"MQ=J<]Krqu]mkt6*Rg;3p"<4IHS(mRdLN(&QY2WQJ@6[W@t@!uN3fugOKRRN)]J^Xg:)egKc-OPXSUhFYXr\CeOQkR';(ehUX/oScqfMP]V0u4:Jti)aY5sh9'oo#*cRRL'N]Ym)JHRaS;Klmb.N`4@p$3I937ZZ4^E2:iN#`FX[2;11!;QhNA5K1VG>p5n*)LiQ0a4f*(tSr8^^ER%3!IuiJ8YJJbjYfJC$pKjEu#L%$-n4/mK`0i*<@2n`@8,XEfnf<#fAcE>D=p0lmqc:7O>/M4DJC0XpP%`lr>`@"7AOlXRPqn=0oUXf"U%'++^0@[b?a<)cDj6Ib+a.8">8]$;s9`i3QN84)5OBG"S;GX=-Y0K49?l[0_49^9]p=tO8BE"s?%Q9%T@OLUM#Q.bnUC"@m>'NULrGChu:km=Np<Yts%dl>R>6pGK]dTkPq-K-=oG73IW-qWWleOuPS9A&V?-j*P`aW$qdA+R-V.>!#c8$8Gh(AM;D&o=X#l`dCo**RS[*88(9b)L0HBIe>6&[Tug:p_FQRO6DgC?83;9C@6l=uChs`9hbt6.8<;2aBt^T]ljd@*,.eR4K#t(2R.C;II&QKm,>;>6EsRViMA(m?g%*:1l%^8<_Ii@:8o)-QEWO+_'cjA8ddgkFK18#JGF4d!eF6G+-dc>DHUO`kApqBcl+4S0_BmCJjU$CTpbC.nVDKQ%f,+`_;)Rn%_UO10#K&2:W%M[d*:Oh4_B'<]AWTPnJGi8Q_SDNFee1mLZ%#,tVrgnd;3f7E0Og.Mak]Ask"[<H"Zl1R=FB[lZu>;.bDJ>F"K9i;;\lK^^Q@VmXg/q9Gp0Br&Um!H)JW6Vq,56QkmJEO:9hTI:&dGSLbtIos<-88ke)dIPB12ZU(J.V(R3\3rb6\3,Vq'r(@MNJNk@0LW?nJNhA=Q=f242L9R4S<ejaH<1FP/5Kk&EJV:8L0h/HI]HHcfZTRHVm[uHrLNr@$Z+L3]hXl\Hg[[FY#djcH2@$h",&k,2R?%U7jB\Ah2r-u9`begjXd&R8nXPA;Kod8_As4c3FW%EmN5I;jr)>c0X$$)_*Bl0%=$W84WQi%qZ;XQD+tq4%%S?&lF=KQr;6KPETI/]1V1-2]m6>ZO`*,X=.Q9`&jP[kif=)eam4:fnb;&*<h:krXZ&<;%q5q$9E~>endstream
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1911
>>
stream
Gb!SmgMYb*&:Ml+%'VD1,'BuF0/]sEn]".YUidWg"]?0c`YIl[,uH9@'e=e4%OicfdT+_8i4Tg/&j[k"*6ubgB4qN2oPs6T0/:7U#01?d3%iF@i:q.2AsY3C*IL"h[NkYOk]`Tk@.82%1Ci9n^ZtLgG@,b]+D]9Y!e%?h>K`Ap)BH.Ra+!Xf>$fs[&"F@1&8$F=&9g43SC1<if?T$#)/FK8)@]!('bD[l#ar<aNJ9,aR80KefD"YDR/IAo(N;8+HG)j7]#VdVHZ]aHMK$GFAKmh5#_t*X?168[qSe$QE3DHQmWNpdL7I\Y)%:q[5I8fVG5ASS5<jh3kP(/OQFMb_;WOBOl+0G?PAXQ7Z:K)!\#Y?Eaq^J08^!r8fjU6`M1PoM',stf3]fn>*9*Etb:$a-+4eSHI!Kc2q;_6b1ms@g:Tp[_PB6MYE/&f)5=t\e\Dcg*`m)mASELFd&;Wch!fYOf0[p%qKUT3p:?D;($Q2NA:N9=\73oSQYSjSpqM#RD;1q'!MZZn3JMC_pDFDJt8lKX!(4-rZ*aqC]0rV=^(hL@aY37*o/$MQH(3:/dp+8ZDotUNej1F+oqX=9?oM7+^V165hmuhpTE,E6aKM&MF"1c3\4[6T?S)8DCE%8Rn*MAA7.6Md,2qV6/%2cOuR/:IH4"QlD1_[,nQ9R]p(=I^n,SYJ35nHR2Y8e5e+`#2LO_]njQoXn%]UB1q1r(ll4'Q;Bh!WtbFaaQo,TEGR$:L#6,Iij?=;K?S*j]I?QnaOS5Xb\IS1G6tR<Vi9bbDK/4'&i6<h3/i0/R9B;oIs?Gu>i0/S?\TK6DMu+GVdfO!3Yce:gk(+GERm6mNp`8fWFVhYufq\eY%u">"H<RrB?V2GS+1J[tZF\+bkR2QE$mq@sm6h"aYmo:._@gr.>iL6#5,f5%0#hg=$E21saa<'u'\0DdNK#enF")2</TX5SCH.p0lTl9SR@QOo%6!>@$l;CJo*A6G5+*1J9NJNuS?OK^1GKX>W9QE2N_caJ+;E_I)ZM`PZcH'Utr"5/u=r^A>P.PsP&KjT_`Bos=-'4M_5KcARc/pa^sllEI%:R(<[U/YkQ#II@)Q.Y!Wd[MM7Tugnjkdn4sZO\EfmWtsJI(/!41sX:aU2c_O"Br8De+0.bp]jMcXqn+=V!ApjG:o,9?kcI>H]@@i;1i0E($3`0%_kQ?N0:I$&Ocp"NKV+^g2nWq%OpocC3ak_7Eh4JR+Dp6VG(9!n_CQ,K%WpTAI`MJnY39PReC5!4-bO,9=50AFZ`eZmgn"p=$I%_8okk9_h).VKL2f_fbkdI]+8!+A,?6%r9`h'lcE@W]__-+eiqIY.5caT6)3A"KsTeO`(?'J/mcXm)Z*,0@VMKIhD-"#$s"ggC:Rt.$^KF!BLWHY:]EL[\VB,DB@1(-j:qhSY`7]!($Z<2_&i?l]r7q+FLn53M9aI=/#Eq6eKgjt.r<t<SA(d_4M*2*>#J.H^i-8\i'sM5MEoRi:Su5[CS,)6oBtVF%A$dI>q`u4fVeOR>u>!e>3AO,X9Jrhi[B"=Ip&ocZ<*P5O:r@c?$8a/:=2q<f%le_E!8%N;,t.?GpiIPR7@,ueDrlh+-k!oTUGJBV6]h@l'#*GhD`/5ClTj=a(%le'6CK8dWq'n[qRKtG.SgQ=/T2o@`i91[>O7;.VXScjg)R5@+Mb1)H3pq^Ec[KCN!NV6g$A">5't2(2s3?pAb<!7[ci8Mul!Be@]9#s,H5m>U`6"]0*to"r9b2OFCcD;*7k-bt>]g9=j$8WM[B&6YQ9c$Mo%=nnrYR+gmHerc$1K5cs+,=OS.+S(l).]S_Qi@',V],[uqGW1S'PCG_OZQ,-R_CXb4Rdc?]&GtiftI8\UloZkSG,"4l#jm=KGKe2BB<0TU_'hOePd=Z1aK>.CLZL%G~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1680
>>
stream
Gb"/(95iiK&AIV:QqN"+FW=&[--*Jh;\oVC)`2P].!-=sN5LgA;G&^qrq.:0ngdkSS_Z0M6X`4lb_LuFpqC[6!!`'%h"*6*\=UOu1k6^%fb4M+rX`\NnC-a[?giSaAho)d9`!f>eX0Ca!%eMc]7_o_EjttTh.U1T)Rti2!6i04ZO;#]CAA!j1l>cfQEl#RYWu$PThuH&1eNKl<"dV5N2j29:>HB,a:Lt'#$!`ge\Xj/=/G/R_sG^o:L2_[PnoD^FLco/CH$Q"%6N<J>L/2pleq8J_jXkABY=sP_@Qp(8c,fTF%OF$Qtt8bO9sKc@C._ZVlZ5lR.'p)gd,rXPs0u.$qAuR^)Dk6hD!EFa233>pYc=C"q/``Hq-r,0K'+'gi=5!DRnb^H*Z2cTuL80mXP,Dqs&o]N*e&@UaNV#JhRYoJ_<94+CXZ4:2XYXoqUH1RJ3Fn9MkgC(Nr:#:D,bLa5)\YREN0^K5LfE+<%_-hi1,;gK,5P?ELdsN@kZf-u4-(Chcg9F\ho*B#+pBM9/TVG)c1ZlUlOHmOdtY_s#6I&#3h!X,#*\GWf:j:ubfIHYIX(&!AUs#KemEbH)lVkQ*1V]^XH3;LJufYr!.*5`<G`:s$uQdaXXO<O/]><6n]ed_oH!a=If:('f+FI_l'r=n-<*Aar1!5K=u.d1C4S8/i+CAE"^PmYJ6+N3t6D^'tbN,:?sh#,B)P1e]+>R6-5gd;o0W"j:+T3R$e@0"dGE$Sk#oq?DAlmE7b(h&adE)Kpd-'i$I632*\K2\Gu9LeMcE?L";E4piMVJjmeH&&Ej2GT=S`55Zt5N`'8AamYD=)kA4M5GWttHVXBeKo`-[qkWEPTliA=g#`W'76tZSYcPeOfp.Daeu.^#/1R1];LRGsPEY\H!K("t^>^./MXr5?N+-,p;@fuu7I4Sq79Y"o`iB(=]menSf^JJX5l0r_^+.ZKYB0@](Zqem8^D,r>[c3^CaVY@`*b-FBYr&Hb+8o-4JUM`Ops5%8QMRa=p4`Kj9;s;F&]:_2R8LbipmD`G(2@^InRJ:[\7m3F"JDo+0agSlVb6^:<CHu_Eq@nWhl`;bLD1aKZYap9TUebj\_E9,oG=*#RAlP'll`bb#>+7@:e8h.\UoLXs9#X8s=H-B>%7/XPp/6Q:C5+#-SWhpkX]FH19q)B*01`>)c[S;HjBB=V-#^=*4)D/%"sHTF>Fm1^SqaRgF'.Ym4si_Cb,WBp=oaLbgou>sJIk69]-=(%rRCDi[=P[s+q<FT5Uj\g]OmgLZBB6>JMEZGID)L9^WL'brAc@n$\8cWVeRHCN_nA"*=oUl=<O-RlqYn4Cdmn[lTCE??7EU=j^BD:dZerj@fW;Y'QJ5>9CMHP]nhX"uA>Q#61jf7"6V4Ao$<:'@oCC.0I7RA&V?%Spi.cU'476ghW3o,(etrm$3p@Vs5*jgn]TkL..%V(\ONc6[8L=rplAFm[VG\su)Gs3N\"H3Ea%1(US<=E1rf8!H\@0),ST7$Um'6Rj[HI$;8s>2'Zp:^E'Yg;lFY.XT4\^L#l>'_<AUk"`#5j\l]TVFJJ9$7C=hW[V2t)A4h1Sh*6YK5B_AS0CQ'\g"q37"%ZBe2!&k>jQ-<bJaVFhaA'$P;a;8K[`'GQN$#MK52Rk/u#-m`D1GnFVD)JRK"<fb)YHEC6'Coh;9,LSh^~>endstream
endobj
xref
0 14
0000000000 65535 f
0000000073 00000 n
0000000124 00000 n
0000000231 00000 n
0000000314 00000 n
0000000391 00000 n
0000000596 00000 n
0000000801 00000 n
0000001006 00000 n
0000001075 00000 n
0000001358 00000 n
0000001430 00000 n
0000003666 00000 n
0000005669 00000 n
trailer
<<
/ID
[<dce34e8c50d6ac12657c6e6737c1c340><dce34e8c50d6ac12657c6e6737c1c340>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 9 0 R
/Root 8 0 R
/Size 14
>>
startxref
7441
%%EOF

View File

@ -0,0 +1,51 @@
# -------------------------------------------------
# GPIO ÖZETİ (Raspberry Pi 40-pin Header)
# - Tüm pinler BCM numarası ile konfigüre edilir.
# - Parantez içinde fiziksel pin numarası verilmiştir.
#
# Güç / Ortak hatlar:
# 3.3V : Fiziksel 1, 17
# 5V : Fiziksel 2, 4
# GND : Fiziksel 6, 9, 14, 20, 25, 30, 34, 39
#
# 1-Wire (DS18B20 hattı tipik kullanım):
# GPIO4 (BCM4) → Fiziksel 7
#
# Dış ortam DHT11:
# DHT11_OUTSIDE_PIN = 5 → BCM5 (Fiziksel 29)
#
# ADC / MCP3008 (SPI0):
# ADC_SPI["ce"] = 8 → BCM8 (Fiziksel 24, CE0)
# ADC_SPI["miso"] = 9 → BCM9 (Fiziksel 21, MISO)
# ADC_SPI["mosi"] = 10 → BCM10 (Fiziksel 19, MOSI)
# ADC_SPI["sclk"] = 11 → BCM11 (Fiziksel 23, SCLK)
#
# Brülör grubu röleleri (BURNER_GROUPS[0]):
# igniter_pin = 16 → BCM16 (Fiziksel 36)
# circulation["circ_1"]= 26 → BCM26 (Fiziksel 37)
# circulation["circ_2"]= 24 → BCM24 (Fiziksel 18)
#
# PWM / LED çıkışları (OUTPUT_GPIO):
# "buzzer" → GPIO18 → BCM18 (Fiziksel 12) [PWM]
# "rgb_r" → GPIO12 → BCM12 (Fiziksel 32) [PWM]
# "rgb_g" → GPIO13 → BCM13 (Fiziksel 33) [PWM]
# "rgb_b" → GPIO19 → BCM19 (Fiziksel 35) [PWM]
# "led" → GPIO23 → BCM23 (Fiziksel 16)
#
# Girişler (INPUT_GPIO):
# "button" → GPIO21 → BCM21 (Fiziksel 40)
# "burner_contactor" → GPIO20 → BCM20 (Fiziksel 38)
# "circulation_contactor1" → GPIO27 → BCM27 (Fiziksel 13)
# "circulation_contactor2" → GPIO17 → BCM17 (Fiziksel 11)
#
# Notlar:
# - Basınç ve gaz sensörleri için:
# pressure_sensor / gas_sensor pinleri, sensör BESLEME/ENABLE hattıdır.
# Analog ölçüm, MCP3008 üzerindeki ADC_CHANNELS üzerinden yapılır:
# "pressure" → CH0
# "gas" → CH1
# "rain" → CH2
# "ldr" → CH3
# - GPIO2/3 (fiziksel 3/5) I2C hattı, şimdilik boş tutulmuştur.
# - GPIO14/15 (fiziksel 8/10) UART hattı, debug/ileride kullanım için boş.
# -------------------------------------------------
Can't render this file because it contains an unexpected character in line 18 and column 14.

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

3
ebina_log.sql Normal file
View File

@ -0,0 +1,3 @@
-- DBText log file for table ebrulor_log
-- created at 2025-11-22T16:18:17.441370

8
ebuild/__init__.py Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
"""ebuild ana paketi."""
__title__ = "ebuild"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina otomasyonu, ısıtma/soğutma/sulama sistemleri için temel paket"
__version__ = "0.1.0"
__date__ = "2025-11-20"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

133
ebuild/config_runtime.py Normal file
View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "ebuild_config_runtime"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Çalışma zamanı (runtime) ısıtma/soğutma parametreleri"
__version__ = "0.3.0"
__date__ = "2025-11-22"
"""
ebuild/config_runtime.py
Sık değişebilen, sistem durmadan güncellenebilen parametreler:
- ısı aralıkları
- kazan limitleri
- saatlik tasarruf haritaları
- override tabloları
"""
# -------------------------------------------------
# Sistem / Hedefler
# -------------------------------------------------
CFG_DEBUG = 1
BUILDING_AGG_STRATEGY = "robust"
BUILD_HEAT_TOLERANCE = 1.00 # ısı dengeleme toleransı
BUILD_EXIT_HEAT = 0 # 0= bina ort ısı, 1= üst kat, 2= alt kat, 3= en düşük daire ısısı
BUILDING_RANGE_C = (18.0, 28.0) # bina hedef aralığı
BUILDING_SETPOINT_C = 23.0 # seçili bina konfor sıcaklığı
BUILDING_INCREMENT_C = 0.5 # B modunda kademe adımı (ileride kullanacağız)
MAX_OUTLET_C = 45.0 # kazan çıkış emniyet limiti
OUTSIDE_LIMIT_HEAT_C = 30.0
USED_OUTSIDE_ELAPSED_S = 300 # 5 dkda bir used_outside güncelle
BURNER_COMFORT_OFFSET_C = 1.0 # seçili konfor sıcaklığı offseti
CIRCULATION_MIN_RETURN_C = 25.0 # Çıkış ısısı bu değerin ALTINA düşerse pompalar DURUR
OUTSIDE_SMOOTH_SECONDS = 300
BUILDING_READ_PERIOD_S = 300 # bina istatistik okuma periyodu
# Hafta sonu lüks artırımı
WEEKEND_HEAT_BOOST_C = 2.0
WEEKEND_DAYS = (5, 6) # 5=Cumartesi, 6=Pazar
# Böylece debug sırasında bu min süreleri configten 0 yapar gerçekte devreye alırken 60300 saniye gibi gerçekçi değerler verirsin
BURNER_MIN_RUN_SEC = 0
BURNER_MIN_STOP_SEC = 0
BURNER_HYSTERESIS_C = 0
# -------------------------------------------------
# Ateşleme bant haritaları
# -------------------------------------------------
BURNER_FIRE_SETPOINT_MAP = {
-9: {"fire": 70}, -8: {"fire": 69}, -7: {"fire": 67}, -6: {"fire": 66}, -5: {"fire": 65},
-4: {"fire": 63}, -3: {"fire": 62}, -2: {"fire": 61}, -1: {"fire": 59}, 0: {"fire": 58},
1: {"fire": 57}, 2: {"fire": 55}, 3: {"fire": 54}, 4: {"fire": 53}, 5: {"fire": 51},
6: {"fire": 50}, 7: {"fire": 48}, 8: {"fire": 47}, 9: {"fire": 46}, 10: {"fire": 44},
11: {"fire": 43}, 12: {"fire": 42}, 13: {"fire": 40}, 14: {"fire": 39}, 15: {"fire": 38},
16: {"fire": 36}, 17: {"fire": 35},
}
DELTA_T_MAP_HOURLY = {
0: 6, 1: 6, 2: 6, 3: 6, 4: 6, 5: 6, 6: 7, 7: 7, 8: 7, 9: 7, 10: 7, 11: 7,
12: 6, 13: 6, 14: 6, 15: 6, 16: 6, 17: 6, 18: 7, 19: 7, 20: 7, 21: 7, 22: 7, 23: 7,
}
SAVING_T_PERCENT_HOURLY = {
0: 95, 1: 90, 2: 85, 3: 85, 4: 90, 5: 95, 6: 95, 7: 100, 8: 100, 9: 95, 10: 90, 11: 95,
12: 100, 13: 95, 14: 90, 15: 85, 16: 90, 17: 95, 18: 95, 19: 100, 20: 100, 21: 100, 22: 100, 23: 100,
}
# -------------------------------------------------
# Zaman bazlı override (örnekler)
# -------------------------------------------------
HEAT_TIME_OVERRIDES = [
{
"enable": True,
"start": "19:00",
"end": "22:00",
"days": [0, 1, 2, 3, 4, 5, 6],
"outside_c": [7, 18],
"set_fire_c": 45,
"note": "sabah konfor",
},
{
"enable": False,
"start": "12:00",
"end": "13:30",
"days": [0, 1, 2, 3, 4],
"outside_c": [14, 20],
"building_c": 27,
"note": "öğle sıcak tut",
},
{
"enable": False,
"start": "16:00",
"end": "18:00",
"days": [5, 6],
"outside_c": [14, 18],
"set_fire_c": 50,
"note": "akşam artır",
},
{
"enable": False,
"start": "20:30",
"end": "22:00",
"days": [0, 1, 2, 3, 4, 5, 6],
"outside_c": [8, 15],
"set_fire_c": 38,
"building_c": 26,
"note": "gece ekonomik",
},
]
SHOW_OVERRIDE_NOTE = 0 # 0: not yazma, 1: yaz
SHOW_ALL_OVERRIDES = 1 # 1: dış ısı aralığı eşleşmese de listele (gün eşleşmesi yine aranır)
# -------------------------------------------------
# Mevsim parametreleri
# -------------------------------------------------
SEASON_SPRING_SAVE_DAYS = 20
SEASON_AUTUMN_SAVE_DAYS = 20
xSEASON_SPRING_SAVE_DAYS = (5, 20) # 520 Nisan
xSEASON_AUTUMN_SAVE_DAYS = (15, 30) # 1530 Eylül
SEASON_PROVIDER = "SunHolidayInfo"
SEASON_LANG = "tr"
SEASON_NAMES = ("İlkbahar", "Yaz", "Sonbahar", "Kış")
SEASON_ENG = {
"İlkbahar": "Spring",
"Yaz": "Summer",
"Sonbahar": "Autumn",
"Kış": "Winter",
}
SPRING_HOURS = ["00:00", "22:00"] # 00 → sunset bazlı başlangıç
SPRING_START_MINUTE = 30

158
ebuild/config_statics.py Normal file
View File

@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "ebuild_config_statics"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina ve donanım topolojisi için statik konfigürasyon"
__version__ = "0.3.0"
__date__ = "2025-11-22"
"""
ebuild/config_statics.py
Statik (mimari) konfigürasyon
-----------------------------
Bu dosyada bina ve donanım topolojisine ait, sık değişmeyen ayarlar tutulur.
Rasp2 pin eşlemeleri, brülör topolojisi, bina/geo bilgileri vb.
"""
# -------------------------------------------------
# Brülör çalışma modu
# -------------------------------------------------
# F : Dış ısıya bağlı kazan kontrolü
# B : Bina ortalama sıcaklığa bağlı kontrol
BUILD_BURNER = "F" # ilk deneme F
BURNER_DEFAULT_ID = 0 # seçili brülör id
# -------------------------------------------------
# Bina kimliği / konum
# -------------------------------------------------
BUILDING_NAME = "Gunes Apt"
BUILDING_LOCATION = "Ankara"
BUILDING_LABEL = "Resat Nuri"
GEO_CITY = "Ankara"
GEO_COUNTRY = "Turkey"
GEO_TZ = "Europe/Istanbul"
GEO_LAT = 39.92077
GEO_LON = 32.85411
# Lisans ve sistem ana anahtarı
BUILDING_LICENCEID = 10094
BUILDING_SYSTEMONOFF = 1 # 1=On, 0=Off
# -------------------------------------------------
# Daire / oda topolojisi (DS18B20)
# -------------------------------------------------
# Her kayıt: bina içindeki flat/room dağılımı
FLAT_AREA = [
{"serial": "28-00000660e983", "flat_no": 2, "room_no": 0, "floor": 1, "direction": 5, "size_m2": 0.0},
{"serial": "28-0000066144f9", "flat_no": 22, "room_no": 0, "floor": 1, "direction": 7, "size_m2": 0.0},
{"serial": "28-000006605827", "flat_no": 11, "room_no": 0, "floor": 4, "direction": 1, "size_m2": 0.0},
{"serial": "28-000005fd46c4", "flat_no": 24, "room_no": 0, "floor": 2, "direction": 7, "size_m2": 0.0},
{"serial": "28-00000660dc99", "flat_no": 25, "room_no": 0, "floor": 2, "direction": 7, "size_m2": 0.0},
{"serial": "28-00000141b977", "flat_no": 28, "room_no": 0, "floor": 2, "direction": 7, "size_m2": 0.0},
{"serial": "28-000006616fc3", "flat_no": 6, "room_no": 0, "floor": 2, "direction": 1, "size_m2": 0.0},
{"serial": "28-00000660ca02", "flat_no": 1, "room_no": 0, "floor": 1, "direction": 4, "size_m2": 0.0},
]
# -------------------------------------------------
# Hat sensörleri (tamamen DS18B20)
# -------------------------------------------------
# Dış ısı
OUTSIDE_SENSOR_ID = "28-000000b2aa05"
OUTSIDE_SENSOR_NAME = "Dış Isı 1"
# Kazan çıkış
BURNER_OUT_SENSOR_ID = "28-946778126461"
BURNER_OUT_SENSOR_NAME = "Çıkış Isı 2"
# Dönüş hatları
RETURN_LINE_SENSOR_IDS = [
"28-566978126461",
"28-506478126461",
"28-46fa45126461",
]
RETURN_LINE_SENSOR_NAME_MAP = {
"28-566978126461": "Kuzey Hat4",
"28-506478126461": "Güney Hat3",
"28-46fa45126461": "Guney Bat, 5",
}
# -------------------------------------------------
# Donanım / röle / pin eşlemeleri
# -------------------------------------------------
GPIO_BOARD_REVISION = 3
BURNER_IGNITER_CH = "igniter"
RELAY_GPIO = {
"igniter": 16,
"circulation_a": 26,
"circulation_b": 24,
}
# Brülör grupları RelayDriver buradan beslenir
BURNER_GROUPS = {
0: {
"name": "MainBurner",
"location": "Sol binada",
"igniter_pin": 16, # BCM 16 → phys 36 (örnek)
"circulation": {
"circ_1": {"channel": "circulation_a", "pin": 26, "default": 1},
"circ_2": {"channel": "circulation_b", "pin": 24, "default": 0},
},
},
# 1: {...} # ilerde ikinci brülör
}
# Temel röle kanalları (soyut isim → GPIO)
RELAY_GPIO = {
"igniter": 16, # BCM 16, Fiziksel 36
"circulation_a": 26, # BCM 26, Fiziksel 37
"circulation_b": 24, # BCM 24, Fiziksel 18
"gas_sensor": 22, # BCM 22, Fiziksel 15
"pressure_sensor": 6, # BCM 6, Fiziksel 31
}
PUMP_HAS_DUAL_PUMPS = True
PUMP_FEEDBACK_INPUT_PINS = []
PUMP_ROTATE_HOURS_DEFAULT = 24
# -------------------------------------------------
# Buzzer + RGB LED
# -------------------------------------------------
BUZZER_ENABLE = 1
BUZZER_ACTIVE_HIGH = True
BUZZER_BASE_FREQ = 2000 # Hz
BUZZER_PATTERNS = {
"cfg_reload": [
(1, 10),
],
"startup": [
(1, 80),
(0, 0),
],
"shutdown": [[200, 0, 1]],
"reload": [[300, 100, 1], [120, 100, 1], [300, 0, 1]],
"alarm": [[500, 200, 2], [500, 200, 2], [500, 200, 2]],
}
OUTPUT_GPIO = {
"buzzer": 18, # BCM 18, Fiziksel 12 - PWM buzzer
"rgb_r": 12, # BCM 12, Fiziksel 32 - PWM kırmızı
"rgb_g": 13, # BCM 13, Fiziksel 33 - PWM yeşil
"rgb_b": 19, # BCM 19, Fiziksel 35 - PWM mavi
"led": 23, # BCM 23, Fiziksel 16 - basit LED
}
RGB_ORDER = "GRB"
RGB_COMMON_ANODE = False
RGB_BASE_FREQ = 1000 # Hz
INPUT_GPIO = {
"button": 21, # BCM 21, Fiziksel 40
"burner_contactor": 20, # BCM 20, Fiziksel 38
"circulation_contactor1": 27, # BCM 27, Fiziksel 13
"circulation_contactor2": 17, # BCM 17, Fiziksel 11
}

1
ebuild/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "analog_sensors"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "MCP3008 üzerinden okunan analog sensörler için basit hub"
__version__ = "0.1.0"
__date__ = "2025-11-23"
"""
ebuild/core/analog_sensors.py
Amaç
-----
- MCP3008 ADC üzerinden okunan analog kanalları tek noktada toplamak.
- Bası, gaz, yağmur ve LDR için basit arayüz sağlamak.
Notlar
------
- Şimdilik çok sade tutuldu; istersen ileride eşik, durum (state) ve alarm
mantığını genişletebilirsin.
"""
from typing import Dict, Any, Optional
class _SimpleChannelState:
"""
Tek bir analog kanal için durum tutucu.
"""
def __init__(self, name: str) -> None:
self.name = name
self.state: Optional[str] = None
self.last_raw: Optional[int] = None
# Gaz için latched alarm gibi ek alanlar:
self.latched_alarm: bool = False
def update(self, raw: Optional[int]) -> None:
self.last_raw = raw
# Şimdilik state hesaplamıyoruz; ileride eşiğe göre doldurulabilir.
self.state = None
class AnalogSensorsHub:
"""
MCP3008 ADC üzerinden bası, gaz, yağmur, LDR gibi sensörleri yöneten hub.
Beklenti:
---------
adc nesnesinin en azından şu fonksiyonu sağlaması:
- read_channel(ch: int) -> int (0..1023 arası değer)
"""
def __init__(self, adc) -> None:
self.adc = adc
# Her kanal için basit state objeleri
self.pressure = _SimpleChannelState("pressure")
self.gas = _SimpleChannelState("gas")
self.rain = _SimpleChannelState("rain")
self.ldr = _SimpleChannelState("ldr")
# Kanalları sabitliyoruz; istersen config'ten de alabilirsin.
self.pressure_ch = 0
self.gas_ch = 1
self.rain_ch = 2
self.ldr_ch = 3
def _safe_read(self, ch: int) -> Optional[int]:
"""
ADC'den güvenli okuma. Hata olursa None döndürür.
"""
if self.adc is None:
return None
try:
val = self.adc.read_channel(ch)
return int(val)
except Exception:
return None
def update_all(self) -> Dict[str, Any]:
"""
Tüm kanalları okuyup state nesnelerini günceller.
Dönüş:
{
"pressure": <raw or None>,
"gas": <raw or None>,
"rain": <raw or None>,
"ldr": <raw or None>,
}
"""
vals: Dict[str, Any] = {}
p_raw = self._safe_read(self.pressure_ch)
g_raw = self._safe_read(self.gas_ch)
r_raw = self._safe_read(self.rain_ch)
l_raw = self._safe_read(self.ldr_ch)
self.pressure.update(p_raw)
self.gas.update(g_raw)
self.rain.update(r_raw)
self.ldr.update(l_raw)
vals["pressure"] = p_raw
vals["gas"] = g_raw
vals["rain"] = r_raw
vals["ldr"] = l_raw
return vals
if __name__ == "__main__":
# Basit demo: sahte ADC ile
class DummyADC:
def read_channel(self, ch: int) -> int:
return 512 + ch # uydurma değerler
hub = AnalogSensorsHub(DummyADC())
values = hub.update_all()
print("AnalogSensorsHub demo:", values)
print("Gas latched_alarm:", hub.gas.latched_alarm)

590
ebuild/core/building.py Normal file
View File

@ -0,0 +1,590 @@
# -*- 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()

574
ebuild/core/building.py~ Normal file
View File

@ -0,0 +1,574 @@
# -*- 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
# Konsol özet
self._print_header()
self._print_sun_and_season()
self._print_burner_topology()
self._print_line_sensors()
self.print_config()
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()

2
ebuild/core/devices.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Device / Sensor / Actuator soyutlamaları için iskelet."""

306
ebuild/core/environment.py Normal file
View File

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "environment"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina dış ortam sıcaklığı (DS18B20) ve analog sensörler için ortam merkezi"
__version__ = "0.3.0"
__date__ = "2025-11-23"
"""
ebuild/core/environment.py
Amaç
-----
- Bina dış ortam sıcaklığı (DS18B20) ve MCP3008 üzerinden okunan analog sensörleri
tek bir merkezde toplamak.
- Üst seviye modüller (ör: BurnerController, sulama sistemi, alarm sistemi) bu
sınıf üzerinden dış ortam ve analog bilgilerine erişir.
Notlar
------
- Bu katman donanım erişimini kapsüller:
* io/ds18b20.py
* io/adc_mcp3008.py
* core/analog_sensors.py
"""
from typing import Dict, Optional, Any
import datetime
# Donanım bağımlı modüller
try:
from ..io.ds18b20 import DS18B20Sensor
except ImportError:
print("from ..io.ds18b20 import DS18B20Sensor NOT IMPORTED")
DS18B20Sensor = None # type: ignore
try:
from ..io.adc_mcp3008 import MCP3008ADC
except ImportError:
print("from ..io.adc_mcp3008 import MCP3008ADC NOT IMPORTED")
MCP3008ADC = None # type: ignore
try:
from .analog_sensors import AnalogSensorsHub
except ImportError:
print("from .analog_sensors import AnalogSensorsHub NOT IMPORTED")
AnalogSensorsHub = None # type: ignore
# Config HER ZAMAN ayrı import edilmeli, donanıma bağlı değil
try:
from .. import config_statics as cfgs
except ImportError as e:
print("ENV: config_statics import edilemedi:", e)
cfgs = None # type: ignore
class BuildingEnvironment:
"""
Bina çevre sensörlerini yöneten merkez.
Özellikler:
-----------
- outside : DS18B20 dış ortam sensörü (OUTSIDE_SENSOR_ID üzerinden)
- adc : MCP3008ADC örneği
- analog : AnalogSensorsHub (bası, gaz, yağmur, LDR)
"""
def __init__(
self,
name: str = "BuildingEnvironment",
outside_sensor_id: Optional[str] = None,
use_outside_ds18: bool = True,
use_adc: bool = True,
) -> None:
#print("BuildingEnvironment Init:", name, outside_sensor_id, use_outside_ds18, use_adc)
self.name = name
# ------------------------------------------------------------------
# Dış ortam sıcaklığı (DS18B20)
# ------------------------------------------------------------------
# 1) Önce parametreden gelen ID; yoksa config_statics.OUTSIDE_SENSOR_ID
if not outside_sensor_id and cfgs is not None:
try:
outside_sensor_id = getattr(cfgs, "OUTSIDE_SENSOR_ID", None)
except Exception as e:
print("BuildingEnvironment: outside sensör ID okunamadı:", e)
outside_sensor_id = None
#print( "BuildingEnvironment get outside_sensor_id:", outside_sensor_id, "use_outside_ds18", use_outside_ds18, "use_adc", use_adc )
# 2) self.outside_id ve self.outside MUTLAKA burada tanımlanmalı
self.outside_id: str = outside_sensor_id or ""
self.outside: Optional[DS18B20Sensor] = None # type: ignore
#print("DS18B20Sensor", type(DS18B20Sensor), DS18B20Sensor)
# 3) ID makulse DS18 sensörünü yarat
if use_outside_ds18 and DS18B20Sensor is not None and len(self.outside_id) > 5:
try:
#print("BuildingEnvironment: DS18B20Sensor yaratılıyor, id =", self.outside_id)
self.outside = DS18B20Sensor(serial=self.outside_id, name="OutsideTemp")
#print("BuildingEnvironment: self.outside:", self.outside)
except Exception as e:
print("BuildingEnvironment: outside sensör oluşturulamadı:", e)
self.outside = None
else:
print(
"BuildingEnvironment: dış sensör yok veya devre dışı:",
"use_outside_ds18 =", use_outside_ds18,
"DS18B20Sensor is None =", DS18B20Sensor is None,
"outside_id =", repr(self.outside_id),
)
#print("BuildingEnvironment 5:", name, self.outside, outside_sensor_id, use_outside_ds18, use_adc)
# Dış ısı cache'i (kontrol katmanları için)
self._last_outside_temp_c: Optional[float] = None
self._last_outside_read_ts: Optional[datetime.datetime] = None
# ------------------------------------------------------------------
# MCP3008 ve analog sensör hub
# ------------------------------------------------------------------
self.adc: Optional[MCP3008ADC] = None # type: ignore
self.analog: Optional[AnalogSensorsHub] = None # type: ignore
if use_adc and MCP3008ADC is not None and AnalogSensorsHub is not None:
try:
self.adc = MCP3008ADC()
self.analog = AnalogSensorsHub(self.adc)
except Exception:
self.adc = None
self.analog = None
# ----------------------------------------------------------------------
# Okuma fonksiyonları
# ----------------------------------------------------------------------
def read_outside_temp(self) -> Dict[str, Optional[float]]:
"""
DS18B20 dış ortam sensöründen sıcaklık okur.
Dönüş:
{"outside_temp_c": 23.4, "read_ts": datetime} # veya None
"""
temp: Optional[float] = None
now = datetime.datetime.now()
if self.outside is not None:
try:
temp = self.outside.read_temperature()
except Exception:
temp = None
if temp is not None:
self._last_outside_temp_c = temp
self._last_outside_read_ts = now
return {
"outside_temp_c": temp,
"read_ts": now,
}
def get_outside_temp_cached(self, max_age_sec: int = 60) -> Optional[float]:
"""
Dış ortam sıcaklığını cache üzerinden döndürür; gerekirse sensörden günceller.
- Eğer hiç okunmamışsa sensörden okur.
- Eğer son okuma max_age_sec saniyeden eskiyse yeniden okur.
"""
now = datetime.datetime.now()
if (
self._last_outside_read_ts is None
or (now - self._last_outside_read_ts).total_seconds() > max_age_sec
):
snap = self.read_outside_temp()
temp = snap.get("outside_temp_c")
if temp is not None:
self._last_outside_temp_c = temp
self._last_outside_read_ts = now
return self._last_outside_temp_c
def read_analog_all(self) -> Dict[str, Any]:
"""
AnalogSensorsHub üzerinden tüm analog kanalları okur.
Dönüş sözlüğü:
{
"pressure_raw": ...,
"pressure_state": ...,
"gas_raw": ...,
"gas_state": ...,
"gas_latched_alarm": ...,
"rain_raw": ...,
"rain_state": ...,
"ldr_raw": ...,
"ldr_state": ...,
}
"""
data: Dict[str, Any] = {
"pressure_raw": None,
"pressure_state": None,
"gas_raw": None,
"gas_state": None,
"gas_latched_alarm": None,
"rain_raw": None,
"rain_state": None,
"ldr_raw": None,
"ldr_state": None,
}
if self.analog is None:
return data
values = self.analog.update_all()
# Bası
data["pressure_raw"] = values.get("pressure")
data["pressure_state"] = getattr(self.analog.pressure, "state", None)
# Gaz
data["gas_raw"] = values.get("gas")
data["gas_state"] = getattr(self.analog.gas, "state", None)
data["gas_latched_alarm"] = getattr(self.analog.gas, "latched_alarm", None)
# Yağmur
data["rain_raw"] = values.get("rain")
data["rain_state"] = getattr(self.analog.rain, "state", None)
# LDR
data["ldr_raw"] = values.get("ldr")
data["ldr_state"] = getattr(self.analog.ldr, "state", None)
return data
# ----------------------------------------------------------------------
# Yüksek seviye snapshot
# ----------------------------------------------------------------------
def get_snapshot(self) -> Dict[str, Any]:
"""
Dış sıcaklık (DS18B20) + tüm analog sensörler için tek sözlük döndürür.
Örnek:
{
"outside_temp_c": 14.3,
"pressure_raw": 512,
"pressure_state": "SAFE",
"gas_raw": 80,
"gas_state": "SAFE",
"gas_latched_alarm": False,
"rain_raw": 100,
"rain_state": "DRY",
"ldr_raw": 900,
"ldr_state": "BRIGHT",
}
"""
snap: Dict[str, Any] = {}
snap.update(self.read_outside_temp())
snap.update(self.read_analog_all())
return snap
# ----------------------------------------------------------------------
# Güvenlik yardımcıları
# ----------------------------------------------------------------------
def should_shutdown_system(self) -> bool:
"""
Analog sensör verilerine göre sistem acil kapatılmalı mı?
Örn:
- Gaz sızıntısı
- ırı bası
vs.
Şu an sadece gaz latched_alarm'a bakıyor.
"""
if self.analog is None:
return False
if getattr(self.analog.gas, "latched_alarm", False):
return True
return False
# ----------------------------------------------------------------------
# Temel temsil
# ----------------------------------------------------------------------
def summary(self) -> str:
"""
Ortamın kısa özetini döndürür.
"""
parts = [f"env_name={self.name}"]
parts.append(f"outside_id={self.outside_id!r}")
parts.append(f"outside={'OK' if self.outside is not None else 'NONE'}")
parts.append(f"adc={'OK' if self.adc is not None else 'NONE'}")
parts.append(f"analog={'OK' if self.analog is not None else 'NONE'}")
return "BuildingEnvironment(" + ", ".join(parts) + ")"
if __name__ == "__main__":
# Basit demo
env = BuildingEnvironment()
print(env.summary())
snap = env.get_snapshot()
print("Snapshot:", snap)
print("Gaz shutdown mu?:", env.should_shutdown_system())

395
ebuild/core/season.py Normal file
View File

@ -0,0 +1,395 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "season"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Güneş / tatil / mevsim + tasarruf sezonu bilgisini sağlayan yardımcı katman"
__version__ = "0.3.1"
__date__ = "2025-11-23"
"""
ebuild/core/season.py
Amaç
-----
- SunHolidayInfo kullanarak:
* gün doğumu / batımı / öğlen
* resmi tatil bilgisi
* mevsim ve sezon süresi
* tasarruf sezonu (saving) başlangıç/bitiş
bilgilerini üretir.
- Bu bilgiyi BurnerController ve legacy_syslog gibi modüllere
SeasonController.info üzerinden sağlar.
Not:
- SunHolidayInfo import edilemez veya hata verirse, basit bir
fallback mevsim + tasarruf sezonu bilgisi üretir.
"""
from dataclasses import dataclass
from datetime import datetime, time, date, timedelta
from typing import Optional, Tuple, Any
from .. import config_statics as cfg_s
# SunHolidayInfo'yu paket içinden al; yoksa fallback'e düş
try:
from .sunholiday import SunHolidayInfo # type: ignore
except Exception:
SunHolidayInfo = None # type: ignore
# Tasarruf sezonu parametreleri için runtime config
try:
from .. import config_runtime as cfg_v # type: ignore
except Exception:
cfg_v = None # type: ignore
# ---------------------------------------------------------------------
# Veri yapısı
# ---------------------------------------------------------------------
@dataclass
class SeasonInfo:
"""SeasonController tarafından sağlanan özet bilgi."""
date: date
sunrise: Optional[time]
sunset: Optional[time]
noon: Optional[time]
is_holiday: bool
holiday_label: str
season: str # "İlkbahar", "Yaz", "Sonbahar", "Kış", ...
season_start: str # "YYYY-MM-DD"
season_end: str # "YYYY-MM-DD"
season_day: int # sezonun toplam gün sayısı
season_passed: int # sezon içinde geçen gün
season_remaining: int # sezon sonuna kalan gün
# Tasarruf sezonu (saving) bilgileri
is_season: bool = False # BUGÜN tasarruf sezonu içinde miyiz?
saving_start: Optional[date] = None # tasarruf dönemi başlangıç tarihi
saving_stop: Optional[date] = None # tasarruf dönemi bitiş tarihi
# ---------------------------------------------------------------------
# Yardımcı fonksiyonlar
# ---------------------------------------------------------------------
def _parse_time_str(s: Optional[str]) -> Optional[time]:
if not s:
return None
for fmt in ("%H:%M:%S", "%H:%M"):
try:
return datetime.strptime(s, fmt).time()
except Exception:
continue
return None
def _parse_date_any(v: Any) -> Optional[date]:
"""
v: datetime / date / str / None date veya None.
"""
if v is None:
return None
if isinstance(v, date) and not isinstance(v, datetime):
return v
if isinstance(v, datetime):
return v.date()
if isinstance(v, str):
s = v.strip()
# "2025-09-23T22:42:51.508546+03:00" => "2025-09-23"
if "T" in s:
s = s.split("T", 1)[0]
# ISO parse dene
try:
return datetime.fromisoformat(s).date()
except Exception:
pass
# Son çare saf "YYYY-MM-DD"
try:
return datetime.strptime(s, "%Y-%m-%d").date()
except Exception:
return None
return None
def _as_int(x: Any, default: int = 0) -> int:
try:
return int(x)
except Exception:
return default
def _get_saving_param_for_season(season_name: str) -> Optional[Any]:
"""
Sezona göre ilgili tasarruf parametresini getirir.
- İlkbahar cfg_v.SEASON_SPRING_SAVE_DAYS
- Sonbahar cfg_v.SEASON_AUTUMN_SAVE_DAYS
Yoksa None döner.
"""
if cfg_v is None:
return None
if season_name == "İlkbahar":
return getattr(cfg_v, "SEASON_SPRING_SAVE_DAYS", None)
if season_name == "Sonbahar":
return getattr(cfg_v, "SEASON_AUTUMN_SAVE_DAYS", None)
return None
def tarih_gun_islemi(tarih_str, gun_sayisi):
try:
tarih = datetime.strptime(tarih_str, "%Y-%m-%d")
yeni_tarih = tarih + timedelta(days=gun_sayisi)
return yeni_tarih.strftime("%Y-%m-%d")
except ValueError:
return "Hata: Tarih formatı yanlış olmalı! Örnek: '2025-06-30'"
def _compute_saving_window(
season_name: str,
season_start: Optional[date],
season_end: Optional[date],
today: date,
) -> Tuple[bool, Optional[date], Optional[date]]:
"""
Tasarruf sezonu penceresini hesaplar.
Buradaki tanım:
- SEASON_SPRING_SAVE_DAYS -> Nisan ayı için gün aralığı (nisan)
- SEASON_AUTUMN_SAVE_DAYS -> Eylül ayı için gün aralığı (eylül)
Örnek:
SEASON_SPRING_SAVE_DAYS = (5, 20) -> 5-20 Nisan arası tasarruf
SEASON_AUTUMN_SAVE_DAYS = (15, 30) -> 15-30 Eylül arası tasarruf
Mantık:
- Eğer bugün ay < 6 (Haziran'dan önce) ise -> Nisan aralığını kullan
- Eğer bugün ay >= 6 ise -> Eylül aralığını kullan
Dönüş:
(is_season, saving_start, saving_stop)
"""
if season_start is None or season_end is None or cfg_v is None:
return False, None, None
# Bugünün tarihi (sadece date, saat yok)
today = date.today()
year = today.year
# Config'den değerleri güvenli şekilde al (varsayılan değerle)
SPRING_SAVE_DAYS = getattr(cfg_v, "SEASON_SPRING_SAVE_DAYS", 20) # örnek: 20
AUTUMN_SAVE_DAYS = getattr(cfg_v, "SEASON_AUTUMN_SAVE_DAYS", 13) # örnek: 13
# İlkbahar mı yoksa Sonbahar mı dönemi aktif?
if today.month <= 5: # Ocak-Mayıs arası → ilkbahar dönemi aktif
# İlkbaharın son gününden geriye doğru
saving_stop = date(year, 5, 31) # 31 Mayıs (dahil)
saving_start = saving_stop - timedelta(days=SPRING_SAVE_DAYS - 1) # dahil olduğu için -1
else: # Haziran-Aralık arası → sonbahar dönemi aktif
# Sonbaharın ilk gününden ileri doğru
saving_start = date(year, 9, 1) # 1 Eylül (dahil)
saving_stop = saving_start + timedelta(days=AUTUMN_SAVE_DAYS - 1) # dahil olduğu için -1
# Sezon içinde miyiz?
is_season = saving_start <= today <= saving_stop
return is_season, saving_start, saving_stop
# ---------------------------------------------------------------------
# Ana controller
# ---------------------------------------------------------------------
class SeasonController:
"""
Tek sorumluluğu: o anki (veya verilen tarihteki) güneş / tatil /
mevsim + tasarruf sezonu bilgisini üst katmanlara taşımak.
"""
def __init__(self, info: SeasonInfo) -> None:
self.info = info
# ---------------------------------------------------------
# Factory metodları
# ---------------------------------------------------------
@classmethod
def from_now(cls) -> "SeasonController":
"""
Sistem saatine göre SeasonController üretir.
GEO_* bilgilerini config_statics'ten alır.
"""
now = datetime.now()
return cls(cls._build_info(now))
@classmethod
def from_datetime(cls, dt: datetime) -> "SeasonController":
return cls(cls._build_info(dt))
# ---------------------------------------------------------
# İç mantık
# ---------------------------------------------------------
@classmethod
def _build_info(cls, now: datetime) -> SeasonInfo:
"""
SunHolidayInfo varsa onu kullanır; yoksa basit bir fallback
sezon + tasarruf sezonu bilgisi üretir.
"""
geo_city = getattr(cfg_s, "GEO_CITY", "Ankara")
geo_country = getattr(cfg_s, "GEO_COUNTRY", "Turkey")
geo_tz = getattr(cfg_s, "GEO_TZ", "Europe/Istanbul")
geo_lat = float(getattr(cfg_s, "GEO_LAT", 39.92077))
geo_lon = float(getattr(cfg_s, "GEO_LON", 32.85411))
today = now.date()
# ------------------------------
# 1) SunHolidayInfo kullanmayı dene
# ------------------------------
if SunHolidayInfo is not None:
try:
tracker = SunHolidayInfo(
current_datetime=now,
city_name=geo_city,
country=geo_country,
timezone=geo_tz,
latitude=geo_lat,
longitude=geo_lon,
)
data = tracker.get_info()
sunrise_t = _parse_time_str(data.get("sunrise"))
sunset_t = _parse_time_str(data.get("sunset"))
noon_t = _parse_time_str(data.get("noon"))
season_name = data.get("season") or "Bilinmiyor"
ss_raw = data.get("season_start") or ""
se_raw = data.get("season_end") or ""
ds = _parse_date_any(ss_raw)
de = _parse_date_any(se_raw)
if ds is None:
ds = today
if de is None or de < ds:
de = ds
total_days = _as_int(data.get("season_day"), 0)
passed_days = _as_int(data.get("season_passed"), 0)
remaining_days = _as_int(data.get("season_remaining"), 0)
# Eğer harici toplam/remaining güvenilmezse basit hesap
if total_days <= 0 or remaining_days < 0:
total_days = (de - ds).days + 1
passed_days = (today - ds).days
remaining_days = (de - today).days
is_season, saving_start, saving_stop = _compute_saving_window(
season_name, ds, de, today
)
return SeasonInfo(
date = today,
sunrise = sunrise_t,
sunset = sunset_t,
noon = noon_t,
is_holiday = bool(data.get("is_holiday", False)),
holiday_label = str(data.get("holiday_label", "")),
season = season_name,
season_start = ds.isoformat(),
season_end = de.isoformat(),
season_day = total_days,
season_passed = passed_days,
season_remaining = remaining_days,
is_season = is_season,
saving_start = saving_start,
saving_stop = saving_stop,
)
except Exception as e:
print(f"⚠️ SunHolidayInfo hata, fallback kullanılacak: {e}")
# ------------------------------
# 2) Fallback: sadece mevsim tahmini + saving
# ------------------------------
month = now.month
day = now.day
if (month == 12 and day >= 1) or (1 <= month <= 3 and (month < 3 or day <= 20)):
season_label = "Kış"
ss = f"{now.year}-12-01"
se = f"{now.year+1}-03-20" if month == 12 else f"{now.year}-03-20"
elif 3 <= month <= 6 and not (month == 6 and day > 20):
season_label = "İlkbahar"
ss = f"{now.year}-03-21"
se = f"{now.year}-06-20"
elif 6 <= month <= 9 and not (month == 9 and day > 22):
season_label = "Yaz"
ss = f"{now.year}-06-21"
se = f"{now.year}-09-22"
else:
season_label = "Sonbahar"
ss = f"{now.year}-09-23"
se = f"{now.year}-11-30"
try:
ds = datetime.fromisoformat(ss).date()
de = datetime.fromisoformat(se).date()
total_days = (de - ds).days + 1
passed_days = (today - ds).days
remaining_days = (de - today).days
except Exception:
ds = today
de = today
total_days = passed_days = remaining_days = 0
is_season, saving_start, saving_stop = _compute_saving_window(
season_label, ds, de, today
)
return SeasonInfo(
date = today,
sunrise = None,
sunset = None,
noon = None,
is_holiday = False,
holiday_label = "",
season = season_label,
season_start = ds.isoformat(),
season_end = de.isoformat(),
season_day = total_days,
season_passed = passed_days,
season_remaining = remaining_days,
is_season = is_season,
saving_start = saving_start,
saving_stop = saving_stop,
)
# ---------------------------------------------------------
# Legacy / debug uyumlu satırlar
# ---------------------------------------------------------
def to_syslog_lines(self) -> list[str]:
"""
legacy_syslog.emit_header_season veya debug için özet satırlar üretir.
Örnek:
"season : İlkbahar 2025-03-21 - 2025-06-20 day:92 passed:10 remaining:82"
"saving : True 2025-03-21 - 2025-04-10"
"""
i = self.info
lines: list[str] = []
lines.append(
f"season : {i.season} {i.season_start} - {i.season_end} "
f"day:{i.season_day} passed:{i.season_passed} remaining:{i.season_remaining}"
)
if i.saving_start and i.saving_stop:
lines.append(
f"saving : {i.is_season} {i.saving_start.isoformat()} - {i.saving_stop.isoformat()}"
)
else:
lines.append("saving : False - -")
return lines

191
ebuild/core/sunholiday.py Normal file
View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "sunholiday"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Astral ve eholidays_tr kullanarak güneş / tatil / mevsim bilgisi üretir"
__version__ = "0.1.1"
__date__ = "2025-11-22"
"""
ebuild/core/sunholiday.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
Eski sistemde kullandığın SunHolidayInfo sınıfını, ebuild projesi
için bağımsız bir modüle taşıdık. Bu modül:
- Astral ile gün doğumu / gün batımı / öğlen
- eholidays_tr ile resmi tatil (yoksa graceful fallback)
- Basit mevsim hesabı
bilgilerini üretir ve SeasonController tarafından kullanılır.
Dış arayüz:
class SunHolidayInfo:
- get_info() -> dict
- print_info()
- to_json()
"""
import json
from datetime import datetime
from enum import Enum
from astral import LocationInfo
from astral.sun import sun
import pytz
# eholidays_tr yoksa sistem göçmesin → dummy tracker
try:
from eholidays_tr import HolidayTracker # type: ignore
except Exception:
class HolidayTracker: # type: ignore
def __init__(self, year: int) -> None:
self.year = year
def is_holiday(self, dt) -> None:
return None
class Season(Enum):
KIS = "Kış"
ILKBAHAR = "İlkbahar"
YAZ = "Yaz"
SONBAHAR = "Sonbahar"
class SunHolidayInfo:
"""
Astral + eholidays_tr kombinasyonu ile gün / tatil / mevsim bilgisi üretir.
"""
def __init__(
self,
current_datetime=None,
city_name: str = "Ankara",
country: str = "Turkey",
timezone: str = "Europe/Istanbul",
latitude: float = 39.92077,
longitude: float = 32.85411,
) -> None:
self.city = city_name
self.country = country
self.timezone_str = timezone
self.timezone = pytz.timezone(timezone)
self.lat = latitude
self.lon = longitude
if current_datetime is None:
self.current_datetime = datetime.now(self.timezone)
else:
# Eğer naive datetime geldiyse timezone ekleyelim
if current_datetime.tzinfo is None:
self.current_datetime = self.timezone.localize(current_datetime)
else:
self.current_datetime = current_datetime.astimezone(self.timezone)
self.location = LocationInfo(
self.city, self.country, self.timezone_str, self.lat, self.lon
)
self.work_date = self.current_datetime
# ---------------------------------------------------------
# Yardımcılar
# ---------------------------------------------------------
def return_init(self) -> str:
return (
f"SunHolidayInfo: {self.country} {self.city} "
f"{self.timezone_str} {self.lat} - {self.lon}"
)
def get_season(self, date_obj: datetime):
"""
Astronomik mevsim aralıklarıyla kaba mevsim ve gün bilgisi üretir.
"""
ranges = {
Season.KIS: [
(date_obj.replace(month=1, day=1), date_obj.replace(month=3, day=20)),
(date_obj.replace(month=12, day=21), date_obj.replace(month=12, day=31)),
],
Season.ILKBAHAR: [
(date_obj.replace(month=3, day=21), date_obj.replace(month=6, day=20))
],
Season.YAZ: [
(date_obj.replace(month=6, day=21), date_obj.replace(month=9, day=22))
],
Season.SONBAHAR: [
(date_obj.replace(month=9, day=23), date_obj.replace(month=12, day=20))
],
}
for season, periods in ranges.items():
for start, end in periods:
if start <= date_obj <= end:
total_days = (end - start).days + 1
passed_days = (date_obj - start).days
remaining_days = (end - date_obj).days
return {
"season": season.value,
"season_start": start.isoformat(),
"season_end": end.isoformat(),
"season_day": total_days,
"season_passed": passed_days,
"season_remaining": remaining_days,
}
return None
# ---------------------------------------------------------
# Ana API
# ---------------------------------------------------------
def get_info(self) -> dict:
"""
Astral + HolidayTracker + mevsim hesabını bir dict olarak döner.
"""
sun_data = sun(
self.location.observer,
date=self.work_date,
tzinfo=self.timezone,
)
tracker = HolidayTracker(self.work_date.year)
holiday_label = tracker.is_holiday(self.work_date)
season_info = self.get_season(self.work_date)
return {
"date": self.work_date.isoformat(),
"sunrise": sun_data["sunrise"].strftime("%H:%M:%S"),
"sunset": sun_data["sunset"].strftime("%H:%M:%S"),
"noon": sun_data["noon"].strftime("%H:%M:%S"),
"is_holiday": bool(holiday_label),
"holiday_label": holiday_label if holiday_label else "Yok",
"season": season_info["season"] if season_info else None,
"season_start": season_info["season_start"] if season_info else None,
"season_end": season_info["season_end"] if season_info else None,
"season_day": season_info["season_day"] if season_info else None,
"season_passed": season_info["season_passed"] if season_info else None,
"season_remaining": season_info["season_remaining"] if season_info else None,
}
def print_info(self) -> None:
info = self.get_info()
print(f"Güneş bilgileri - {self.location.name}, {info['date']}")
print(f"Doğuş: {info['sunrise']}")
print(f"Batış: {info['sunset']}")
print(f"Öğlen: {info['noon']}")
print(f"Tatil mi?: {'Evet' if info['is_holiday'] else 'Hayır'}")
if info["is_holiday"]:
print(f"Etiket: {info['holiday_label']}")
def to_json(self) -> str:
return json.dumps(self.get_info(), ensure_ascii=False, indent=2)
if __name__ == "__main__":
s = SunHolidayInfo()
print(s.return_init())
print(s.to_json())

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

Binary file not shown.

View File

@ -0,0 +1,679 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "burner"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina ve/veya dış ısıya göre brülör ve sirkülasyon kontrol çekirdeği"
__version__ = "0.4.3"
__date__ = "2025-11-22"
"""
ebuild/core/systems/burner.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- BUILD_BURNER moduna göre (F/B) brülör ve sirkülasyon pompalarını yönetmek
- Bina ortalaması (B mod) veya dış ısı (F mod) üzerinden ısıtma isteği üretmek
- used_out_heat mantığı ile dış ısıya hafta sonu / konfor offset uygulamak
Bağımlılıklar
--------------
- building.Building
- environment.BuildingEnvironment
- season.SeasonController
- io.relay_driver.RelayDriver
- io.dbtext.DBText
- io.legacy_syslog (syslog/console çıktıları için)
- config_statics (cfg_s)
- config_runtime (cfg_v)
Notlar
------
- Brülör, igniter ve pompalar relay_driver içinde isimlendirilmiş kanallarla
temsil edilir.
- Bu dosya, eski sistemle uyum için mümkün olduğunca log formatını korumaya
çalışır.
"""
import datetime
import time as _time
from dataclasses import dataclass
from typing import Optional, Dict, Any, List, Tuple
from ..building import Building
from ..season import SeasonController
from ..environment import BuildingEnvironment
from ...io.relay_driver import RelayDriver
from ...io.dbtext import DBText
from ...io import legacy_syslog as lsys
from ... import config_statics as cfg_s
from ... import config_runtime as cfg_v
# -------------------------------------------------------------
# Yardımcı: DS18B20 okuma (hat sensörleri için)
# -------------------------------------------------------------
@dataclass
class BurnerState:
burner_on: bool
pumps_on: Tuple[str, ...]
fire_setpoint_c: float
last_change_ts: datetime.datetime
reason: str
last_building_avg: Optional[float]
last_outside_c: Optional[float]
last_used_out_c: Optional[float]
last_mode: str
# ----------------------------- Isı eğrisi --------------------
# Dış ısı → kazan çıkış setpoint haritası
# Örnek bir eğri; config_runtime ile override edilebilir.
BURNER_FIRE_SETPOINT_MAP: Dict[float, Dict[str, float]] = getattr(
cfg_v,
"BURNER_FIRE_SETPOINT_MAP",
{
-10.0: {"fire": 50.0},
-5.0: {"fire": 48.0},
0.0: {"fire": 46.0},
5.0: {"fire": 44.0},
10.0: {"fire": 40.0},
15.0: {"fire": 35.0},
20.0: {"fire": 30.0},
25.0: {"fire": 26.0},
},
)
class BurnerConfig:
"""
Brülör çalışma parametreleri (runtime config'ten override edilebilir).
"""
min_run_sec: int = 60 # brülör en az bu kadar saniye çalışsın
min_stop_sec: int = 60 # brülör en az bu kadar saniye duruşta kalsın
hysteresis_c: float = 0.5 # bina ortalaması için histerezis
# ---------------------------------------------------------
# Yardımcı fonksiyon: bina istatistikleri
# ---------------------------------------------------------
def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]:
try:
if value is None:
return default
return float(value)
except Exception:
return default
def _merge_stats(old: Optional[Dict[str, Any]], new: Dict[str, Any]) -> Dict[str, Any]:
"""
Bina istatistiği için min/avg/max birleştirme.
"""
if old is None:
return dict(new)
def _pick(key: str, func):
a = old.get(key)
b = new.get(key)
if a is None:
return b
if b is None:
return a
return func(a, b)
return {
"min": _pick("min", min),
"avg": new.get("avg"),
"max": _pick("max", max),
}
# ---------------------------------------------------------
# used_out_heat hesabı
# ---------------------------------------------------------
def _apply_weekend_and_comfort(
used_out: Optional[float],
now: datetime.datetime,
weekend_boost_c: float,
comfort_offset_c: float,
) -> Optional[float]:
"""
Haftasonu ve konfor offset'ini used_out üzerine uygular.
"""
if used_out is None:
return None
result = float(used_out)
# Haftasonu boost: Cumartesi / Pazar
if now.weekday() >= 5 and weekend_boost_c != 0.0:
result -= weekend_boost_c
# Konfor offset'i
if comfort_offset_c != 0.0:
result -= comfort_offset_c
return result
def pick_fire_setpoint(outside_c: Optional[float]) -> float:
"""
Dış ısı (used_out_heat) için en yakın fire setpoint'i döndürür.
Eğer outside_c None ise, MAX_OUTLET_C kullanılır.
"""
if outside_c is None:
return float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
keys = sorted(BURNER_FIRE_SETPOINT_MAP.keys())
nearest_key = min(keys, key=lambda k: abs(k - outside_c))
mapping = BURNER_FIRE_SETPOINT_MAP.get(nearest_key, {})
return float(mapping.get("fire", getattr(cfg_v, "MAX_OUTLET_C", 45.0)))
# ---------------------------------------------------------
# Ana sınıf: BurnerController
# ---------------------------------------------------------
class BurnerController:
"""
F/B moduna göre brülör kontrolü yapan sınıf.
BUILD_BURNER = "B"
bina ortalama sıcaklığına göre kontrol
BUILD_BURNER = "F"
dış ısıya göre (OUTSIDE_LIMIT_HEAT_C) karar veren mod
(burada dış ısı olarak *used_out_heat* kullanılır).
"""
def __init__(
self,
building: Building,
relay_driver: RelayDriver,
logger: Optional[DBText] = None,
config: Optional[BurnerConfig] = None,
burner_id: Optional[int] = None,
environment: Optional[BuildingEnvironment] = None,
) -> None:
self.building = building
self.relays = relay_driver
# Runtime konfig: varsayılan BurnerConfig + config_runtime override
self.cfg = config or BurnerConfig()
try:
self.cfg.min_run_sec = int(getattr(cfg_v, "BURNER_MIN_RUN_SEC", self.cfg.min_run_sec))
self.cfg.min_stop_sec = int(getattr(cfg_v, "BURNER_MIN_STOP_SEC", self.cfg.min_stop_sec))
self.cfg.hysteresis_c = float(getattr(cfg_v, "BURNER_HYSTERESIS_C", self.cfg.hysteresis_c))
except Exception as e:
print("BurnerConfig override error:", e)
# Hangi brülör? → config_statics.BURNER_DEFAULT_ID veya parametre
default_id = int(getattr(cfg_s, "BURNER_DEFAULT_ID", 0))
self.burner_id = int(burner_id) if burner_id is not None else default_id
# DBText logger
log_file = getattr(cfg_s, "BURNER_LOG_FILE", "ebuild_burner_log.sql")
log_table = getattr(cfg_s, "BURNER_LOG_TABLE", "eburner_log")
self.logger = logger or DBText(
filename=log_file,
table=log_table,
app="EBURNER",
)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
# Röle kanal isimleri (eski yapı ile uyum için fallback)
self.igniter_ch: str = getattr(cfg_s, "BURNER_IGNITER_CH", "igniter")
self.pump_channels: List[str] = list(
getattr(cfg_s, "BURNER_PUMPS", ["circulation_a", "circulation_b"])
)
self.default_pumps: List[str] = list(
getattr(cfg_s, "BURNER_DEFAULT_PUMPS", ["circulation_a"])
)
# Bina okuma periyodu (BUILDING_READ_PERIOD_S)
self._building_last_read_ts: Optional[datetime.datetime] = None
self._building_read_period: float = float(
getattr(cfg_v, "BUILDING_READ_PERIOD_S", 60.0)
)
self._building_last_stats: Optional[Dict[str, Any]] = None
# used_out_heat için parametreler
self.used_out_c: Optional[float] = None
self._last_used_update_ts: Optional[datetime.datetime] = None
self.outside_smooth_sec: float = float(
getattr(cfg_v, "OUTSIDE_SMOOTH_SECONDS", 900.0)
)
self.weekend_boost_c: float = float(
getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)
)
self.comfort_offset_c: float = float(
getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)
)
# Ortam nesnesi (opsiyonel)
self.environment = environment
# Ortamdan başlangıç dış ısı alınabiliyorsa used_out'u hemen doldur
if self.environment is not None:
try:
first_out = self.environment.get_outside_temp_cached()
except Exception:
first_out = None
if first_out is not None:
self.used_out_c = first_out
self._last_used_update_ts = datetime.datetime.now()
# Çalışma modu
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
initial_mode = cfg_mode if cfg_mode in ("F", "B") else "F"
# Başlangıç state
self.state = BurnerState(
burner_on=False,
pumps_on=tuple(),
fire_setpoint_c=max_out,
last_change_ts=datetime.datetime.now(),
reason="init",
last_building_avg=None,
last_outside_c=None,
last_used_out_c=None,
last_mode=initial_mode,
)
# Mevsim / güneş bilgisi (syslog üst block için)
try:
self.season = SeasonController.from_now()
except Exception as e:
print("SeasonController.from_now() hata:", e)
self.season = None
# ---------------------------------------------------------
# Bina istatistikleri
# ---------------------------------------------------------
def _get_building_stats(self, now: datetime.datetime) -> Optional[Dict[str, Any]]:
"""
Bina ortalaması / min / max gibi istatistikleri periyodik olarak okur.
BUILDING_READ_PERIOD_S içinde cache kullanır.
"""
if self._building_last_read_ts is None:
need_read = True
else:
delta = (now - self._building_last_read_ts).total_seconds()
need_read = delta >= self._building_read_period
if not need_read:
return self._building_last_stats
self._building_last_read_ts = now
return None
try:
stats = self.building.get_stats()
except Exception as e:
print("Building.get_stats() hata:", e)
return self._building_last_stats
self._building_last_stats = stats
return stats
# ---------------------------------------------------------
# used_out_heat güncelleme
# ---------------------------------------------------------
def _update_used_out(self, now: datetime.datetime, outside_c: Optional[float]) -> Optional[float]:
"""
Dış ısı okumasına göre used_out_heat günceller.
- OUTSIDE_SMOOTH_SECONDS süresince eksponansiyel smoothing
- Haftasonu ve konfor offset'i eklenir.
"""
raw = outside_c
if raw is None:
return self.used_out_c
# Smooth
if self.used_out_c is None or self._last_used_update_ts is None:
smoothed = raw
else:
dt = (now - self._last_used_update_ts).total_seconds()
if dt <= 0:
smoothed = self.used_out_c
else:
tau = max(1.0, self.outside_smooth_sec)
alpha = min(1.0, dt / tau)
smoothed = (1.0 - alpha) * self.used_out_c + alpha * raw
self.used_out_c = smoothed
self._last_used_update_ts = now
# Haftasonu / konfor offset'i uygula
final_used = _apply_weekend_and_comfort(
smoothed,
now,
self.weekend_boost_c,
self.comfort_offset_c,
)
return final_used
# ---------------------------------------------------------
# Isı ihtiyacı kararları
# ---------------------------------------------------------
def _should_heat_by_outside(self, used_out: Optional[float]) -> bool:
"""
F modunda (dış ısıya göre) ısıtma isteği.
"""
limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 17.0))
if used_out is None:
return False
want = used_out < limit
print(f"should_heat_by_outside: used={used_out:.3f}C limit={limit:.1f}C")
return want
def _should_heat_by_building(self, building_avg: Optional[float], now: datetime.datetime) -> bool:
"""
B modunda bina ortalaması + konfor setpoint'e göre ısıtma isteği.
"""
comfort = float(getattr(cfg_v, "COMFORT_SETPOINT_C", 23.0))
h = self.cfg.hysteresis_c
if building_avg is None:
return False
if building_avg < (comfort - h):
return True
if building_avg > (comfort + h):
return False
# Histerezis bandında önceki state'i koru
return self.state.burner_on
# ---------------------------------------------------------
# Min çalışma / durma süreleri
# ---------------------------------------------------------
def _respect_min_times(self, now: datetime.datetime, want_on: bool) -> bool:
"""
min_run_sec / min_stop_sec kurallarını uygular.
- İlk ılışta (state.reason == 'init') kısıtlama uygulanmaz.
"""
# İlk tick: min_run/min_stop uygulama
try:
if getattr(self.state, "reason", "") == "init":
return want_on
except Exception:
pass
elapsed = (now - self.state.last_change_ts).total_seconds()
if self.state.burner_on:
# Çalışırken min_run dolmadan kapatma
if not want_on and elapsed < self.cfg.min_run_sec:
return True
else:
# Kapalıyken min_stop dolmadan açma
if want_on and elapsed < self.cfg.min_stop_sec:
return False
return want_on
# ---------------------------------------------------------
# Çıkışları rölelere uygulama
# ---------------------------------------------------------
def _apply_outputs(
self,
now: datetime.datetime,
mode: str,
burner_on: bool,
pumps_on: Tuple[str, ...],
fire_setpoint_c: float,
reason: str,
) -> None:
"""
Röleleri sürer, state'i günceller, log ve syslog üretir.
"""
# 1) Röle sürücüsü (igniter + pompalar)
try:
# Yeni API: RelayDriver brülör-aware ise
if hasattr(self.relays, "set_igniter"):
# Brülör ateşleme
self.relays.set_igniter(self.burner_id, burner_on)
# Pompalar: her zaman kanal isimleri üzerinden sür
if hasattr(self.relays, "all_pumps"):
all_pumps = list(self.relays.all_pumps(self.burner_id)) # ['circulation_a', ...]
for ch in all_pumps:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# all_pumps yoksa, config_statics'ten gelen pump_channels ile sür
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# Eski/çok basit API: doğrudan kanal adları
self.relays.set_channel(self.igniter_ch, burner_on)
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
except Exception as exc:
# legacy_syslog.log_error YOK, bu yüzden ya loga yaz ya da print et
try:
msg = f"[relay_error] igniter_ch={self.igniter_ch} burner_on={burner_on} pumps_on={pumps_on} exc={exc}"
lsys.send_legacy_syslog(lsys.format_line(98, msg))
except Exception:
print("Relay error in _apply_outputs:", exc)
# 2) State güncelle
if burner_on != self.state.burner_on or tuple(pumps_on) != self.state.pumps_on:
self.state.last_change_ts = now
self.state.burner_on = burner_on
self.state.pumps_on = tuple(pumps_on)
self.state.fire_setpoint_c = fire_setpoint_c
self.state.reason = reason
self.state.last_mode = mode
# 3) DBText logger'a yaz
try:
self.logger.insert(
{
"ts": now,
"mode": mode,
"burner_on": int(burner_on),
"pumps": ",".join(pumps_on),
"fire_sp": fire_setpoint_c,
"reason": reason,
"bavg": _safe_float(self.state.last_building_avg),
"out": _safe_float(self.state.last_outside_c),
"used": _safe_float(self.state.last_used_out_c),
}
)
except Exception:
pass
# 4) Syslog / console üst blok
try:
lsys.log_burner_header(
now=now,
mode=mode,
season=self.season,
building_avg=self.state.last_building_avg,
outside_c=self.state.last_outside_c,
used_out_c=self.state.last_used_out_c,
fire_sp=fire_setpoint_c,
burner_on=burner_on,
pumps_on=pumps_on,
)
except Exception as exc:
# Burayı tamamen sessize almayalım, hatayı konsola basalım
print("BRULOR lsys.log_burner_header error:", exc, "burner.py _apply_outputs()")
# ---------------------------------------------------------
# Ana tick fonksiyonu
# ---------------------------------------------------------
def tick(self, outside_c: Optional[float] = None) -> BurnerState:
"""
Tek bir kontrol adımı.
- Bina istatistiği BUILDING_READ_PERIOD_S periyodunda bir kez okunur,
aradaki tick'lerde cache kullanılır.
- F modunda kararlar *used_out_heat* üzerinden verilir.
"""
now = datetime.datetime.now()
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
mode = cfg_mode
print("tick outside_c:", outside_c)
# 0) dış ısı: parametre yoksa ortamdan al
if outside_c is None and getattr(self, "environment", None) is not None:
try:
outside_c = self.environment.get_outside_temp_cached()
except Exception:
outside_c = None
print("env:", getattr(self, "environment", None))
print("tick outside_c 2:", outside_c)
# 1) bina istatistiği (periyodik)
stats = self._get_building_stats(now)
building_avg = stats.get("avg") if stats else None
# 2) used_out_heat güncelle
used_out = self._update_used_out(now, outside_c)
self.state.last_building_avg = building_avg
self.state.last_outside_c = outside_c
self.state.last_used_out_c = used_out
# 3) ısıtma ihtiyacı
if mode == "F":
want_heat = self._should_heat_by_outside(used_out)
else:
mode = "B" # saçma değer gelirse B moduna zorla
want_heat = self._should_heat_by_building(building_avg, now)
want_heat = self._respect_min_times(now, want_heat)
# 4) fire setpoint F modunda da used_out üzerinden okunur
fire_sp = pick_fire_setpoint(used_out)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
fire_sp = min(fire_sp, max_out)
# 5) pompalar
if want_heat:
if hasattr(self.relays, "enabled_pumps"):
try:
pumps_list = list(self.relays.enabled_pumps(self.burner_id))
pumps = tuple(pumps_list)
except Exception:
pumps = tuple(self.default_pumps)
else:
pumps = tuple(self.default_pumps)
else:
pumps = tuple()
reason = (
f"avg={building_avg}C "
f"outside_raw={outside_c}C "
f"used={used_out}C "
f"want_heat={want_heat}"
)
print("tick reason", reason)
# 7) Rölelere uygula
self._apply_outputs(
now=now,
mode=mode,
burner_on=bool(want_heat),
pumps_on=pumps,
fire_setpoint_c=fire_sp,
reason=reason,
)
print("state", self.state)
return self.state
# -------------------------------------------------------------
# CLI / demo
# -------------------------------------------------------------
def _demo() -> None:
"""
Basit demo: Building + RelayDriver + BuildingEnvironment ile
BurnerController'ı ayağa kaldır, tick() döngüsü yap.
"""
# 1) Bina
try:
building = Building()
print("✅ Building: statics yüklendi\n")
print(building.pretty_summary())
except Exception as e:
print("❌ Building oluşturulamadı:", e)
raise SystemExit(1)
# 2) Ortam (dış ısı, ADC vs.)
try:
env = BuildingEnvironment()
except Exception as e:
print("⚠️ BuildingEnvironment oluşturulamadı:", e)
env = None
# 3) Röle sürücüsü
rel = RelayDriver(onoff=False)
# 4) Denetleyici
ctrl = BurnerController(building, rel, environment=env)
print("🔥 BurnerController başlatıldı")
print(f" Burner ID : {ctrl.burner_id}")
print(f" Çalışma modu (BUILD_BURNER): {getattr(cfg_s, 'BUILD_BURNER', 'F')} (F=dış ısı, B=bina ort)")
print(f" Igniter kanalı : {ctrl.igniter_ch}")
print(f" Pompa kanalları : {ctrl.pump_channels}")
print(f" Varsayılan pompalar : {ctrl.default_pumps}")
print(f" Konfor setpoint (°C) : {getattr(cfg_v, 'COMFORT_SETPOINT_C', 23.0)}")
print(f" Histerezis (°C) : {ctrl.cfg.hysteresis_c}")
print(f" Dış ısı limiti (°C) : {getattr(cfg_v, 'OUTSIDE_HEAT_LIMIT_C', 17.0)}")
print(f" Max kazan çıkış (°C) : {getattr(cfg_v, 'MAX_OUTLET_C', 45.0)}")
print(f" Bina okuma periyodu (s) : {ctrl._building_read_period}")
print(f" OUTSIDE_SMOOTH_SECONDS : {ctrl.outside_smooth_sec}")
print(f" WEEKEND_HEAT_BOOST_C : {ctrl.weekend_boost_c}")
print(f" BURNER_COMFORT_OFFSET_C : {ctrl.comfort_offset_c}")
print("----------------------------------------------------")
print("BurnerController demo (Ctrl+C ile çık)…")
try:
while True:
ctrl.tick()
_time.sleep(5)
except KeyboardInterrupt:
print("\nCtrl+C alındı, çıkış hazırlanıyor…")
finally:
try:
rel.all_off()
print("🔌 Tüm röleler kapatıldı.")
except Exception as e:
print(f"⚠️ Röleleri kapatırken hata: {e}")
finally:
try:
rel.cleanup()
except Exception:
pass
if __name__ == "__main__":
_demo()

View File

@ -0,0 +1,678 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "burner"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Bina ve/veya dış ısıya göre brülör ve sirkülasyon kontrol çekirdeği"
__version__ = "0.4.3"
__date__ = "2025-11-22"
"""
ebuild/core/systems/burner.py
Revision : 2025-11-22
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- BUILD_BURNER moduna göre (F/B) brülör ve sirkülasyon pompalarını yönetmek
- Bina ortalaması (B mod) veya dış ısı (F mod) üzerinden ısıtma isteği üretmek
- used_out_heat mantığı ile dış ısıya hafta sonu / konfor offset uygulamak
Bağımlılıklar
--------------
- building.Building
- environment.BuildingEnvironment
- season.SeasonController
- io.relay_driver.RelayDriver
- io.dbtext.DBText
- io.legacy_syslog (syslog/console çıktıları için)
- config_statics (cfg_s)
- config_runtime (cfg_v)
Notlar
------
- Brülör, igniter ve pompalar relay_driver içinde isimlendirilmiş kanallarla
temsil edilir.
- Bu dosya, eski sistemle uyum için mümkün olduğunca log formatını korumaya
çalışır.
"""
import datetime
import time as _time
from dataclasses import dataclass
from typing import Optional, Dict, Any, List, Tuple
from ..building import Building
from ..season import SeasonController
from ..environment import BuildingEnvironment
from ...io.relay_driver import RelayDriver
from ...io.dbtext import DBText
from ...io import legacy_syslog as lsys
from ... import config_statics as cfg_s
from ... import config_runtime as cfg_v
# -------------------------------------------------------------
# Yardımcı: DS18B20 okuma (hat sensörleri için)
# -------------------------------------------------------------
@dataclass
class BurnerState:
burner_on: bool
pumps_on: Tuple[str, ...]
fire_setpoint_c: float
last_change_ts: datetime.datetime
reason: str
last_building_avg: Optional[float]
last_outside_c: Optional[float]
last_used_out_c: Optional[float]
last_mode: str
# ----------------------------- Isı eğrisi --------------------
# Dış ısı → kazan çıkış setpoint haritası
# Örnek bir eğri; config_runtime ile override edilebilir.
BURNER_FIRE_SETPOINT_MAP: Dict[float, Dict[str, float]] = getattr(
cfg_v,
"BURNER_FIRE_SETPOINT_MAP",
{
-10.0: {"fire": 50.0},
-5.0: {"fire": 48.0},
0.0: {"fire": 46.0},
5.0: {"fire": 44.0},
10.0: {"fire": 40.0},
15.0: {"fire": 35.0},
20.0: {"fire": 30.0},
25.0: {"fire": 26.0},
},
)
class BurnerConfig:
"""
Brülör çalışma parametreleri (runtime config'ten override edilebilir).
"""
min_run_sec: int = 60 # brülör en az bu kadar saniye çalışsın
min_stop_sec: int = 60 # brülör en az bu kadar saniye duruşta kalsın
hysteresis_c: float = 0.5 # bina ortalaması için histerezis
# ---------------------------------------------------------
# Yardımcı fonksiyon: bina istatistikleri
# ---------------------------------------------------------
def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]:
try:
if value is None:
return default
return float(value)
except Exception:
return default
def _merge_stats(old: Optional[Dict[str, Any]], new: Dict[str, Any]) -> Dict[str, Any]:
"""
Bina istatistiği için min/avg/max birleştirme.
"""
if old is None:
return dict(new)
def _pick(key: str, func):
a = old.get(key)
b = new.get(key)
if a is None:
return b
if b is None:
return a
return func(a, b)
return {
"min": _pick("min", min),
"avg": new.get("avg"),
"max": _pick("max", max),
}
# ---------------------------------------------------------
# used_out_heat hesabı
# ---------------------------------------------------------
def _apply_weekend_and_comfort(
used_out: Optional[float],
now: datetime.datetime,
weekend_boost_c: float,
comfort_offset_c: float,
) -> Optional[float]:
"""
Haftasonu ve konfor offset'ini used_out üzerine uygular.
"""
if used_out is None:
return None
result = float(used_out)
# Haftasonu boost: Cumartesi / Pazar
if now.weekday() >= 5 and weekend_boost_c != 0.0:
result -= weekend_boost_c
# Konfor offset'i
if comfort_offset_c != 0.0:
result -= comfort_offset_c
return result
def pick_fire_setpoint(outside_c: Optional[float]) -> float:
"""
Dış ısı (used_out_heat) için en yakın fire setpoint'i döndürür.
Eğer outside_c None ise, MAX_OUTLET_C kullanılır.
"""
if outside_c is None:
return float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
keys = sorted(BURNER_FIRE_SETPOINT_MAP.keys())
nearest_key = min(keys, key=lambda k: abs(k - outside_c))
mapping = BURNER_FIRE_SETPOINT_MAP.get(nearest_key, {})
return float(mapping.get("fire", getattr(cfg_v, "MAX_OUTLET_C", 45.0)))
# ---------------------------------------------------------
# Ana sınıf: BurnerController
# ---------------------------------------------------------
class BurnerController:
"""
F/B moduna göre brülör kontrolü yapan sınıf.
BUILD_BURNER = "B"
bina ortalama sıcaklığına göre kontrol
BUILD_BURNER = "F"
dış ısıya göre (OUTSIDE_LIMIT_HEAT_C) karar veren mod
(burada dış ısı olarak *used_out_heat* kullanılır).
"""
def __init__(
self,
building: Building,
relay_driver: RelayDriver,
logger: Optional[DBText] = None,
config: Optional[BurnerConfig] = None,
burner_id: Optional[int] = None,
environment: Optional[BuildingEnvironment] = None,
) -> None:
self.building = building
self.relays = relay_driver
# Runtime konfig: varsayılan BurnerConfig + config_runtime override
self.cfg = config or BurnerConfig()
try:
self.cfg.min_run_sec = int(getattr(cfg_v, "BURNER_MIN_RUN_SEC", self.cfg.min_run_sec))
self.cfg.min_stop_sec = int(getattr(cfg_v, "BURNER_MIN_STOP_SEC", self.cfg.min_stop_sec))
self.cfg.hysteresis_c = float(getattr(cfg_v, "BURNER_HYSTERESIS_C", self.cfg.hysteresis_c))
except Exception as e:
print("BurnerConfig override error:", e)
# Hangi brülör? → config_statics.BURNER_DEFAULT_ID veya parametre
default_id = int(getattr(cfg_s, "BURNER_DEFAULT_ID", 0))
self.burner_id = int(burner_id) if burner_id is not None else default_id
# DBText logger
log_file = getattr(cfg_s, "BURNER_LOG_FILE", "ebuild_burner_log.sql")
log_table = getattr(cfg_s, "BURNER_LOG_TABLE", "eburner_log")
self.logger = logger or DBText(
filename=log_file,
table=log_table,
app="EBURNER",
)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
# Röle kanal isimleri (eski yapı ile uyum için fallback)
self.igniter_ch: str = getattr(cfg_s, "BURNER_IGNITER_CH", "igniter")
self.pump_channels: List[str] = list(
getattr(cfg_s, "BURNER_PUMPS", ["circulation_a", "circulation_b"])
)
self.default_pumps: List[str] = list(
getattr(cfg_s, "BURNER_DEFAULT_PUMPS", ["circulation_a"])
)
# Bina okuma periyodu (BUILDING_READ_PERIOD_S)
self._building_last_read_ts: Optional[datetime.datetime] = None
self._building_read_period: float = float(
getattr(cfg_v, "BUILDING_READ_PERIOD_S", 60.0)
)
self._building_last_stats: Optional[Dict[str, Any]] = None
# used_out_heat için parametreler
self.used_out_c: Optional[float] = None
self._last_used_update_ts: Optional[datetime.datetime] = None
self.outside_smooth_sec: float = float(
getattr(cfg_v, "OUTSIDE_SMOOTH_SECONDS", 900.0)
)
self.weekend_boost_c: float = float(
getattr(cfg_v, "WEEKEND_HEAT_BOOST_C", 0.0)
)
self.comfort_offset_c: float = float(
getattr(cfg_v, "BURNER_COMFORT_OFFSET_C", 0.0)
)
# Ortam nesnesi (opsiyonel)
self.environment = environment
# Ortamdan başlangıç dış ısı alınabiliyorsa used_out'u hemen doldur
if self.environment is not None:
try:
first_out = self.environment.get_outside_temp_cached()
except Exception:
first_out = None
if first_out is not None:
self.used_out_c = first_out
self._last_used_update_ts = datetime.datetime.now()
# Çalışma modu
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
initial_mode = cfg_mode if cfg_mode in ("F", "B") else "F"
# Başlangıç state
self.state = BurnerState(
burner_on=False,
pumps_on=tuple(),
fire_setpoint_c=max_out,
last_change_ts=datetime.datetime.now(),
reason="init",
last_building_avg=None,
last_outside_c=None,
last_used_out_c=None,
last_mode=initial_mode,
)
# Mevsim / güneş bilgisi (syslog üst block için)
try:
self.season = SeasonController.from_now()
except Exception as e:
print("SeasonController.from_now() hata:", e)
self.season = None
# ---------------------------------------------------------
# Bina istatistikleri
# ---------------------------------------------------------
def _get_building_stats(self, now: datetime.datetime) -> Optional[Dict[str, Any]]:
"""
Bina ortalaması / min / max gibi istatistikleri periyodik olarak okur.
BUILDING_READ_PERIOD_S içinde cache kullanır.
"""
if self._building_last_read_ts is None:
need_read = True
else:
delta = (now - self._building_last_read_ts).total_seconds()
need_read = delta >= self._building_read_period
if not need_read:
return self._building_last_stats
try:
stats = self.building.get_stats()
except Exception as e:
print("Building.get_stats() hata:", e)
return self._building_last_stats
self._building_last_read_ts = now
self._building_last_stats = stats
return stats
# ---------------------------------------------------------
# used_out_heat güncelleme
# ---------------------------------------------------------
def _update_used_out(self, now: datetime.datetime, outside_c: Optional[float]) -> Optional[float]:
"""
Dış ısı okumasına göre used_out_heat günceller.
- OUTSIDE_SMOOTH_SECONDS süresince eksponansiyel smoothing
- Haftasonu ve konfor offset'i eklenir.
"""
raw = outside_c
if raw is None:
return self.used_out_c
# Smooth
if self.used_out_c is None or self._last_used_update_ts is None:
smoothed = raw
else:
dt = (now - self._last_used_update_ts).total_seconds()
if dt <= 0:
smoothed = self.used_out_c
else:
tau = max(1.0, self.outside_smooth_sec)
alpha = min(1.0, dt / tau)
smoothed = (1.0 - alpha) * self.used_out_c + alpha * raw
self.used_out_c = smoothed
self._last_used_update_ts = now
# Haftasonu / konfor offset'i uygula
final_used = _apply_weekend_and_comfort(
smoothed,
now,
self.weekend_boost_c,
self.comfort_offset_c,
)
return final_used
# ---------------------------------------------------------
# Isı ihtiyacı kararları
# ---------------------------------------------------------
def _should_heat_by_outside(self, used_out: Optional[float]) -> bool:
"""
F modunda (dış ısıya göre) ısıtma isteği.
"""
limit = float(getattr(cfg_v, "OUTSIDE_HEAT_LIMIT_C", 17.0))
if used_out is None:
return False
want = used_out < limit
print(f"should_heat_by_outside: used={used_out:.3f}C limit={limit:.1f}C")
return want
def _should_heat_by_building(self, building_avg: Optional[float], now: datetime.datetime) -> bool:
"""
B modunda bina ortalaması + konfor setpoint'e göre ısıtma isteği.
"""
comfort = float(getattr(cfg_v, "COMFORT_SETPOINT_C", 23.0))
h = self.cfg.hysteresis_c
if building_avg is None:
return False
if building_avg < (comfort - h):
return True
if building_avg > (comfort + h):
return False
# Histerezis bandında önceki state'i koru
return self.state.burner_on
# ---------------------------------------------------------
# Min çalışma / durma süreleri
# ---------------------------------------------------------
def _respect_min_times(self, now: datetime.datetime, want_on: bool) -> bool:
"""
min_run_sec / min_stop_sec kurallarını uygular.
- İlk ılışta (state.reason == 'init') kısıtlama uygulanmaz.
"""
# İlk tick: min_run/min_stop uygulama
try:
if getattr(self.state, "reason", "") == "init":
return want_on
except Exception:
pass
elapsed = (now - self.state.last_change_ts).total_seconds()
if self.state.burner_on:
# Çalışırken min_run dolmadan kapatma
if not want_on and elapsed < self.cfg.min_run_sec:
return True
else:
# Kapalıyken min_stop dolmadan açma
if want_on and elapsed < self.cfg.min_stop_sec:
return False
return want_on
# ---------------------------------------------------------
# Çıkışları rölelere uygulama
# ---------------------------------------------------------
def _apply_outputs(
self,
now: datetime.datetime,
mode: str,
burner_on: bool,
pumps_on: Tuple[str, ...],
fire_setpoint_c: float,
reason: str,
) -> None:
"""
Röleleri sürer, state'i günceller, log ve syslog üretir.
"""
# 1) Röle sürücüsü (igniter + pompalar)
try:
# Yeni API: RelayDriver brülör-aware ise
if hasattr(self.relays, "set_igniter"):
# Brülör ateşleme
self.relays.set_igniter(self.burner_id, burner_on)
# Pompalar: her zaman kanal isimleri üzerinden sür
if hasattr(self.relays, "all_pumps"):
all_pumps = list(self.relays.all_pumps(self.burner_id)) # ['circulation_a', ...]
for ch in all_pumps:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# all_pumps yoksa, config_statics'ten gelen pump_channels ile sür
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
else:
# Eski/çok basit API: doğrudan kanal adları
self.relays.set_channel(self.igniter_ch, burner_on)
for ch in self.pump_channels:
self.relays.set_channel(ch, (ch in pumps_on))
except Exception as exc:
# legacy_syslog.log_error YOK, bu yüzden ya loga yaz ya da print et
try:
msg = f"[relay_error] igniter_ch={self.igniter_ch} burner_on={burner_on} pumps_on={pumps_on} exc={exc}"
lsys.send_legacy_syslog(lsys.format_line(98, msg))
except Exception:
print("Relay error in _apply_outputs:", exc)
# 2) State güncelle
if burner_on != self.state.burner_on or tuple(pumps_on) != self.state.pumps_on:
self.state.last_change_ts = now
self.state.burner_on = burner_on
self.state.pumps_on = tuple(pumps_on)
self.state.fire_setpoint_c = fire_setpoint_c
self.state.reason = reason
self.state.last_mode = mode
# 3) DBText logger'a yaz
try:
self.logger.insert(
{
"ts": now,
"mode": mode,
"burner_on": int(burner_on),
"pumps": ",".join(pumps_on),
"fire_sp": fire_setpoint_c,
"reason": reason,
"bavg": _safe_float(self.state.last_building_avg),
"out": _safe_float(self.state.last_outside_c),
"used": _safe_float(self.state.last_used_out_c),
}
)
except Exception:
pass
# 4) Syslog / console üst blok
try:
lsys.log_burner_header(
now=now,
mode=mode,
season=self.season,
building_avg=self.state.last_building_avg,
outside_c=self.state.last_outside_c,
used_out_c=self.state.last_used_out_c,
fire_sp=fire_setpoint_c,
burner_on=burner_on,
pumps_on=pumps_on,
)
except Exception as exc:
# Burayı tamamen sessize almayalım, hatayı konsola basalım
print("BRULOR lsys.log_burner_header error:", exc, "burner.py _apply_outputs()")
# ---------------------------------------------------------
# Ana tick fonksiyonu
# ---------------------------------------------------------
def tick(self, outside_c: Optional[float] = None) -> BurnerState:
"""
Tek bir kontrol adımı.
- Bina istatistiği BUILDING_READ_PERIOD_S periyodunda bir kez okunur,
aradaki tick'lerde cache kullanılır.
- F modunda kararlar *used_out_heat* üzerinden verilir.
"""
now = datetime.datetime.now()
cfg_mode = str(getattr(cfg_s, "BUILD_BURNER", "F")).upper()
mode = cfg_mode
print("tick outside_c:", outside_c)
# 0) dış ısı: parametre yoksa ortamdan al
if outside_c is None and getattr(self, "environment", None) is not None:
try:
outside_c = self.environment.get_outside_temp_cached()
except Exception:
outside_c = None
print("env:", getattr(self, "environment", None))
print("tick outside_c 2:", outside_c)
# 1) bina istatistiği (periyodik)
stats = self._get_building_stats(now)
building_avg = stats.get("avg") if stats else None
# 2) used_out_heat güncelle
used_out = self._update_used_out(now, outside_c)
self.state.last_building_avg = building_avg
self.state.last_outside_c = outside_c
self.state.last_used_out_c = used_out
# 3) ısıtma ihtiyacı
if mode == "F":
want_heat = self._should_heat_by_outside(used_out)
else:
mode = "B" # saçma değer gelirse B moduna zorla
want_heat = self._should_heat_by_building(building_avg, now)
want_heat = self._respect_min_times(now, want_heat)
# 4) fire setpoint F modunda da used_out üzerinden okunur
fire_sp = pick_fire_setpoint(used_out)
max_out = float(getattr(cfg_v, "MAX_OUTLET_C", 45.0))
fire_sp = min(fire_sp, max_out)
# 5) pompalar
if want_heat:
if hasattr(self.relays, "enabled_pumps"):
try:
pumps_list = list(self.relays.enabled_pumps(self.burner_id))
pumps = tuple(pumps_list)
except Exception:
pumps = tuple(self.default_pumps)
else:
pumps = tuple(self.default_pumps)
else:
pumps = tuple()
reason = (
f"avg={building_avg}C "
f"outside_raw={outside_c}C "
f"used={used_out}C "
f"want_heat={want_heat}"
)
print("tick reason", reason)
# 7) Rölelere uygula
self._apply_outputs(
now=now,
mode=mode,
burner_on=bool(want_heat),
pumps_on=pumps,
fire_setpoint_c=fire_sp,
reason=reason,
)
print("state", self.state)
return self.state
# -------------------------------------------------------------
# CLI / demo
# -------------------------------------------------------------
def _demo() -> None:
"""
Basit demo: Building + RelayDriver + BuildingEnvironment ile
BurnerController'ı ayağa kaldır, tick() döngüsü yap.
"""
# 1) Bina
try:
building = Building()
print("✅ Building: statics yüklendi\n")
print(building.pretty_summary())
except Exception as e:
print("❌ Building oluşturulamadı:", e)
raise SystemExit(1)
# 2) Ortam (dış ısı, ADC vs.)
try:
env = BuildingEnvironment()
except Exception as e:
print("⚠️ BuildingEnvironment oluşturulamadı:", e)
env = None
# 3) Röle sürücüsü
rel = RelayDriver(onoff=False)
# 4) Denetleyici
ctrl = BurnerController(building, rel, environment=env)
print("🔥 BurnerController başlatıldı")
print(f" Burner ID : {ctrl.burner_id}")
print(f" Çalışma modu (BUILD_BURNER): {getattr(cfg_s, 'BUILD_BURNER', 'F')} (F=dış ısı, B=bina ort)")
print(f" Igniter kanalı : {ctrl.igniter_ch}")
print(f" Pompa kanalları : {ctrl.pump_channels}")
print(f" Varsayılan pompalar : {ctrl.default_pumps}")
print(f" Konfor setpoint (°C) : {getattr(cfg_v, 'COMFORT_SETPOINT_C', 23.0)}")
print(f" Histerezis (°C) : {ctrl.cfg.hysteresis_c}")
print(f" Dış ısı limiti (°C) : {getattr(cfg_v, 'OUTSIDE_HEAT_LIMIT_C', 17.0)}")
print(f" Max kazan çıkış (°C) : {getattr(cfg_v, 'MAX_OUTLET_C', 45.0)}")
print(f" Bina okuma periyodu (s) : {ctrl._building_read_period}")
print(f" OUTSIDE_SMOOTH_SECONDS : {ctrl.outside_smooth_sec}")
print(f" WEEKEND_HEAT_BOOST_C : {ctrl.weekend_boost_c}")
print(f" BURNER_COMFORT_OFFSET_C : {ctrl.comfort_offset_c}")
print("----------------------------------------------------")
print("BurnerController demo (Ctrl+C ile çık)…")
try:
while True:
ctrl.tick()
_time.sleep(5)
except KeyboardInterrupt:
print("\nCtrl+C alındı, çıkış hazırlanıyor…")
finally:
try:
rel.all_off()
print("🔌 Tüm röleler kapatıldı.")
except Exception as e:
print(f"⚠️ Röleleri kapatırken hata: {e}")
finally:
try:
rel.cleanup()
except Exception:
pass
if __name__ == "__main__":
_demo()

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Yangın alarm sistemi iskeleti."""

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Hidrofor sistemi iskeleti."""

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Sulama sistemi iskeleti."""

1
ebuild/io/__init__.py Normal file
View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

423
ebuild/io/adc_mcp3008.py Normal file
View 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ı 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 ı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ı 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ü ısından kill-switch gerekip gerekmediğini söyler.
"""
return self.gas.should_shutdown_system()

333
ebuild/io/config_ini.py Normal file
View 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
View 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 ı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
View 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
View 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 ı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
View 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
View 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 ı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
View 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))

View 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}"
# configteki 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 .0sı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
# Configte 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
View 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 ı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())

View 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
View 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}"
# configteki 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 .0sı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
# Configte 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
View 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 ı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())

95
ebuild/reloader.py Normal file
View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
""" ebuild/reloader.py
İki ayrı konfigürasyon modülünü (config_statics, config_variables)
izleyen ve dosya değişikliklerinde importlib.reload çağıran yardımcı sınıf.
"""
import os
import importlib
from . import config_statics as cfg_s
from . import config_variables as cfg_v
class ConfigReloader(object):
""" Statik ve dinamik config dosyalarını izler.
maybe_reload() çağrıldığında:
- Değişmiş dosyaları yeniden yükler
- Hangi grubun değiştiğini dict olarak döner.
"""
def __init__(self):
self._statics_path = None
self._statics_last = None
self._vars_path = None
self._vars_last = None
# Statics
try:
p = getattr(cfg_s, "__file__", None)
if p and p.endswith((".pyc", ".pyo")):
p = p[:-1]
self._statics_path = os.path.abspath(p) if p else None
self._statics_last = (
os.path.getmtime(self._statics_path)
if self._statics_path else None
)
except Exception:
self._statics_path, self._statics_last = None, None
# Variables
try:
p = getattr(cfg_v, "__file__", None)
if p and p.endswith((".pyc", ".pyo")):
p = p[:-1]
self._vars_path = os.path.abspath(p) if p else None
self._vars_last = (
os.path.getmtime(self._vars_path)
if self._vars_path else None
)
except Exception:
self._vars_path, self._vars_last = None, None
def _check_and_reload(self, path_attr, last_attr, module):
path = getattr(self, path_attr)
if not path:
return False
try:
cur = os.path.getmtime(path)
except Exception:
return False
last = getattr(self, last_attr)
if last is None or cur > last:
try:
importlib.reload(module)
setattr(self, last_attr, os.path.getmtime(path))
return True
except Exception:
return False
return False
def maybe_reload(self):
""" Değişiklik algılanırsa ilgili config'i yeniden yükler.
return:
{
"statics_changed": bool,
"variables_changed": bool,
}
"""
statics_changed = self._check_and_reload(
"_statics_path", "_statics_last", cfg_s
)
variables_changed = self._check_and_reload(
"_vars_path", "_vars_last", cfg_v
)
return {
"statics_changed": statics_changed,
"variables_changed": variables_changed,
}

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

24
ebuild/runtime/main.py Normal file
View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
""" ebuild/runtime/main.py
Uygulamanın ana döngüsü için iskelet.
Şu an sadece config reloader'ı test eder.
"""
from ..reloader import ConfigReloader
def main() -> None:
""" ebuild ana çalışma fonksiyonu (iskelet).
Gerçek sistemde:
- ConfigReloader izlenir
- Building + Environment + Systems kurulup döngü içinde çalıştırılır.
"""
reloader = ConfigReloader()
result = reloader.maybe_reload()
print("Config kontrol sonucu:", result)
if __name__ == "__main__":
main()

Binary file not shown.

149
ebuild/tools/relay_test.py Normal file
View File

@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
__title__ = "relay_test"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "Röle pin eşlemesini ve brülör gruplarını sahada test aracı"
__version__ = "0.1.0"
__date__ = "2025-11-20"
"""
ebuild/tools/relay_test.py
Revision : 2025-11-20
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- config_statics.RELAY_GPIO ve BURNER_GROUPS'e göre röle sürücüsünü yüklemek,
- Her kanalı sırayla ıp kapatarak kablolamanın doğru olup olmadığını
sahada test etmek,
- Brülör bazlı test:
* Belirli bir burner_id için igniter kanalı,
* default pompalar (circulation.default=1 olanlar)
ayrı ayrı ON/OFF denemesi.
Kullanım
--------
Rasp2 üzerinde:
cd /home/karatay/ebuild
python3 -m ebuild.tools.relay_test
UYARI: Bu test gerçek röleleri tetikler.
Kazan/şebeke enerjisi bağlıysa dikkatli kullan.
"""
import time
from typing import Optional
from ..io.relay_driver import RelayDriver
from .. import config_statics as cfg_s
def _sleep(s: float) -> None:
try:
time.sleep(s)
except KeyboardInterrupt:
raise
def test_all_channels(driver: RelayDriver, on_time: float = 1.0, off_time: float = 0.5) -> None:
"""
Tüm röle kanallarını sırayla:
- ON (on_time saniye)
- OFF (off_time saniye)
yaparak test eder.
"""
print("\n=== Tüm kanalların sırayla test edilmesi ===")
channels = sorted(driver.channels())
if not channels:
print("⚠️ Tanımlı kanal yok (RELAY_GPIO boş?).")
return
for ch in channels:
print(f"\n→ Kanal: {ch} -> ON")
driver.set_channel(ch, True)
_sleep(on_time)
print(f"→ Kanal: {ch} -> OFF")
driver.set_channel(ch, False)
_sleep(off_time)
print("\n✅ Kanal döngüsü tamamlandı.\n")
def test_burner(driver: RelayDriver, burner_id: int, on_time: float = 1.0, off_time: float = 0.5) -> None:
"""
Belirli bir brülör grubunu test eder:
- Igniter ON/OFF
- default pompalar ON/OFF
Burner grubu bilgisi config_statics.BURNER_GROUPS'tan gelir.
"""
burners = driver.burners()
if burner_id not in burners:
print(f"⚠️ Burner id {burner_id} tanımlı değil.")
return
info = burners[burner_id]
name = info.get("name", f"Burner{burner_id}")
loc = info.get("location", "-")
print(f"\n=== Brülör Testi: #{burner_id} - {name} @ {loc} ===")
# Igniter test
print("\n→ Igniter ON")
driver.set_igniter(burner_id, True)
_sleep(on_time)
print("→ Igniter OFF")
driver.set_igniter(burner_id, False)
_sleep(off_time)
# Default pompalar
enabled_pumps = driver.enabled_pumps(burner_id)
if not enabled_pumps:
print("⚠️ Bu brülör için default (default=1) pompa tanımlı değil.")
else:
print(f"\nDefault pompalar: {enabled_pumps}")
for pname in enabled_pumps:
print(f"\n→ Pompa {pname} ON")
driver.set_pump(burner_id, pname, True)
_sleep(on_time)
print(f"→ Pompa {pname} OFF")
driver.set_pump(burner_id, pname, False)
_sleep(off_time)
print(f"\n✅ Brülör #{burner_id} testi tamamlandı.\n")
def main(burner_id: Optional[int] = 0) -> None:
print("RelayDriver yükleniyor…")
drv = RelayDriver()
drv.summary()
# Önce tüm kanalları hızlıca test et
test_all_channels(drv, on_time=0.7, off_time=0.3)
# Eğer config'te burner grubu varsa, onu da test et
burners = getattr(cfg_s, "BURNER_GROUPS", {})
if burners and burner_id is not None and burner_id in burners:
test_burner(drv, burner_id=burner_id, on_time=1.0, off_time=0.5)
else:
print("\n BURNER_GROUPS boş veya burner_id geçersiz; brülör test atlandı.\n")
print("Tüm röle testleri bitti. Tüm kanalları kapatıyorum…")
drv.all_off()
print("✅ Röleler güvenli moda alındı.")
if __name__ == "__main__":
try:
main(burner_id=0)
except KeyboardInterrupt:
print("\nKullanıcı tarafından kesildi, röleler kapatılıyor…")
try:
drv = RelayDriver()
drv.all_off()
except Exception:
pass
print("Çıkış.")

3
ebuild_building_log.sql Normal file
View File

@ -0,0 +1,3 @@
-- DBText log file for table ebrulor_log
-- created at 2025-11-20T18:55:06.091200

198
ebuild_burner_log.sql Normal file
View File

@ -0,0 +1,198 @@
-- DBText log file for table eburner_log
-- created at 2025-11-20T20:45:06.451003
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:06', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:06', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:mode=F avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:06', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:mode=F avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:06', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:mode=F avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:06', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'mode=F avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:45:16', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:15', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:15', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:15', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:15', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:15', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:20', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:25', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:30', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:35', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:40', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:45', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:50', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-20 20:56:55', 'EBURNER', 'burner', 'relay_error', NULL, NULL, '0');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:10:22', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:10:22', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:10:22', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:10:22', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:43:03', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:43:03', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:43:03', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:43:03', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:48:46', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:48:46', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:48:46', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:48:46', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:58:48', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:58:48', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:58:48', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:58:48', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:59:49', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:59:49', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:59:49', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 13:59:49', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:00:50', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:00:50', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:00:50', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:00:50', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:09:38', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:09:38', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:09:38', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:09:38', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:10:38', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:10:38', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:10:38', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:10:38', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:19:25', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:19:25', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:19:25', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:19:25', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:20:33', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:20:33', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:20:33', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:20:33', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:24:26', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:24:26', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:24:26', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:24:26', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:25:35', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:25:35', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:25:35', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:25:35', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:37:42', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:37:42', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:37:42', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:37:42', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:38:42', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:38:42', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:38:42', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:38:42', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:51:23', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:51:23', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:51:23', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:51:23', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:52:14', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:52:14', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:52:14', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 14:52:14', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 15:18:00', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 15:18:00', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 15:18:00', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 15:18:00', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:24', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:24', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:24', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:24', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:54', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:54', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:54', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:18:54', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:19:55', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:19:55', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:19:55', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:19:55', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:24:58', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:24:58', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:24:58', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:24:58', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False cfg=F');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:33:10', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:33:10', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:33:10', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 16:33:10', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:19:04', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:19:04', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:19:04', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:19:04', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:24:01', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:24:01', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:24:01', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:24:01', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:50:28', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:50:28', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:50:28', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:50:28', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:51:30', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:51:30', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:51:30', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:51:30', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:58:59', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:58:59', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:58:59', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 17:58:59', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:09:40', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:09:40', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:09:40', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:09:40', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:23:41', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:23:41', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:23:41', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:23:41', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:37:24', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:37:24', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:37:24', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:37:24', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:43:05', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:43:05', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:43:05', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 18:43:05', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:00:16', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:00:16', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:00:16', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:00:16', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:02:44', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:02:44', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:02:44', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:02:44', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:03:45', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:03:45', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:03:45', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:03:45', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:10:53', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:10:53', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:10:53', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:10:53', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:23:45', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:23:45', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:23:45', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:23:45', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 44.000, '°C', 'avg=NoneC outside=10.0C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:50:54', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.312C used=19.312C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:50:54', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.312C used=19.312C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:50:54', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.312C used=19.312C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 19:50:54', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 35.000, '°C', 'avg=NoneC outside_raw=22.312C used=19.312C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:11:38', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.687C used=19.687C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:11:38', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.687C used=19.687C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:11:38', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.687C used=19.687C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:11:38', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 35.000, '°C', 'avg=NoneC outside_raw=22.687C used=19.687C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:02', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:02', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:02', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:02', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 35.000, '°C', 'avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:16', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.812C used=14.073406354148933C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:16', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.812C used=14.073406354148933C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:16', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.812C used=14.073406354148933C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:51:16', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 39.000, '°C', 'avg=NoneC outside_raw=22.812C used=14.073406354148933C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:22', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:22', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:22', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:22', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 35.000, '°C', 'avg=NoneC outside_raw=22.875C used=19.875C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:36', 'EBURNER', 'relay:igniter', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=14.07425982530695C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:36', 'EBURNER', 'relay:circulation_a', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=14.07425982530695C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:36', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=22.875C used=14.07425982530695C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 20:57:36', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 39.000, '°C', 'avg=NoneC outside_raw=22.875C used=14.07425982530695C want_heat=False');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:09:00', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:09:00', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:09:00', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:09:00', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 45.000, '°C', 'avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:41:46', 'EBURNER', 'relay:igniter', 'state', 1.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:41:46', 'EBURNER', 'relay:circulation_a', 'state', 1.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:41:46', 'EBURNER', 'relay:circulation_b', 'state', 0.000, 'bool', 'F:avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');
INSERT INTO eburner_log (ts, app, source, event_type, value, unit, extra) VALUES ('2025-11-22 21:41:46', 'EBURNER', 'burner:mode=F', 'fire_setpoint', 45.000, '°C', 'avg=NoneC outside_raw=10.0C used=7.0C want_heat=True');

1
listtilda.sh Normal file
View File

@ -0,0 +1 @@
find . -type f -name '*~'

23
requirements.txt Normal file
View File

@ -0,0 +1,23 @@
# ebuild Raspberry Pi 2 runtime dependencies
#
# Not: Bazı paketler Raspbian üzerinde zaten sistem paketi olarak gelebilir.
# Buradaki liste, tipik bir Python 3 kurulumunda pip ile yüklenebilecek
# kütüphaneleri içerir.
RPi.GPIO>=0.7.0
gpiozero>=1.6.2
# DHT11 / DHT22 ve benzeri sensörler için
adafruit-circuitpython-dht>=4.0.0
Adafruit-Blinka>=8.0.0
# MCP3008 vb. SPI ADC için
adafruit-circuitpython-mcp3xxx>=1.0.0
# Zaman / saat dilimi vb. yardımcılar
pytz>=2024.1
# İleride log işleme / debug kolaylığı için (opsiyonel)
psutil>=5.9.0
astral
holidays

1
run_burner.sh Normal file
View File

@ -0,0 +1 @@
python3 -m ebuild.core.systems.burner

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Komut satırından ebuild.runtime.main modülünü çalıştırmak için yardımcı script."""
from ebuild.runtime.main import main
if __name__ == "__main__":
main()

1
tail_burner.sh Normal file
View File

@ -0,0 +1 @@
tail -f /var/log/syslog | grep BRULOR

Binary file not shown.

5
tests/test_building.py Normal file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Building ile ilgili temel test iskeleti."""
def test_placeholder():
assert True