From 46fb1e62edd03442ad6adfb3e8beb42239fa7dc8 Mon Sep 17 00:00:00 2001 From: James Ide <ide@expo.dev> Date: Sun, 5 Mar 2023 13:35:55 -0800 Subject: [PATCH] Add small site with a service worker, make MQTT optional, use HTTPS (will abandon) - A small website registers a service worker and subscribes to push notifications - MQTT is now optional, set `MQTT_ENABLED = 1` in settings.toml to opt in - Configured the web server to use HTTPS. However, even with a small 1024-bit RSA certificate, performance is too slow (~10 seconds per response) and the service worker doesn't even load. Going to deploy this to the cloud instead eventually, and have the Pico just receive push subscriptions and send push notifications. --- .prettierrc | 6 ++ assets/icon-1024.avif | Bin 0 -> 15296 bytes requirements.txt | 3 +- scripts/certificates | 11 ++++ src/boot.py | 9 +-- src/certificates/certificate-chain.pem | 12 ++++ src/certificates/key.pem | 16 +++++ src/code.py | 79 ++++++++++++++++++------- src/public_html/icon-192.avif | Bin 0 -> 4474 bytes src/public_html/icon-512.avif | Bin 0 -> 11545 bytes src/public_html/index.html | 16 +++++ src/public_html/manifest.webmanifest | 21 +++++++ src/public_html/script.js | 16 +++++ src/public_html/service-worker.js | 22 +++++++ src/public_html/style.css | 6 ++ src/tls.py | 48 +++++++++++++++ 16 files changed, 238 insertions(+), 27 deletions(-) create mode 100644 .prettierrc create mode 100644 assets/icon-1024.avif create mode 100755 scripts/certificates create mode 100644 src/certificates/certificate-chain.pem create mode 100644 src/certificates/key.pem create mode 100644 src/public_html/icon-192.avif create mode 100644 src/public_html/icon-512.avif create mode 100644 src/public_html/index.html create mode 100644 src/public_html/manifest.webmanifest create mode 100644 src/public_html/script.js create mode 100644 src/public_html/service-worker.js create mode 100644 src/public_html/style.css create mode 100644 src/tls.py diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ee33faf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "singleQuote": true, + "bracketSameLine": true +} diff --git a/assets/icon-1024.avif b/assets/icon-1024.avif new file mode 100644 index 0000000000000000000000000000000000000000..31a20b087a5e66cf844b99736cc4ef986d7e85a9 GIT binary patch literal 15296 zcmXwfQ;;Z359QdlZQHhO+qQAXwr$(CZQHhI?(F^k-A$^4b5cpabX7kD004l%%*E5e z(9O~e;Gg{GZ7t0hZ7mJWWCa<8{y~qnrY?s6asPzE!o<eu|Aqhn4wl9)|3CiE+gKX? z|2A+AmM*sc;{g9zJWCsU<Nq=d006*$`kw^=!~y`|D*YEzSX$csZ}$Hftbd6q(0|4M zY7E^N7=`U^?Eg2?%F@Bf;U6w->0oUC&!bp6JDC1g2mk=`{|*3xgQbV%e+V!H1jIiF zW9Y^x90&^YAB1ddZ{uWVY~uj{^iRP8{z2%L4z`B>asTAs#Xtaog8sGQs<kySbU^}u zg2FWO|NU(Q0fEI}N^uoY0cZ$Ckt>6UL<EilCrmf+cV!i4-v@*#Fxl{g4e^Y{jO<YU z5hMUHECB@_H=QwqCqo!{3&B5u1CYQ{P|)c%rZD!<{HuA7z<`KA|IaGW&NCn|Fu*I( z-#Z}6G11S>GtAw|*C`-4CeYtG+&?cTHz71?xLL}_&E40{(a*)pHz3TzFCe(4JSWyC zq`tv^00sgC^altmQ3@nz-=wj}(LO|PdQsfd=XTzOH~}AHN-PY5DU)Jqiek`F;A%#b zjr$h^(3RbmBxZXO%Gz}H2P*e?JngsypG|1;&FM+ZnQknLb&9wO>);9rr9(0;WbnCD z*m?+s&O}1M3Xqh@2xw>Fw&(A&W$3!tjrFz6oR#s@H;rCMF+~+_^OBtSnRBO^%~FAH zDC3uL<be?ExH_u{j>7@oSUBh7F0bwN8O_!C8LqYzhOF8lkjj~IN#G7N8G_7l+$*jF zv6fXp`4@CK_o;0Fz&ug*#EecLcz!0VXy~IVe>l-Hap$-68uqdQwpAZt^9;rbwRNG; z(&6EGOqWIh&ZgfR!Go=i-t{?m;jU6UUx_Z3aYHL_p;ku-y8fo2`}UJ6`NUg>CLr+_ zGYY*d4hg<Ue<7qSN-;w1J|l0-&8#L`nY&mKJe~CtWo1|iuQNF^|AG<Y&43Y;O356! zAvdbcLPRO6u}3hOenIygLqpHBTt+fSV15{FFZ!SlQr<i1Cj@ZhdwFIvHRX!cy5kC> zQaek!#V{7tt$_fQ+FMF~B6q!#W{tsKBF%Y%QVoabg-X)uvF=l63+|?{*@~G(a#Yw3 zN98=CnCONxL7@f>G-N=pH4!Ana8o7<D?_Gd6kogsFP!{6^u>*$3BAOyd6G3SUZp2J zX$>6K$N2pwz!n~E{}o6oE%DG!^fMi6OQYn_m#L49nax%AEV{c+021!RQ@7~h(&OZ1 znFNPRCr}7t$Q$y92ww~{ivmyzv%$=IVg9Z0?%Z}GrINUXiw!p1RZ>)Erqy-59q})L z07zY|TL<o4<i!YLcqqsNnqepN75|za571nwX43AQq3YM*WQ7KvO1^2u@SbE0FptFX zS-k4A-L~p}NtE;a;$nH)8pX%OBidhD`2*>nCCNm7-qUfwsSkyM`%tuqGT>`;HYQfj zyG75AXie1o<NM*~8Zgz~BNDo}<|fIgxnrIKnKSizV3*xOv8#OZd9UX7Xn}ySK2hIB zJDpu36Ibm#@u4>76ASG%)n*(FgyF;t{6jHG7r3yd5ykf4p6aIJgEsVsaMs~f!$r}8 zbNLpR7hE@R^2C}&@`&RRsA4Ml%?HvbjQ3ZKT_Jypnt<~;L)~3Ww6JSoE33=9%DW3( z?>LJOsAARav|y7Y(Z|i=5OBaSH~KIOv8pt0AxAVaTrFOMjJ-?HD;zUMAMA|wzzZ;i zPx;g5tFP)AV(4P6rNDb6+mH#z<(4S-P$FM4|J7ZP2n5Sb@GPsh5G$@gUe;X@BSnOQ zilzK$FDkoDa03n%W1#e<hNP4nUNOkk>e*Q`#$X(dhI)%ixx;PZ+(IsU9y?~B#4|zj z`!BnT5c%J?s|XO-{;bR)luD^_A68UzkSZ|7`6gi_4sCa$FdR;KN<*)E$g6Bxc9n!d zcXmLh5iLnwR);FqT}g*Ff3qw7sO=^~kp=FMI!{~|bJ@{%0nOky2agG2YW0)_r>?Wx zpbwu$k_6X+(WC<YGsfS8waQ8Y*L-W-+O-Mwgrs(cE91gAz7{@BrATCnLHy)RH1hC? zC3kA<>WFa<2!7Be>HdS^=Tlt4Z4SQ~YC;t4!S;(AH#);1<M5`3kimT(q6*B~M|F0U zJ3uE@R(aDV1wHaE%(L(_jc{=DDzVct+$1H95s}tr7Ywubk%dUDHMD_5??l2FY-J#K zKTb{FTpykd6V{2bbRJ;*!5%MWpLL6-&?M0-(ZpV=?%vC`@+6UtfCpo;5(Y31<ahW0 zypwzw<Q0bNU2FZ?c+Tr)Gr&AFta23t3bNPSzpti5!JGjea}p5_L;?r3<>F8ma2!J; z39)38K<?+_tn1W#H+X?!fz^n8({fWiTM_pYa4RMnO$c;KWy(G`kp!|Tq>l|Be#d7T z{YnxoVupSwU$+nOyo9V7id3uxTvFyN(sg{|-%|R3Q-S_OFj8=;06S3vN^|Z4F==Al zwnsoJ46B$3J50U`;=((pC)^*?4ND-4Yfiv&)TJmx``5S7#!rY9YBnHuOFs&RB0Qq+ zQ(1Fut#WBMtR_nNE;wax<T+5xdJEtj1s^|@<7UVpOVivv0$*B&AtShiHE;{k7v!>* ztrHRhpuh@woJdAzt8ScApWFG+4M7P_dERLrBD<~U2%-Gn=BO+@o?dlxjg^Wjo+)*A zG(x~PelsHE>CGsAlrWBuypb%iBI3qJ0AQkSHL~AolC#m9KL%_+6jz<Re&B2bj}7yy z<VYUo*86K6ZyRPK(eHmxP!|=z{N`cOyM>8pj{4%kuJcOK1yY_#pv_5-QGIM|W$wek z`~u})P;+iC+~2K2qf%q0Q0~YHpK3N;<FUBmsd>Gx^e=<BUDscZH~08-gbYK;7u>v| zcYX&EqAZqqAfNcUw#8<^E)pC&k-^dAAd*6~p}ZS*c0)JOM}cW7cs@M_8d(ID1Y^K* zNj5{tbL9`3vUl$SMWw?iBzGfVuXx(>%#An?{pk!Vip5*$V;gkpSjMXYAX8#Cxf})R zQ7fq8qRYoph}MkW^QbGXj9QR55kyXcY>@<bR%LdXcE%ZihUlUqxUyIr_i!r1CyHDc zn&q(ofLi%t4TfnJdQ4<A12EYmkiKZ6E#IB}v(yAP_P`sDZG^Yf|BfPMB@kMHL+O1g z(CZ=+qF{ww(%`zVRG!rMM#8xIiFLImEGGV5dy&t93o#zL2c!gS63CjW1ORIDiI0h_ z&A*A&ud=%n0B0y&&636tOHL+N_?HuZfqxder+qnR3|`>h04yC*a<b^2(E^aN2-`#Z zF&`3F2Z=xV4LfRkMGYgc>e%+&<_V7i8Amq_n8DEcD2YQB5cE^|V=IVQ;*hWI&W^&) z_fO-~h)BeD6tG+PfY?T4&aN1UvS0*-i2eE9d<J;=bBVR!$tSJvGU6X1oSpui*e}!O zLOcPFCMO{AF;Hnxl6oo5-C(^z(etuJhyqmivrm?mN*f4+*g^TA2H%~kN^yf=^301{ z@^bj|f$Zbl#|b|RWWxT^W|=_hF)~Boa2)1<&5_(px-7EeM*3LmSZ$|~YT9=+$^Zug ztC)Dt2tDVCg_I9rNq-s=3A9X?h~Cu-N*g%<8?mZ3$0R()hnDt^z>q*I9v|!R$eCG( zL8<AKQoQSVXo*hCVW|G~lRpVWh?5fW+iLnf%V<VJv_Ul<&L+rsh8hCH!y0%`jRvDU zoeG{+S+#oc)>u;QRw2+c08yIraD;J{#{aZd8Ov8UH*V4$<<C7G|0SpOX&zxu0Guwn zpQn)@VS&SRgWZ5)x&AB+T|g#PN4PshY7c;nZK1r3MHxdOgsNUJ69Oar&Z&xYV%!9j zvwO-*^Q*QnJ&|cB<SI@mqB9KC@P{ta4g(y!oNq2dZ@%)WeUOGNRpC9~rI5ooKG`au zM6Qz$jguFCUx26Rep50FdvCWHwq6lo6Wzd*+gKkTV1jHL4MZ(K;@BxuAvj6^oR>CC z^iKNij+l3Oe9ZXlQ@(FsS%xtTAk-5yl0RuPwOu9eXdn<2PWi#dhdM#RW*pgBX9Syj zQ$^HW9AA|rRTJDeQVNP}bX5q^Y|oNusN~pR!yMyTWqGkA>6I>8g}Lh<yB4i}xL=cV z0xzFgcIy=8i(>n8bA4MzWagMTDG;lp-p(v&j)`{q@Iitp7oEd*bg2c^ylCWtdkHXW z8%jS&PL<7rP?gT3E!^a?v4%<s7*lMaDFRtDgjFguW-pnq&m^_JyQGfs?52~*$asm@ z<QiYXdCc?H4<5&#U@m5Qr@G|Grz{p;qQ$1Ipdv{*7wxC!*D|%TX|Y{3S$@OKx_N1a zw~|HNf$&~uB+#xgH|ZX6MSv*Q_KZ2d$``tZ1Ry>Xpd#V|Yg*NAvZFY%*2q$4)ELdg zP6QdIEPMSlWE>zZjUqPKNP1y`=N#WgUt-|%WQYqqK?S-ixn52zU+7Dcpv|?lmbXW4 zHlHTxZdpjh6KkAlkux#^*XN2Im7c371}hqQ-5FCIV0P7Od<zfr$Hx2G$?WW{R_VtU z1IBACR_b6Cla}G}-`fViD+;hgK@Jd{=6Ph%T-rAmXyZlL(fFCh-5EyQ8rpHM;~jX> zM>{$Pdo~GO7DSXXD_Pf%bdI)hKXPDkRBIA!Ix_EacL0H(Nw|)p5zzQl$Ziuu#UjCk zUozA5fm}a(IPn_yS1>qaOact#jEBm|nT5HFW=Ugj@yo#=izmc$<h9s9USJ}ZfPEcf z!QnD-CLr(7-i5Rq{yG#$rwjFqR)KnxTci;mbF->i_OV^kuotaI)qm%X!beoP=ZJ1( zE5h+gaV}73n#K=u%g`(`^jy+cEX;KeCANJ?{JPj-QYk>{Rb?8bj#ef&li2X0ck6QG z3WP0I+#66GX3_TX<EmslfpxOgP@azf_3UvMP04L=HZ~m`K#*^QNcDBqOiu&6S6Gi$ zJJn2?#uXe)T9Sgzj#iR&;BWzvHO-Npz2Puwv92pMO?wt??Kl(hxG-t?O57gBgzfeq zbHLnyeTby%odLznOt3Q8dBI!PHx5-025aXXYm(IQSuE|#Do3228or$1-(YarT_)c4 zrgzDePk=M*(@+9jPN#ep1O3t$8d5J~5$Eq_i*te+C9y+rPQ4MC<#1Qkg=OoU+M*KB zwVFE`HiqV<h8E>l{#?HX3NgId9oo3{l0x922{>IU!#{TW;3P0s^x3e2Kz=Iq5*A<3 z#gR1^>mkr~VYBRgyi`UO%5rjQs?^NXOBqWG&1PoYFCKtGud2ED=KG!=>(mIt8kxZq zd2BSlMyd~EZnlS~Xxjz#tX|gU;T{MM5DlTVK^5gDQ#E@U_TC+0a?{^bayj`rg0Wka zN+_iIB+3!W5CtfI<K08RfpWzfsZr<l%u6;yzu>i!f(-LqO^=;wBZB`k;9BDU{b9J3 zl2Av?EA`|eftj#a6|Ad>vN>i<;N(Wa<e5$P_>ftAgLa%Hk@g}e<;3;}vTk9g!J8R- zqtM7oZ--<y$heMCK#WC1%L>;>tMrm_Xrw9sk#dO=_NUbq07RC3(idDKRI4~DtX5O* zzWiar`_R7)A5QRgIXVtQ5}q66hztbtfvmfSoH#LZ=@aE@i)-*1w<2Q)!z`_OrnQV| zupjT|04R4qrZafiz(?TnZx&!~ut0b3*%=c+>)m^&VyARttV3hkqfTgGCZXws4jlV< z2k(nf&8o!SxQaA{yy&72IhwMR4o;>9TijA~0KJLfJ*v3oPI_9cICKvt*>hJur)(zM z(PB$lXkDpaa3l);EePjp{2yxr8o7(&2|rSulG?G)x%ngvY-K5zEWM>VGI_v~o~9<J zKx*;7uoRBOpL<-6sYa4uLQEi)mzxZi+`_=ue^97_<8+Z42~;yvnHttJ1L-3NOeHfU zzI5%_@a&HIUG~w&x|XnRD2`Ft29HbwclESSgdbW$LR9x_+C=BkY41LVe*iX3I&hX3 zls*RH6)htEj(K?8Xe3PEl}pKFY*_#jT2!kSIC8Twc>2;nm+AC4vq^d^W4Oy{_k<_p zN7|2tDE6}tOzliwe1v7)sW)x*!XF+Yw$Z`1KP^uq9_n81vy9#kcJky6-7>HYg5K+b z3SM0izViK?a3`71RNBkwuBlH_5i%L3!L8pA{@}+@LGfy3_Y!&w@?v&!NFEl~scdNz zHhn^U*VP*TI7(<5lbu=oP9$b^V*7C5NN0xfk7=%0Vmiz<sh|zaaky4=tL=Eu=Dc{7 zYN9GiliAn8dRXfL5hyXW5E?&@;pBLESe&9o#rnAT9e($@PrR+Lv<wZ`HE-pvd}%O6 z>kE@?b%uKFFYWYplRA{WSXtHu9Ox|e&Ebpfe>v)0>i$Jzlz)e^>I%S=T-{PgX;jrt zJAYRqT6%#AwZLF~&qwLMdP~iTgJT<#?GsvU<-fnCXtN?J8TKa7r>#aEnXjA7$3*)R z>MFQGw+`U0L^i5%BsNK&?Ht<`V?ezz_#x<ADfl2>?P||!RTG|{#jPKkZy7Ic(^SN$ z^o?H$Xph7)UrVwCKY`V5e3hjd;{EhR8{me&XSKNhb9t?-TeKPOYmYgrDr(qg>&h!J zJJH7*TuO%P`O?EU6&<4VLbsHF<)Wa{SR*#I;4uXii6Aov)#`AZdk)7fjcb_|%QSiq z*BGr`50crv4?`_rt%@#hUJI?n`?*AP4bLW^FI-aB1+uw|HaqnQ*WDb>FA52GGHu3G zn@Hv(@mZ>;16r)(a>qIU>4K5$cmS-{8JC&@@9OhHuanq0%K@>aGLD)1o3Vo{!!= z65^g%DpwGLlafZM=3@5@c`N`d5A-)IE3~>GUft?3L8_N0E^+}FM|0XbXZM^)GMVP_ zY4UU7t-<jP`O;@B05dC}bxavspC)AE&aL;SgpselUo8Dy4>GC4P*u~j?smY!#2R#1 ze9vq_6ap!waM+%>#ap^!W}hbo$sy@+4mDR<_u;}?MJ^QB$yB;nAR|KneJidi=GFST zbKBy%>%nXxiIrYmz;9H>%O;E|Q5C7&Odl?Ktu5#Lq#sO2q#w(fk{dVzI1mD#`GdEU zo=N48Y3L#BbvgsH?c}EqBB2ZK>bXnGCJWDm!Rp{jR<NHv;JuN6U(6;4ojHnHMrd8I zou>lJ%iF?LS_N987oPu&{TZo=jpg$F%r+2?77LU`X-bY0@DvAu5F1g&0A&m&sJUhz zGZ8P*>?}5-3p)9F<>L8gPE2=#2$hE?^gLuG-k_Z(Vj0=zaR0S)S>#ocyxe&1{R<6( z`<;gA^T&`F_$S}UTAk3{P(JU&D9aQwoRu~$<YSv7q1y7Wk?p<_5<91Pkd5&IRn+Y? zlE1@?16y)FXH%QL*MNzm{}vKvna$o)=xowur`3I4Fs$A$8B6?O%Lb)slouoZGyttE z?p|}=Yu!)dTMqlB_I|`YIDi(D0hMuhV-2mOy2@p_5~$fBY1sAs0bI8b`EDa-hcK<y z%i_UiYc+re?mmoA)V5j=XsughFzyMpf0SW9*;3b5Nj<PB1-tv?CL%$60vT&wk|ZS& z4%jka^uk`-=PhWtJkmEdRn?Ew5DoL^NqiYJgtFjPZSGxACO*sX-W76nPz%3xTe}Bk z?&o0+=B#Jd7xLesSp-_y*UG?rl{xiF1B5vJW#Rgu1_ez`3CCo8NEsf`t}yV(n@KF6 z7~!XU8{l+J7ODX`Wr_gcUm$v#&nq1XDeId?R%oWdIz5==b!04*1Ukcp!q&?AQ|8mp zDu16tc-DNiHaYO*czivnu&~rBL~@mOF9AuK9Q_UrWNj8PD7wo6kmdZMnrd3{A>|IJ zsMflOeIseui>}@|2k=Cw%e<jqFD*U91>7re!)g9tjOUjDk?#5r%=5x#tYpq@eD=Rs zB9Zxn@{w)ihwXu-aYI0g_P9u>#AShOFM>{XPd*TCa46j4LKe#t{26T^xtiJDHa$x{ z3qm~nN+$ic2|WQls?X>(LITAh>kOO{d1<~+D)3?L9svh{pe?g#g<_&c;aa*QZ#2jb zvGw&!(LnYX0ZtaLE_uTYQ|<bgalN)-z&g;khpzD|Dqn7~tfM(O5nCjkFi{>=W%nxz z%l2`GeP1)OyHVQ<D9_LZ39M(L=0&5P5A+;P^PM^|*DoG1%<&^j-}l-aKJ<`-Rm8&C z;V)12GFLbPz!$*28{w)qZXr8{J!u%N<;f_R#)1)5SAjD?nm0i59of?KSe#fP2(cwK zIW3&juI3K(dbT)Fkh{gkKy6x0sqkd0`}xClxf%jB7B-V_JHUo#F?pWATZ+gdqzmpC zokdBZ6vk<x-F6_JJ+CItdLJ1Hnw~Ua4R`mmAGiTQOBW(auV8gwvBb48!s}KWXQz4- z)hXbu84EbTo5-{uQI}U;>58BE<@Nr0aP>Kg=nRprd$yz5ecb24ZXGIp_xY*cPr~-3 zvviOF*^`|j=!Ma00Znz=KOwhA$tt?{P>RdvF09Ir<A0=Mq>hBtJ(GnT#m)rQ@W%}C z^RXKwyZJKVE!N>RI&3XlFR<wqqetft-v$|#)~;>PtpsnFSe*@EbB@$JE#woOZ_J7V zH`Z0{<{y1Nuk-VtLv+&VxH!e{vTDkJt|&EGCvm<f#kNIdOJp51W45!N&PUE<hlsm` zg`#of`Z@`BX}=UHFx9+eEsjaQ9GJLv!t|BP{jiV@ba%uSDYgd9CnFHP^0DEY|IYWB zuvIS}*N+AcAO0EL1?v>XwK7x9fyL%oWhrl!B;+27Z_MAS7h9ENaLQ|aJnw2X{n{sK znVdKzLRvV9_AdVdL+BLInQq(@v=I?ocDN`NT;E@S`iy8Uem~Wqj^e`3G0^!TpJQwr z;_QRbhE}nZD}|~SDE`qM*1;khXbWCT>5N?Yxmj+TR}t&(wr1X%6}@=xwC!jj9WYK2 zCM>FT#Vt!G%_@Xs^bX+0w4|eJoi!o0FtF9$EJhMW_m0@Nm>Su)535NI6Y`xq7%$^9 z4PJigtKa5hgxQr9=o7mmB02wK1R0d_K2LoKy=t%S(Lpbp<{J{qgOsKLcq?bZ%nA$# z7w4U4^_GnqNqnjB^At_AhOrL*oDH9iY#@TVSn@OSuA-S^(m<~A%!y=3yN>#jF>zBc zJn>6Rb?Z`(u{J*5tGd>EQ2jHxYhWH`FuOh}tTg7z$YZ+FYZzrxnhd%(JN<_T>X?m3 z0o_DNmx?xd_``(Fw3#gg9a?O+*H=1XC`Uu5<(A_Cu#|pFP9CEoj%qxh4Non%Om*&P zj5Yp^@?$>w?sVagg}M4W-Sco&E!#s);@Df3&#Y9{tD3n$oeY7+x2No+>^y{o>-=7? zksF$kRG*=0AWOfa<-LTEeS~v&<HMMZkNXX){3IdyyrEn%kcneo(3+K9W;UPUCBVCm zf@z?G&-^mW&w3GzVJDl(mlI|!ZZJ2WqTM}o^}K7uafu`)n=8oVEYTgJ*xFF0a;rH9 zx78L1!;MQwVnaVj<C|*yv{o$a=(IoGQ=s7`LDB938^Eia<7-VPDPDn=BL&(lmfvfO z5gHDuG$O*Z_7A?KVARt9^moDHG0yZ0fMEI{qM$)HZz2ERNh>F~?#$|psq6(4YaY4d zc(Fe@9Tn~_lSx@0Cq{VyjxwdSdX@3qQ-Z5L0uEx_u+adGWuGCnK-gCiv|u1}nZb2z zzVtqf?AzR{W%BOzx~7|44jJemPi-}<aG-SO%gte_f1X|p9Xfk~t!OO?WTrmfbw`9_ zTW1_xw>do1`U91QFpS0lEUm3tFT9HWJ|-;QVwizk2CY;4va*8(ITGN5N_q0`BWs0z zMb?boa1mHuC*<W}s?raC=C^^)b!Kzf^5Oi7$RWz3CKTApL<yXVZ@Xt*4#jr~E>wLH zeRGZtg5ACd&CaJxqB0HQW#1)2a!H<$Wa~yr-pIis6%kA1N!Zy2Br%45%Ip;KM5Dd1 zN)0yz5rm6iZK55=XF%G5x3OM#BNI#w7}-T|>;f-4TXEG%a40v^FR|wy9r62_?y3)2 z<$x};9(y0!3-6=G)aK#K`xQZ-1eO_PdHD8(4-Iw0o7umN54-0gM!X0=40I~i2B=gS zbsGSmb@7|UO8KQ18^CZ4U8*qa`Php&9s_RhCukZ0aHIYO$$43+b1o3Mz3%s&k#%XA z0OXIJbZYh{m8dy!uqK5&M@uj#uMa|^wQ=RfOz6fEptz{K*YZ9EUP^aI2F&hHN(>9{ zybDvhp&v40_cskhEClhBwZp@Ac6RA2QD=K2KaU*9PAE&*cvED}7a>E0rOlGEHQT1l zP(>6IWMbV^S)JHnG7NOj%wfd($kC1S@tcUZ$cCr06P6k*rPO-b5ru&!&d<vC@ihwT zxh)`_zGaG&OMP@AtN;hAi9XLj8M7ZU3JQ|?xD2nCCPK9g72mCd$%<Nx3D^Epc_c>w zWLsS;e7`WRXA~+w4h>_+x?*dVuR@N<O_u<@0c4ZinX3ToW88ha(n-Y_>e*{XAu<Z4 z>wtI6Db27(c{M<=51{&*0B_&Lk`PA)78IT;47ej2TM}?{5(a`fd^~{~a59OYG&$rT z9CGg1Gai<T6)b$`{^&1HlJwxLKxi3>k1{Tll0}!N4}6_Hj@Yn}m5Gf!s@A;t+mG1V zp*R6m&Cj#yRWSvZ1BFRNct~S#NxZ9+SDOlV0&x|2BK`impSR6+_cu~jNX3`;eMT24 zV^&Hs`wa#hYbQ)O;;#no;0YGD$;rV0V@DxW7TuCht6wTpi{rclWqAKcvB2i2GJ1tr z8$fE${S@2feJR)*vCO_4&!H>7r2)ogW3eaPc8uL#iN{F3FSUNArmC3`1E4#bZ^)QO za~rDMDZeH#o#;_HL&h5AqB6i_tQxA}TFUqX^NErWvFR2E7&Pb13debWJOigAuqGGH zaY%5bas@CixmR%ET}b!QyhFeRDaitKs=a!LXskPv*PjIJ!vXl8zf33~^zjltXd2_v zU-JMT&4ol&oLm(URXOE~m>0mF(R(?XFysKE+5n<O|9;UzUeH0r=n5s789#1=F13$w zKW<+k6(Y|CN7kAL8a;m2LGQ%Vz94*LU(4yP*<&t4rp=Lg>5r1iNonJ|_7uW7<Vs^L z(eeO~)x>7n@5``82-R8@hbg(a8=sMu42PE%s&oYfa}mzWr6Lk5&bD*&7Ih;^k<W52 z;!CCBC^mYl=2;sB+D+*UzhYoT<Xm-!DX1YwCgMW&tn`T0+_U+0AWLL=e_n<Y$G~Pt zlH86Dh#WyQboP}~Ydh1KUZ%I`y19r*)sw1V<prMpTsVwB^cv@_(UwYst1R_wxYsFl zRX2e4?eZnbA^#E0A@<z~k&Qj6!k{f+inv<`1;)4t9kJ1GYwkWHZPast{76VLnsx_; zn`!STt)`5^Pps4N-`rr`U1@(a=;{V|!Ll$rcZuBUt+h`XGq>$(sEJ6Po2k2PlxHE3 z&-Ykjm)Z~8BV`6Ola@R{LMK#?EwB%_Cfxa=9hCpTMt21?hk)vVGcc=*MkjUV8Vo7A zJm`_l`51QY-aqRseseJ49=m%Y`$W4g<J3Ln$}H^r#R_h|2=Em6<g6E}x+whMuQk2a ztW1f2{*17|e}$NaWl8j|xAQt${LS#<q2-(Kc-Nf)d=JaF6&gxM6xFuMHZl4D;Y+q8 z_b0og4+k_O7c;oEg`Y>X3__W;T>R+pbZ3*-NQgpvUW%#T3R)Z?JOOwK^P6o@US7uu z$Pt`2hr5&2<r;!SBY2P__InW9GVRn+7TxqnGI0fauzMbf5Vvlkrisr1L|C404{psA z)Kj8b{UQ*SB{e)CWVS_x&sa1=P#glxOk3d#d2`vejF$}Hn#JL=EbY)G-R>9j<ro3d z7a7!I-N^hqkcfLIK*taw{A)XX%>w%umhgxw7~p~RY>=}6sDQ9{mhFJ4?a4Bhqwd_f z#pA(H4yzsyUK%y{{MQgICDnWwAky}w1T7s)k`19X7gs%r>`wQQ9`tL15UOZ@S8YK? zfvS_wx#R%F0Mg~AJy<#%{t0&>5MT?g`&rSxRRMVe6;>I8azm<Y90f449Ow(5e;iQq ziT^`}=Q7yld8##`G5I3IDoWtNr3E(-f7tEHBzh{RMd7Bu+H0fd*t5gH{0K8Cgpq*M z9D^#ziAPIgK3ib%sE#6~@#)_VfKi+$K;P>&U18g9Ads`vfH&2aj4<$G0;Pbms%J>U zBX_=+nDWN&gprG<SHQ2?V>fV@l?Mngd{`U`9s$T9QiyXHH!37M#%0rM4UlpK?OisE z!BsPc^pJiD+`~x2ly<|uHEk<Jf19c{Tucc3q-W1Hj)%5Z(JE03P|SpJkKc!~6vjIL zALO}mHL3!<!QzRCcv@c(DEFWbKZa3PtcdLIR?k7SsGXCH%K?reLw4P1WJhr@X6cSv z%I5?Ps@LF*Wrn|o#g%3Aj6rBSp_rKD{*=-O5I_29F5ep3>@usRtyy($-KAgR8>%jV z2ZZ;YYD4HvbUFCzFTi0d8-@9Dg#!!hzH8N$+C(WI?~(5LFTXXk4<KqkFpwx3u2fBh zACs!AOCAVPo?yBGDP}$5wkT(EAnonmHT$$3<LGJj&+kjX0&>L{M1@ZY#>Bv-iPOrM zcRZ6JT0U`!1TU)?kcZl7a~DPW5m}nq8m-ss7X{#bZlc1qg0|5chW6slhN<{U*)VYL z;=Op=UFARa>UTT_(=$hAfRvjN0nX_3jY7CHA`|43zn>D=b4Rb=qV8#|O2DUVp}nK* zS%n+L#;Zg|Pl%mq8vrwLdbxCkR2MMN4R8EDE$eyb(RLE5KrsiXEaz!p$5-M5SY<<k z4G%4Yd=8J!{d>`oqW!rOEa2)BDmle#!5uHV8Z=@3gz9PWX(2V=Q(F^K$E}<8hAdSB zBFCBgI3?Dt>#3c-NC<!Q8`CDUIQ5`^Jyzd;PgrY*XtlA7gjycXc?$Jq06i#tX6oUH zEdZKR(&$w(3iX?+3v;!OhI2U(Yvsz1$uJ*`p#*~f&(}+Uc}&EWBBJtdmV}(C_%GF! z1niyn>T3vAFY?>Mxx-rcqUE&;xA<+19Dse+N`7|Lk{>3@pz4nbqX%E%E|n&!7r>wJ zHNxwH3QjN!o=I$#(br5_VLi7sFbf5Z;k(nHUT!0d#mNTP!e|Enx~M1#Ey`G(Zu&am zQh`GF->-o8GQpuf0e4>J9BwQ7r0|eHcZ7pvob6EHWU2=L(aMlf>*7ZwjYH()DY|mk zQ_hC$Fvts|qZ-ox5&|1HO>Ix9kIfUQul0-H&L^h$##i^7Rk)|*ZlxJ%k|frOuIe^A zeWcb=#+GtFDl<UU)5K|{4<qj<f;<4|C1*EsxgNnSqDhmR$*8J?nhG}OG;HN&gYb)W z#)cBg)c|}2X+KC6C4GmfR*LdofZOtrFc9|Qu;~vymgTgiUz4k(S+bLw(fvc<gI_37 zkFHBj2lXy%BV12wGAe?8Cu8Qn)XZk$i;I(O7|S1<9Me0F`Py!^{Eb`%K99aF(k+P5 z`DQO_VOV`Wo5)=Cr7ET<iRCi(rq{`T&%?f#vIfr1GZu~^W{X8wNaymH>9yR~cwI8t z88rgdsuT<sds8(9zl#^Oc_aPTz^UBq>SNn)9|i~0NHf+P%d}zL$Cv)T1=C4lBwI7& zD7e-DB^n2@r(|>DnHh%`m~uONxbzj_!-YGh=P^DC|7uCaP<QrPWOsH=$6d=)ibMeB z8+V1yRvUEO(sL-4HtJ?I=?}q!?LwsNF<WT{!i+s=3opk2ISN)hb@olf!1hNdDj&k= z!jgQg=9C-oWB<^@ssNp5tYYU?R-sU>5`InegD-&SfKH5ST<Rl-#~ehMY48gT!1>K} zi%vMIkFSn6TAa!>@Re6L4BAqT{G{%w;%J860n3Z$P{P>S&&HMwb|s6><~thQ>y1g% zO{Agfy+Gm&|E}VhbT`klnu{iLR)~!G{`8yxqe1n}TkV`1mRulf%IBv8*g;)d(;hgS zG}+={kjgZP?yzEG)(HA>Y5zpv(Y)Ad)2A|NxXbN6W*FiFTC8xP_(-dY-+GfnZd}9O z^4W_M93Hd9$5z?Pec4Vre#w^HPE~qLQDU2XuXY3|mTe}acwPd2Y*V>piE(7BMFd+M zr+O61;dYa`y4<}i?Y`CMw4R{ox|$zRIm~`=0a>s;$6>cLUYJ{DquJ^$N!Bn_KGx%k z0(%?7rk0|I<Vb&hcDHHWFZyH)b+<Wb%@Kwj9f=eE${)OOhlX4mPr`^Hh@zZ#uBdXp z(5FXu#;K#L0k;KsU`Ol}ZdU#Dhkf9T+(Aq0@#~N~dB>i?AL*Pm)X6iavE`X$fZnY2 zjT9LRmHL`_67mtpP-Aao!~piibwh5PSQ$ZpHp>7S1~Y=7sT~$@E$DRwZXTy;@NWXe zB$bD$T(a~X_uF5Ntf9T$siu20{-lev*prRDY_#oXu{;?x^sxjzrwOMga^^dgh#qBY zRy-#Als;B@-KsWPY@1WX7;bk;?cEES=il*1hnoxq$<e&0wdtzE%Cqg=i-sNwG#65o zrPQXcP*sQeiP`n9G0T`0=4e6TI@1yOTpTS1Em+mLT0uYN9WFbiAA5(jgXZ@G0otu~ z<HvWY^0GvUZW#6Z`h14zKoW1LNn>7@Kbgb3)!!`aA4*P|-8~CiOPon;hYxBP@0-W2 zWn4t=*%bs*R4N)V#b2y*u7tq>Vm2_|nv@_dfk+~fYFx23qBEXLJ;KD@?k%el*i#LK z;=g>Tv+m=v3|g3m(WPWyqlqPMjA}gnaWGDWMt|?ji?y=rY*bH${6FsdZ#0YHq!gX~ zGAPnfCZydTW`OtowQ9CSh0s9gIds7;-?~_A`y%(uulR{DRS;%(<YURl4+Q#kZuR|O zn?$IuVgS=2@=c+J$TmjQBiD877h+U^;5Y3PPkTwg9PZBf84UdpphPpFX24xV4I_~n zq{(>}Q|x||<wsi*p8>g5ZL-u!z$X9HVPTDl?<=}&iaYupQZnrS(0-(<h_%^&zcFcw znIZ`G(Z1$&kD4#p`KC#3>)g*zp;yZ~+=npVJ<(JT7-NmNhiZn)u*~|<m^OS`7uyd@ z;O%<;9zC+i4lskQa57m7cpY~)o<Grr$56E@=gVY;D|*ocmTh3n0k(={E;QET1`aBg zQr5o6tNM`{cI)IR0|@Q-2ge(>y*eKF*ogn!hKa){kH5LW42r5~f{RO(4$*%t)QIJ8 zb}CzjyatF9wcE#RZ5|9wJ_0qxXnES8YV<ZjVatx7BoJl_E6F(W?Jzo0C<J|qbU1Dv z%b8=Bg+Q(km}e1pxkjOb*mv)Z_44lDX-Oh9rc1U&?0e5^m#VHOC97oDAwym|C-Kjm zdi@@;u`Sb&r5Uvu`RI3CzWA%5b<NN|o8W;16hAL~;Vd_EttQIC$*kxST!wf6`iNDa zHXN4N5UYK1{`JrqpQ`j<8(~Lj$~&cU1w0*-s%doU;6|po*_LJ@7W1DevM1%QF(wy+ zYuGP?-}}{(XAyjiOR4PM{>*h+kLO?BPmSp!j6h=S$2QIUs_d33$Scn0wBgA$tE?P? zjQk*TNzylbiy*NCg8n>AT&U^7Lw;@mMjq-+CNqL|2j3u4?>jdjme*GqKmnD&-ySs% zf4#ggRUFOY__T$IC_e_vU1aqSIRbDTXH4?lA{c`2xhB)92%Kcxpt+M~auV<GJeC%U zODB|}cJ_tzexu4tF#<KOluI@y+Tz!{CU)?G_`X@-(vlhl8eV&}IjlZ>(Tw@Fy-LDI zrA^xz$lbyQtgl0t<LQG?+B&u1uiL`lT6@Y5$^E#|rS8nZW4?OpYiOhfdpSlBQe(!r zOcXw0?)8j4Xe;2j?zqS(CJ|nr#TAw*+Xz^};x^BRyRvQ;uJ2K<M{G^BpCo%vZXkcI zUlv`c=|}-|LaVrhe_g(}{Cxea=D9^feH!|ByTfks1C+YAo52i8^rU=O%O9q-b}YVw zDaC{9XAW!>Oy*I$D(z-B+fBEj?Tru`WU?q%pQkrFL-}(%MqXITCi1R9VvKXDVU{-K z{7(C>kv6RxFmvIW(5o`$kl8R+K5B3rF*wvs0RYIcY+2fQ=Fmj*+XmNSlw<TAcHaH? z`}gIwpYw>z$ZS#h_-Qkyf5p|ra`*2Dkxv~xS?a%y5`5jgp4sBR`3ZlD<#H*)ySnvF zq#W_+XCk#;C?z)NBw#KDa}pHNN)*dMVKyE?7N3FGf~YOtQ@8h5$l!7>rjU<Mh1slC zT-Cm{)_39)cp7zZy2tlKK{AD3$t)d)l9<|g%deQ4MWRmFfl;7sdcw|yd;#5qZnC9F z$G>6dJK{#CDoa)j3T4OE@AU*G1A$PuofN+5X&POju8QoRP99ESpDRcRCPSm<f<EgB z5L875)~hj^2Rhmti`#0D!~VDdEv&hqn7Q^6kJ~!lR<zgj12^_}%hTAduF^DM9Ed<z zSV&taF8??Z?^BJu>x&_C>QoUtPWWtyBhEx8B<rUI4vjHHv^!f(KKBk?wU}$jNm%by zdgoNwal)}Np)*n+VhC@W|3tPLzfUJbiYe>q76~DHfV4?vA6i!6D({}TbS+*85Yu+i z_)kJ+9!rKJlhO2L-K;5xmf@|k3K`Z_*PGo)YgKr83X!qD?85oHD`~O0ds<hlM7b8D z#dOZcubYRRG|WozCZ3y<F6z1S;I;PUI}Wc#c5b#*X>z029t_5#-s+l7RY>K5wwFk; z;%KEyp<0M^nXp+D^VxT-t*WDS^ro?`(40oMp=wnUFSeZU5i6tD@yI)BJpxfs_NxhK zPxs0+Is~~0eHl_#Vzk{ld^EB_mn@SILT?4{<wMa)smo$H;P`pQBIDa`%tOP>J_j%{ zNDi%fC8WG6+eRh9^MQV~@<DA0?G1}q-MfnGDz4xRW$Xf51Y?yI57>uT)pr7A1USQ! z84}gE(v3pUPi3Bza^`%j+c)IgM^{RVYG4CB4|7g$DSx*PeCqY2aw%0alur<(vMcD> zJ_(F$IK6Mi&#Ksr?9ubk|78B^t^~TIeau7ICpNDy>0%&TO+HnnO^KxNuPBD>wia)o zrpC=Lnp^s79bO&YC?1)uZIB+O(S=S{<Pk${!wr|h_nqNHjrJTy(LeL|>(n=Jkpvu@ zin5_Fmio<0l(%6#D>O6zTF*hu7QqBa?wn>E)~B*$$yQ<SU=x?z9&~}SMf6}jmT+|# zG7&Q-wGMImP$^5C!kH0q15Hrs6wTO5&VC}AK=Y_#SY;9%K=5b5#pKo{0VSL;*%sIG zr%egnP`FFGg)nd8yZ@Crinw!ZFLM+kBYv@ZIqC#5w|y``PT`yle}+m13or=!x2Hr5 zv<xWt^0!;LlehkOMeV3_p`Ky_CC5kK9F(I5mxlp8__p^)2#92*St@3Em%fz!{Ypc< z$eH_m_-Oir`phpvN6xxX#X_8KEY+Zl<_udKv?rc($v;@Nq^~-)u0POpH2pX>;5I$x z9FhI!QmEn}t$yu|!t7&$&T|SHhowoa9)I56DJW(F;gMb_Xz(B%_Xuo5rF9rkCOHtw zwbJk--;|Fh-X4DoZ>z2vnzh2&66pNb(IkGXE<iy%MVJDZDr?fRN!tzvQ!{PZ(0QEy z>Hfnue~R`pO4^;vSJHPR4%qYr4H}fCK1X60|GO`ls+Vt0Er2luq;<H@Y7&DDA9h8A zIOm{cL$!<K9QenCgiOxC?EF5SFO0BBgzI;BGSqZ{7CF!L7zXjkj?2M#w}GGWG?GQU zvI8PKyeGR_ujq6F5h+d{3vuP!PV*w|*u$MNCIBPdHA48xl-t}dWpS`P7dhS<Wjf^~ zB*Oujhhg^o=2d0a;5y-l7UmezkJuC&>9v$I9eg6tg6}sD`)z=V11MiMxs4QCAe3pz zH&LLT94@<#lckq{C+8{3fw+VY({EcaQ(Eu)ZdTnfVUAj3t;<AOAQIGG5!Fo{CwA$_ zMv)a<5N<Y=lIfswEjxGx<t<FVi8nEv3mBp)7?Zhy%36{IygGd*n&$E<1`|AN2@^v2 zFasDB?vTZ*9k^@C^SWvKZ_O#{C4K=ilPIq@DP@}DJ}E8~gzca`T&$pa0dj6lxRRkE zGmzD?U~*pk9T0}j=JBbcr<0Pw@N@f@B|KShgIkPpGR}(?o*I2s^<HprA5SLdok5-F z8oM6XhQqQz)obvs8KKOxR*}%U7{O7q=s01MnpY*%AG`R#Aj^U(IlWYMr}uSr_2k#W z{;u+V=ST93ZA7<>^Vh8Px)Ja*!Cm;w6H^b6mEpUW>IbZ}h<ztX8yIJG#aQ>V3PBg# zYpSB!wlxlb5F=TWsn7MT%4l@Huv%qpeo<84V+H$rc?!KB9iDBf62vkXSY9}Flh8wm zhvE&bh*R<!`G*X@BG`zlt3DXeFP><6%jo3!GkA@DdI>9<lq_YRwd%=^@bc*^+iusQ z`FICj)}#+<>y4!u6eA+lR(8=a(n|DQH{<Si!cx*0zh`~@j-)M005$g-%QwgF^%5ls zr@AUu$T_v$xNQcJeuLZ&OT*^dSQ_Z{FknjVW_BcaYMYh&^&fe5ztZZdL47jF4kPyS zVrP?=AW$%Z<=#0-;CJRmfpt~d#=m(Yf~>W!VgsObH?EP@HhT}Ev4v2YF%PTm=%oSg z@snt;`mmAB(9d+L_2xj5sqQR7lBW<E&#fIQDDi<N)%iu83C?$G(aP|FpsSZx7@ory zPP8OP4WA-tNBt)XvEQn+9v_S8Z$YA4jFql6w+=j1z*tmL%pbo^fj(8*zdbq;15!ro zEcHftB_)^{xvGT{bm^pJJ5zd`Ar*hHaNE=+;%!dcHk}4Jz-3SJ-94*uE$lcWeDMi< z2UEU9o?J(+e3LJAZUdRs!tvSwpg#;&@2(G6{SC2P&Dm5#D48K=16yow$A79%S=V*6 zEXMC&@8Q8OPx`v{@!vze#EmsQI_ty|rbDXlxVzq29u#B7gj!b)^($8wNnOW%PEo65 zz+bl#K&tDbVn!k>)D_$<r=ucmFy5W{q92TR9ksGzVO4OPaa+%o7wj`JPm%C?IY<YU zo{Er3?8Lv%N`<w6zOdceZ_Z%Ht-AshDaXr7kf!Ni?VGy%T23eFQ%fb^T_0%HhSpEO z8H|OS#EUenp1XYbtbyF%_W^`!{+8@S)ZXS%7|Mm?+YVF9s=8Z$wJSO<^~mSKXv>Ys zn5#%yQEOjK`DPvJ^xO!z>j89@x4&%V8wS(0$g{P^HR|wgcOI;32d;Lz;5+fye!tUw z=68O1tmBZ(Iu-c)zMW23$*kdCmfH9TC1!9u%p4loKpS%)2e0eniKRp>xEO8a&_O0+ zs(T+0`fUW9+3Y2_`l6$8`*M#>YxSs%B)~PQy_4K69)@Wc`b|SU*o={X_e0(r++<`8 z)7v&Dq=ex+g;5@+k-UQ%c-}wfP3;qq#;Fjt0lQ{{CU8>qiK)|69ghjR%t;r%`Q7V! z8T~#ahs38)A(nlh;S#iC0`sx3VhF)JY}mD{#5}H-RR->-T7Bj<u!v^?JGLeuhs~)& z_8|Y!9k-^5b4i~!LvR`g##LlfQZBk!ROedyQJ;XEu|F6>?MVbbXCsWdnYPg^tJ@W5 zkqPU>gyP=E<kJzY6_yYFI+;*u4}P|L(N}%)@3#VQKV^@ldhtR;Zd<2nJ?qK-!-f0T zJ|XQ((vm@9D0%m@LMNf4(xJ@`@M9%ot3+4nIj2My-bcRZ54Hp?CKzHuFDwyzj9BpC zk}ae;sRi#`$Fa)CC2M+7SrPhI%)n<L^%@*GRZib;gX4_Hp^*>ZExIMNC436cOp>@e z13D=b?^ExSuE4XUY`Y|R)Xhpx6!if=<T9T7W3%J+#!Q4Wnlmdo)O!Dr4>DGM@#iML z3!_XMV}Zl8jFWvJp7BbM!1P$nm~^f+Z~EChQ}PnLeHmQ$qDa4ovYRD2*lC3bv%d(l zyd=}{4$x_U4!@<_!mvMgPF^0y7*^Q;ifH~~9bf~ZuJ@;o0F5stZrVDU8$?2lw)es? xb!a9mMORrP(0S*M`rM{%uK?^$;as|75&mDN)XGtGNCd=j*UUK_1F<E7{{!ohaa8~S literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 3b1c8ac..1c3ed38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ adafruit_debouncer==2.0.5 -adafruit_ticks==1.0.9 adafruit_logging==5.2.1 +adafruit_ticks==1.0.9 +adafruit_httpserver==2.3.0 adafruit_minimqtt==7.2.2 diff --git a/scripts/certificates b/scripts/certificates new file mode 100755 index 0000000..467785d --- /dev/null +++ b/scripts/certificates @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Derived from https://www.linode.com/docs/guides/create-a-self-signed-tls-certificate/ +openssl req -new -newkey rsa:1024 -x509 -sha256 -days 36525 -nodes \ + -subj '/CN=garagesensor.local' \ + -out "$script_directory/../src/certificates/certificate-chain.pem" \ + -keyout "$script_directory/../src/certificates/key.pem" diff --git a/src/boot.py b/src/boot.py index ca15b5a..0287c12 100644 --- a/src/boot.py +++ b/src/boot.py @@ -1,8 +1,5 @@ -import os - import storage -if os.getenv("LOG_FILE"): - # Allow CircuitPython to write to its flash storage - # https://learn.adafruit.com/circuitpython-essentials/circuitpython-storage - storage.remount("/", readonly=False, disable_concurrent_write_protection=True) +# Allow CircuitPython to write to its flash storage +# https://learn.adafruit.com/circuitpython-essentials/circuitpython-storage +storage.remount("/", readonly=False, disable_concurrent_write_protection=True) diff --git a/src/certificates/certificate-chain.pem b/src/certificates/certificate-chain.pem new file mode 100644 index 0000000..c8f077c --- /dev/null +++ b/src/certificates/certificate-chain.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBszCCARwCCQCj+re8WZ4+aDANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJn +YXJhZ2VzZW5zb3IubG9jYWwwIBcNMjMwMzA1MTk0ODIxWhgPMjEyMzAzMDYxOTQ4 +MjFaMB0xGzAZBgNVBAMMEmdhcmFnZXNlbnNvci5sb2NhbDCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAxSb4Ao/A6wxE/a4w9YOkHY+mzmnO9naqE/DDasJYQO5+ +MgLnpDgOcStQojfTJHHu8V2o0TqSadYZuY75c6A1oji0kG5CcpYOyhtpKdKjvEQ1 +3WPt+vBOCU3mmL1W7pS1RiNy3f/g3xC7qPEvW6nZZonafCO5f9edupNsBfbgSDUC +AwEAATANBgkqhkiG9w0BAQsFAAOBgQAP51g4jkm2YQ3ADmNesKewHK2vSw248PS3 +SNAaxaj0n+6PEUZO/COOD5Of6yHZVDa4DAxKdGUdL/YIBkf8mceLfzNFbW8Le+t5 +OQ/YNFMXLobDf7HRkVzIfVXAwtoJqUY/DSEI0cMfpHIcNC4ThYgdreiANn2f1WDK +YDe8O9X5UA== +-----END CERTIFICATE----- diff --git a/src/certificates/key.pem b/src/certificates/key.pem new file mode 100644 index 0000000..e88c972 --- /dev/null +++ b/src/certificates/key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMUm+AKPwOsMRP2u +MPWDpB2Pps5pzvZ2qhPww2rCWEDufjIC56Q4DnErUKI30yRx7vFdqNE6kmnWGbmO ++XOgNaI4tJBuQnKWDsobaSnSo7xENd1j7frwTglN5pi9Vu6UtUYjct3/4N8Qu6jx +L1up2WaJ2nwjuX/XnbqTbAX24Eg1AgMBAAECgYAiN0cnuqcyo+h9VnPsyDH9Z2b9 +v+NJZwLRfyGLL7t9WWbRayuklo37Ghdeb+3XD2b2wNiBp3ato5jHWYb1iEKGXPGg +biF4gYAasjB3HiauOeG3UNLOM27+yI04K6XQZSV3s6CrM+C1ILWGHU0S7LQxjeBA +hsMdH18jVjUP1RS6QQJBAOI8pJ4x5fCp7KbnBY6lwBMn5Q2KxAqQHqCL6brrgeqh +Bzp1fR/OnzsXcn6Cb5BMs4cMWxeV3DyfBTFCwO51wu0CQQDfFssAl84TcRrNyriq +fuZBth5z9auGmS38FdbrDqXPzgDa2MM4e4/LUYBvF7FQtneiekHXKis5fJlxoytT +fQlpAkAwSKcNiDK9+VYjjNy3xBJJRFNzX3FVm8qdkx7QIOE6VSG4zUhmGHANaYSr +EWWEE4qhQPbUAszdN0cha1DH0+RFAkAKz6f212R9PLX30yMv4AZ4mMLRC87MLxAz +bzuDGKqgb3NLJ8YOLq7BQ6nduGA3cSBLF3GpY7nEh21IPIgU+7JBAkBjyMupNQHW +HHll7Fa4YtBfEXK2sIB/wvdtroT05s2YM1F2XUwLfLyepSSniyLMVnd+gOZwr4c/ +KRbIBmqaBrh9 +-----END PRIVATE KEY----- diff --git a/src/code.py b/src/code.py index 7eeb815..d0e125a 100644 --- a/src/code.py +++ b/src/code.py @@ -1,5 +1,6 @@ import binascii import os +import ssl import time import traceback @@ -13,9 +14,15 @@ import wifi import adafruit_debouncer import adafruit_logging import adafruit_minimqtt.adafruit_minimqtt as adafruit_minimqtt +from adafruit_httpserver.methods import HTTPMethod +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.server import HTTPServer +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse from logging import create_logger from mqtt import publish_homeassistant_discovery_message, publish_sensor_state_message +from tls import SSLServerSocketPool def main(logger: adafruit_logging.Logger) -> None: @@ -27,6 +34,9 @@ def main(logger: adafruit_logging.Logger) -> None: switch_io.switch_to_input() switch = adafruit_debouncer.Debouncer(switch_io) + # The switch is open when the door is closed + led.value = not switch.value + # Connect to the Wi-Fi network logger.info("Connecting to the local Wi-Fi network...") wifi.radio.hostname = os.getenv("WIFI_HOSTNAME") @@ -42,31 +52,59 @@ def main(logger: adafruit_logging.Logger) -> None: pool = socketpool.SocketPool(wifi.radio) # Connect to the MQTT broker - logger.info("Connecting to the MQTT broker...") - mqtt = adafruit_minimqtt.MQTT( - broker=os.getenv("MQTT_HOSTNAME"), - username=os.getenv("MQTT_USERNAME"), - password=os.getenv("MQTT_PASSWORD"), - is_ssl=False, - socket_pool=pool, - ) - mqtt.logger = logger - mqtt.connect() - logger.info("Connected to the MQTT broker") + if os.getenv("MQTT_ENABLED"): + logger.info("Connecting to the MQTT broker...") + mqtt = adafruit_minimqtt.MQTT( + broker=os.getenv("MQTT_HOSTNAME"), + username=os.getenv("MQTT_USERNAME"), + password=os.getenv("MQTT_PASSWORD"), + is_ssl=False, + socket_pool=pool, + ) + mqtt.logger = logger + mqtt.connect() + logger.info("Connected to the MQTT broker") - mqtt_device_id = binascii.hexlify(microcontroller.cpu.uid).decode("utf-8") - mqtt_state_topic = f"door/{mqtt_device_id}/state" - publish_homeassistant_discovery_message(mqtt, mqtt_device_id, mqtt_state_topic) + mqtt_device_id = binascii.hexlify(microcontroller.cpu.uid).decode("utf-8") + mqtt_state_topic = f"door/{mqtt_device_id}/state" + publish_homeassistant_discovery_message(mqtt, mqtt_device_id, mqtt_state_topic) + publish_sensor_state_message(mqtt, mqtt_state_topic, not switch.value) + logger.info( + "Advertised Home Assistant discovery message and current door state" + ) + else: + mqtt = None - # The switch is open when the door is closed - led.value = not switch.value - publish_sensor_state_message(mqtt, mqtt_state_topic, not switch.value) - logger.info("Advertised Home Assistant discovery message and current door state") + # Listen to HTTP requests and serve static files + ssl_context = ssl.create_default_context() + # The Pico is the server and does not require a certificate from the client, so disable + # certificate validation by explicitly specifying no verification CAs + ssl_context.load_verify_locations(cadata="") + ssl_context.load_cert_chain( + "certificates/certificate-chain.pem", "certificates/key.pem" + ) + ssl_pool = SSLServerSocketPool(pool, ssl_context) + + server = HTTPServer(ssl_pool) + host = str(wifi.radio.ipv4_address) + server.start(host, port=443, root_path="public_html") + logger.info(f"Listening to HTTP requests at https://{host}") + logger.info(f"Serving the website from https://{wifi.radio.hostname}.local") logger.info("Monitoring door sensor...") while True: switch.update() - mqtt.loop() + + if mqtt is not None: + mqtt.loop() + + try: + server.poll() + except OSError as error: + if error.strerror.startswith("MBEDTLS_ERR_"): + logger.info("TLS library error %s with code %d", error.strerror, error.errno) + else: + raise if switch.rose or switch.fell: is_door_open = not switch.value @@ -74,7 +112,8 @@ def main(logger: adafruit_logging.Logger) -> None: "Detected the door open" if is_door_open else "Detected the door closed" ) led.value = is_door_open - publish_sensor_state_message(mqtt, mqtt_state_topic, is_door_open) + if mqtt is not None: + publish_sensor_state_message(mqtt, mqtt_state_topic, is_door_open) logger = create_logger() diff --git a/src/public_html/icon-192.avif b/src/public_html/icon-192.avif new file mode 100644 index 0000000000000000000000000000000000000000..9bdcee6c84d1f5dcde9b393798fa046036900f46 GIT binary patch literal 4474 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 literal 0 HcmV?d00001 diff --git a/src/public_html/icon-512.avif b/src/public_html/icon-512.avif new file mode 100644 index 0000000000000000000000000000000000000000..f82377c1e8c5437984d9c102b45f3a5fd26322e7 GIT binary patch literal 11545 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 literal 0 HcmV?d00001 diff --git a/src/public_html/index.html b/src/public_html/index.html new file mode 100644 index 0000000..cb5afa7 --- /dev/null +++ b/src/public_html/index.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en-US"> + <head> + <title>Garage Door Sensor</title> + <meta name="viewport" content="width=device-width" /> + <link rel="manifest" href="/manifest.webmanifest" /> + <link rel="icon" type="image/avif" href="/icon-192.avif" /> + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" /> + <link rel="stylesheet" href="/style.css" /> + <script type="module" src="/script.js"></script> + </head> + <body> + <h1>My Garage</h1> + <button id="subscribe-button">Get Notified</button> + </body> +</html> diff --git a/src/public_html/manifest.webmanifest b/src/public_html/manifest.webmanifest new file mode 100644 index 0000000..02e946d --- /dev/null +++ b/src/public_html/manifest.webmanifest @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/web-manifest-combined.json", + "name": "Garage Door Sensor", + "short_name": "Garage", + "start_url": "/", + "display": "fullscreen", + "background_color": "#252526", + "description": "Check on whether your garage door is open", + "icons": [ + { + "src": "icon-192.avif", + "sizes": "192x192", + "type": "image/avif" + }, + { + "src": "icon-512.avif", + "sizes": "512x512", + "type": "image/avif" + } + ] +} diff --git a/src/public_html/script.js b/src/public_html/script.js new file mode 100644 index 0000000..c9528ff --- /dev/null +++ b/src/public_html/script.js @@ -0,0 +1,16 @@ +// Keep the service worker up to date +navigator.serviceWorker.register('/service-worker.js', { type: 'module' }).then((registration) => { + registration.update(); +}); + +// Subscribe to notifications when the user wishes to do so +const subscribeButton = document.getElementById('subscribe-button'); +subscribeButton.addEventListener('click', async () => { + const serviceWorkerRegistration = await navigator.serviceWorker.ready; + const pushSubscription = await serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: + 'BP1ZvYiUs2YDFtbJu2weWZdBS4DFA0kKw3pFK6iMVV7zue-fmg_gJM8lEshHkbJO5KkiO8wYh15Xn8y0BZNpRCs', + }); + console.log(pushSubscription.toJSON()); +}); diff --git a/src/public_html/service-worker.js b/src/public_html/service-worker.js new file mode 100644 index 0000000..b1e2819 --- /dev/null +++ b/src/public_html/service-worker.js @@ -0,0 +1,22 @@ +// Keep the service worker up to date by letting the most recent script become the active service +// worker even if another is registered. This is acceptable since there is no API contract that +// could break between the web page and this service worker in the event a newer version of the +// service worker becomes active while the user is viewing an older version of the web page. +self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('push', (event) => { + const message = event.data.json(); + console.log(message); + event.waitUntil( + self.registration.showNotification(message.title, { + body: message.body, + icon: '/icon-192.avif', + tag: message.tag, + timestamp: message.timestamp ? message.timestamp * 1000 : undefined, + }) + ); +}); + +self.addEventListener('pushsubscriptionchange', (event) => {}); diff --git a/src/public_html/style.css b/src/public_html/style.css new file mode 100644 index 0000000..c867eed --- /dev/null +++ b/src/public_html/style.css @@ -0,0 +1,6 @@ +html { + background: #252526; + color: #fff; + font-family: 'Inter', sans-serif; +} + diff --git a/src/tls.py b/src/tls.py new file mode 100644 index 0000000..c0298ae --- /dev/null +++ b/src/tls.py @@ -0,0 +1,48 @@ +import ssl + +import socketpool + + +class SSLServerSocketPool: + def __init__(self, pool: socketpool.SocketPool, ssl_context: ssl.SSLContext): + self._pool = pool + self._ssl_context = ssl_context + + @property + def AF_INET(self) -> int: + return self._pool.AF_INET + + @property + def AF_INET6(self) -> int: + return self._pool.AF_INET6 + + @property + def SOCK_STREAM(self) -> int: + return self._pool.SOCK_STREAM + + @property + def SOCK_DGRAM(self) -> int: + return self._pool.SOCK_DGRAM + + @property + def SOCK_RAW(self) -> int: + return self._pool.SOCK_RAW + + @property + def EAI_NONAME(self) -> int: + return self._pool.EAI_NONAME + + @property + def TCP_NODELAY(self) -> int: + return self._pool.TCP_NODELAY + + @property + def IPPROTO_TCP(self) -> int: + return self._pool.IPPROTO_TCP + + def socket(self, family: int = None, type: int = None) -> ssl.SSLSocket: + socket = self._pool.socket(family, type) + return self._ssl_context.wrap_socket(socket, server_side=True) + + def getaddrinfo(self, *args, **kwargs): + return self._pool.getaddrinfo(*args, **kwargs) -- GitLab