From a9c7381977f7dfa8a66ac0f2be4fc26d91fec529 Mon Sep 17 00:00:00 2001 From: pepev-nrt <pepev.nrt@gmail.com> Date: Mon, 27 Jan 2025 17:49:36 +0100 Subject: [PATCH] weather api working -> 2025-01-27 17:49:29 --- .gitignore | 4 + assets/output.bmp | Bin 3966 -> 3966 bytes assets/test-characters-2bold.bmp | Bin 3966 -> 0 bytes assets/test-characters-bold.bmp | Bin 3966 -> 0 bytes assets/test-characters.bmp | Bin 3966 -> 0 bytes assets/test-fonts.bmp | Bin 3966 -> 0 bytes assets/weather-icons/placeholder32.aseprite | Bin 0 -> 1270 bytes assets/weather-icons/placeholder32.bmp | Bin 0 -> 2102 bytes assets/weather-icons/sunny-icon32.aseprite | Bin 0 -> 1270 bytes assets/weather-icons/sunny-icon32.bmp | Bin 0 -> 2102 bytes config.json | 9 +- raspagenda.py | 79 ++++++++--- test/test_weather.py | 65 +++++++++ weather/weather.py | 148 ++++++++++++++++++++ 14 files changed, 288 insertions(+), 17 deletions(-) delete mode 100644 assets/test-characters-2bold.bmp delete mode 100644 assets/test-characters-bold.bmp delete mode 100644 assets/test-characters.bmp delete mode 100644 assets/test-fonts.bmp create mode 100644 assets/weather-icons/placeholder32.aseprite create mode 100644 assets/weather-icons/placeholder32.bmp create mode 100644 assets/weather-icons/sunny-icon32.aseprite create mode 100644 assets/weather-icons/sunny-icon32.bmp diff --git a/.gitignore b/.gitignore index 1800114..4221e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Specific for my project +.cache.sqlite + +# Python generic .gitignore # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/assets/output.bmp b/assets/output.bmp index 8bde19697f4b1f31c55beaa57ef633b45ab987f3..18d689d2ae55a3536652881b0bb7c5bbc38ec0f6 100644 GIT binary patch literal 3966 zcmeH~PjB2r7{=2gk_%d&k*efER6arxX^c3~6MTUlibS=0h-lzKfb8xx>In(K0S;Ly zjSsz-11bSk#4CY{a*1znpuv$U)d!@wuz=&*o`+{_Z;~bR8U*6jS&cn2zj^=6yfZuF zmtXBWQ)OD@^LggS%<nUw;m@R($iHn>`7G-un}o>}OjgU<@I>3c<3X$IB2Be|U4FOy zp4ZG(Ge7To$n$+s{|veAK9u-tmo6+QEd2Vm;9sjVNL_5cdsW~cRlF<lz*#@14rlw4 z{^JR@&(HtNbL#&bBR~gM`$Yy^MiQe~wsKuS16hJhn4caV14V+L1-`xCqw#Dl5qRi% zbg|!;;<v3L%BS!I`8{M0fEC0aTm7fsMF_-5l$GnG9A7AoFWH}^f8q62!G4Tb0Y1LA zbG@t+h1AdX>>MGK52!**TvItzCi30L@E12F`K=$x`{6Z-FWtEX^lpDI%wM>4hw|it z{J~Ra6!7IUPYe7rItT5e5ctZ2*^nXna8(KM2@hgqp}Ol^IT&BAZpJsT^MXBqggxy1 zKt=TbE8G-#>}^thyD!Dxd%e%~*;|wLhf;7+6vzerU4(%df^x0?aeqc5vp+ri_;NZo zGW#2LPB2bZ{bR`Y6MkYc>f(PtACGlDncw5N7?=LZ{HE;OVV35Phm+oF;M_umu_wvv z48J#V^oHgyM3jf`x{bVBiAwfy7s+=``g!>J4ZPst-Dp1^UTowcegZF8l7D$v93YPx z#;>6oc|ksEkiQwIJZ<31Tk5y`7s)?t|FCrFk-pl*)!{>#-=98<{E@8xin$`l@z?Ra zi+%$yVt+3@&96_nK)L=IU*2*4I(~&KHP3i_o#J$9x#st=YvDWdD>V;YOZ@fD{A$e? zhnDq2{k@874XteUdGHzkc{%gF(|l*%S0(5E3{H=a_J<WW``g0VT5%T2G#OtLl19Gs z+$ZGahmh?tP5zb*)NRw$PhOd}JWtp!XXmJkSCw>SzpS`bQ}y(!SIwhV$8p>>FMnZU z0W1x0BiJyrQS0Arx7!_fUOE_fnxz4M8%D!1=LMNO9YA-Yl7Cuqj)UW;{EWl9!Qr#( zbNYOAc#b_P&Q29?D@0MLe<n&d)tK|jUGok`qmsYk<epn|yn|~n`W?sN_g#Kr!PRZG zIOpBK2gW|Wz?<Y%&;};PAjh0%N+#W#dgF+HvsN~>X0Se2miYMNXnl`cCf|R@%_l>( Rzb54-YUTc$Xql`G{{$5l1$F=c literal 3966 zcmdUy-)kdP6vw+N?Sq&>Uqoz@7Yq6qY)eB2eAj=$s*pUnwJ!}?H)AOnd@(FO1#0F` z@Ih>ati6%ImV!7BK4~|5Q&uzhBgsmzB_=b+@40s-nT*pETzv2h=iYnH=bm%!y)&64 zufFz_v7lrF?aR==pzlL(;9|L#T3C4QX{}GdW@RBF)_Ky1Kp7c_!~dSIzA$XiV9!1< zlIyPxhYk&F`+)u(&y9yJ4IHOtCC6`kFdWdpb!;<{duHyJA$>pi&bAZz*sQ%drslAd zbrQM1R@?rT9#-9D@RMTytL;H0wJs=-2IV&1JEuX@tz{+Ei~XZp{hjVvg!4q3o1r8D zUP|Cw%r7MEbN)HJUhImk1a5OY^4E*MCe~+6g!LQl>L1NYYSmH>H1X%7FQ|`eA`|X$ zJP|k=Xdn|_(meEsJ-9ey<H%C+p+D@wWt|NinGBtg9(-Jc?Hm28_{awL9z7rBlNBQV zk`0e8ui~Fyf{zs6ucW<Z3%B|F;fVvUI-fqrkFaUnzn!kvYT=lBt-4^$=Xd!Vdc_@a z;e~u87&9M9^j=AlEQ?Pj{6xU3_@77U{jW{%N@?u1?yLMlFy{E)E)SYGC<pa#w_4V0 zeJPnku)a$Avu4Y#8(6=ge2AaiE^swV0X0_nwW-KH7CK3%{2U*O3<r_^f5q3nsp5*P zKDPh0D$8T=Yx2J`tm3*l|BY+vKik~Ll@Pz0Wv5)@c2@NL(E$9;EDiS`@ady6nWp1& z%9a!FbfEc$#U9B`OceJ_liiY=m?%CrZFWm;VxqXec3{CTV~kB*KesM0mQXF(a1d}B z33z1a_4yO6MaPF+lry=TWLUk-_ADP;u+``P?8xuYGp*h%R~C4&I|BE+$=ng;<>i}{ z@_XsT_;UHelV4EJ?<dEX>-cK<a!&qwC6n8+GL%}KS^qd2tWUjZ$>-N$AZq_lwgX;P zJ@`bJ!_KlpauX93Z&BNVU&a`l%*RjxmC!(tQVVllj0_YV%M2f3f`k*_Z{^H7-8U=g zi}cupN9SkGcN84wLv_3*FS_cEYwm^Isqc6r@KX0M%6*@NiZ9nc8ox+)P3DpOl#!O^ z`xo91Bsl(e5?o$CU*cG=5Ba-4s``iB-3Ygi)oiS<l36$JT9xJ3)9Q&1;w_&n<N`+< zB<2xBpYJvNm@I+05*im3|A2%9V2z97KVeS*G7e!YKC)~JV+oCm;v_cNFXIrl<_{DB zpM7~v_<6{yY7l93ymrWHa~MHtILl)%cg%?KYx2il{_*&|kbk&fs_Iwex9E*T-qiJd zqOC+8IQ`QV{>{TOyMFB>@ZDfI!vh~7|LtpdX)Zrq5tg2iod0yCF_$CuvH0mrpN8@J zpU`Z(2)-G(Q0gCo{U_C*%+uUpJG#Fq=lXtOE-w|_RAFhU(X<`Ia9xup9~kZYyXZN8 zsj!&O=L?Oh?dJ<2C%2Qg0-n#8G^b9Kb5$RED=p?1^M$3x$Ig`3EVr{cjYqiK*mJ33 zw0|}cMz(pb^AE{Ja#jD%0P&qo=Duy~`m{s0{K8VoSTY*bp#%Pw;#tRSKf**hq<7Tz zT>{7YI|b&n?6}+!-CKB@`1rt^dt?}Gz5iNPeGlNTOycvcLRE7e$NGcGiT7ukDEa<H F`QKSPyifoD diff --git a/assets/test-characters-2bold.bmp b/assets/test-characters-2bold.bmp deleted file mode 100644 index 56a90113e147d62ded957ea1033ce42b051afa59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3966 zcmeHJy-ve05C)cP%_DSRVb4OPsz&U*23e7^AO=E|A#4bV<)o;76-Q=ZsOneXM}k+N zRmpKqi`15$Js_m2%2^l5`FubA&c5XI>^j_QWI)d&(kbZ`={^lBCxkzC=6XPLt5(o2 z%Cn@!-2}D#wEVajXwQd>9mJNOmLC@bJAXbhz~gRZf^ahnN&@x}#^<pck3V?4kHGsc z00$|U_ms8&62cWfp9xu+kj)?Ko}6rUPH$C)*cTJSEm+i_+1(EGkMem$gWEo;a%ANX zxX1s)Y(#_if8&XN`%=;1?MoJF09E3XYX#N+HFYf5g8rZ#>DliaIC0SY<sd@^P`T~F z<KKg49vpnQ_QZE(Z};cH-SD>jKS(ak#rfoMIgUfAb1kd4Le!UOP>h#bsgl(C1r<Sp ziA0@<C@PnmrIMzT5-xds7!aOOkKSmXn@;2ZZepTteUYyFx2@mCzo+<z(*%FxaVQ|y spz|X7OfV>hEBV{{il)ev_AC(s<x2iJ&BV_YqW!Pq9I=b{|9=0!0RVWOl>h($ diff --git a/assets/test-characters-bold.bmp b/assets/test-characters-bold.bmp deleted file mode 100644 index 27288d81dc3b305f53eed922ac18ea446f8bf488..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3966 zcmeH|F>4z!6oA_rdYe6D$`TJA(yrCwK(I>pX6b)$)+V>KC7d}lh)%_1$%Di1uh^(d z!B|6oK{5E|a!VBst51^S+R53T8ffu5q&wZG_w@AMJKdYNN#lu^4)rgn-BSBR?I~SO zUK9UX5A7L^U2|f0%W%h(J>MRneSG%u*%pC)eQb-4wfp$&<FhRS|5SbC4|lTWe^J3z zkt(AUDmA&P;!q$M00fx#9;(|RabpZ|Q_VjY+N1_F7|W~l(;}aO5xE*w^Zxh3d=ppz zRC7?q{6HiZHCEY&DZo#^1zX8gp_EBgs#G<vu(|qbExxN@@%%_!+IiEhXzn&qayML_ zZ_Jk34VT=Y8xkK0HB!1@IMxiZ_UU2SX<wNWylFa2`E{ZN@j>jLO;)roAv^TgaRk(i zc<0h{n-%O;c9Jdlh<NIG5_ow^eDHD5^Am=ybYMmQ#*|(kdEnlraP|j5|B&o6S``CV z-hUC{cyiG`CI7QFbNr?mI0eL5O|?_bd;X0r!!7hf&!?8-_>QmQ|Ns90LWvYdpzGb| zF_Zk7NiFp?<f)H?&p%R%Cf#llGsce@(@egE9Pc>g-=%PN=6UV7pltr(sjCmqkCM2> z_(8*SmHT?6`Vk0H_ESCUB=2f0Vv%(Ec^(e4P+w#~&yJGX0i~94$CV0))e1jLS`G&e zblx~w`mbl+e>Z{jPnMkON2IAD`;N<quA1kI{d+5$yGQ!IH#)FbA2&_d$#HVtKnenm zrjpM;^ddQFtQZ^Oq<hZkBcu#4K94zLS;$<NG(=tR5qBBdzy}d=DzR!VbluzkVl->c z-}mm@r^55RPxf24v3<zT8=Q(5l#uLKY$G{X^B>4h?m9XIY#DQIam{~0Bd)?`Ebae3 LUyx0=zn=f!^915) diff --git a/assets/test-characters.bmp b/assets/test-characters.bmp deleted file mode 100644 index a806aced2e4af419275e8743829fa903e0cce7dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3966 zcmeH}F>ljA6vu%j6Y&Qib_W*r46W3PL+tPc*vSftlvE&MAxqqb#b_zi9|F+~u5_-9 zlr5f1Vp&yI><$QrHnAViZtU2}*_}XC%Cqi#zTds~{`c+_<>|AYa3`_H*gwK{h3yTt zyZFTR1o^l6b-Rb(af=OZHuHuhTV7qDZGE=&sj9&CepFQlU|XMUeX1&Od+x^tcN;7H zC0tR(&~jkdJ)aKksWF@ET@I<}rol|s@j}M|qw6~cbBx(|&^0<<jt61N=gdXU*7(Pa z57}uPe^&5UdyCMSB=H%>58OeKWBzxJBd7U)y;#q$ik~e<Hw4r#OzF@5;rWm@$Vxws zQnd_V=E4l0%uEF!bkhY1$1cOS=l5Y}r8z%J&p+ZHKbVY42fHB1zVtfpmyYA}9}glQ z?mK=umw%hA>&=lzUKyv2#xB$&U*!AKAauWgA6=CA<7F9tt%z@^(m_X-puWe~ZzO6X zJmtLXY;4k$XH$hiTED_%?&JUAxJ8bw^p$XfO8YP&su+k1vKhdP>kkIrg20<IE)VC( zd){#g|FC!fCnV<F_(~9-0gL^A06)I>J;{IUz2$OM+9xNn+ITMZWuCrxj%|YFjG5ez z3SR1Y4OaE#FZ@Sb=V#CU-;{HG)Dm<{vGmg^CDEZ=qaz^~%+yCOTSU_|U9+1cYLjNO zZx^^-myzq`{HTWfy?)B~SL}KcU%j-OJJBn7J-)Wg{|WOyylTTHZwW+GEX`7A^LHC+ l^r)a;L$M_80A(-Qgn(YquizK4mOH}zuk$4-d;RtE{R5-CZ6p8y diff --git a/assets/test-fonts.bmp b/assets/test-fonts.bmp deleted file mode 100644 index 345f5679a6cb906b8e9d59816052914c331e6bb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3966 zcmeH{v1=Pi6o>6oAr20yxW)oITcl9RGpbC4WYU-vSGl+{MZ%`EgCUVF@y;3-CJ?Mk z+*lAa<^2gRjL`-K+W}o%9Dxh6uo4HEgcz@~g?EneynC~|vUcTN$;A~ZJYq&W^UZs2 z-kaff>sKu~;<Ox&H?aSU{der6_>*lK`Rjz+f55ryvcd3X4B2wU2N&q7K3DY_RDrAa z(X9^cegyt|^@-s6wKx@{AmV>$@fLxW_G6ji7-u{t2v9>Gk5MkUo{brKFtVe_jclyT zsokMAhNPvjPDueAk>iLfjy+|JGRbMVLn+V{Lr%-M)Z_N3<g|vIPf5Pwa9sg?TH1R& zj@&FtzI@I{BzI=?rGoqwll=L*j9(!@eSDoEFO?-<UymVQMXuxd-P3yKPqC*8yiCQB zZ#$gFFms%F`A;!D50w-!PCq)_sT^{zens{Im&=TC#dNrL9f9)8h(fWEk9g|>1NIu$ z7eL_z^l*;%15W+)>7G&OSMUBqF<g!xg&X6lYd?T5)o6{kOcRoc1-npWoOAzfgO+KT zw^dbB)dee)595@#HI=m0*$BSG+w*ObP-i=6Z(Q(SLjU*Mnx@X0zYtdE<A3_H!T8U+ z@n^qf;PWS5j|X_I?4CcNrmYP7VDkD*fa8RF3wXYc&FjJZeB3S&%x^rveZImzFLys- zmhRgBVt+Qo{~*4+D)Rf;0$jU!a=23^+SzHqnNfXscF=5WXvd9{<|c%4XdY~k<HlSB z=Sp+`5t(Sr{j~}6A^sQ9|EYG?n0vPZkF<%6{`gy7{JHm4c&r`&*2h=hI_IDGhc>HQ zi`rNJ?cw{c;`RgAq`4_H?YKX`9zWc{{3iOiYZ^IT^8N&g@Zi};bAA4ZCq-gr`2(0w zZl__TW*GvXspUE}X!|~Db#8LA4yO;ErXe#1R)FuNacizvtdNt;o3toqFK{!7{IEj! z0`e*3M&2G@>-tCgJ>+{8!Yq%!CG542uSs65kW<NTr@%t~>F2Kf_VvjU<T6vw$WdlD zE*@M5Ya999E^pl{D3F_3Ho#oiy(5Zw>+dgq?(ECEy8*=H7#!Ld|J+^@k?eAaEy?`s zpD=0JE_pxh=8*CGun)dz|1k|mzmHm`h}Lh)TSDq{gnpO$^f^p_&-M9peSrRcL;Xd4 zp79;fpg8gCFY2>Dn*f*kYk!COJ4`>Dc0ml#PuR~(eT>r2Cjos7*T0@V4$!~%)vd=v N^lx#9{%IHV?>}rJrB(m{ diff --git a/assets/weather-icons/placeholder32.aseprite b/assets/weather-icons/placeholder32.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..8eb1fc2ffb70b53f4a11d6b8675c2f6ea34574c9 GIT binary patch literal 1270 zcmeyy!ocugDI<dd5DG9bGB7Zt05K9^WCRHU<phufsmCk>+W+e#8`xGhps`{=%&q{o z63G%SW(Ecpu<L+=Ko>DFF)=eUv#_wRva+(Vv9YtWb8v8Qa&mHUadC5V^YHNS^78WW z@$vKX3kV1Z3JMAd2?+}ei-?Gbii(PfiHVDgOGrpaN=iyeNl8mf%gD&c%F4>g$;r#h zD<~)^Dk>@|DJd%}tEi}`s;a7~si~{0YiMX_YHDg}X=!U~>*(m{>gww0>FMk18yFZE z8X6iI85tWJo0yoGnwpxKnVFlLTUc0FT3T9JSy@|K+t}FH+S=OL+1cCMJ2*HvIyyQz zIXOE!ySTWxy1Kf#xw*T$dw6(wdU|?!d3k$#`}p|y`uh6$`T6_%2LuEJ1_lNN1qBBO zhlGTLhK7cPg@uQQM?^$KMn*<OMMXzP$Hc_M#>U3Q#l^?RCnO{!CMG5&B_$^(r=+B$ zrlzK)rKP8*XJlk#W@ct(Wo2h)=j7z%=H}+*<>lw+7Zel}78Vv26%`j3mz0#0mX?;4 zm6ey5S5#C~R#sM3RaIA4*VNS1*4Eb5)z#P6H#9UfHa0djH8nRkx3sjhwzjslwY9gm zcXV`gc6N4kb#-@l_w@Ai_V)Jm_4W7nPna-a;>3xQCQX_=dGeGgQ>IRxI&Ipt>C>mr zm@#AK%$c)h&6+)X_MAC$=FXivZ{EE5^XD&EuwdcBg^Ly~TD*Aik|j%)E?v58*|O!! zm#<i{V&%$}t5&UAy?XVUHEY(cUAu1Gy7lYVZ`iP5<Hn7fHf`FxdGnSnTefc9x^3IG z?c2BS*s){h&Yin<?b^M2_ntj__U_%gZ{NQC`}ZF>aNyv<gNF_sI(+!>kt0Wr9zA;O z*s<fskDoYk;^fJbr%s(Zefsp7GiT18J$vrlx%21GU$}7L;>C-XE?v5O`SO)3SFT>Y zdhOb^>({T}xN+m=&6~Gw-MW4I_MJO-?%ut7@7}%p_wPS=@ZjOYhmRgTdi?nDlP6D} zK7IP^*|X=*pTBtV;^oViuU@@+{rdHrH*em)ef#d+yZ7(kfB5j><HwJmK7IQ9`SX`A zU%r0*`t94d@87@w`0?ZC&!4}3{rdg;_n$w1{{H>@@87@w|NkoiGXRSMKT_WK4`g#O zI3*@$rx)dy=B3DkRRA*#9Dr1?Gx#J{rWPp}8UZ=13J|&fOc0s@nE4eLD(3wDUoX<r z6llQZc2GltRhgH8;ZgRFtjk~>kSvL=V{WLU&=CWUPygpHTYfhxM8#8IR++7Ll7m8? zc+*9#Fx!{ujbC0Vr|;jj{Q4P{#}n<;cb#(DnEE@0al-Rsk4nn#zu)x6c=f~?wMVbc zYWx?szDm7L{xzTThqIfP9xnfTc&1|2xAGtBv`n4yFFtoKxx1{oV%@*6wY>YovsY>U ze)aQX`r$x^cT(<a_wenT|L=|A#t&bW)1PYYI3M%!`8n;+$$!74h0k3aKiinq*1bOR Q%Jq}c^<S93AKUE;0D|ZM_5c6? literal 0 HcmV?d00001 diff --git a/assets/weather-icons/placeholder32.bmp b/assets/weather-icons/placeholder32.bmp new file mode 100644 index 0000000000000000000000000000000000000000..996beed89d8461b5cb797ca969c81bd5da6f42d9 GIT binary patch literal 2102 zcmZvc2V72V7{>o4Nr|+C5K*awP{_<GL}o>@Wh<Lx@0FHKcD9O)jI#I0-aBQFtcbq% zocEmbzWI87zjHtLecjh}J@<Lu;pyXMfVkZBz(JMSNRJLuLxr0fsULJyP2mwX9UXLa zbtzP+5PEug6fRsCeSLk36e)s%fdNH}7RAue5F;ZaiWMt{v9U45ix;Ovi4vHYm{780 zNlZ;mF*7s6+}s=s3kxhQEh$y16joMNlrCMGGG)q8wrp9ft*t3nt{mmdm#0F73fS1# zP_be~Dpjh4t*tGUD_5pUl`7cT*<o*QkAs5)RjXFT(a{knCnubpovBu>8ZIs_RIgs0 z8Z~NAvt~_PU0tbFs}{9u*QQRLI=H#H;qLBE-MV$DSFaxR>(|G_!-ED58qly|LmD+| zMB~PdY0{(#O`A5w)6<h?&6?4?d2?E{XhF-CEos%N6<%Imczb)(x^-*Xv}r@zwr%n8 z@u6M2cC>Hb9$#NyI&|nj$BrH8)TtAlJ9nl_mo9Ye+7&-PKm7gu2?z+FTeog>@7|rj zz(9KR=s{3W5IuYLq*t$A^zPl8K7IPow{Kti_3KCf{{0y+U;x3v!3-QYkU@h6F?jG` zh71|P(4j*aHf$KfhYx4Oh!KQ@gb*4U%E*x;88vDYqeqWs%$PBZ9XpnB<Hj+5{CFlz zn83t|6A23oW74EagolSSdGchYOqs&esZ*IYZ5q?3PiMxA8O)qHlUcK7F?;rG=FFMH z+_`g^H*X&E=g()sf(0yGxR6DQ7O{BoVj?0USh8dZk&%%^MMV)E9ZgJ346(7XEM2;k zWy_YaeED)#tXRRyl`C1bY89(juV&4fHLP8`mbkb$)~#E|`t|GCuwermH*RFprcG?# zyqPUqwh$j5&(^J5*|u#P+qZ9L$BrHB+_{royLPdA_ipy=*+W7?0*Q%<Bqb%Wckf>I z?b}Ckax(k(@8`gQ0~|bfkVA(Kk&=?a;lqbHa^wg{j~?aNv16pBrjnMH#_{9FIdS3y zCr_T_)TvXPK7E=qXU=f;>{-s8JIDF+=ecm<0v9h{<kF=}T)upnD_5>?_3BlwUAsnl zdOFvyU+2b+8{E8klUuiLar^dd?%cV<-Me?Wckdqe@89Rag9ki(_>f1B9`X3`W1c*D z!qcZudG_oX&!0c%#fulbeEE`$j0|4AdPQbtCRtfoyng+fH*enX_U&8Vy?e*|_wV`e z;R7E(e&o}qPkjFTnJ-_yke!{)*RNms_U#+rzklb)k01Q}`IDTS9De=!#qZz0$<58> z&!0d1{ri`^ygb#fUlj{ps*chwfJL4_5Q4D&M^FHDqLeaS184}spOG|>rmc9(qsqJ5 zNp5SS+PO5Fwx2Ste3<MZJ8El0;Xi7Dmk*K(aeT>CyRV%qT1AuJON51>5ot0-TyT;P z)fmWn@mfS{5s_h;sH)YFl>E~dQ2~<^)n~81Xf;uWwfe$5!V*>L%TX0lqE?ArB^B9H zkU>$b#>Df)AWb2v<seNjm0B`|pi1PWcGrAS<kO^n{!;YPf|QHQ$Ck~sFGV0oxJoa* kxu{a?<V4;jPgy3tm255=N)!Q;RQbgUSvXMkD`<!R07|>TF#rGn literal 0 HcmV?d00001 diff --git a/assets/weather-icons/sunny-icon32.aseprite b/assets/weather-icons/sunny-icon32.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..8eb1fc2ffb70b53f4a11d6b8675c2f6ea34574c9 GIT binary patch literal 1270 zcmeyy!ocugDI<dd5DG9bGB7Zt05K9^WCRHU<phufsmCk>+W+e#8`xGhps`{=%&q{o z63G%SW(Ecpu<L+=Ko>DFF)=eUv#_wRva+(Vv9YtWb8v8Qa&mHUadC5V^YHNS^78WW z@$vKX3kV1Z3JMAd2?+}ei-?Gbii(PfiHVDgOGrpaN=iyeNl8mf%gD&c%F4>g$;r#h zD<~)^Dk>@|DJd%}tEi}`s;a7~si~{0YiMX_YHDg}X=!U~>*(m{>gww0>FMk18yFZE z8X6iI85tWJo0yoGnwpxKnVFlLTUc0FT3T9JSy@|K+t}FH+S=OL+1cCMJ2*HvIyyQz zIXOE!ySTWxy1Kf#xw*T$dw6(wdU|?!d3k$#`}p|y`uh6$`T6_%2LuEJ1_lNN1qBBO zhlGTLhK7cPg@uQQM?^$KMn*<OMMXzP$Hc_M#>U3Q#l^?RCnO{!CMG5&B_$^(r=+B$ zrlzK)rKP8*XJlk#W@ct(Wo2h)=j7z%=H}+*<>lw+7Zel}78Vv26%`j3mz0#0mX?;4 zm6ey5S5#C~R#sM3RaIA4*VNS1*4Eb5)z#P6H#9UfHa0djH8nRkx3sjhwzjslwY9gm zcXV`gc6N4kb#-@l_w@Ai_V)Jm_4W7nPna-a;>3xQCQX_=dGeGgQ>IRxI&Ipt>C>mr zm@#AK%$c)h&6+)X_MAC$=FXivZ{EE5^XD&EuwdcBg^Ly~TD*Aik|j%)E?v58*|O!! zm#<i{V&%$}t5&UAy?XVUHEY(cUAu1Gy7lYVZ`iP5<Hn7fHf`FxdGnSnTefc9x^3IG z?c2BS*s){h&Yin<?b^M2_ntj__U_%gZ{NQC`}ZF>aNyv<gNF_sI(+!>kt0Wr9zA;O z*s<fskDoYk;^fJbr%s(Zefsp7GiT18J$vrlx%21GU$}7L;>C-XE?v5O`SO)3SFT>Y zdhOb^>({T}xN+m=&6~Gw-MW4I_MJO-?%ut7@7}%p_wPS=@ZjOYhmRgTdi?nDlP6D} zK7IP^*|X=*pTBtV;^oViuU@@+{rdHrH*em)ef#d+yZ7(kfB5j><HwJmK7IQ9`SX`A zU%r0*`t94d@87@w`0?ZC&!4}3{rdg;_n$w1{{H>@@87@w|NkoiGXRSMKT_WK4`g#O zI3*@$rx)dy=B3DkRRA*#9Dr1?Gx#J{rWPp}8UZ=13J|&fOc0s@nE4eLD(3wDUoX<r z6llQZc2GltRhgH8;ZgRFtjk~>kSvL=V{WLU&=CWUPygpHTYfhxM8#8IR++7Ll7m8? zc+*9#Fx!{ujbC0Vr|;jj{Q4P{#}n<;cb#(DnEE@0al-Rsk4nn#zu)x6c=f~?wMVbc zYWx?szDm7L{xzTThqIfP9xnfTc&1|2xAGtBv`n4yFFtoKxx1{oV%@*6wY>YovsY>U ze)aQX`r$x^cT(<a_wenT|L=|A#t&bW)1PYYI3M%!`8n;+$$!74h0k3aKiinq*1bOR Q%Jq}c^<S93AKUE;0D|ZM_5c6? literal 0 HcmV?d00001 diff --git a/assets/weather-icons/sunny-icon32.bmp b/assets/weather-icons/sunny-icon32.bmp new file mode 100644 index 0000000000000000000000000000000000000000..996beed89d8461b5cb797ca969c81bd5da6f42d9 GIT binary patch literal 2102 zcmZvc2V72V7{>o4Nr|+C5K*awP{_<GL}o>@Wh<Lx@0FHKcD9O)jI#I0-aBQFtcbq% zocEmbzWI87zjHtLecjh}J@<Lu;pyXMfVkZBz(JMSNRJLuLxr0fsULJyP2mwX9UXLa zbtzP+5PEug6fRsCeSLk36e)s%fdNH}7RAue5F;ZaiWMt{v9U45ix;Ovi4vHYm{780 zNlZ;mF*7s6+}s=s3kxhQEh$y16joMNlrCMGGG)q8wrp9ft*t3nt{mmdm#0F73fS1# zP_be~Dpjh4t*tGUD_5pUl`7cT*<o*QkAs5)RjXFT(a{knCnubpovBu>8ZIs_RIgs0 z8Z~NAvt~_PU0tbFs}{9u*QQRLI=H#H;qLBE-MV$DSFaxR>(|G_!-ED58qly|LmD+| zMB~PdY0{(#O`A5w)6<h?&6?4?d2?E{XhF-CEos%N6<%Imczb)(x^-*Xv}r@zwr%n8 z@u6M2cC>Hb9$#NyI&|nj$BrH8)TtAlJ9nl_mo9Ye+7&-PKm7gu2?z+FTeog>@7|rj zz(9KR=s{3W5IuYLq*t$A^zPl8K7IPow{Kti_3KCf{{0y+U;x3v!3-QYkU@h6F?jG` zh71|P(4j*aHf$KfhYx4Oh!KQ@gb*4U%E*x;88vDYqeqWs%$PBZ9XpnB<Hj+5{CFlz zn83t|6A23oW74EagolSSdGchYOqs&esZ*IYZ5q?3PiMxA8O)qHlUcK7F?;rG=FFMH z+_`g^H*X&E=g()sf(0yGxR6DQ7O{BoVj?0USh8dZk&%%^MMV)E9ZgJ346(7XEM2;k zWy_YaeED)#tXRRyl`C1bY89(juV&4fHLP8`mbkb$)~#E|`t|GCuwermH*RFprcG?# zyqPUqwh$j5&(^J5*|u#P+qZ9L$BrHB+_{royLPdA_ipy=*+W7?0*Q%<Bqb%Wckf>I z?b}Ckax(k(@8`gQ0~|bfkVA(Kk&=?a;lqbHa^wg{j~?aNv16pBrjnMH#_{9FIdS3y zCr_T_)TvXPK7E=qXU=f;>{-s8JIDF+=ecm<0v9h{<kF=}T)upnD_5>?_3BlwUAsnl zdOFvyU+2b+8{E8klUuiLar^dd?%cV<-Me?Wckdqe@89Rag9ki(_>f1B9`X3`W1c*D z!qcZudG_oX&!0c%#fulbeEE`$j0|4AdPQbtCRtfoyng+fH*enX_U&8Vy?e*|_wV`e z;R7E(e&o}qPkjFTnJ-_yke!{)*RNms_U#+rzklb)k01Q}`IDTS9De=!#qZz0$<58> z&!0d1{ri`^ygb#fUlj{ps*chwfJL4_5Q4D&M^FHDqLeaS184}spOG|>rmc9(qsqJ5 zNp5SS+PO5Fwx2Ste3<MZJ8El0;Xi7Dmk*K(aeT>CyRV%qT1AuJON51>5ot0-TyT;P z)fmWn@mfS{5s_h;sH)YFl>E~dQ2~<^)n~81Xf;uWwfe$5!V*>L%TX0lqE?ArB^B9H zkU>$b#>Df)AWb2v<seNjm0B`|pi1PWcGrAS<kO^n{!;YPf|QHQ$Ck~sFGV0oxJoa* kxu{a?<V4;jPgy3tm255=N)!Q;RQbgUSvXMkD`<!R07|>TF#rGn literal 0 HcmV?d00001 diff --git a/config.json b/config.json index 145f58b..1df5663 100644 --- a/config.json +++ b/config.json @@ -1,9 +1,16 @@ { "isDisplayConected": false, + "screenWidth": 250, "screenHeight": 122, "imageWidth": 250, "imageHeight": 122, "rotateAngle": 180, - "hourFormat": "12h" + + "hourFormat": "12h", + + "latitude": 40.4084, + "longitude": -3.6876, + "timezone": "Europe/Madrid", + "locales": "es_ES.UTF-8" } diff --git a/raspagenda.py b/raspagenda.py index f275424..8490131 100644 --- a/raspagenda.py +++ b/raspagenda.py @@ -11,12 +11,15 @@ CSS stylesheets in the "render" folder. import datetime as dt import os import sys +import locale import json import logging from PIL import Image,ImageDraw,ImageFont +from weather.weather import WeatherHelper + def main(): # Basic configuration settings (user replaceable) @@ -26,45 +29,90 @@ def main(): isDisplayConected = config['isDisplayConected'] # set to true when debugging rendering without displaying to screen - screenWidth = config['screenWidth'] # Width of E-Ink display. Default is landscape. Need to rotate image to fit. - screenHeight = config['screenHeight'] # Height of E-Ink display. Default is landscape. Need to rotate image to fit. - imageWidth = config['imageWidth'] # Width of image to be generated for display. - imageHeight = config['imageHeight'] # Height of image to be generated for display. - rotateAngle = config['rotateAngle'] # If image is rendered in portrait orientation, angle to rotate to fit screen - hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM + screenWidth = config['screenWidth'] # Width of E-Ink display. Default is landscape. Need to rotate image to fit. + screenHeight = config['screenHeight'] # Height of E-Ink display. Default is landscape. Need to rotate image to fit. + imageWidth = config['imageWidth'] # Width of image to be generated for display. + imageHeight = config['imageHeight'] # Height of image to be generated for display. + rotateAngle = config['rotateAngle'] # If image is rendered in portrait orientation, angle to rotate to fit screen + hourFormat = config['hourFormat'] # The format the hour will be displayed. eg. 13:02 or 01:02 PM + latitude = config['latitude'] # A float. The latitude for the Weather API. + longitude = config['longitude'] # A float. The longitude for the Weather API. + timezone = config['timezone'] # The timezone is necesary for the Weather API + locales = config['locales'] # Set the locales for the month name + weatherService = WeatherHelper(latitude, longitude, timezone) + weather_data = weatherService.fetch_open_meteo_data() + + locale.setlocale(locale.LC_TIME, locales) + # Set the hour, this is important to see what time the e-Paper has been syncronized if hourFormat == "12h": time = dt.datetime.now().strftime("%I:%M %p") else: time = dt.datetime.now().strftime("%H:%M") + day = dt.datetime.now().strftime("%A, %d %b") + assets = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'assets') + weather_icons = os.path.join(assets, 'weather-icons') fonts = os.path.join(assets, 'fonts') # Drawing on the image - font15 = ImageFont.truetype(os.path.join(fonts, 'wavesharefont.ttc'), 15) - font24 = ImageFont.truetype(os.path.join(fonts, 'wavesharefont.ttc'), 24) font8 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator8.ttf'), 8) font16 = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator.ttf'), 16) font16_bold = ImageFont.truetype(os.path.join(os.path.join(fonts, 'pixel_operator'), 'PixelOperator-Bold.ttf'), 16) - #image = Image.open('assets/test4.bmp') image = Image.new('1', (imageWidth, imageHeight), 255) # 255: clear the frame draw = ImageDraw.Draw(image) - draw.text((0, 0), time, font = font16_bold, fill = 0) # draw the current time in the top left corner + draw.text((0, 0), f"{day}. {time}", font = font16_bold, fill = 0) # draw the current time in the top left corner - draw.line([(0,16),(250,16)], fill = 0,width = 2) - draw.line([(125,16),(125,122)], fill = 0,width = 2) + # Frames + draw.line([(0,16),(250,16)], fill = 0,width = 2) # Draw a line below the date + draw.line([(90,17),(90,122)], fill = 0,width = 2) # Line dividing the screeen in two parts - cal_icon = Image.open(os.path.join(assets, "cal-icon3.bmp")) - image.paste(cal_icon, (129,20)) + # Calendar icon + cal_coordinates_x = 207 + cal_coordinates_y = 2 - draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0) + cal_icon = Image.open(os.path.join(assets, "cal-icon3.bmp")) # calendar icon + # This seems complicates, but it just draw a white rectangle bellow the calendar + draw.rectangle([(cal_coordinates_x-2, cal_coordinates_y),(cal_coordinates_x + 28, cal_coordinates_y + 30)], fill = 255) + image.paste(cal_icon, (cal_coordinates_x, cal_coordinates_y)) + + # LEFT SEGMENT (WEATHER) + # Todays information + today_icon = weatherService.iconize_weather(weather_data["current_weather_code"]) + today_icon = Image.open(os.path.join(weather_icons, today_icon)) + image.paste(today_icon, (29,20)) + + today_temperature = f"{str(weather_data['current_temperature_2m'])}°" + draw.text((66,28), today_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon + + + #draw.text((160, 26), 'Agenda', font = font16_bold, fill = 0) + + # All the tomorrows information + tomorrow_icon = weatherService.iconize_weather(weather_data["tomorrow_weather_code"]) + tomorrow_icon = Image.open(os.path.join(weather_icons, tomorrow_icon)) + image.paste(tomorrow_icon, (6,74)) # The icon is in the bottom left corner + + tomorrow_temperature = f"{str(weather_data['tomorrow_temperature_2m_max'])}/{str(weather_data['tomorrow_temperature_2m_min'])}°" + draw.text((6,106), tomorrow_temperature, font = font16_bold, fill = 0) # The tomorrows temperature is bellow the icon + + # All the day after tomorrows information + day_after_icon = weatherService.iconize_weather(weather_data["day_after_weather_code"]) + day_after_icon = Image.open(os.path.join(weather_icons, day_after_icon)) + image.paste(day_after_icon, (52,74)) + + day_after_temperature = f"{str(weather_data['day_after_temperature_2m_max'])}/{str(weather_data['day_after_temperature_2m_min'])}°" + draw.text((52,106), day_after_temperature, font = font16_bold, fill = 0) + + # RIGHT SEGMENT (CALENDAR/AGENDA) + # TODO: writing this part and also the logic part # draw.line([(0,50),(50,0)], fill = 0,width = 1) # draw.chord((10, 60, 50, 100), 0, 360, fill = 0) @@ -78,7 +126,6 @@ def main(): image.save(os.path.join(assets, 'output.bmp')) - if isDisplayConected: from display.display import DisplayHelper diff --git a/test/test_weather.py b/test/test_weather.py index e69de29..91a1ce0 100644 --- a/test/test_weather.py +++ b/test/test_weather.py @@ -0,0 +1,65 @@ +import openmeteo_requests + +import requests_cache +import pandas as pd +from retry_requests import retry + +# Setup the Open-Meteo API client with cache and retry on error +cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) +retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) +openmeteo = openmeteo_requests.Client(session = retry_session) + +# Make sure all required weather variables are listed here +# The order of variables in hourly or daily is important to assign them correctly below +url = "https://api.open-meteo.com/v1/forecast" +params = { + "latitude": 40.408, + "longitude": -3.688, + "current": ["temperature_2m", "is_day", "weather_code"], + "daily": ["weather_code", "temperature_2m_max", "temperature_2m_min"], + "timezone": "Europe/Madrid", + "forecast_days": 3 +} +responses = openmeteo.weather_api(url, params=params) + +# Process first location. Add a for-loop for multiple locations or weather models +response = responses[0] +print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E") +print(f"Elevation {response.Elevation()} m asl") +print(f"Timezone {response.Timezone()} {response.TimezoneAbbreviation()}") +print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s") + + +# Current values. The order of variables needs to be the same as requested. +current = response.Current() + +current_temperature_2m = current.Variables(0).Value() + +current_is_day = current.Variables(1).Value() + +current_weather_code = current.Variables(2).Value() + +print(f"Current time {current.Time()}") + +print(f"Current temperature_2m {current_temperature_2m}") +print(f"Current is_day {current_is_day}") +print(f"Current weather_code {current_weather_code}") +# Process daily data. The order of variables needs to be the same as requested. +daily = response.Daily() +daily_weather_code = daily.Variables(0).ValuesAsNumpy() +daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy() +daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy() + +daily_data = {"date": pd.date_range( + start = pd.to_datetime(daily.Time(), unit = "s", utc = True), + end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True), + freq = pd.Timedelta(seconds = daily.Interval()), + inclusive = "left" +)} + +daily_data["weather_code"] = daily_weather_code +daily_data["temperature_2m_max"] = daily_temperature_2m_max +daily_data["temperature_2m_min"] = daily_temperature_2m_min + +daily_dataframe = pd.DataFrame(data = daily_data) +print(daily_dataframe) \ No newline at end of file diff --git a/weather/weather.py b/weather/weather.py index e69de29..984e0ad 100644 --- a/weather/weather.py +++ b/weather/weather.py @@ -0,0 +1,148 @@ +#!/usr/bin python3 +# -*- coding: utf-8 -*- +""" +This part of the code exposes functions to fetch data from the open-meteo.com API. +The bellow code is the generated by the https://open-meteo.com/en/docs itself. +It has been slighly modified to return a dictionary with all the information nedeed. +""" + +import openmeteo_requests + +import requests_cache +#import pandas as pd +from retry_requests import retry + + +class WeatherHelper: + + def __init__(self, latitude: float, longitude: float, timezone: str): + self.latitude = latitude + self.longitude = longitude + self.timezone = timezone + + def fetch_open_meteo_data(self) -> dict: + + # Setup the Open-Meteo API client with cache and retry on error + cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) + retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) + openmeteo = openmeteo_requests.Client(session = retry_session) + + # Make sure all required weather variables are listed here + # The order of variables in hourly or daily is important to assign them correctly below + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": self.latitude, + "longitude": self.longitude, + "current": ["temperature_2m", "is_day", "weather_code"], + "daily": ["weather_code", "temperature_2m_max", "temperature_2m_min"], + "timezone": self.timezone, + "forecast_days": 3 + } + responses = openmeteo.weather_api(url, params=params) + + # Process first location. Add a for-loop for multiple locations or weather models + response = responses[0] + #print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E") + #print(f"Elevation {response.Elevation()} m asl") + #print(f"Timezone {response.Timezone()} {response.TimezoneAbbreviation()}") + #print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s") + + + # Current values. The order of variables needs to be the same as requested. + current = response.Current() + + current_temperature_2m = current.Variables(0).Value() + + current_is_day = current.Variables(1).Value() + + current_weather_code = current.Variables(2).Value() + + #print(f"Current time {current.Time()}") + + #print(f"Current temperature_2m {current_temperature_2m}") + #print(f"Current is_day {current_is_day}") + #print(f"Current weather_code {current_weather_code}") + # Process daily data. The order of variables needs to be the same as requested. + daily = response.Daily() + daily_weather_code = daily.Variables(0).ValuesAsNumpy() + daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy() + daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy() + + #daily_data = {"date": pd.date_range( + # start = pd.to_datetime(daily.Time(), unit = "s", utc = True), + # end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True), + # freq = pd.Timedelta(seconds = daily.Interval()), + # inclusive = "left" + #)} + + #daily_data["weather_code"] = daily_weather_code + #daily_data["temperature_2m_max"] = daily_temperature_2m_max + #daily_data["temperature_2m_min"] = daily_temperature_2m_min + + #daily_dataframe = pd.DataFrame(data = daily_data) + #print(daily_dataframe) + + # Here we are creating the dictionary that will be returned + weather_data = {} + + weather_data['current_temperature_2m'] = int(current_temperature_2m) + weather_data['current_is_day'] = int(current_is_day) + weather_data['current_weather_code'] = int(current_weather_code) + + weather_data['tomorrow_temperature_2m_max'] = int(daily_temperature_2m_max[1]) + weather_data['tomorrow_temperature_2m_min'] = int(daily_temperature_2m_min[1]) + weather_data['tomorrow_weather_code'] = int(daily_weather_code[1]) + + weather_data['day_after_temperature_2m_max'] = int(daily_temperature_2m_max[2]) + weather_data['day_after_temperature_2m_min'] = int(daily_temperature_2m_min[2]) + weather_data['day_after_weather_code'] = int(daily_weather_code[2]) + + return weather_data + + + def iconize_weather(self, weather_code: int) -> str: + + # This are the WMO Weather interpretation codes (WW) + # More info in: https://open-meteo.com/en/docs at the end of the page. + # Here we only put the name of the icons, the path will be handle in main() + + if weather_code >= 100: + weather_icon = "placeholder32.bmp" + return weather_icon + + if weather_code in [0]: + weather_icon = "sunny-icon32.bmp" + elif weather_code in [1, 2, 3]: + weather_icon = "None" + elif weather_code in [45, 48]: + weather_icon = "None" + elif weather_code in [51, 53, 55]: + weather_icon = "None" + elif weather_code in [56, 57]: + weather_icon = "None" + elif weather_code in [61, 63, 65]: + weather_icon = "None" + elif weather_code in [66, 67]: + weather_icon = "None" + elif weather_code in [71, 73, 75]: + weather_icon = "None" + elif weather_code in [77]: + weather_icon = "None" + elif weather_code in [80, 81, 82]: + weather_icon = "None" + elif weather_code in [85, 86]: + weather_icon = "None" + elif weather_code in [95]: + weather_icon = "None" + elif weather_code in [96, 99]: + weather_icon = "None" + else: + weather_icon = "None" + + # If there is no an icon for that code, search the next highest icon + if weather_icon == "None": + weather_icon = self.iconize_weather(weather_code + 1) + + return weather_icon + +