ilk işlem
This commit is contained in:
commit
1d458ca9f6
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
find . -type f -name '*~' -print -delete
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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#6Ih!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
|
||||
|
|
@ -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 |
|
|
@ -0,0 +1,3 @@
|
|||
-- DBText log file for table ebrulor_log
|
||||
-- created at 2025-11-22T16:18:17.441370
|
||||
|
||||
|
|
@ -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.
|
|
@ -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 dk’da bir ‘used_outside’ güncelle
|
||||
BURNER_COMFORT_OFFSET_C = 1.0 # seçili konfor sıcaklığı offset’i
|
||||
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 config’ten 0 yapar gerçekte devreye alırken 60–300 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) # 5–20 Nisan
|
||||
xSEASON_AUTUMN_SAVE_DAYS = (15, 30) # 15–30 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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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ınç, 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ınç, 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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Device / Sensor / Actuator soyutlamaları için iskelet."""
|
||||
|
|
@ -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ınç, 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ınç
|
||||
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ı
|
||||
- Aşırı basınç
|
||||
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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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 açı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()
|
||||
|
|
@ -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 açı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()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Yangın alarm sistemi iskeleti."""
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Hidrofor sistemi iskeleti."""
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Sulama sistemi iskeleti."""
|
||||
|
|
@ -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.
|
|
@ -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ınç 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 açı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ınç 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ü açısından kill-switch gerekip gerekmediğini söyler.
|
||||
"""
|
||||
return self.gas.should_shutdown_system()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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 açı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)
|
||||
|
|
@ -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()})")
|
||||
|
||||
|
|
@ -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 açı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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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 açı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())
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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}"
|
||||
|
||||
# config’teki 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 .0’sı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
|
||||
|
||||
# Config’te 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))
|
||||
|
|
@ -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 açıldığında / ısıtma başladığında devreye alınacak default pompaları temsil eder.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
return [
|
||||
data["channel"]
|
||||
for _, data in circ.items()
|
||||
if int(data.get("default", 0)) == 1
|
||||
]
|
||||
|
||||
def active_pumps(self, burner_id: int) -> Tuple[str, ...]:
|
||||
"""
|
||||
Şu anda gerçekten ON olan pompa isimlerini döndürür.
|
||||
(GPIO'da HIGH durumda olan kanallar; RelayStats.last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return tuple()
|
||||
circ = info.get("circulation", {})
|
||||
active: List[str] = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel")
|
||||
if ch in self._stats and self._stats[ch].last_on_ts is not None:
|
||||
active.append(pname)
|
||||
return tuple(active)
|
||||
|
||||
def set_igniter(self, burner_id: int, state: bool) -> None:
|
||||
"""
|
||||
İlgili brülörün igniter kanalını ON/OFF yapar.
|
||||
"""
|
||||
ch = self.igniter_channel(burner_id)
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
|
||||
def set_pump(self, burner_id: int, pump_name: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen brülörün belirtilen pompasını ON/OFF yapar.
|
||||
|
||||
pump_name 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())
|
||||
|
|
@ -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=(),
|
||||
)
|
||||
|
|
@ -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}"
|
||||
|
||||
# config’teki 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 .0’sı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
|
||||
|
||||
# Config’te 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))
|
||||
|
|
@ -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 açıldığında / ısıtma başladığında devreye alınacak default pompaları temsil eder.
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return []
|
||||
circ = info.get("circulation", {})
|
||||
return [
|
||||
data["channel"]
|
||||
for _, data in circ.items()
|
||||
if int(data.get("default", 0)) == 1
|
||||
]
|
||||
|
||||
def active_pumps(self, burner_id: int) -> Tuple[str, ...]:
|
||||
"""
|
||||
Şu anda gerçekten ON olan pompa isimlerini döndürür.
|
||||
(GPIO'da HIGH durumda olan kanallar; RelayStats.last_on_ts None değilse ON kabul edilir)
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return tuple()
|
||||
circ = info.get("circulation", {})
|
||||
active: List[str] = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel")
|
||||
if ch in self._stats and self._stats[ch].last_on_ts is not None:
|
||||
active.append(pname)
|
||||
return tuple(active)
|
||||
|
||||
def set_igniter(self, burner_id: int, state: bool) -> None:
|
||||
"""
|
||||
İlgili brülörün igniter kanalını ON/OFF yapar.
|
||||
"""
|
||||
ch = self.igniter_channel(burner_id)
|
||||
if ch:
|
||||
self.set_channel(ch, state)
|
||||
|
||||
def set_pump(self, burner_id: int, pump_name: str, state: bool) -> None:
|
||||
"""
|
||||
Belirtilen brülörün belirtilen pompasını ON/OFF yapar.
|
||||
pump_name: BURNER_GROUPS[..]["circulation"][pump_name]
|
||||
"""
|
||||
info = self.burner_info(burner_id)
|
||||
if not info:
|
||||
return
|
||||
circ = info.get("circulation", {})
|
||||
if pump_name in circ:
|
||||
ch = circ[pump_name]["channel"]
|
||||
self.set_channel(ch, state)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Yardımcı: özet
|
||||
# -----------------------------------------------------
|
||||
def summary(self) -> str:
|
||||
"""
|
||||
Kanallar ve brülör gruplarının kısa bir özetini döndürür (debug amaçlı).
|
||||
"""
|
||||
lines: List[str] = []
|
||||
chans = ", ".join(sorted(self._pin_map.keys()))
|
||||
lines.append(f"Kanallar: {chans}")
|
||||
|
||||
lines.append("Brülör grupları:")
|
||||
for bid, info in self._burner_groups.items():
|
||||
name = info.get("name", f"Burner{bid}")
|
||||
loc = info.get("location", "-")
|
||||
ign = info.get("igniter", "igniter")
|
||||
circ = info.get("circulation", {})
|
||||
pumps = []
|
||||
defaults = []
|
||||
for pname, pdata in circ.items():
|
||||
ch = pdata.get("channel", "?")
|
||||
pumps.append(f"{pname}->{ch}")
|
||||
if int(pdata.get("default", 0)) == 1:
|
||||
defaults.append(pname)
|
||||
lines.append(
|
||||
f" #{bid}: {name} @ {loc} | igniter={ign} | "
|
||||
f"pumps={pumps} | default_on={defaults}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Temizlik
|
||||
# -----------------------------------------------------
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
GPIO pinlerini serbest bırakır.
|
||||
"""
|
||||
if _HAS_GPIO:
|
||||
GPIO.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
drv = RelayDriver()
|
||||
print("\n🧰 RelayDriver Summary")
|
||||
print(drv.summary())
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
|
@ -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.
|
|
@ -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 açı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ış.")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- DBText log file for table ebrulor_log
|
||||
-- created at 2025-11-20T18:55:06.091200
|
||||
|
||||
|
|
@ -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');
|
||||
|
|
@ -0,0 +1 @@
|
|||
find . -type f -name '*~'
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
python3 -m ebuild.core.systems.burner
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1 @@
|
|||
tail -f /var/log/syslog | grep BRULOR
|
||||
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Building ile ilgili temel test iskeleti."""
|
||||
|
||||
def test_placeholder():
|
||||
assert True
|
||||
Loading…
Reference in New Issue