From 2d21a40fd6a6469f24e318d99d50ae794a98a527 Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Fri, 24 Jul 2026 15:19:02 +0000 Subject: [PATCH] 48 files --- .../__pycache__/__init__.cpython-314.pyc | Bin 19438 -> 0 bytes .../__pycache__/button.cpython-314.pyc | Bin 5073 -> 0 bytes .../__pycache__/camera.cpython-314.pyc | Bin 50461 -> 0 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 40786 -> 0 bytes .../__pycache__/const.cpython-314.pyc | Bin 5958 -> 0 bytes .../__pycache__/coordinator.cpython-314.pyc | Bin 99484 -> 0 bytes .../google_scraper.cpython-314.pyc | Bin 22822 -> 0 bytes .../image_processing.cpython-314.pyc | Bin 15148 -> 0 bytes .../__pycache__/number.cpython-314.pyc | Bin 13314 -> 0 bytes .../__pycache__/playlist.cpython-314.pyc | Bin 8212 -> 0 bytes .../__pycache__/select.cpython-314.pyc | Bin 19468 -> 0 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 10113 -> 0 bytes .../__pycache__/store.cpython-314.pyc | Bin 2724 -> 0 bytes .../__pycache__/switch.cpython-314.pyc | Bin 4658 -> 0 bytes .../__pycache__/text.cpython-314.pyc | Bin 5175 -> 0 bytes .../album_slideshow/config_flow.py | 242 +++++++++-- custom_components/album_slideshow/const.py | 37 ++ .../album_slideshow/coordinator.py | 328 ++++++++++++-- .../album_slideshow/manifest.json | 2 +- .../album_slideshow/strings.json | 30 +- .../album_slideshow/translations/en.json | 30 +- .../www/album-slideshow-card.js | 2 +- custom_components/midea_ac_lan/__init__.py | 98 ++++- .../midea_ac_lan/binary_sensor.py | 16 +- custom_components/midea_ac_lan/climate.py | 242 ++++++++++- custom_components/midea_ac_lan/config_flow.py | 35 +- custom_components/midea_ac_lan/const.py | 24 +- custom_components/midea_ac_lan/diagnostics.py | 2 +- custom_components/midea_ac_lan/fan.py | 105 +++-- custom_components/midea_ac_lan/humidifier.py | 8 +- custom_components/midea_ac_lan/light.py | 8 +- custom_components/midea_ac_lan/lock.py | 8 +- custom_components/midea_ac_lan/manifest.json | 4 +- .../midea_ac_lan/midea_devices.py | 410 ++++++++++++++++++ .../midea_ac_lan/midea_entity.py | 108 ++++- custom_components/midea_ac_lan/number.py | 10 +- custom_components/midea_ac_lan/select.py | 103 ++++- custom_components/midea_ac_lan/sensor.py | 146 ++++++- custom_components/midea_ac_lan/switch.py | 6 +- .../midea_ac_lan/translations/de.json | 339 +++++++++++++++ .../midea_ac_lan/translations/en.json | 172 ++++++++ .../midea_ac_lan/translations/es.json | 259 +++++++++++ .../midea_ac_lan/translations/fr.json | 347 ++++++++++++++- .../midea_ac_lan/translations/hu.json | 339 +++++++++++++++ .../midea_ac_lan/translations/ru.json | 309 +++++++++++++ .../midea_ac_lan/translations/sk.json | 339 +++++++++++++++ .../midea_ac_lan/translations/zh-Hans.json | 275 +++++++++++- .../midea_ac_lan/water_heater.py | 30 +- 48 files changed, 4181 insertions(+), 232 deletions(-) delete mode 100644 custom_components/album_slideshow/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/button.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/camera.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/config_flow.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/const.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/coordinator.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/google_scraper.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/image_processing.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/number.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/playlist.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/select.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/sensor.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/store.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/switch.cpython-314.pyc delete mode 100644 custom_components/album_slideshow/__pycache__/text.cpython-314.pyc diff --git a/custom_components/album_slideshow/__pycache__/__init__.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 2f08b75198dff10826fbbea73bd6f10f410c7bb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19438 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 AU;qFB diff --git a/custom_components/album_slideshow/__pycache__/button.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/button.cpython-314.pyc deleted file mode 100644 index c96fa01661dcdb04db0cde8fee167faf8b36fd0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5073 zcmds5U2GKB6~41Ov;RM4v9V)su`ytK19%BP#UUxpVoY{}*^sfj6l9o=cW1+d^{lyb zH;w&dB~?Tu)b`PpDixlmfXXLcP6t2q|Puk18Tvsx_w4eDwmsC$CkPfn7I>bWhFbk(6EW-P}nP|F=we=F! z*GW`=Cn>eDZ5?D6Q3KEpUUDCCi+!*d?+rn3_)@IVyPfw&pf}po+pf0lCW&p2acM$w zg1Tjuna<3zWkWM`vcg!|8nc+m=5TnlY!%J-p-r(FPM2qllx>?f(=7%a+f#)CpKKcT zs9q{f>-nn*+3}ou_iQSYbz&1^YA!Q6re()|I;l;JXHJbd9qWDS*lBfaBCVyyPyXbr z6HZM|s@hm~QoW#MPQjzwQl;t2%xJkx3#O&BGR;e{W&C0B!4Hlv)rLS>AUSf`HS!BW z3eXoP?@3oARZ?B5Ta{Hs^{8Iemz3ey{>Ltu?#Nek+om2=6$6h(@kX7PZqHeHO)nHQ z=rtLfiF$FsLk(7;mLq4&ma&9p^AyW;MrSr|r{6i8%yTm(^A($wXS95IW)@~z%uecf z2+b~;1;f5l{&{km+jn?&&IxH+-S&p2(N1_!2!8gfAQs3|;*~s4P~1GQiq+>qUR5uY zM18HoM{6*fss>+l{B|z z-8t7TGKFU1<-C+@;BpH5=Goq1*OB@q071R7J|)QPD^k%DD8RzzPyrcqc$+fZynfycA)R09J8eRKyX&Ngw?Hr(qh zUN%6W7y9`PoCM`Dm~Cax(Cy5v%&hjMDtD42^`*?KD$J*P0LES?sGZd9x)CN{-Z7T^Gy;Dk42K&R z)RHjOP8azo?SeJ=WG|g$>d@#|=4~2#SytH@S@UL`q-w3Sq4E)vCB{tvK63ju% z^38w97pPgR$3)$J z!GiIjnK!_Q#j?}cY{I62ow#Te!~$9MkZ|{Jb2le%U%Ykk!QPSOy(885>(!p)E1_6* z$G}Z?d;Zq^gTZ6VgU6}^Bh~iTS3=#@_<>cg+!k0RvOmDh4wsErEH;RAkWKgB!Un<$WoY*aoaEAhUn{K{7{c*1Iccp^_+ ziV~=~L0dyjtj4LEs05&dMXDVKsW+3!Ad*QZM8(4p1%XU_kUGdnD@Z7yD?lZl9C<@x zUVwy${s12E@+?Ak!(+aa8{#eey@?+%k~jE4R$hHVKp;Orh5P^&=Lf8jAD|X4*31t` zSzs4`tpv)O(n6DqO~^U&GZ;S~+Xzv;BNUF9I5P3`cCRzn9N2(J1`bbpN~(ZExp_ycPwzOCVHf1HY@x}`6+NvSUR zi*HAx%;xGzCsBo6Es2v00gzJ#c8>Nz_LFgHUUVhHE1oDy9 zIJKb**qC8|^C@}i0^ZV>q}TkTENX%YAkX>sr+X_21BEn~*;ob;OJVC)1=Lcjb!ICXYickD0s?=l=}jLqsS4?&MmIDiQCYI_KUb}*Z1c(`Yvt^U$p6aC?@dv7#8bf zgcLwgAa^yB(GZf+P%9bX%xt0qGu2`RlW<5Rk&m3-EAR;h0=@VIGraMsVOZoH6eEih zZ=Ho-arlcae$+s3aAK`fAkoJd=j`1)aK$J=reY7{I|G~3W=lH5k00X8hyodj&6+~b z#`W<0o4+^Wn>EjPIT>>!v?Rrj?@f`9pP+e|jApaZ6IR`lBt0Vi|0E-i$dN~6|06Q? tFEVkDOgtiquRQQwWUW0S1%ADMjX<^59|ryE8i8u955!{6*Qj$Z`8U}W2P^;p diff --git a/custom_components/album_slideshow/__pycache__/camera.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/camera.cpython-314.pyc deleted file mode 100644 index 570a7e1a2d29a0070e6c31cc24282bd94c3a46d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50461 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 a3VpYZ(rUWz;^}z4J(6$#pA>k=$oRjKH9Nil diff --git a/custom_components/album_slideshow/__pycache__/const.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/const.cpython-314.pyc deleted file mode 100644 index 9c07ada2318f7d5093b5c5f70e70a70ff82da82d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5958 zcmZ`+Npl;=6&{cfNKsrw5+F7%q9n_rC2lrM=#VD$$`+NKA*RNZ*hKB|O z`u%xx`-j?$ApDar+P}fp9+xA&4Oe;RH-z7{VBV z2u2}_F(slzmDqeQ#+3+8LJX%Mj?*xSGcbi$U>av(2Cu>uyauy)9j@YMa1C$3b^IJY z!<%pe=iqbv0&e0hn8Vxf1>S*MI1jgR0q)=;%;S=>pe!m&WMNrZ!n?45E3k<7U|@CZeCjL+apOu!RN!dEE4 zQ(T3waSgt~b!AmqQ`X5!N?FBkDaz*{Vj7-d1`?QsB+4LR4p#9y$^^g~=3yNRkisH- ziyQD9UnrpDm4Z@KHk23h0elH*+=L9if-JrU8Q(w-75EOf08mvnl~>AZ<&B~!TZ%g0 zhuDC0ip_zrgP zJ#1eS<_tEXRkru5YNKM5^~Rq0p7qx)^BpZLcF`s6TNO6oPHS3KX9E?pq*c_qhI{Oy zrI+@thVf2k!$wux)zy+#+SAqQHoIKa%Z8?w%o^%7RE;tlFsfCfw5M`~#O=(rx}m<+ z4_K(RZs--ggoasD@u04=@iQ>#8Et{C!G`fsXG1M$$EYBAe!+2<1)NWV1?sxhFl+p4 z)+;)@xTC!@Ed%ui>(we17O=Eh*{rfaL)WYlMO8AZb+bVXtgmkA?+l&Nva?^QIJWvp zbl0e96*fXN(yXhyx>+*IdVvk?n&xgrR~vhprI*>Do2nhN!t*)kW~5>6TP2+ZT$kBE zea}R*ZW#@7z$jJB{W9xs9MsH;xqH9{+cPI?quo_Qx18PRLUpatcyC%|HrC-PlkX*+ z=YF_@HR>8!?~1uQODy9Z7EfaP-PBRAfmTdC{~;I7JX53=8m-V1i(k9s&y)l>Lf5(OXf5Ijzg zdf--V4Qll|6@M$&L)?|_vVX@wb!qwGBGn6@Jz8$fmhatPWEV;%)eGz6XF@Y-%)edP zx0t`K85X72s8Qum>Ao|`pG)qx(01~|sp;?a2CAs>+z#?;>aJQetz6~>*P^+rUUO%M zU8-or{6H;h2MsDnzPP*Su7=#z{d?|m0QZcBS|#V`2srZtIuGm;Ka8A08rTTMtM1n; zrbgye4OyR2D^>Q(bW-#W&g7$i>HgzKAN#JYIzQ%LdAL|-{s+r6URYY>iwjFDD~t92 z4XixBa&vj<{$hR3IPwXC(WI-*c<86$rwcUH`voEU@#!97z43Tq;r;vf^Y#7hib36n zTGqU2?JiLJSa`R*@Lu1hU0Kkp+j{v(% z!1}2%SqF4HN@QK&|M-tg3%&I3Q=NuGVMEyJdH6d)C=)g*eCONqEl@{&Lp1mIh(6`3 zoc4y?cYkjvhSn_5o`LUnUwbYHcYAKfx~nhg?!G}w>d3wS;##sSnh{^BAm!zBv5=Cp zYJOdYf+{7~q&Yu}tGut44c;iJ6>GJci5g!tRF(P5MhRJO11;8T)Nszn0z0Nv)v&=M zyvmjrNOs5AT`280(5#XsYIZeBMPtD&y3Sp|85=lXr#7Uvjp(Xs?U5d?t8s^hL*Z{i z>@Py>KRbawU(YWB4S!hqx%cP%pH}|(>Zg&v#uxw1v0rurf-nAw1cbSO^@2XuOFnG! z;T0cV^C3>d+<~=C9~MZ;88MY*LkT&%s^&o6NF^m;!_HDnKPzU`BBa@fXPp%@QVU4S z2{Ena#KL+@0;EI{@~I7pjXM&ljJPJL2{ExQshMXirOuPN@8PPd^MFWP}CDG z7rM%w46yL|c>WwhPJ+CgJ(nQo(h`fEQMD>=$dD>Xc@{iF6VvH)RDqbKT|0-6mqbXc zpJO8-XL53$jC5?cnaF{(k&<4pP|LE5t`^g2Cr2k?@^USe71Jzwf}3-ya*ppso<)<= zs#r|ZuDYA)NqEpnI2oi2N$Q#;C*-8W!fk=mgo$=7s%x^mmX_#niXbIfsDr!3w<@Q3 z!FFM{y7F=n5>%!gZr33;-T`v!azV~PDxdLkCDL*+$woU|`OU1Hme)4fsN;Tn=~Ss( zT3skFfpa!a;W;s%e<4GXg-@zTQtlHHuYb4PJBso-kz7Be#XXC}?3Q|m*^}XL2Xr*3 zZY|P=g-#J#sSI}DmXZvww=T1b@_ZIE>@;&)gf)r9Ppj}2L%hAW=PMu4OWF3>bsKdW+Xw*Y{3;nQE8R84b?PO?%*O-n!jy zfI?2w%|kti4Yual0>A$*I;&^tBCseqLl=|i+%6gF4`P9?rJR@o7C)DiPi3h=vDmpp z>h82JgLM8WZLQ9I$z8dZ%b2^!N-v~*K`n^9@LL#fZKtsopO{QiGd#)XVneRJc1Ipb ziUmn^w_24~SJ{{sNQC;kLl^lv3!T9?={BPN?lqrP z9f_p4dD3Lt+41)FdZg%;mdf)Ks}4xzrxtT*S>*8tPmogCM7l_K%1Kmuc^=xf*txJy zXv*-bX`<~+K22{THAT-A*x=o?jf1pGkH>X&o$i`E8*Rf*B{>>q+t9g{;joN#S$Kgs zSg?(W`5ZlRsA{MbyEi4TL^YXm9%^dtC5yG$PY@DxZ9&(m?!&ATmV3N9Z?lP0kKuNP zI{YW;md2lt!CdiKIz^8_{!o;%$(&4SVq?@IoVzV6y)39s`si9|VePEB)juQ_3P6$t z*rc1#dG*fuoTsVtlK#fpv*U|RA!0{wH-!m1biFBz z9YwG3dF*Oa2%i|IZgT$cWK#%w@#8^ZdX6YZVUi2l5t0kJmN`=j)C+~=R_X$n#D zVrq$h2ZacUkeX>Saug-S6Ly6BBtd5!i%|#@q-orCra|HxC8^Lg`nH^PU5?FjH{F0- z#0ls)AjCi8ln6N$qE*CkI}+zM!ZZfk&PB+%8FDU8&duEA-+p0yhRg@ae9IO=A(9!j z-4Lhdn~TZjq(qU}lXrML{X+P2?(Fn^Qea2E;8xtky5sDVrZ8>KJZuV+t+YG79?g*S zqOo+jzIJw@9YI~oZSZ8&~-$>!M`J3Eq+3!}Dw#P&~g{*H(GMtcs2j>m-Y z2<0L~TF2}O(l>60Nq5kWQg&(2NMp#3QS4!^CFsb9ZNBd@XY1p~BmH6We4I3K!Ey4O z(nl`3sdPpzI_(JY&4>_tkXH)jGI|uHypx?6w-cP=dvN`$rZ8)VX1PaULa2&}Cu~oV z`j{Pz5DyuN(SA?-BUtNJ?BJQP#eg;s#3G3{sRANgzakBy5s^4-PGhaS}Gk zeWh&oiHehmZq%Ae(uzvlYo$#arMLIR@|yPMy+;ys03+(PyL>k{{l31Biu67?cHh14 z_n)%>Btc3}?~yo|GiPSb%$%A3Hvj*BwxwE49Ip4CIQ#Tp?^SXCL^txK6CNd5Y#;>MLA7CevwQ}@fL~pk(PJTfVbEC~$5PHaQE0Lg!{-v$IGjaykTuvsfs0mIx)zQlZpYCX_kLg>vT> zVT-dusBl&amCh=m%DGk8>f9!5b5;x0&KjY{Su4~!>x4Szc4517hp@xBQ`qUO7wVn6 zgk8=@gh!mah273Q!XD>dVXt$au+O<)*zY_b9B>{K4muABhn$Cn!_FhZ5$B`Aqs|7Q z!Fg0T>TDDmolQcMvsq|%wg@fGR-u)(-P-1Kwh3)T+-XDiH{{O{|1IWDAK#@C?}hf$ z+AOY^bEW2RF551(n7T{KEOcz=T&<%jp%cGO{C2tQLiZVi@EGpfaPPwJG5q$p(ojA< zha0gAy{-(zGg#7VmQ%7f!lzaG%TWA3tsG{_}XNx8FV z`KYCU)pE+U3GqS}KkeF#coB=AaXAn#W^uQx1o2W9?{}3UUe4kk*B0Tdt3nuXRSI5L zmEd!26$a0!grPHPVfc(j@So8NPrJ4W&$z0Eku#aX=$SlW?2JhWobd`lR}Jc^W%WGk zszZD`i}S7>i0@?akgFc?T`VrR9zlFJi;uhZAikHypL6X)d_Rkya~(kZAd5fmI)wOP z7C-Mgg7~8>KH+LW{3wfm!PSU(6N|sVzG5@(TTbWh!b+n{_@YZ)%2l=hnr^C8F|%iE zEFgFUe_$-siF>2hBX|ZzJfRTX2>wx@*Eb?~aHH+^2*dc*G>o0ckGU~0Hs~K}9us)F zwK@Z%zJ^f99}+xc0upjOq4Q${?jfJx9vJca#)Oa$PXl9kr0Wp^yst_f%WA^QP6Y8v zUt=J^d;Mb`Jc(Jvq!y2V#OJN5iDh=5INILU*wB9TM2EY%v#GnQt+O{~ZS3l7ad*4A z`r4YBU9nVlE9N=jYLD62eR~&*xw{*Bov|#I(9zt~*5F38x4E;|-PR-e2Nn1xlYqPtj?aAg?dSZG@TYE2hnb*|Z(r}_3Ef<@TkX>w1 zx3jCatJ~Gq)4`fq!0JdyY&g;DbayoOI=h;r5lML1+jYFTQ)+2K@`;{iS7$?q+=qlG z-3>iGC%artvCXUBv#R%-)_;x>5XUW{-h^(6qm%GV8i_)ABoZDa6iX8aud%)BM3Yl185#aiM?^4P3OS4Jj>HAKnZ)ZEwB*h~{o{9ej~ zp6-UmW@#=ciQ;rdW^wi?=?SebkjEj}!yF}%RqyV`r0(i$XpiN{rB+Y$l%}qZhPKXF zw!E<1tzBKM?U<6z23IrYjFK)ct(LBKT7B_kae?)8op7N$HYK+>Ut<|cLj1EO=3-wU zmZs#A+YhLb+B4$!`a;8ja}0b85CD@e3Am~5{tPLT+-Xiwxi}CJt%9Zu#Kmc?OT7h8 zTpGl*N=(OMDM-^RF$0Sk1s%(4lI~L2omslmvpb7)XK-0jL#lLVWOp{{&g7!n?b4l@ z-K9x)7Iv4;?lJ_cD-$`h*j=`e>M{aI=fsR1KCj=?Ciq5UnvlT9G~@iprzZcH;CR8& z85r}$bm#nDVK|mD?DG!|3!j>hNq%G;1@LwY=YzhOVGxjX%rolijF|^KL1CQtdEFi% zW*!fY1Uz2x#(4I;;B$xk6TVL^gGg51VHg2=5XQZ}m~kX9HY7$3BY^>iHDYFblmXr! zBoq=eh63aKfY0ssw#2k&1A&p4#rM2_&>b28p7D9Fb5*vO!R@BDxZN?6+dUfaj*n2> z;&wkf?irDu*xc?xKOYiC{A0ec03IVD0aLl%d<(uO-%5d#f@2gMr=XpJ4hlLc=tdCB za=U?-1pfefE%5%cHM&^B)YjoihaDvG1)dX5!O1EEgVvvQzKz*F+FdadVz8l6d~>Du(oK~5HF9q z3XA1(CN95dvVFQNqTS3tF1N}*mBDcXMA}K@9j8Sc>qT|ffygA+$DL8{{Tj!4k+z9@ zOf{@>sa$H8#-*(zLP7UyA^>AL%zzOeucvql@52ll1R&M?pVWFbCHc*Qgd~8J5l!}cS`OC z8h%QmO*u}c11qFLL##6{RVi|HOWcO^i2caaY&Ugi)g?a*s`^w(>QkLE$OZNxA`TnT zRDG&$v-~9f>(a<{wNh6u4fHy>wX1WlsNWi|SB|-q55Zu#jj3uofBipkRT*L?f5<;ZB;bILKa4lWbbv_yU`#g>IOpSI>F%E6 zZQYuqoDi2-JLcp-Go4>+dd2mNE-Scbb3WYfv!qabsL9BO{Nx2H27J8yLELk=$={J99%Rv11U--QjrW+&r+(muv zOv7vKueQ%+L^jndZmOZ@CiBIL3l-C)FI7)AM)jttvtK(u{n)fHeR9cBzm!#vEFYTf z@p&!J-xRcoEyFU_xOJDI+NE{r>NNOTDg0B&8Z(PsWHh?CAk(Fkz?cAt#XvrP8d(!p zD1QbC3Coi$NETeA2gL|htjYnlDsP6^&znNR;#3wLifHkNPU!BYuUeeF+<{e#rwDjh zp4{E^@m5{RvQx*Gc(3Yru0?~zpxz5GI3zANO)@aBc5zC}O^fBMcIh5iK1J@4ytt+E z`clq?I;sqwLl~IEHRd05cpPW>z`2mm>tNpvE6nS2j06JDgd8LOXMA`b68s}04*#fU z$X9DJ_4g-y@&0~?Kja{0%b{>v6(Pq!z{7`p4xnV;P=G)0sPxqi)jIn7SsnfT)u`k- zP)`9wk?QsHz5#Gr=llDs4&Z+6{1IOx4Er1%Z5_=Hq9PpUhJ9nEXT}2O#%diW(aeDX zUr<16S^Mds!-qBO0}SyGjd?~w4$qj_hib&U4&U>^5&rUm+-)bN883?r!0@&Sqm z5yaG^12HX;LLr*0TM|Hq*lTJ%K@V0L|2*=r1u=%mBzMownXFS!MXa0Wt(#|#hK)rL zWBG!yJZ!9(JbK%lI&UkE*ed326|=6exiVs|T`<>%&D$rNqI%=h$*+BJM*EuWRok2) zlDlIucL#$kNz?!RAVSSRgpPp-{s%)RsJWqfHHd^+! z*$e^#wi^sl+iq%Dq6pBE!h+OQuwWxZ?KLuF$jR$RMBIJcdWbs?LzEKY`c8`GUFziP zbA$TVplk1D5napg^JYuvTaU3?b;aR{1%!u%+eu^r`ckF!5K!yzu+@z@9vFE}S^khF zI6P;O1ftXt3Scn+%ld?YVaJH)JXRAZXFvy!*Wni!Fc9S`QY9k3!)g_2vXHz4QAVk= zzkgT|f|a2vWTTe>%X>^Azu*I2C(`~opCbs-jw3XVcX~pA2X$;|)Dek4Qde7Bi-MJb zK_C~0XN328yyw{xM-QI!drWv$f4^s381C^8A>*^-KER)#hxd$D)jC{>Z@|*#@h;vs z=zkuC3FA|5!Ow^k1K=1~8y^qg;h=d-tvwL$2kt_sI0W)zm=BB(4Lioi28KOjLq2b< z=~F#*a(`Xjrv{3I5R0km013SKMk0J5&;a6<5hPV=agj_QEkv_;%NCK6$Z?MmB|8E( z$Sr>;wtgm&X%>GWKLQ3BEZNfoSL!3?qD6DjwNTWSK4k*_|NSrh-j}BR5xrwk@0e}8 z)_Jw_#{Njz;l;AUD{>4(^!bbW{MVk3rsrR-yHqz@8cyFbpH?xanH`_E)yx}eqUO}e zV{7RKf?}G7C*MVyL5r_F`e~}yKOj7X{#QF7!#gd4ghtdSQSs};g}PV4`V{gYs6K^q;?<`&m&ulA5%TIl2$dkgZ_bM*qnM9nb^=0 z{+i@|X*)cniCXLzofn+bJuh`kscwU0^<2q_m+^dq^%<2sd(LpxG?S$(s5(WI zIJ>%mj8dl8p00~kz)SAK_8et0dRhTC^Ebz1d@}s141I)P5|?vT1OKf2nIz;&L?63l0;Dg z6=knMMZhqF0R4^;pXWK$!;*)A61+el)JSwQ7~n~cR1IR&PZ-cAgiPZh2Sf{mn-WI= z=n#A=hSo2H9JGT#BM~1mhj7k6R%_~vsgCeGK5CUNzDB{6f)o`X6=Fh2CT(~3oM>rj zd))16hNNx$tb`vR!vahY5ro8Q+T3ig+>EfoXXU?v+TX-qs0{%SLprw-F%XYvPQAGA z!oKOAnc}c{)68QZn2RQxZkrtubJ>EqY}PfG88+8~Xf)d{9-KNjy=z{Z6JIFu3jUoL zMx!U_chhKvCelW*Kl4;Yhb&dBI32S>c;^X11juqQoDkCh4L=Yk%u}DsSfB56Wva|O z6hv)~WexrAILdxM)*0^Ta4&WzkzACzqr2-xDv6;K0!t(bSFw+00ITab$T(EMB16C; zngmFp=}xQ}O`=OC{es#)tz5TAULG$2{MU37eVt#DdLX%IfjM$)GJ#_JwC$)b$uLqe1&?Pyv@E1+UyhCT+G^>L^}1b zW2lT}N?*##Rvw2GurK8t#-D%Qr3xC8w{Z}XE6BX`KK5--hQ^fdI4~CPF)>7?qIM6^eBOMK*#|;zz9X4EkvquWC&L)DS3CKYNjSv$^U95F0h=ptSc9Ptc*mB)J}2Ev5>6@Xj3o52GIq;}|10IUJ1Y)6mJ2k}D)vcl3GduvTC zy%0(f{1b`k7K*hZfK1OPczuJO@ezT)fP^X&|1ElWnI2l$h`Z08cN2M}_6z)rlx*;g z@qqb0Z%j+DJElR`W9so>OfS7YL<%Luz>AkiuV=Z%Vs5FgZdN(}9b{zy>JvmD*vjNA zS+8|`XtPHT9hq$ZWop5c`gUsJ%-PolUmXmmmQQIuGN)dspFVoI^HS$xX6=oVAJ_h{ zc4=pO)RG&qlrC6GXB*$ByK+lpm&q9ecvIy;ItcARffy^y~|I z(0=JTQ+lO>;@N@k1fqqGmph}``BSY4>1V$)7A-82(myh%U)U41WX`DmpaT?P5ogO? zc5s%=$*$iWhd|`$N+F5Db-lB$0X3^ofO>@my>T z@H40^W^8_*#Ac)jq2>PsDg2*OvSwhMkEw#9jB2Ya)|OZW!p47wTx=X^Q)`l=`PTSL zYZ_PtTi#6R6(M3PTeOwMGIFA+X%~ks3{87pdiq5T=4fjAvO%NEylqRLu7BzIDcdKv z&6)Q&l`b=8%UIUn_tOv%2=`-CgPOagHaDbcZrN1`2Z-*VKe`A!1N^G)mLW0;WWC;= zkvwns?;+y@Cx!;~D-)xD*h&O5wafH#15zja+{|Z*>rM>aT?255qCg zeMBqJMA4m6K6;-vPVdV&SivM(X`-kxQMAdpfj*sr3AOS>>C=jo0dbKl-oy0YFbr#U zr@Yvb12)9A^WJpq)*v6d(^M<1ihKFXw~X#u#mMC8iTRkE%1hJ(g!N-=YrKk~)3%h_+4VnhRfT;fkYGpLYe;V6blM z)22(m5sK&w7WD-m7dmElUpX}un(qCBiFw1$xe$UCIhNIWV{SA(>+-fs+hzu4_lDD} zry8Sad6Be=1rou9)2gSAMr~;q&tEvdXmdofaxOo4>B-sWuf1^fh3O~5S-Ymyw==S* z`O7a{df}BX&1pZ#sSan<%v);iq(Bt&$vqus&jXA#<^moA#u{@!4PnW1Z)qB;8+USV z?=&~5H1BXKgafohq-DYZ{Cs}Vo+fbiX|@f>ShtBx#^J3L^qqbnATCA|LIvVi;I^nX z-JyWw{`i@%I>c_*f?x?N=u?a_F&9^E8Y%LTOf(?JzfH>wJ#Tq-Cbs!1s^ z0L_9NY<~Bt`!s#pK3%=erA4{{Pc%K6Vqg||(HC(&+O@lXJw(W-O#Dp?wu#y5!IsgG zFC<{+3_)NJqKg5@1K5UA!j>A7XNwdqR(1<%mM}^3ezxz%HomEX5G2aj=mya{GE&2K zx)@nbIw^n%Jq$ERQ5!>)%Q3p)t0*9faKVCYh-+gtP^KC4x}~vVoW7gdTYHd+_(O+l6LmGJX)?&d z-V|Nd9S#8rd+{@ionZW|oePAzSU=vA`jJe;D}#BU<#wQDRf3pT^*}K%rYDpp6q*dL zkuH}qXB0$^)-x50WVsbFvyvNt6GGwR9MOnCCQKwu8}3^E1-yd)B7)u)NP1JmFBlgj ziMx$z5h}ifY%3&12KTThL~`C()q}@h%F4#R2{nPlOH03;dnxx@`BM!<;}t`5!B)Iv zD~{UJE?X{HW}b-HD;Dh)b35KRc>Ul_Q)FA?(zeEb%qh5I&^UuapR3KJRRc zX=#fz4q{?j!e1MLl(*0UTF1&Pvg-=&a0sRwm+5DwiQTQ8MT9q4`(6f7cMy;yK|ryA zx{vpNvhU3$qwN#s+<)gdCa6eHs8zi_Iq1by#w=3Xxf7akbuBlpZUGUrQt-3^*CEa- znzfov_3Qj_=&ZzeO4M@R15a(@=jw$Ofk}Fy6cv_D^hL?N4f%5E~ zecFJb!+%O4KM$xL(;nlzTd3|L2y5yYJRUKC097Z7!tIF-D3@B%Cu5Hu?^HN32+saR98(u?Lc$m2Hu$2M zY}8i zYBW{>HP3UCIy4y6IU7Du_m61&DxP>+aUp49>e}r*q`KwYv>u7{{^Zwa=DefnC*~U^S99k|u5Fs#^qUjeT|>tuQyJ&&Tn?!&dHzI^U4de&o1RuMhiC2Zd)p-xm{E;yXV_4ygqQF><9j( zqCL@y+F9)<9~C;j-LvFqx~YFt{ePIlj;7aoUhDb86)BOzrp3ahc=5^~j4l-&h;G|C zYxvNfH9hbx)7>ntuu z03V@%<{uk*{zHnz5QM1aEsxSA&d8i~z6t*i$jFGv{fPYABnM^Ik}~XUlvhkT|1rIL z^7#L&1^IB_4kU1oFu|QNPG_1b{n|0?eMHO!^X7u-z4O|FIA)+#M;9$NK)26DC&e_p z=Nyw;Jsh3H)ZvcNI+3ZLL!_jNVCL{L{mgmUUA%RyX$WY+u-5%G+UYTPj3gsTMghfX zm4~H%yq}Af5itN!G3&fPLDH2#eW~LzEm>5cKD38mSZ-ag-tx4c1Am(5gG5MELFe@-NzO8umX*TE$#1o(w zH2(`)t6;!~ZmmhinL%wrYqcA+b_ccg6p)qFGldrER#b-NXB^j^L}(;`0c=s}PAMXz zRkI{Zt|QLb1yd4Qn6g0&fetaADaiyzF7vt@`uc=E>E%)RU)d*!ur?TtfH5wnk|;0G zer%o@L3vq-+jZF=g4R-ZWHMsG+F=JNk+iJbJ7uk6o?@+H^^@^tp8@Z)N!+!e$ADVa za7uA_;j)6`NsAdsRk9cAp1goB2P->hY!s{5p#6#b`ZI|bD~I@X1SGt{&m1;e@RJ;4F-|Lr(g#%!&1rp?t;q** zS}!Y{7WJ21abh@`U|oSF*yMJJ+z&7*tr}A5NucgG;@6->Mf}=E^-DV8j2GHCXEG=r z*Y8wTTtX{*bcy_loI_GMfOJ6Yi^xB%=o5E}NI7=N7*8(k1^8z>5na!w9~5F*$}wAGI)99(#R`g)0${THg+is((iG`%=F)6a+@5)M|i%M5y>cD z%qWKlX3zCKKiGFuzqIYhn>*g$b8FAr`y!1e7aC8_Ki(f{^ei@dVg*GXW|Yr*zt-ps^?$Fk-4X$qDQfVKN4 zil0LO6?%MTiE$7uObF2P|AT@f6iiS+AB6u`1aWo_pG)Q%($;(_UJdBODDFQ;RSxXx?$d`{Uss4&VI3&yI$7cSUxe zSlE3cy!+(*Q)icUpGBdM(=spTT*{d#2&a`t()P}$?VZbbBj%oSU8q+e#v~N9Jvh%nn=|x;ivB_}##y zlS)@!fi)2+2;WZ2njU^-b0lrcLfV!{TJ=I&bvUhdDkW-8`^HmCW(V5x&!&v&#+Pa* z8&~XLU`AfvcWK{D&unoxqipuE4>BsI3=dJ;$Iv#ey;K`cFPYLq<>96JC5G|BsqGi* zFV?>pzm1sN7tQT>|9vg+2g4{r=I~K+p$mZ{FtGosS{d$OPig(0XN0I*<521T(;<_?fL|KXV6JqKH%> zs8lYA4b3g`mmgi|9A`>eXV`cAaO~dB<>ySFh@n zU5>9ob6^u_2fJ6*4J5Yuq8)7PCQBm?#yPQ0qTSfFrcMm%CaN({&Jq8W$&GHH*VUK& z%w=S(3`6j}x+-W9xJB2;N$j~ z^_msC*eEOSVxuIGy?7xH)&L`f^g&f2*ZyTWSC!dg#6UC##;|_Kn99-ocw393wYvvf z%#~i>P&Ia{p-GFKYqIicz9IBNvBLJDXD|VQs_&3EVx5nQ5y$I~ z#&n+0fZq>6*bw&neY95&BpBdBOeI*%HaYm{VVYSnRZmQHQciobDwR<$Fnp%rVzde$ zrffQ98y_R9UoNJ{mm?c>=py?DVE;xh5UC%g{w50HZ0@08$Snli()Z(YQJ#S> z4Ibf=1|03wP>z-z2`>84<5gn_WEhysb#eedO@i`FEgrdw!AbtrMaD+ zGozVl_@k6pYNqrnp1{!DYM*KPX6*+S$87yv_MaU5z*0BaEaIeb5$P_gHO4I3Ro{AP z>&(fy$EUZ3(|1ob-b>*O>G9q9SH?Tqam7>J)JV8i(1mB zeZR1j&5VCwDWfK8IBPk6qh|YL`zQDEIBWJdCYDNf&6^)tp~LjqJ&UyEjs|4D*hri1 zS)YbVF}OE&H(}>JKclITdxy(z%GbP;r=qaX+Ek-?r&@(@0K=th!vF3V2ewo^;XVk* z17jk_VN}?IFwWnijDm6Osx8hrqn4t=vwwdFuHRUa{IDV!#6p zPqwK}_6MHCWgPVQd4M3r5fIc>0R|kf^unGzHv7pk4s-+Ai4wFDRac>z^Q9d-b~r}K ztZaa-x~giEXgoj)uvqn^3;@&AF=jEwOuEKN{f+@GG{7&MuNx!p8E0X8F&qeZVRm=c zH$rs)Op2DcWDQSXa2QR2xdOC>2OVQR4-buAN&gordQbxZKplf^hpAI0cc9iYGD3E^ zHQQ^qi*Qv@JYrqy?-w6Pc7Mw7z>eMld)+a(tUyhofvzi%f0hG^c%i{tIyoO!B>$((q7(MEUnF51@P)v5yuE1#|@Q+gPpyhcR zEl`$cEorYJm>F26pSckGxd9V6+-8CRl=y z6I4iPUQrg!C6!_*vSKASS|`d4p+u>DSlRBlMvm5HR$!w_~G z$!&tVjxF)g!W(HiuuE>0(icWT<9!>Al&)8&%3Ldy?J$gfVl~81)EST=Rh5$ayrPLZtLcvO;&HN4X(J=}Oi z@%A4iyd85d%y1EEb+RX6jnAd}hs{Y_BL?1iwri}w$KxgBBju$%~3 zFB!na)k{gKlmTgY7y=$*h9^uPQZ%|Qc>>q|j*k6JO%A7%B0Y?_CNpUwe?^s6QUa|t zwTY^u<&6r}YY~SY(G24}akpQghs32ikj9AGz@(4ji$(WH`N5Ef1sD$Bcxm}{u zhyc`P;ZI@!KADIG5+Qu8iNAp_+&Pg}{>1+B(f#FKN4axt#rDIob{%;W)aA!_yVKcUna3jUfNS-Zf}fzLQ6auE8i9u|Q_ z^`9tD8CCg1iv2kSe?vuN+=dp!j1r!MBCa0KB$FADX~=Xl1!Fh!%7rZ*reDn0phkx0 zE+fLsWpvf#E=RF%CmP&jy;yaj>ZNUyjgTBomy#T5zIaC@edl8OPR35a%phvbg0qYT zILiplm+c8#_fDzduL@qQB&nG%qAy$2ml3P5Iht1*$=kY+w{>pM&Asz^Tf=!xk-TFI zdB?(e?H7)LZ7^j{AN!srVk}!UmPNCQB3b1NS>@rZiYe2t^l8g#t+7!R&2>a_YZr2B z=XW%{`Q)4J^L3Afb6rzy(X@=q`Iqu%_eDzU7fS2lY%6Wog`>+RF4cbV^o7$exu?`0 z>y5vE{`bzqr%%*syZGdVCug32?S)rgn0hj7twyWWlF`J@%LgwVeC6sGwt(~uM2@>WKAa{+hjaB*`D_qJ_& z3&*{4ps3{t_ill?d8_8#Diwt_*5*T+cMqx%zQ^rrIjDK>fQrH+*{yk+_j6SUSFk)< zR16B>;dCpmOFl+&XcE%&5=p5>iam&wvVzf8bSq3`AdY}YDg4Yn&FNW)A#HvHuqj zTsZI&q!^9`vtu@UE$vyL0t$Wad+)PG89e1nF+;CWP>yQfJin&kY&E|d? zYu~#1*J}mL{?Q>(z^o-$`Z-oXKCOac_PYCADOo)sp#56>%t1jGKR1rC#0?mVcQCG# zBn8H=ctJTD9a(+FrOh%)C;yVaIE4sCN!mIkxpIN%k6Y{1ksTG(S(35?N-YW2)!uTd zC80&yWG#wsqAGQ)(~@2;nYEF;%)o-s=Ve!Kf&z-X%#^-CLX1`^iYUE`Do`+`SFvUm z6(M?46kDUjg{T!tRgjYxQq0u|$Ew ziWSW)k%>~PcC>Z(HdfPOe!Qa_-owO=SCEyxyx$k9CaYo4K|X%JC<^ZHZzC6CP4VSO z?!TxG0E0&O0R`n)({L6f176+VZ>p5&sGZeBn|i=1J9g~cUM0HkqBp{G*uegT)eTE% z#jJ^lHRiI@L>>cuqe0=k2#Z*I72xA&fZYV?WsCsAhXB=L zUp5(a*1tDYd~Dksv29(jZJnzR+v-@R+qBPoZTRZ&-1Ffrd&3#~rkY@p9EIMgnaoBr z$eQ%&3s1i^GNqB_xPrJHd)r<zX8!l!W@zU(`^(j88+@N#VXyakPeeee==7+%0n{h1>0oHtww&^U(vExAv;}yH2DO}5s*t${E*8V z{A@tc{2X8Np(z@$2c!v-jGmD}BD6$$6wMpdEna;aNJ}Km5y&NgK9WdG;x3e2+Qh9v zNsor1jpGM7F2TSvE-QgJSO&t^Mt^dI^F+`FeWPh#*9~Mtp}( zEJFnum>}Yr3Cgh@R(3_!RLS4`+SXUM{&Dq%V{EI;4h@Sdp@^+?$yPe2dBb?!c%$?O zsWc?zY3)ZDIhS_9E%euq-?4H970anycK+q3E!vGZ5o6J!vFMjsdFz+RzoU2; z``sZdUq>T^#lNBE#py&?T0yWB!A#FG{meee?$*LmiGJ7JeH^798kR!e4kV$%8OGHt zR>2H~2wX!{K0`IQ;L46E2|$ftWqljNQkn1tv6%v>Q%MFxC+$>|1Si2=#v$s-! zM6*_Y1a%MS zgs(q(hp2R)F6k&K+KrzO4+)U$ClhB$=FBq(jjA>KCMs0TfqA9QC{KN z0n$9u)9eG^O=D7}_wbm(%(DQLBSB`1Otq+#0I?$x$0JZf7zeps;~8N-ESY0X(AIDi z2!H#4=T1mcne%MG>d-KJZiO5oh=+WZ31veJDXfLG`%Ml<4Y3C7)u<9uQkK-;|Ai8O z?2@{Yqn;szJ-%m4_B*~%GUCS%e7$|~i-3{}kPL~{kg%8lya*y89rj3~YS8(3gwp6p zNwuSd`dC6WkWDqC;ytpJGiq{({z652>&j8s75Pc8L>`4um8qL)jffIpprRVdArLB* zGhtU2r^hhI!a$Uec>;bDoiE9&A;u-p8dHz>J7IT*W=$BCYN|9iiDm?rXy|%OgPzO! zrc7Evvmn9gfs<|*F_oce!Gsq^WmLph=%vPiA}8Z;ri>a_H+3-CDbH%5fI+HHQ60c} zCTTa6!$%`iJS1AYJZ!6+H&l{tMER9WQC;q1bK3NYmyZy+?Yri`>c5c^-m)j0v3K6G z_ihT8Uk8b$E%z6;Vg}wnsM;6K*}nq98<7XnON)K_k(UqM&M2EbF}Leao(gB|n76>K zF!O()ybHP-ApP}Gd*+nxmucCu`EYNUJz76c~k{2#T3r$T4ukqo#K7G zg~-Y-146V=clS$Z>m<38Q>_r7aLL3KGBw&w{abapRLS%K(Q!D(X^2u1r#ro z+tkU#8NG^n#sQUEHLf1lxD`9xpr%*Town+wxRu)!#Gau7;{eT|?xszqKo?ghlc8gR zT@e$Hz+q1El5zDq={pB?>*NIeEnf0FVCMt8C|*zW$rvQ4@74Q&c{g!SJS&dVDP>pm ziCs!AL$5*UnV9?cT(Qj&qs(T&{PWd^A1SCFfK6ZF=B4KrO5+yUshd zuFj{t!`y4~f!M&$Df4saI9XIo6SRH>=vO%0e>aQ7DnPLL|Ptx)5J_<6JZkAh`Q)@m$*!Er}sOl1-c+ ztFsV_oAi%bB>M1L_0AH?8MK_Ll}D<3ha8Llx{Oa);&Tu_!L&ik5ewBR#{4R|6;&qB zH7qC>q&%XmP7!yBdQsw5DIK-}!a?>NVp6k93z zGzvi-3iw`9&cp-S&P9T(jY*6iiR4%%gnI)_EC@tgBX8`+MR6b&$VIFJ>P-TePf~1J z`Bu-!IS(2`M%^LBI|{LH)Dzk&4#2f6RGOFq7VUl#>qongH6e2Rip}410Up9W&;p`@ z7G!2V@QHA;7=oj5c%dEeKyOsu5+?y3#s^#>l%Z&=BS0RMNaqSSqPtyaelZ`q5EPR7 zn8&*x{2*9O5MO~Y97RBf)WMrLw8Io?Y@2wdwPL*K%9unN6Y`b_X z<^q3^f)fCSbw&^aDmbiWR^`0z0JKio59`XYHMwyGriK|0!{g0Y;!!V ztEHujzQymM2d?pqBdqd%}p<4H-Dr!w$%`1E@_to5`l0)IV!}mFLe)=*6ZmR-*qJvAbq1fagduVuH$SIQx-E z_M!RgLpL+0HTPjllyN(|Jerwvx%E=(m5y-c*15(vTCcY*?P?BJx1eD;>C+_g&DrvR zJZO7n&exC3hQ9OVdnPWoV5a_+=V!~l@#WX+=R!Z&d&B$sk!VrL{VtW%Hkf)>Y@IP( z>WbpWypUOTzlRF1Dq=Xky~x&+hHzG7FK9_3`T@ zL$(%tdVdG%`UQqSs_wIUdP&m!sOq-CcF}slI-gyCqc>tWGH*Bn1ER@epDpWE_>!N7 z{tUC{Yc;J}?$1+Nv$>z-Iqlr7^j)n7b#Lh#HsR;(G8Kj8COmk1yPD$l1+7)ucWfF8 z)AdMsC;zAwKkrpgruTLlD8Ao-`1@LOYli0ibQOi!*48bW_sdljR#{v3>EGW?)xE!0 zMe%(aiXSvPt;V0}wJ7^DtA;|m*?Ca&vx02r9?j48=nx;kELPN(i8=-4^0}z*m_Fz^ zPfqOy2 zmrSF`w4W&1SY-p6R0i%+&r^HE-l%5+eX#Q`ID^PA2`3Df^ZEKi>49{KR2&s7Yp#(2k^Sj&HwiAfFM(=>g?C zXpD3To5*%Hx^c})@LRO0Y$5Lvq(uoyNo1~}#27p<4Chb0fag3r0~6I$LP9tOiWn-Hq_l%~GYJ%PBThP*9&D6=@SzOk8&n`IOgd|8EPz27 z2*BxRh|ZcYDJ^EZGU9%@(z?!=-s6SoLI~6s>nneLLS5~EEAE8G@SE`^_II+)(9SA- zOiP0aH6M&)OfL-~P8#)$;#7v1C24J8wN^7LvWmIE?YE{tiSFe_RDJsj5ElRrQg|uEol(#8;hoVzTvDdfVkz91k>|KeKaY zXyyqVGd6Fi_(*Sy+H$6BBwnn*@k30LYPR)S$JLH&UEk@7+OjWx@xm8h`trPC^T$#P zYs0BkldbC&DBDJ5ZT?GD?U7Rq+dJp0p%pH%#`>Kl2}X=Dcdtvn=7>E`V?vLbbA z{H6UfX)hmvb^$X$J~6xVkDdr;RLol{L_L6+v~TX6>Vkyux4$uSY0aXWs`t`!T90Vo ztIb3>fbmzz761v6YynKdS6Ns6iY!32t`wKvWpEkmU|k90bO^jKHKb$3MO8#*&O$a= zqD>2q4)O7BtbB->*%UmDB77qS4-zu|mPnlatWo#5ovOM!6hy7|Wet8mHl{A?@C(}4 zWLwr#%)q5(E*mLk;!^B)%!r93kQQ~w7xnh5L<0RWke}!;PExt3+I1!f zK0p-dm5P-}+%43}W|w^rO3>BHtt<_%(kb24w^f%bjoFQXD~1S35#S^fg1XXW6jGG1 zQJ>5YHTst+zeX;zx;-Rpb=l~jER1PYuWT`_bZtOZ$y=Sl&>x0sMXPh;VDJ%`MBZ+sT98S+dskIFY)(H{QYnEYrW0U0hz1VWgKHEzHkVNwIcA6*v zh1wa0DvI&P#1gjf6 zkd9Fj-HoxLus{hiT0=S38bYo!80%Qd(;*m??_{DCSPechC_+;0L?AF4Q}5UwOO;yT zgeoQ9H;(1qojLLGTRH6VA+}UGR@!&<0*FjQ+Q%R&qCJrm^ zfB+7HH6~o^I??Oy5mgN79Ao)Wb7xa`S6gRqEQM&K@euDtLu2}L9-h2JgFYikq(1-> zYQ*OY#_|%1q6#-kHoN*7+EKNS3TkOgML}U|BOSL)GEZhD!YCckRbNK7xP*+Kq}vxM zc!`y#IVRFxPvc&srnamw6-iq`Mv=xQ|0=RF%=k@2$dBl9K4*kK6C9;HT{=B5otF?v zoz_MTxih89=9JWw$(Ch1XUv*@I%=@br2^?T4Y&ct3~BJc7y>G7dxNx2nALl68Lb z{+mrdJ^qv9OLiw^FN|m3tg&v2I?Bj+{f01W3_A`+@()hy?-z?%?(O6(TV{uFj@P`l z_D($#@2JsE=-Z?nfoo6PI=F&EpWM#aL^7*QRC&2rauIDQLDHx9OT`4V4{4v>+X6ut zT$nDHi%A@oilJpkcc?L1vMB4|&4Hh3mJIEGZ_D^>S%)Y87NP@(zmnH#RbAQAV!(+W zs-^>mTN(|;Qwo}^b+?LDO@|D(N*eUId8+gp|*F9Q24HjQr^|DP^UuKclGAx3jMq3>}dwo^X_I4b`xfm90)`(HY;}scL>C_1$_C#Sg0yzQ@tq-_xp4>OCC|!Fz_yttQQT6)FlV zb6RyUib=uK_jNj?yl>JVzJfj_d;zTU`NV3r1OVm6S<49<+>8o+5baruH!9*avnurt zhhVO0nSO5cu)DS5HKN{7PIs4Fh?8Xi)O=Klep%=|dq`i`Trj89xU{k{Op;CLgrjwm zZGtB{J%c)sj{eDXR}uHPCK+7`V;HF=a961aD{wZ8LPg3nluS2>xSteqK-=j{IifP; zXKT0d+7Bu;g+h0(FsMNdGR=ZBLt2j}AWgPigRM0jiw)b#K|HA?SPNF2L=sS$kL$d{ z&k4CVfz2eg9|C;sgmp!cn2P6jpr1fFINS>M^8obV#HDIvi<`%hgZ~x+sJM_UmNt4s z{+Ugd@8cHQ65pl_O7$?}6NY2z!D0S3<$ethSLq(cvc+8wxj}CEt=~mnHo?Azk^taZ zgU%Ywt(dF(Aa~!CHEPX|Sc?{{MYEc@p?Paj#JYRQy8C9yZDRMnzHi1wI~(`mg?P`$ z>DiYnFICR$3#XUESqRwQA?PidOBc4E_#w&6=rE%b(C(`vKlIRu zezv3=1y^5v>a*(O&=_IzJq=8JpqbaKYS{@Hwic#5Xyxh)a_H4bxL1}Ze7g>!6lB)r zkSyww0RW$ao}(V^+KL86NpjM1ecJdjfT)>1=c-GoImt7n<|NNhv(i)4OnN`7E~Vxq z&y<>zJVVV&FHy4suHx0pK*?JsjmidjhUC#nk_T^6q|(CY(*}8zHzmn~Hz{L? zHz^kJYu_}4zHh>OI=&5Zay827@XHq zFBAya*|C8!6LCK0!CS$KC^TH{I1BAfg7w5V(T=xc8*T|o%2H=~Kwpn@<& z%Ae-~<9I7H4bH)Yn{j59#A%7ikoKO1xS5qC@sop#KEwhr%`gSW}wOFBRRey zBteG+he)qpIVruf7xLL7_;4boX`$FL3P^i_Z>OMx0s;$Am;4iaKK@TBxPpY3MTCzy zyb%YRh@LvN5Hmlw#yeFGj;2RH++u$5kj7CG#C;!iF>slJ2sRL59+dN;A@#!}$HG;| zfAWQ!>2GYgzU4;A_qYD!3zO~hRmX4J^QN@Q`H8dIYlf=^7-A#MYM$J)(D7== zRMX2H^i(>uu}nO)@zH2u@oQ&ZJ%gv6tn9@@Q-`1#P2LpRD$M=M$n#72|7AgsgHxFXPm1<=O_s&bfS_=hdaBVccmb%XK{nefVh#x zjV=@7W)?S*PimKy#m(e5n)!^j3l^6h@idlhb)_So!Q!c|OvJNT+~&$gJcq^Yu3W_P zSUk;@k9Yx#r@J;GUdZAZuFZ%Sv3RD-fp{^CXSqrcFJG}YZoL>?fSX7buP(57I!nN(VfMHk|Uu#M?x&Y$P0I@%t=y5tWa zF6z5Jh_;9ou5RdxlbQ%KhpJfn?sJE9H8`C>S2J@OXBFsbZX94gZ+?mW%s+XCJxnf> zMm5H}e~R#+il$kgs)rjxt>V?E#vLZ0bf>|cLb||Nkb*ko9MF@ej<34nlzm)IC)4<- zQ9&f1qei>@3Yl0zJ<6LuM?Gug$*4!!o;`(aQ1SX4H7b%+9QPoNRYDsT1p7H^+$^^u zp&q6GbsSm@qEEa|flw1K`i|XO2-^YLPM7>i{s73vXBCv~Q zfFfaC^|tNt8sykRzD>L0ZOW*#zQxCr-mNGVyL7lyB*pAbQ6ZA<C)~U0iXW1MGPL()@8!=T&qPWsBa&Js47HN@vood( z0`~Y~Cg$Kp2*4yRW)z*hz=coDD&>RQNP1$CZ()1qn=pg)@cKm~kLyr$gxeuL1pDcK z!MkF%@gON`x=FUd!^y-7U`dl4Rz|A3^flH4mUZA_u4_bFTecDRx5&H^XBDeU0{w8P zp5S)JJ^3ek!k;C0NE4Oxl2k^Q>JAHM%fw$1_~Ef~5rxg#?M39EDp4yb?iLY?Ncv<- zIYg2HKILEvHFJ@q*aC3K+*otC-8z^TK%K?8^Vt#emD`?)3vatD_q~bRQlLd z(=RLzHeyv3{C`9X_%|q^wu(ZB3)Frx&E;uVF=l`f6Zs_Pd4fwZqlYjoBN7QR#YFuC zCb!_bs1Sx7xuC)zn#%HEg}9iVO+dFSpZ9xXS@87;Dv}xgLtR$dBj5=>$1>b)z0Do& z#)d{`v%9a^)kBv1_4EYd79d?W%BPF0yyUo$4IIj`Wh14L&;_moRaDijNVqU_*wT=7=-Gco6Jw(id9w!ERATqZ|HNK$p1Pj94Ee zX}>tD_z&oL-ntWs(HSIU`Yk-PXXZ|6Up7Xw^6_Jmem*WNdF{zppCp3$^^T9)hDl=xq9YDo#FCEq@_l;Y`gaK)u*qGT^;+8JG`YCX|~%%rL)b~j$J)= zt@CQ;Oy#VA>~gGv@ew{2(dMk{taLa}0O|L@*UG3W(z*39W%oEtw= z9JS=kq(vy0;bk?oBO+Z)5xO-q}amyMheC(7!Kru(S`*OnPv!~2b?5ko%IlIIJy{lZW!0yump zMMg$|89?!K0_LyMG}}l@{X@LtPYBr3DkfmNR|mhC2JDPhqoaDA+NtI2 zVzq-5VYMp+qWpBD+B^QoI&~}3m=$#^{th0h?)i1;E@b<+o1b*(=t9p71}?o7Fg z6?H2_q5O2iF%j0OTd^ZsQMW?$JyhK->(s3fi!16@6eS+2?%cRXO|t(^HX{=~h7QxV zi$ape-w@X~UTb?+OB%;XYmU$LxIc?}@nklE&CIyX^9bF?Ew@mgV!IS?#VLhgml>4` z>7jUDX!25PTU^I&+(+mNlLR_$t%#YA))BI1*Ga|lkEtR-mbm(gpC3d{sgzdYi?a2& zXLWue=Vf|qEiCB+Er!08t}V6hYDloR_dd$rs@}NIN*k+KUlm5I8~3X(m-f^)ISoo+ zN$vCHFNFT9X+MCJd79Pj|2qo@Sh$CU7KEzye_-KXvG64p_Cv|XA-@INVLoaXY~CI` z4xnMeKI}-KkND~vkAx;Z61@)`iC*l8kPYClvYiJNBzU@L|Bxy1 z=sG8|PmF<-3PK%*6lf(Hb-#nT{g`-^kvtG5!%+yi&5~$vrZVI69)M9g{~U>d%e}BF zgXeOuB$Z~OWhkXSGjVL48hBalWV=}8A?m8y1Zs7F>2f3xi2U0?k~}R4kfIU~#&mt5 z>q@e4k^0V(9}NWp@IdZ{e*riu=Xb9tCKA^naCHMdo)%0jRW1fsfqvM*?WM$YIg$A` zhx|4)%EMeVF*h8xj(hq2lxUZ8Lw_YO5*RwqlS(-%Are?5vAxoE8siw4+KZMPX)!fm zai{Y(iiqmMBppUaLp0dq=tx{}j4DH(=Oh>8Qk7t%7`aR3p@hsnk?7EcB-X39MAO}( z3Xe33x|Zl&lZjC}gCM1t#*2ZoWkgcT7gEc^sTC0e>EteJ^Ni`x3?|LnCC4L6 zwnrd6juh@*DBK+`+#AW+`*m$JEsGq0E|u(EO54kx?pY|@6E55r$=P=){&de$+MZQq zm8ajUTT0u(9&TSK+#W968OhnHJX|VyWGPL2?XHEwUE#vrk(}Mi(+3o-UrMWI74KXq z+!-$170KCkN&C;)CG+J6pbpdVla8B5=1Y%hds+kR!XgWAo=iKJI8q*u+A zh12UImb!UM-Et<>c&5zDX*iTCQm}KOU}v~sS0roKRLe)UteGw3TW9uUB)?`czlL;# zaBzAwv;THx&P>Y8xj(XnGb``uG->Ie+H*HgTR*mCOqqYM5-ymh*Kjz^7-ha&5 z`HwlfS-9&Hn8ewk!ehYqE+|TT;rzz$>`VU6uUn&*wBSz+ z>R65TtrALqYYU~nRYB=*+f>K4Yu~n0`rDb5{&p5iKcqTVuYLP4rN5)5^mjCr4qcvO zyGfUa;@esJ4oZJFPjzgc_T79+hn^1dzgv_`@jW?+zh^6_@R6Me->)n{_%m%Og_YE` zpB+@S*J^)uC=c?_%%<>2CBmOq&{+MvN=@-?G@L(Yy;vi{0!Q;;PKo~;0>}^I zjy_7{jx4&WI~;;J2%_+_mOCOD5x67kUD91wxFaO4zDSA32Ra)xk1E-2zD&I+w1eNYmbZm>(!t-&{v{5V3S`d ztKc~Brrv}W%IOS;t0(q}3XV*j6+5;;+7YGg(!miN+aWMJ^C5Ps1^mhIKw~6Vf$Q_o zGqrSr`67lC?iWe=2@FA+lKd<51us+ZeKZ>~IYJ_^bU~3shqnA}N=||y)*xy2wZ~Bx zVF*Bxh^=hFRwivxN&xS`A0CL7Rnlf;@rRalXuw=FUogLH#VI_-s%U!l#bZ;)0EWt| zNbpt+OA%|%CxBQH`<4a!me-B*ww=%dG^TwBca9qTeg<$zc|Wie|68aXZ~n@bqdL{~ zJOsBix`tftmO<5!t-WQ`Qrv2!^lTNya|;@FYi`x68}@Ox9#K(zw}#^Tj7Qa)x6~Ts zT(K}{ZnA~pKx94HE~${!fhxT&`BG#E9W(kN_qliyDEuoe84~rAKp|g8-V=4Exy|sU zs59McVt3-oA)$tRVSZEVtatw_YC!9ftGFqgz*^1)ycQ^X=+?kQqLy@Pz<)klpNy+h z))d|ceHh4@`!ukoLyMWVjTStc!ru&m#-$TAXj~Hfj43v%E?v^p6l_#r)1pp?^@_KM zm&*VZb+ya*d1^|iUGb+?O=BfpuB2}QN2EwQSfo1o@Sy6-mRtz)a!)m4Y8p@Rt<)6$L*=&?&lW&G9htwW1y`(V=df9QZG29juQS zG24x}NXCY#0+PDMHi`5j99PhGNxY4sq&HWtNfbfHAB*DkzoTjhVkJ_xVzaN{T`}YM z7`ecJ3W!MDk^!9Poa(>PyUW*7vq>mvw$T0w)d5x&mtV`hnmbn!F5Vd_-aB8s_r}>N z-G?O=;mjjf%V&uULwaX$0NN48vuLDO<)vZMA*R|aOwzx{O7UT~#n zHtX9@M(stjX;J&;Srr7G<A#`o>55Q<*Om^tz7{O=9S zReg8tgS=fUB&5ZngmgKTL(8CR9dWcQI9eDnShZjw%Fj|4&Dc7(>;Gr(U7*{z&U4Wj zJP8maKma5`5+uPl!KX-3FG`}!r=FxJ2%@Y+F-?h-L`x!JfQlowiG7@$9y)1jDsd|+ zNtfu@jp;V0R&Lr_?MZL-vh2h!K%ff*k&l(eZSUz_cP&bms>Ja+z2CnF^B_2s!AfbSF)M^h|^LH|BzPr%UfP9 z`yrkMBAs<~*67X~kSv}!&072=U% z4myc=|4b**vCJrTh^2iRKcin5pX@nMwD?+u4HRoBt>y4TEK|AC3MH0;J|yqgh^=Dg zWw>IK#)mHmoKNIr^RkrMQL8`?qrG24cABG+R>a~2PIvUbW_qp$!1EnP_m7Z?UWGFC}{fiQI3|f9z?zT52=U=D!rEAo3tn) z&<#wR7(a;7+kz;Qte6Dm&&zaswwzu=E@6P>*a+h6ZKT5_Di&p$%uxI>s?mg?7fGH{ zgi6s`w!~ZGhg4F9cwfSoVl(p$N;CEj!pgXy-xqVQjJQ|E+-(teTh#qfEa#!A&0uH1 zJ#W>EEir5LHET5migp%|;BM9kM#7Zox;1ac`y%`-)?BmJ(Ako*DLpKHraCEH_(5Ff zsJ`Z?KBs-v_xi_r>8sr~6OL@%+NGRcESZtD^Ze z@q(hWYtF2hT^lW^ix-ugJ$&Zy?9pgZbG*=VcJGPYg3rC=fp|7In(d8a z)!(zA*V@broYrK%wVCH~+xcHyx4Ld~8j}lB&u4p|=?&+@glXQ=@xCLM446!=U)^p? z`t`~CDf$LpeeymDjpF@2&2Mhyzf#zS*81q4!Q5PufoAgZVjcyof|e+X|5LJ zW9RBB9;dIaP2XIueO;Tgxmf#po&m>SFV@qMaxLYn(l@Wszuv5){A!-^Yv}kQ5`m@x zEWrFn9%SwZeuB4XOy7?XScy6%_mxHZPAI8RI`_x|`@NtO_Lo8(OhW=m@O2O_NhX0h z+>zAx)!`0cy!v$#O!7g*pnydGm0aLJ;ajT)izw=+nt}|RzG(ZUDHCl^Zm@FKs=*AO z0ml^(MJjK^afwh7k7B}2!|~*#FCWEm6OOCE46>wGvncna<2Vs!DVxuNw!*p$EtOqz5oIy8RRCM{|D{pI4Zevxt#58&6Ag;*&VYqYT?%@)va()&=ikLBbg zjV)GoVI|s+(*H55$nuLH2?E&ShmdS+<@k?TwUo;h-6CbV5*H%I++Yh2VKRjgEsb`; zgjT?0|FX;yv_y`<=7h535(LM+D*SceE>bKx{qU)WXAVcRD>1*`EaeJ3AC=j2GFfiA zW1*PKhO>~ftIw>CI}2ufC*VFYSDO=SPLsEc;Cux_WL>s}}Ux4)c_8p-xK?(Cj$^^FC&8 ziP&4t?Ty+u#w;7JSvLOiRxLe34VTd-EDsb2O}ReAEFIS@9ls*JS112Ts29EbbaS_n z|I3b*U0LSKRj@kcFV_`yxwtE4zAHy}C0$2(s}aYqk+-=AnUz6RDf{#U94w2&pO>Eo@LaM$2eB!pnRh^*Y3o8Xz0UkfqXHejVcSky*W*o{B+96F90qc z@FkY79#D|XTXB}Ml|p{Mga~~Gpp5LLCB&y;+$rX_7Uk5I1LGRfz6XCROH+0n!cY^epUshyqg@7X@p_MkMLOu??gzA_+iiSAwH`Ony0wk zY4`e|1UG@ZjcSY)`Qz%fp+xf4D{m&32yhsR{C>JS+r%LynPs9)6sF*TJ|=wLU^-(` zLV1Z{BG$}tyCwF48m$B?YiW?J=XdG0al^#|?DX;Y5E0PXhmfc?NkOg$Q;PV>CmqY- zo<#_T(CcIa8{v#g0D0A}6+Z*<`eC8vCt1r?=rT1&+OiYAlv=Rh;`yEObBj5(;LTEh z!-#diM6r=7L$XbmQJY@&dqdH=l4rskC%Yxb9no8_$RtjExpm>Aj;dkkI=5ZmDg0OcU{GYUYh?0Ltl1NWR)9ZfNzneK- zqdSJik9~dw-k;&X=o7l1j@*62SPq{vZq96L$## z6ru`uW6ZrO;$Agv`oNL%jg>cPSl;k>=gQ`qpU?bAkG<0?Pp|yC+KIcI)@i>*34ADI z7segV)2mOdjycLB2>!7<=I~x~c4#**6R7#NH4Xuu@M4c5JOzBostep zx+r0+$^Dcg90g9{Yc#S*q{y75Bn*s12_*~|hU7toN!D@%Mk3ieh)&%H9~(a)pfnm+ zjAxig8ww2u#zUiD5JYuGK}F1i&l<*2ph`kPVj>D)m4;ie7^hea#zSm0JtTm)_fbeq zpr%5jFe3jGRP{@meMk}P$!DLOdh)uxC|tZUYHtZ!T0XQorn_h&iZZ zFV1$owEg+*FZF(>mlf}i+INL5yYA{lqGq90Yud@j5hl!g#v99TjpVnUUlq-7k2%|? zx}h*oTAqyZ0!5-{cRaHL=F;)}lG&1%YM!rop>DP)>{@|~p{x`yDvcKwogFwca68?Q zW&byAT1MtgEd_OP3CMEaseLI-yPG9kc1MzD$D@H0iC^~!C%UUk#W)nLW!d{l#MaYO)V2WEv!Jg96j4<4S~Og|a8beLkFbOgtrATNjHhy!MUhei*> zmEkZsXChNogbg8OW~?)h3W3f@^J{b%dfZY3&N}nKarnkN$fAodV?wyXfY*@V$XFfK zI)=iGJVDeE=nqp2ks*q-c9QvRWWGlhwF}fH+-(qU_CRI4p6N(4{~>i^ER#mW#AsAR zyfhzzCJ0&`0st4_He4f~^pKklPc?HcG9*0TW~RF>DXDuK%#+0;q-T$dJJ92@kZ!`0 zk~c$kYEU>e=~eD2Jtaz5|MPI$NY5rM;RmRTX$e=}qt!E6*EjBA9Y%5K5T+QlvDW~T zRCFg^g0g(@OBhSRG7PFoZzuDQLX3rE*D{4YW$9$Sf;}Un0J4IwW2^|eqc$9{7WqCyxysgW^qSgv?K&KqL*zm5DLN;S6M|k2kt(24n4Ta8l;A#cbXaJRAd^fEGicpUQU&x> z2`yro3J+OCE&n;4AaW>VK~l-64IvRojt|fU_aHfxkIGiEZ=*cJJ2u4rjAhqEvTLTy zA6WAkCGk+y-X6BJFX*(UJ^cHYji41)u9`BDz9Hxd;>OlS?6q?{E{ukE`J(pSG0X0- zW%peRXA`dPjoSOLyx)M*wWB8DsEIo2rqbdh()?+D8AU6VRMF45qlKNZ+|H>j@JxNp zS`48k+*Z#t{ZTDKnwm*wDugt>#zLBAzMOe36sul;t$ID8lf-grBRREmoza{X)7tA$ z?JkSj*G_eUUZz@PlEzgaQZbdYkN?Thcwwo6iYep@%I~f~Xgc=b+oJaEVas+XNHT`` zXHI=4mR%9au1FznesLSX%m%AU9{nR<3WAosnT?kaN2*j=u>sMS&4XvE=**0o448TlTk z;ZoWrr5mk1 z7VTAw9{H#og@WTLYsW5s6sT3)4 zzC~0PNy3QAa+75!l34_D#n0SswkHr<(h`KSwD4dV%J}G;sNpFNije$PZVNAj&cmKS zHfcfnXiA`zBynAIvigu5%;1p~a?ofjuxeZuU0sM-DgipH$ z?W8S0p2_u%2pY1In9krc1q>kTv}h%##;MxZy;9o<@}Ps)G3CZGfm{IBCS_qa2#c~x zm@|o`jHMeKNI`vsn9hp`18|GLN;b-zFEt{^XV{l3dutT#cVo`PIw|r7upT$E*{KmxN3j&sJW9(ULTv$%ldG&lBNNPiiY%(IY=+<|0IPd1 zpxt;7+L;usc`-Eroz3JTpPbwq7fs{1$rB?7LgNRIj!c-3!2Q-S=zCIFuU60FC&uAh ziwFkR4CVxxsgz(DLY+e=CXS%_sDLJZwlX((;K4Y=TrVf*c5=B4H)G?h46ThqTrwf1 zm{Q}MbjF2mLC`Bi1z(%=-W!3=sr&-xD;s{psDwBWaaJXa)a=o!gD z1ND-BvP`)!DRq?SQg~1%2g-fk%!CEY@v+eZ4dCqHy0!TF6Q#>7^$kZRCISugu7c~t zMW1;-NNA4_eL?n{5hNc7g1ZAN<;Zr1c}RwnGiDmb<}SJyt0!S#Pd7~dQ39ia59x&A zD7-Kq7T5wInun5Zkdu7MCTeKOQl1j)7~#=rps`u+BCdXlyW`|cFbdO}T<=@9fq!Gq zou2qc!Ia^8cF~Kw=j`9xea`u6{`d1^Oc-q`hCeIHig_v(Goirw?s{wZ_Z zRWsKW&TTr^9L-rh)w7^6X1ITnTRA)N(#hvfo~w-H?tH_5O#+++>KJRWym@Xy+!Df1Ij8JN8#iG7%nro>4SOKVvE(k z(qcuBm=>!-y&axA^>)w>*{%g6<sL%h_ctXU6+KfoRo3t(6z#0V6~c(p>^+a&(|1_HM$G zC2>DcTBj&&l9%%V2{DVpk>(t4wwapi{%%``U*Uvw+cYf_&GUIZU z$6RY7t~E*dKx@?17qj=lA+jTXX3gxjbGoQwRm`zI>{x%^K9%;<>_SKmtoG@3U;EO0 z$=Z4A+5|vLZqdw%aOK+b<TpT(KcFNmH?n6fb0e{= zmiep}2EQ63IgN3rXKw5H$N%aii~9%%`&ke@Ao-}6%PGS8;@HXGEXDpWL-=61r`!3i z23VQ#T`)S;GmcqIQ|n_s(V;?(=~i>DQu zX8rm_YZ3lq-Ih$<+!7xfR@ zfJ#ak_Rz_AM2*Sq)9fo3r%k3`$Om+r@Ep)DWH}jiu*pt8xydK6>ZCbNq}IXm1K;k^ zN2@_$)*){3aY!pq21dLf@&)a_3%+F1&epi>)4JPs&YCkljQOM`npr)i z{lIF6c6!`ue{S|tc`O!ju zEY}~-^?zMQ#Nrq=`;*XNH2P`2V-5eGJ?(bxPx*G6?z=kV z{;U$mF4}lxFWPH5^xBIX`1VbPiyQUGU()a$2tuKy>n`bdI-=Lok+k%V7VRZRPDi8m zlGi|I8})RgMT`8R$%ckBn{0%yU461y;+K#SoFFak$>b=cmm!LhjQX6L9Fo~i_On!! zMDH67$@qwvCKR1P;&1Szv4I#0s!j%lnNW#a`=%T7^@Jk9y>M; z^NWBGz(AKMT#Vt4j2r`FJk&5YGC`m@xEC)$_+02qNg7+Tql;{<8|e#BG1m~AFEMmk z$2YQWT+r!FIbd!642v!EDsgV)!tlIkCO(-bmW5#oRI=_(nlk__6pYwH# zg9$wFNO>?pVEY>ns%8^swj3lu>!b8HI;ZJ9_u6VXUrax2d`viIfNl9D4>bEW?^|lv7 zKF#;96wWKtW$Kc!vRJ1A-J^GC!T415Np_=ReF57tca|(j16j+IkZec;c7I0yD&^A% z`Tc9eV`w7{zLX6*0<9^Bu)FWy#P=kRGk@0p4Pwb<##t7;(&-de*zbFVovwf;fcjgM zEnsbat1lDI*w`la8&l@Q?9Q|&tsuD7W=opOKXNmx`xk!yV>R1*-?WSH-_a8#% z5pd^8JN`s~4v$T&XH0qF+SIymPpG*e@E(cA5vc_1ezWY+LFtW#C?W9btARs2>?ZX> z{6vUH&>&Jgs7rd7gVg_J9(r%A?GF{XKpHA_WqQ z2$w-wpdnRRfQU>c{t&eV#5*t*hJ*&bnKPKqAiW1a5L+UHR0cbl+l=A%30Y$>VMBa| z!=s=*dr+Wn==g}&xV%zf4~BNIj}peqiXi9JCG_lxVOvZx5~vjlk50eiU?x)(O5w>9 zCZ=sb)UnsY2;ATx7I;Ak;5ya%3?=w#xF9VcL0XT4w0H(d+bBqCM1mS4EsjE1FFjct zBi^%(XHcjNw7j5{FhjSeeu3gIan};&Q8vy7hae0gV2U`Dfzd$> zs6-k@69duIHOI)6wk|X}34LH;Yz!V4A3m8dlOeWnAb5o8GaO`(f{qs2C}~3 zqL#Ge*E*cwpGfdW7=%)4LL`+U1Xz57>QsTnNgYg~@r>5~3%n(Q#dqLZx%Nh;{n^K! zdF(*1Pr#GEK zbjZqxqjJ8gJ?iLyz5{&SK?C@7@u}iiR(T|=e7<61G^=gOKuVsI&rCl13rF``St#&bUr<_X!Zt3fzln}+=lt|hF4pmw~^7yU(YK%oBz%H*=^Ch z#%TkYl^=R>IOeFm=BSOAR!?sS)6edXWmQMAsuAZV@8!I6ozblJDFaN`D{JN|V>NA& znzm@gCc%F-Bvf1ZctJ_5U^SF{&wb&If@r~>SoWT&p1Ux0&so$?X3L(HJJlYyJ5E=e zs+iGz1L_?~J&oLwSZ+fkw;^uJI;}sYpB|6eD#3wpQLs)|JKR-MdLyeK*|MxxvR}@g zdot?X6lIQEGO{6X$+ELc4}T*Q_ej=rRPUZ{&Wxtsw>Uv{gY zFT3`<M$x-t*xm5g>6*YK}78g`^} zSNYY*UtOEMqk(%pgWplBdp%P}c{^S3dVaeTKW{X4y7BW?df|?0?OWAa%GbiX9`{y5 zdT)mIt##JkH0|4IdgR~EptHXoi{!z8@8e3~yO{xBO35!c{VoN*1gRMC?E$&;zkx4$ z18Tv)A(;FB8u+S{vc&UB+0?+#ehG+)gfigYr-jnA__A#gl;+hmUeLF0v9$a<0PhCB zVXqg7wTaL18`kIf53WxF4`TbE!<2kSfi!^NwEhb5j`Ht4K`@lRC9o=hVEH{L)zH(_ zVEZCf?9{6lLuiZN#31xed>a`6j$o&xZVCvUxl9QO7z1QyitQd|teGXC8!Qh1(Aw0y z;HU)h0E5+AA^~Q=VD%C*P`3Lm{q;-lvR`6HpcN!@OWFKs{mm(du)FV9euHQ-uwMe# z0K%)qviAhS@)h>`ULgWu_2-jdzJm3Dd7iNz)US|w)o)XMyR!2r_4AVRsAaCViDQ15 z^I7*fZSE z_nQ|%b2EeH=1t}lXr9Xy?|Vq)&chVcp+yFW>*+r-J~7@;4!tE1?(5#|UyQ^BNK&UP zEI2a{DHPsC{0-oHY!sMQPz^UH;UlD0(AW+Ej6;J?P(NE7pA#sS@ChPVo;W6|!q*dA z1fm`dna9SJDs~E|t^vRb3YU4}_<=E~u>-yj1) zc1m_o(u>3o<#@u6GHMTY(QY>-%UvYE^nNNLWA#D-b^j+7)J@BFu#l1xN@zU`mQh0M zR&9Q<7>^F<{0M$SJYdLS3ZJ{9jviI`+#b#9P=U|aEtT`t-N1}C zxbFlSbh%GYo|-)UzXDpSGw>!bD!Dp0#Un%%L?;e`y&?)``}+Ramj1-sVNy9h12qXh%8>;WZO z-W|1*{lE=dPRv#wvB5U;1FMrYHffb0_=Hp9X#F?pU`yf3JF7pVpE>lcjH#^v)n`Y^ z{K1hYm|n~^MIH51T_P&JH$*Oqxf&v_hPltb^5n};UND7y15wxBPlJ+YTcd>yvBFK^ z!cFH&Uak0k#d*UI>Jhe<0`JZhMm-dufIJJ$J{B!(0=PbVz*P?H?`~V^CMkHEq>ly z(P6>Q+Z%WkdwWxQUy=45v$Zcz`%a#o@B*3R?=GqwY!B0J9(~P2FTYSmstur)DimYQMBW zx~E;_LrCw}^-EAjC`0Lh9!~KLNk8FOZyrCSN~}trVnvXV)CC1|cmUCvl@_va0SA(g zBtmcjCqbqq?J+78zyzoy_m6ZHlQx?CXk$0uQv;}EfL*H^RFc{kFauuDf||1JJ1D)G zbiY}AiX>D@wVxHB60NH#o8PE*cfSOFMB+JM@ms`uBzx)rgI~1Dr)&T?SlDO-5LACU z2|7aBs$N1VZ8fPmV3C8``Yrp#rg44=gs4Su633EQd{}r-5`>hmh73`ws8xb4zgDf% z)l@6}TLyAU;CGqx5@_|Ced!2Sn;bV{Z!>@cpvo+pQ$fEx79|)5orKI~U{Ia#{*NiO+=t9!tyJ1v-MNa`1Nty-{EJ~9t%K?kh7Qv!4z#?rRZBtqbEV2!`F?g23B9Ox; z4jdaj=o#&#xJI7cEO?UOcoz~%KfnRm8bOnK=s20QGHk=3$0$h<71Bp0ZlnVwY80Rm zlTeDthM>q2II|m8G9*HzK&KvNk%QPxbRTk>GAs!q%_PcXRx6O-9U}1@VqP3R0{Aop z$tNU$KsvY&cG=a;pa++uAp!zpqA!>zu@jzZCUs3(95Z@31j!?mzYL=+xYJ}!vNP1+ z4#6c>oq)Ic5++7ZZU((7`UR0Bgvn}HaUhwpR3wu$DpnR#fCvU11b|_>55S;OGNPEQ zQnx}*-be0i1@y=jpvNB*h0TK9UZ*Obp#+mO7o<-+ND3$d%&=@_J*9vc37a6OBmFtn zBOy_AM@N`QjP&Y~qPvU$f*Bx+@5sTUp%cdiXz_=1Z>9skk?up<0OX2i81l~v!Lo&r zlGi3m?^p{M;$YZ;Acg`|e2*@N`(=m=h5O3lINf0a7tsDs-kpKB6o5f&yOwnKbI*Q(nLoQ@_PU6@ZtgR$Jo54*=eI=byQB6VNnYkCj5!)2j)u9%qmHHr zCF~2Xmsid4FZ585+*ojO9Z?8Vz z!bD#si)i;nba^S!92ze4~koMo=qvz^)oqsK5jwefPhGV85AI%_a7{Og%|deVUB7hC#hOfUc^95>oJ(3f&Jp<`M5L z|1traZa@zNB#WI)5Id<-;374hs&@(9MU$GC%5PMN8nl9_!6)II)LMXhBvf-4IF58q z??rv5UJp$>xj!UC0&lxAwQuTIp>xHr1+mT~Jzi*tTKr~)*>D9=jk>@`qO3`WRKyrb zw1juy&m>l2zzU(1x~`~veHKJclBh?1KljzVG^uG``mKK3zTa!Xd`Fu>G>4l;UCKQCdG7e^G+OU*(Q6k>p56OS|824Nz@Z8FNXu)D}(Sc-!J+~!;cw-GIxviY(V;;VO= z5bG6HAy&HLd(r#T7ooLuCd5kLlrEz+R?}ZHqpzHyZ!d${b|>S_3WBN-v^Ea}hae0R z)k^`{l7g*X@bD?vtEXCt)tIIz6hz^ZawG(I2nsDxFKIqX5qt$4XMSQ7A|&P_iu_pp zwnbY45ER5)0FAr1cCAmUQ^GIO_!xW;jUOE)VHw;vjFU$!;5>q5I365^t|SgJFd8C1 zQ4lwYPB)Tjsz9xf<_wty7)TciVn5Fp>Y)Hw4+TK42q@{^o-x9Z=n{&s8)7#SY-WQD zGp32LeuAx2@7&-_SRh|^7 zm{h$0Spg_lqyow%qA}?j!9|t=U3OzU3(~1Alv9yw1<8!x@6EUm_FVRIrie-suE`2@ zJSsJgA<~cnjV9GkU!?9yL8D%3EjWB!kQSUEtu#R}XSiS!B@L920zq&EB{U->GR6BS z(Q3MYRuQy$bW9-*vq*=>PfQ3JtH-DjOcga_LKMEjt=6I7V!WDg2wJCd8ALRoGI+8n7#B z_k}G!xCmRSo2o$E_Pm(ADq;urjoND@2whShu@p^hp57OCt8FDL6-?zj?bH?L%(G~EwsuQhK)=){tjDxR;H)4fnTU)1o%1mW;v8{u#}a5%c$ zb+<*t;XP6N=CEb+eR23cp3A68WN#5oAAN$zto=gSh3s(g=BRzk8wcT9mWj-KP*zRj zGOB+WI*IS&>8?$EI{r0{ky3LVrE46$#oSdh-N3Tn@~;Rc?h6FbMha;=z>lSgSNpVOg>rja zL_@8B9?(y(CX`cerT7#}L}LcOM$J3FLQs@O0)lmD@-RfEfO^P$d8 zLdH%>vXb$j;nFQ>^+ci!=(QY^Y^#+1QX7Jg;KTFxO_mE6D?N${>RPNe(N#kadRdKh z^JYJY{feGAF$jbzH9G%|sSipder_r&s7 zMDka_$63+$i=z1t#X!w;llQEAc(93BYvxu(txXgg-8IuSpYDklm(BLRSKLf>yW{EE zGtT++!r9t$PN*Fr5aVqAd+xfauHWy4cFAn*dxcoxf4@&Gw&=WSeQpm$1J1jN$XH_Z zL9NLlMVET$Liy_r7aPL9J@b|U@d4kr*nhQPM6pjoJ2AMPZg1|f@E2DZDcx9s^io+- zcRF{O=ete1%NiZ!4INsXc+JFP=e6|o9+UR9oa!D#m})cN_*I=AN3NQv%&))ZJy?9f zVOrga&~9Qr5Ayn!n=C!ow!nTb3=8{9S2(bqg%wVNnMGLPFc)L=q;BwGlRUbV=>~Qd z!l{dEiol@qLBsVCM7n{=IMz;KgL+1?4zRv7J(WoZ~E#QKI;(wqiIX?R9q ztCMmZ?y>e^%<^D|frTQ?j{VZwDqh2JB`7Q2A1jJRsr&(VTlkW(o=YpJe4WguW_S7n zx)athPR(W5fMkNZ^bUS4Tz@55HB@^*3#N-SPs{gH?R`Dkj=6)j#SX;O>+tE(x$sB2 zpVTMWMlR=uffwEEt>7vZCa&yZRi*WuT3j3X3iROrP0BL;rQ#jsU)XjS)fl)wiH$10 z>%-~PLPU|&k0qa$qmaH>;bi5B0potc!fcak@M(DI?J~;-PR$j7s@^?eXSTt4LGrGt z&mS=P()=a`J%Rm2>$HAMT7|KqFD|)wMc0o>N=9%5FO(E0kM!i8KzW#a=9D`~W5sXs zrKg-D8&tMoZCCW&|Nmp|FurUzy~B^`TV-SZKYZkMYqn!V5W7?1egj+*%o?iWd_bDZ zQr|P9=}W}Elz-(bSP2>kDVtnkX`SRT?9+d7zKmtQ?M(W%X?_LERI_7{rA?p%<=Ic7 zL*+(-HpDzk))+@P*^vp3qUS&{8f1$sH6=Kx7#YNG1GM3yF$UTYYHXVfXiHGY$+RJB zn}@M>U--3K$i9yIw$KQXL~^Am<{G(xNop6r;{%m;@jDtpKYxtBYY)&eeBI31f@HU* z+Dnp1fqGf=&4fGrU$}!r$D_~LH$mRdc6=n3LIBMN8zZ<@_QzJt^=2(scdt&Y``#l- zEXC+}>(H06MEhVg!QvY*N5m(_Sim3fo0IW=pqvigIT*+kM*%$I&`AB^k@15<@CF}> z!`*>rNNLW&JSGdeN=j}uz`>!>AalGxejLD{9)q$HGgkmbOCFIYMxg7&xbVcUK0sa^C#ww@h2#w>A5UfE^vpPC9{=^tm#5#w@4vrinXIkT9^`W6d zBduV*pM;gg_+b(*c#gyH1YWSgnug*OJsL`rp$oYofU^m7CrK^nu^xT&5P3Q{cJfgK zv^X{dOOXaMGYNq2g(Q{|=zPxzxy~ln%KqSA;XMUQv0hCU(=)X~O$=T^Nd&+Ou{S5U zVHS`|FXjRnGEJ^vSVEzg)|zZ0JX_Q;uN`P3d3z9_C;0!+4S#^dTgddI5~h){6C_Z8 zT3SCUPm~OH_xZMVZt3mr^AGm4@AeP&xBIsY?%UeEqie8xpxd`E_zkM#IZ7^5LXKa8 ze~o7do~GSDqg@Gd1*15IP!05SgB|Vu&Mkwx+XuRX|3Zh8VWF6kQ-H?K@E}Qh1gLd{ z>LD7DIWHCbkX6!Mj?!Iv=)~gQNaPJB2e1%f9)8CT9z6_IyRl)>B`H%2i{R3PJ^3{t zh6G;eAjS1fXh(;?AOJqbr)GffyL2Ch?}!+c@Lkfu^uN(lc$WuszK1ai(#n@y&%5TTq9x6-lC|NIwdW2nvjgC?8-+#TwEP9lPTtgVy|8ljM69s>l!?NG zA*e$+{)Zjqa2en#m@jO5FSqSN$$W0-^`eTi$G>?T+|;55m=c6t4G0v5C=jPNp4#~0 z&Uwd5=vrly-hZrUb);zZxru1ehL~$Z*tG$Ek+ZAjO5StUhMko+T<(~wKH{p6xtb!b zrgOFDACJ1)FSNhs>SjUtVoq7A>MI9G$S>8hEp-#DMUFjNU@}AF&m9$4n+ONB+=gPqrF3o}dmpf;= z3*NL58o6uUQ5~aDmGjNw+6k7U>LFdrbvF%x$YFyg$aAjS*+#IbF=T z>gGwFD{8p=CB(QWWsY9!BRTc(7n`#(W?LDyt%P_XW~+_ZYUjG|J_9jHBO zUmdfoj#ySNShN}0YL}ul4J}h$F-u9rQZic=wbb0&rQz}#5iKKc<1elkl*S60A_Yxw zlaO5$cNJbQD2IEt^= z0#YJ#6~}VxBf0gcCM!Sg%)6fJK{Z)L@Qi!9_f&7VlpL2kcg70}|G0}P$SV5yRyCKs z_NJN3YPkuewKZUb=ftcP5o-mxr?g9;f5*I~7g7RTW3qpI(}Lo^U|!MrjO?G-obVQy zk&Oz~9{!qb<&vTwWB4k&-Blh6b{>8HzfZsNbIL-ObCnY|mhi|c@13>RAqkiV4U z++L))RKssCFkGtDQNFP~A1AJu_#Ixu74tgeU(=OPT3NQEQF9f?>kL=TI?88uxN+i* z6*)+MWZ-*O8GdA>qCfIFDc#8TuGRgh&5QhxtN7kl-H)qvly7kM)pKuZ_`X`*n_3;^ zjag{jn=V+#aBmjyXvLdFMLR0>Z*Jr%Z7bYSqJOKvfU|Fv=yBw&N~-6rI?zj+w;GFj zoAqyJ@|0R#z4iLHtElAL^?EweOeNo5OEtgU%F+#Webw4`s+&i^A?*2}Fdf$5O zJ8Ly)!#k}!<=1N|-^TCUpnaEf?kv!~>*ROlroEf3r+hv;UXZ@iYkap-LuIOY%6lD@ z-$2*?wwWuhVJVTAE4z6;+*(k2u5W?;UYHQ}@4;Mw1JfjPWk1GeVZbC7(-d>tRMP%d8VM zW{FSoW=QO22o-p>7RVZ1%Q2o{pQBCM=Kz?o;L0ihC>gO1jCb{5!pZYx?7T$g%Uvoy znE<1_;1|(gn z;c9w+dw)Czc9gI!*N^Kp>orPN%wiVb35-j6`0ZmDEoo}ZC$SF)jCDjMCbvMwK*oNF zEwvUsFpTE`gR@*L(Z52>$-gC>e9b_HvOe)mE@e&Z2Xj@}_I~EUdwM`}IgwgpAoJeO z309z_$v6y#HG27m+PfM^r+(Y}uZlN7krA zJW1KaOH!Zv*I^6c4wyYsH<^*d^!UD(WNb$} zY%>PR{rUuNc?tVOwLLQXgw?Ckb29UrjfP*NW-$0vMu1#zvBX3YgsejwvCR4wvs~a- z6Hrm$kuc^4d||@T#srz2i7qic2>uA>!^x2e@GZ8CkHH7#A$r zs)t7o4V{3yj`1-su}a`L;PXQVPu5c;#L&Uf@e?5yFDpcB1JCeih=tB1ZbskuaneN` zJ&rTS;I4zXH&7}=V<_MxbGb=Pl#~_)-NNH=I7rb+JiAXEL~N`OoP-Fi24@BglqZ6t z6B8q2%+n_LJ>c%pt%OG(9jk|@{e$4LK*GF~yCM`~8e$FRgi(0BP~T)jvLii7=r2Bm zz?VrDTki=S9SsD~a3Q#(NhnPF#0m-iH6^c7Qi#M`93RT{@$~%NYs&5$!Wx%o#f97~I^wvvX%x zw}RJ#gT&rg+!n@w36l2+#<1~P1#!2;AQ1w*Qxq9^AOUA46nv8I^c3C1bPVy&@JPpw zC3Ms%QsJXJ3=7PYuTl?K*b@&AvN4-5!d-nJGy?5VwjY8zhk}>F97hKCQD6L@Xi%bN zDMMvB#tx(EpGb045bVX~{15>XCddQD?7En>{+hM^T=}at->-=^cSV}Jt~K`qpl4)J zaP*j^2zqESOKHRc7m%c&H*eYXVOI5Bqt0f&#UX)uTZZ%5-e-El`G|_Jowzb!J}l+f z0J7$~Khhhj%si9`c z!GfMMITkovdp8qucDhHVJ(k%R$%Ny}rdV22IIZc1w`EEfODl||70w)vq*Y#TYB~3r z9~4W+#v^GB@f8rvC(DdQ(&|4%K+f`4yf1qZ#2j8S3yK9~A1zoJ%U(HUMoZkqQzijs zU$a(3tyPfg+Z|KJ1)bN_aNSipt-GE>o+t}nE<}W!%)iNu=4=MXrnoFt+#D%xjuo$t z6t6xPiWaY*wtV0yj+d_h+eGAx%r(TE>(1|b&)F7r62}MK07X57LH}0moaw#%rfK-? z%%3k@J?~h3J-_5^{Wt69tJ^PZey@6aG=E3Txg+e{@nI2y+Z3Gr+?mhKb-mK}a^LxF z;PVtyV2nKbv>p}0WyRJrTfen^+Vn3DSG=)B(7~M3Mx1r=axXQeVCL~Z+A+QP2JuT6 zLuM>yTXjC`JzHzkw(15NP}CH2fFD*oy_s=knj`tm(R@_Y5_Yyc0H0<(msfVTjkD!0 zXtY^~-44diqAC^)YMcmgaM1dUET~gPELC&5f3Vd5@>aJ7BLGe)P0hD5xttbahxGEd zba>XAZZ3P(Pn>JO9LZkw3&u*}v+{o8bb;HEmG?2mo*EP6u63DaU2}qELi`wIB?x$@ z!Y@)~ncT40#D>3TssAMOWlRW9x3B3h<}bE3Aid-@Qo6#{Td%vUPp357N~zaCX=5?Y zULn?l?uxa&6g#gqfeWI0t+^2StG15SebtOHvKEYyYRWhAz3UA>T2X-fk1cTeQO&)X zlTB$Y-`i$*v(AP5TROgvGrXnGr+jf5YI>_0x{cgh>-j#7?yU__SmfU3oqf67+u3|y zw(jj59p&@$Q1b0nJf&-ST>Cc4BW&r*X(FiA)k#q<#c z#!2d;a3ons-6XOPO$E&6$^?B3YJAGppEOcZ9#)X>oKgoQz{|0f26&luA;L=OuD?fj z1+yYcS*wuuNg6K7$W8;=d(<|_YSXIeQ;IDGgG0ARe?r>`Qo9GlD)S)Kt!S-YmM*5Y zHfb&foq_>5tOV<~?RcR5WQ58#nW3OKCTX)A&}@e?KXX~44d}^NsSM0~@SzEOo80Ja zfPkL^0dEkGfy^(XrwnKmGHMB0i={x>@^b7cu!Oh(4;e15+N9a>j_@l>mKo?^Z5ibl zajff`EZrM2 zqunBuLnjC{lo;*lI9utTR%KkG?;~Cuy ze?7Nkw&ca#v0N`Gk?gV$8yeZ~bDcB%H+sI^Im>^m=j(kx@ig9|Ye>RT22H=p8dAC| zy_v`5b|yVIX-owY|Gn*kcHYtj$-*COCdR#~;FFImxcZl&0X*Q-*qZhJ%;P`CT@{C9j^+m3c@nTkJS~xrj%Z%Lq7^p}kz|>PpjI(du#hN}3)= zu4I^y|834}bu_by*4_qvH-D3(bPkGl^t*I&qlt)3Zee7E$&Dd$?4@q~_&ifLA93<@ zsYf}kQK`zZDDo#hAk5SF6b&h#Vk!F+`TKl|Q2!ODl4O`ReiLF7u`q4O$sZ#7__RS9 z{`dL>=!I6~jP7D@BAJw8@QE%Y_H5J&U2!*|z{pc(vEQNw#B{|Yh(7+5&PF1Kpr~C4MavJWJVbZZwB4Ga{R_DUDqrSJ0Y@E zH@uG#r{QV7y^{Y*%^L16_*J@BbZfXDXmH@K_%+Fs6Oi<+GFBJD+&0S7M@>FG$B z7WrRuWbUCZyWg7ILL*aLa~{VW}pG)-YxW_;vEci(HxJ=8kK{zu~NnFQV{{P)e*jBfVXfyeKWMz@j+DJ zgUA#w!rFwDh;(~=8klH{he&W#{f*FjPoM0$L&=a^(Y`L!%QYO>x6V!P%f#XNPf~LZXAMff(tql00;h z_#{c5$a*kZP@886oC>J3z-LkaNw8GL8iz*5DPp!~Pdm;axXQ`#6P^R$`5YY?ZZPkK z@(J`8pew^TCCR&xk0R38q2Fre9~J@E5v2UkNNA$rF+^&4^iksWn1wb|n~1%0a6HJu z#*yu5=marr#?gMnlSKse26JC9h;B=0VaYjZ8)Ei*Ap{RWZ1M3Y8Yde()wR^hP-|l& z9NHW?ai9U=HX27x4jnr@bgVI@ycZ$7BebUe1tm;r;|0pmI8CGtQrwA=!9m3E08|vA z!J053?z*57qX@B$`QrZ5&{mp;%Gs^%a-GZ_3ZQjehezY6O*t8qKOT zx_o)!ib4M613K9vN-Y+EFJBg;`t@ph@M8anE%n18qUxOs#1f(hb|$jHsQ0H4J}F~} zN#V54?boTSPS2sbH#ds=6=D^m ze>>*aBTnxqpR&#Y>j8tA6z$3Jo0RwVo8XoqBVgKON+ItGPkbLY>!h$%w3rB<^VrPY zFdjV2;!!ajP58YYKaQpDpyv?Ek`f)cD8O6|A9_qH5vzsSPw-2SR|p>2hQW;82HHSHL3y?l>k15olF3j~-&ppfFg$k;g_3V(Fu1QK%x}W+PBZ#H|wM z5cBG{V|45&MLhy@oO!w)eF`66BAp!&d|r>PMb{>>pwM^h#PEphYrH36`8-y#@m(hn z4r}6MLQic9Br?I=2gDS8O!yOK%#*&65X2>+zJwv^`P+#_S#Y)|I4)o-QShs@oRNnC z#7lytBQO!1!jVZ!GOTeui(#x`QKW)jqcb^zI|OD0$H)L$VOu>3J?qhg@i?RPz`18ioKOk(x47rGN8RnilylAC&)+-YPj&L!AcH~9zrl!B5Q@SA3< z;4mdJ@|I;K*XVK`%=|+@@65=Ki57lNy=i8R8>A>%2{RKju!j};o5A!MY7F6N2G|p( zVBp|jvJdftP+EssxL7fKJjoUqc28t6iN&DedDX}Yl4>Z+uoaNxEsU&iaan~k!)M3N zjKyv4nc=vtaAqQIE1TUDw-wDA<2KK1Ih`0?u%=~XE;tpXSsCfHNsrUnwmLI7@#Rnh zoibm~@l5sHu(`tdE8;fSOeK{3riT`cy4=jE%?sv?j2-;-g6deoifaWc&e>xHE%^Ul zK}#IK-~GJ%x~nc;SboD@j#%_@cTKpi4N7n&`3pv_v;htPNITNVv?Fa?cAlWQIPa`Y zYEr&X5G#P*<=t!!EQo@Z+itF5?Rnh~hhz1f*XlcGbZ67fq|N5eyS#5Wr?wK~!TX|L z2xuSAFFRX*rhd+JZc{YB6FmLG@@X?-+Mmuim9e1Vv)6p+bj{?PEjUx~t-`3Y3Jm$U zvye0ZXDg#l@9lI0g4JhoS$R-V{p$9c-8`3Da(fHUSqtKs-np7s&E{~;=JVFDcME*k zfkd;K5sk`ZzO^0=y{W+i-L!M=da>PT`n7_VU)|1O5wLmcFob!kSbx6r{HkzSdo-ux z4Q<%Aoq!+CeG>W$456nt^|bL9HKjdn&Bb!QdyV;Gg_iQwo*vE!8mzn3e92Tr`4xQk z2J@vR9pzW0qs(QF?`|_(=5>_UcT}UI*BVTeuHkX$wN@TQU)#XaHac>ZGxX%8Up4SO zCfik`neuiGrB15is*A_9SM%uhSKaA7>-ATwtUassS6AyPzg~;8iwHp?WI$9s0FO68 zAI$ym<{uKFCvrlzXb5mpI`@e1yA*OJNjuX=hs+{rr%u>O3mi1hp!*_`hd#;B3*NUR z1~2L$kdU&83v_^@vT6`w9tMesv8>su+X6Nw2qc{vfwb4A1};S+BNdi1c5JhPZ7MN= zANiP>-F30{+r*}+k`g+#>%}_)oa+5r1?d37iXbbVSu#5lYlp8rHH|MsDkj370eCQj zQ?~)$XB6+2`g}0Gku=5phW#G#TCu$No|0srK}}c8CwcSg-WKz7ec-_wMxqMXyWmOmR$l6N`HNx7)*_hj;Cc z=Tg9EDfM+guXrYj5g>OI%ue-kuRs{c2lSit-P#?m3OO030yJxP4B#Hg-sdB%`Nf9BTQ_Rc1sZ5Vv=l*W-tn65)A3d15DOz!lwOr6!RkXtRsaqkf{A#U`M|C0e z$aj2a5%b`6Mu7x)H(zx!5?aMr_M z>o73#ddA)$EY4UQV1_H8A#p?|YZ=y0UI>Ft!3*I=W_K#sX(p8yH&zVG?*B$_WA>fw z`V`lCs#ETamVrY)>`fS8g9>(p5RB_z@yde#ObHEwUF6nY!17Z!{=jkxQ( z!9`)t_rQs#L8rI`bgVME*Jj$?f@HEMrOAS|DlA+pppePJEC{_$s+iRwufSGV)EP`s zaPpy)dhXS2OCB9v>4h@NF6zU*D({i8sU!!V(2fm`92`G9Haf{PSDBV*g9l6;A<{#& zV3^#zV#Ny2L2!{mUgD0l_Kk*(BC8y_DL~A#{kRTG`92hsp)c3}dtafF&g@LCXD4Omc|zO(6sa*$Yb% z%?g^QWYQ`)rz8<;@MBz*%CY$wop8_zc;(=NzfZd^+WoJTV@ec%N4Zbv>_*Cchmt?W zZbCnN;&>nw{5qW?eoycYUHCsJ=R*PyPcq}@G!7&(gwJz$Z~}6*5%v&*Mh0W`{5!5D z-q7KZq#Waq=^9gLWMoVTkMMJGud9C5RR_K7%ybM-KYow+XmCh7`G33l# z;8;0}$sW%`!Ny2l<2mDbM>KC^%&~E*>vld@-F&Y6hq_qR#%om@r=6!=r(84D^I2sV znx_oJ0;+tmJZ7!BX03{6=T7x}m|ZtF5Y28u{PA%1p82&q!~Oo~+C2z@UQsn=`XD`f zs`Ywl#S0Z+ALW)zYp*-XXLrXO-fIpoR6{?^DW27SV{BUcL2f>FodBjJW zIXDk{+k&FAYtF2JTgQTiSV4QZp#5Cgt5x5xI$!yN#_6={u9DfZ*(c8%qppWyuFr;D zpS`jb{t|b8CffDcY5n!Q(%F@B*7L4tUPmnNk#OE4S4PAAyTf}v7wvfjf!M+9a{PVn z-{fA%kFMMC*1=eV?^=UzI&}JpQ%}rH&ga&IAKnjD*rFA2XYNe#TmxPSQ$j7?2xe6A z#t&gOt(|)WA>SADT$UTgudvlVIM*6=ua3F_8+7(zgKwKV5?OfX*cHy(HCIU^RxOhj@-W#^`ewb!`*7A%cZrOI-Vx2a9tq%%F8Buow+GR3h#3D|` z07Y=0HAb_mW7!+R*&CqtyY1y|vAPYBx(#uQV|pKkt_>_eTyX1gExrjcUWLw-nN;wI z+a8YF9Czs}xJ5%8%s!kX<6>tUOo|I`b7qtM1FM_b44v$_^E21+Rx_f`O0*-(j&@|b zXFeAzYzr5*%?-cu*vpT_8rmWaZKO*4Io#f5N3|#i|C{!OY}ECu+lMra-)KO`$Q`ln zLobZoe^?&#Zo1~(^jz1B^K9Okyl)l2w*89#jq=N%iFH35>3;Zn?W%K!z-X=QyjI(Z zQb|_nytC>p=NpIKbjP+n650C5U6UTS)Zz}Ggl=QLdU}n&iT~U3ovXNuMf^_4lZy?M zuUL!2mx}T!U6tSXzagEj!;>Ihq{+Ob{TXgJAN|?lgS!^)n19E*Fe2tRVDY;C^RZ7lL^0!F59=0R} zche%_r(_Q$Kce$*Q}P}q|AUh2l>9R#A5ua-EQ0S-@&P4Hw8S+~LaML9MoMnbiJwtI zfp>yGr-Zr6AkPuZ+960fg+Wq_3X-aWuoBb46WohtBr*mE;Y=hHA}1w-gP@FJI2FPl z_mH5H_&q9039(i~-@}Pt{2qsqT;LAtav`WMDh})N@9H+{>lZjAcfQ20)jRJ@@D{!I zjyqp(zO%L(iJvdhck*{0;@4s4^ZW*+zstK(a6_Tqey7c@Uv;P2sL#1$-wHwhodL~e z`tfNH-tf*OPZ3h?9MHfA>z%daw(d@cmQ1VfJf?x7@SSo}rMqK;{?eTqQWLnlO+&2Z zj~IiQF_u~L+YD&>$GV0C#{)vmac1yiHFR>mP!(&G^TLhu|Y;H285^8OX?!2ak<> zY-$(|L7)Y9FCUwjGwe{q@PRgPCh3Ml!6%R&3^pSjMyqrUVTr?+dwe>Nhfb8?DfO` z2BZZSBnfVioL1Bq{UT}o2CzCZn_!uPnp&&6*f?1S+jR;DW5+@}@ z)dfkI5d^LW%}PlgCB#w>l9RQdo039GiYO_j#6w95C8d;xpOQ}PHUk5V#7$pK1+D4C{&nWa5T zxgXNq-=yTfQR1RwrIgfDLVU4c7bTl0f%Grw-{*^zBa^n^3zYmBC2XqOMXh?6l6^=L zX&a7@51%+T(iUvRcY{(~h!zP*`FQ?)uJq?z=KEa9`&`jax%8h>-t|+?^gfsKbI$ob z*YZAB_dd7feXj8X?%}(-23~WQ`?Sd&y5bHxgD<^d&Wf1}u9*vFwnWVpPZ=@!YSQm& ziuvLN4#`Ja>@LvmEj+NboqsAlk@@gb=`>dthq>H%egRVW_RiVKhdKK#gEm(a<{Wn{ znS9or(qv*?@NhX5IDN;U=WUQM=M|haf73j>>0HOFz2EPRx;nyK_8mh8@4DkD=bd-@ zcoR}r0dKprp^|ss@$nn^tc5-eS6m+E3gZ9o6xA*;$lVZ?02+8hP7x#`B6)#9@P>#e zh!hZP;6-8r9V+eubeixD8K8S5Z-@hxiU8X}B4QU91a61{4G{(^QPsG>p!k(d66k0P z0YC4yFo}wGE)+_Zr!?d-}ipM`}@AzTk`WP9In4V)qi1NFUS2eJ*byX@$h%v z)^OYy=jP(vInK>@@rN~WO%?eyUD~*o-F0ytyX)h6+_hbX!^XIg#nE+{4x8g<_<6U! z%W~Knw^ngyb-iV3Hre|I?rdHQuRO=|-G;dRtiD&szE^!4IJXgRs@3Q0y#*zuD_-a} z#f#kLc(L0OFL`JxR=jCYzQ}bmsjP9^;$`l3i#JDf0eru{&mbRxYxs9#rzxG0{k18 zzuH|5e+~23xHrZ(x@+UL?mC2RVqtX;tzG?N+Gx{5zjQw4%X;@_|KLc=M>vb-RCcjcwz@Sn zoU_$GujRPTXml_v3cW+ocvKeTA-^OB1%J4IWLOAku!=7W^Lckw$SuA~V$sZR7FIyVZmt~tvI3&gcL+l%tvT(xPt$b5I8j24I z{Ze#P7A4^XoW{06%1w?6=^`QQIVD4jAPAD^4+}B>px7WpqH)159O-L}M#7g-G$m(QOU`oqzFp;4)Ze^^8@B;4YnB&W-|Nk(ym5ws7bG&&?k&^&@1 zkfPzR%<_(iVo(-_qY`~RD9SENvrFiO52;GgkwK-Aly>jGEc71l^MoRULOjZHjg9n& zLje@A-;eebFNuMXxQK#@kzgzuio{v2)Sy)L^XKJATi|E_@7=uzg?b8;so2qoC=9ah zji(ked@eJW%I3!6?v>ofGSWJpi)@ZySMj6r` z9v+E=;+F-yxF81NvJehk6fNh^UyAl`jW;P3XjE#@7>`E77en#$=UqZqoOJ+di9s-d#6TVvo3?Flac!l4LZ5%YFNM;dB>c*#JbmsfXE*Pcl*!e4M&5kpG&$Avr`sBz z@i!*6Ha_WVJiEntrW(Nw_=}8$!w-5_`yionp8d+EUkW`O7ixfHOqA_CJvwyuOmIv6 z{?^9RO5#X*^Tv?omDhBk@c;ZPQy^EArR5=irw*ZG+*vNpLyl|C9f)h)+PKcmV+GZ) z6;zLBgIkAHRG+f=+Gx2vDqfZYB#+ZFnKQtT1(v_Fk)`J4&S`f20mlUqR>?iZ5Akl^ zt#NDdr*rF@v`EYFODaLi%$5NkR;rX%j!P*MEnMu;Ac^r2DU#ClU@pJQp&t!2KKyZ+ zGObSSZoQY8L4>Q^ao9i&@|uMB)OdZRVod;n{p=Jv?LF zNk=5}>ZA}++YQ*MhI^Oqc~>J@@m|V<ZBawi`=Npa&N5bSslRP`(52xSQ zeZGN^B*!u7#7Gpc%|4$Tj>cu5PpY6&l;D}>T)^j#W4dCli}cPx8L`}`tddD}4!|Q9 z!1;Gv(%~HIy025)Q;AIxQ$|`5u&NB=0oT4S{a#}XBg}1JW6UTKhE187Job%9VTl?> zs-{5wKsf4;OZo68s^cS=^`|lZgy#hsZZz5D`032LarP{$n3I>N3d#Uvogo!bg5(^b z7nJfCcc0tJmo1Sqjj@8;^&RXs+Z|z{ti}NvAav1CFyrs*9H+nYPYA8%s*uQ;%n&)r zdo^zUiS*#=#I(Es|0RA*+pV{7qxeE<@oES3G}YW17uT$*;aprye%zpKd2GGZmMbog z;Yoxj%fQi6j`J9LsRdVDtADEk{wmI+@1-VQad~VxF{~?UV?C3pn@6w4J5#JCvFpbi zH;DGBy2FZ z{}8w{v~;t}zy$%SE4i2@Cl^|kR$OlFsh3yFrzJgFs$v0WH7}fGJ=P0(^r} zhJF;(s(>Wm-<8DI^R3Q!f$h4g^lsh6G3yi|dY^^i{y z%MkT4WW5+)5l-<_=8j7NF-A?FGWxoYcXoETQ^rw0t6EA6;F{868Bb~b!C=bZkHygN z5MsyLp7wQlI*#?F^g*$IWH6-*MFyhC*@#^V;aO=T#;~&P>sbDJ*7+$tYbpo`QH+p$ zkW)q~hA$LMdm_=4MG2ucO=(3*O6ifg7))tGahcYby1I=(|LIGS(AJ##O8XIq35Vkt z^1sB&em9>h-#oqP{lWRNo`u4muO1z1Pa3UXdp>C|nsAR_8n=JGu4=5~cE!54YR9^g z73;@3llgWymi(m2KC$gp+qiL2V>TDwu75J=STi|#SF3U61E197-?UYYA6YEs3JS;d zpA}Y)A4u8@u0D6=xykZ*d-bP|vZSr#>ai=wCab?&_wBlQ+lHjA;A;1k?#aS=TP6Gx zPrZ6%yyLdLaALRb^RDD2ZM{ib(Hnh}6|eiI#2@s{ zl>GTKclBDc<-Un?thrV*DZaJ&2bTGY?K9%_XaCa$B>D@T^`-8wb|+08w@udZj&E2d zI`M(tuxR3J1><{Wb!+~mkLQYx^Gl_u!LRNbI7j(iPLJx@imo2Nay;ocjaps{Ox3=1 z0Uze&-!pM}g}+|3BFb-MERWpR3Obtj*ZKBsx;N_K`-!7{C---Ge0z)T@A3`gFK!bM z{=t)NcHC~{)sox7NsV8{;eAFdW#6xsyW8xxsLWe0!Kf9Nnr2MzwtDqXVhRSglaO=U(ob8M zNG6=fuHFo6P(vWX^1OOAk1EdX7(xBqCit-FeF13p zm=w=sAkfnW6?()>9@79|JwU~yYhYKupz_yn05XKp+2zTkra*)4n*av=_&bWfy#lv| zW3|(%kkiOnI2F(gV$@V}A#lFy0HWw$w?`5402Z_rU>ATz4aJKYA5+TwQ)|=~W$mm1 zkTAuJ`;9f}`wAFQkZ4md1PRg=q#N-H;!2V%2LWsW{Qz9y`ofYWgc; z=nsN7Q{;{voJw@m2gWf3U=o*mzOWqxwS@g@qvvu=OmQh4rJmAJ)`|LDiQ5W_N4yln zS7MOyQS?z99|^TmV&n3d#FHQv00)ynLap42zr?oXu>#Q%Ffqg}hNGyPN+FVHBJ(# z-CRNKthIJlS9_-xUoSbh;@WIFtR^h4U*9d{Y$d3az9N&>p6j7`(=nv>GgJAm7xj4c z8~FpY;n(syO8E()y^fpV+cl>5wDA3~O=!31KPb+F|AP&DyUy@IwT}FCJR*OvsfgaT z^6duG2fKCT-*3g+4=sGVN%x_ZVt#0EM$8);9{w9T<{Eg!ykTN~i=n;7e8a)F7vz2Yy z3Gj?ThCPIt(ludYa87l3M+1FWNyqfs)a_Yh!hMkYCRSRdRSPo3!K^WdH`$f92g3^CRxr60tuDdyC$#!T_UGJRer zC^NPPeu*5`PBgE_xZ9?p1yjXMQ^jPGf(~K zZ5y5A=%|wK;L9iYu<1x2xc$hMbO9r4hEWn($Piak0nk7oCZ;SR)r8aTCy z@;ywhoIbbj?B;#-uFdl|8aj>lq zL%IF9r>(nN^3n@E21F>9GGVtVlP*tcM5t4zEE6Z;mv&M#gA8>^=#oqwREG_rGSrXL zdve%NxeN~tb<0)r%jVfa*NpCm{XZIhZ}>+4PeXqf`q|FobEaN(=qGJ?S39qCPPBdF z$bzl%rmb?$wtlQ5nO8M-C~364^ukwPm>5_vuDfMimvod&M6MN0df!?*X`6M_E;zQ_ zbZnV(G>+?&R>#%7<9jFCXLV)Sk@$N}mv>iP9lx-z;7T7mXJ*g_nK@Iz7c6mbrVcLB zZTdKS%9a#y*_EC%`a3t+oB_efXDQO`G6*^n12U!oRhF4cy{pWa4V*W#w8gZZRmc6A z%xL#y#;-b#tGJ`+b0Q5_F1s#Tb~C#OFMoza2)nXNXiUcl+6)z%nL`Gz4mEfRH85uJ zzSpSc8Z&uKB8c@$(6_zCYP#9GHZ&vm=+q?srWZ_K4hxo5zCsBgLdXu&fT`9LH|KK}qys{2TXE~j)C zWYAc^#r~k2vLER?-qQ|7GAMSCMmS{vCMNc(9EvLxArMc~W5h`LXhwl;8unjeON~^I zE(TpjI#w|!p5#Kf)I`o!I6%^bl~Wc}H5fvrMHGU{f=z`aitwXK(i0Q~3MS&xB>yP5 zGNQH^s$R}wm1l4|<0z3kF=G^28bKOt7XK8UF>cYu)zpq1x$UT#YX8LH9M>&swC0lA zMQh${{buWAaIUCkynWHYnIYfHZ<@;IE1GB8=PI83#MHWE{WE*fxc0VPm@1mH*N*FwR{P6)CR@I{>)X4g;`3{_d}=FB znrv51S4>B!C5Glj^4kZMFx*pNJB)-ziFG!|Xr-4(W)MgnMXY)a$3@OA}3c+=z({W9aa2SV6< zV;@P|a%MHtc8J-PV8ZsLEoWNQNt#WO*-l&n6%X43wxN$>82crKNfxM)$nXNT6N29& zV+#z;WH>$R7269i-t?U^rPEpg*m0nQ!ghh#;G92CPhG*iq~wDk1=A%gW7V@v{$S`i zn6|9=0_1pFEy0rACin;ap@_vFj)Hn;Sx^a>{mz4iBdT@)wLq*0s|vQ!tkAUDxGa6) z(BKf&f7CC>Md(3;iIF@yljB$%;{)QLVNI|z%fx2?0o z*Vpw-2VnRpLZwS^6d;~fCRh2W7nQlqT9Iw+vx8H59$&D*2C+j^y_7Spxwd)Uxbaha z!KYSxR#2;uf+$4{E&dKIT94Xr(Ijyj-KHioEifw^FWLzxO5-QV$EHFx`5?>`-t>#K zvUUaNXu#^NYHuIUn*JkFlZmAu)V`MAx(UQ7H!rL)@Maa+>e8t) z8S3857Z2G*XuNt@VQ5`C7w^h!`@lQu&SZE~Z-xuBaoeFPhC(s>Zq>4>`Lp;Yw;{{$ zQP&bX>$bXW?mTz?>UFcha&q;$Aw^x2K4u2%oIz8WDsm@ZI{CWW@kq{@&t+s|7O&+| zd@N9pWi{2bip;>*E>F*jw!}YmKLw6-+50Lki{GBMs;xw-*X~aIZNOhqnp@>BpwAU` z{$se+%{eO6g2Z8hLxN&y#B`so^lAn5)!ypr^XHiw5^E9BhJMmoDyyDQPk1T>)gD__ zoUR`1T$mMI$_TSW7;FU+h5;AMmi8vj48`LyxwWZDQSG@Di!?H(YZ__V+T?3ec2Orn zO@y(Ua+lKmQY;V(%6pZh>*VN&6c8I@u<6>{#JJif=>YmuIs_-tw2WqL9335PBx9dO z=;Xu*fu`VYtZzU+@x3QJhZ=WD2k9&A=BCXl!*MADOqM8rXi-x}w>ThTmzNM88Yf+( zgnmB|-Ci=mp$bXuR6<&SoEB*iHz*iKhGTNpG)JQODjlI$I+(u2rEYreqUZH#yq4Q2 zY4Ao1xzncbo@~ius~zQ&(#D{C)Q2MI2ikBxCx)Y7hjr8%3fyBw>SY=BM}wDTqN!jT zlu<$|yG|!UC34tOSBZ6sRFB{st%X;c#+xQQV+U{B99NILd}LDh-TZIoPiv<=Gc7;b z``+H!HEnaY15iNPi>@YKPE0;MH8LHX>6)$FH@kNKY{i%6>}_L*lX*pNbWGO2etfoQ zBM8Vbz&qaDc)M{s(%ARjzPZK&vkeCcS9S1UR`sR~7h5`8)&)j(qL$IJjY(6%Y{`z9 z;Ey8jMP_$&yno{3mjAlu&H4@)z<5zfsJSztqs)1nQD+ugbr%&O-ir4Y`|5c)JWr(y(DtQh?`CQZkp8dtl~uA&z+L!j4VBhrdA~sd&Ar zcuz;5o4Pgu zmaylz2IA+^*P~e_uSPB5AltLLuHS;M7s+8l$WMnG|#rO2(7TbpAjgwq;$ugQHM(`8JYvZc|T zDOKzAKxux*+IzA%#E?_|44OE{?|K8f3RD)F%4`V?FxtxLn}@ef5ZX-`M6HN_j0cNi zTs1$ijOF%wE*_88IM0I4vZDpy<8V^POF4CSl? zN)Lmz@tAvQLaw+x8I%BS2`xq3(y2)rePCF!qmSnMiVF-2KsPZg%6J`t-n1sev;6D6 zcIy4*n1H<`dxaHT2x;h|5roqw1{*3Bi_iuPTfIZfT34ut$t`FZGOF|sfF)tuou~Wy zx?uD!f-ZHo!g~Jn>FQCb-!&jrHwe`VVj$lhqEH&s2mm`u2*rH}Co^f)txiLh>az`k z%jG)Dwsb6}Li+$}KvBvHF&K+qRITV)t|a15Z#ygORcIp+PJVV6gzeK&D)xRmz0cX| z)Fig8DpH%`>4u{%>kVZm2zANl4MFX4oATdD*J=Gf+1d__vA1c@)sBeKoC$QIPVO+mx;?`G>9r>)FbX*U0(1du+ zvtX^Bw^qL`Pw)7CV&-|fg>^`0>NBZ9>8$a@?tpEbQaLrv<8kJ65 zc)Mo7T6fD@m(59+lT+)Qqi$S}sOy9&{aZ(-3$FG2AaT9+ef}r)H`*7Px^6XfP3R~5 ze{A|#8tb}US@l4qg9}ZEZ#5lGM+(An%jlRm@CPp@i_6B%%%bZvd)W#SYu>t6HNh(W z#I~N`NjWNOE10#eoz<=V)!iLv>;-GpO=}gnDt%eH=r7xWtn_8SCOz@PUw^^qp${~r zog8#8Pe?2f^BMIwU+O)}$F`Q1@G(8rjY{B5l zpcEHxSmNMJHZRg`wx*dqK8R9?pGU6xJJmoMV>u?%(3hp9KTt~Ahb5I=L9p(SLWFSG zmDL?UGYpbLUDu%v!^y;4`Ls;q!2b?8WOmz--ZEMFTco!vK1)yM%~%!%zr=b8 z!$P^doLisU7g>q;KzVD}ml>_g$}efFH3*#}6y(~~-evp$A!CG)47O?*LmB-IF~PZ| zTiF8&o>&=+)mgu4E_#Mv9?WG~8$2|ZWwBT;1|Sz>R#O!OS%mUuTNfvAbIbpTu?=%p zLhj4st0OH48TH`X6=U3#ldtKqUSP-C09(4*!8YZL9ysj&D2^H`{54cg%5gPD zmVlO3;E&7z%rKoXJ1Smo5Ke=%0M*+d3^7E4fd0#I(MJUKS%4IrAcd{A81xOx4FZUb zkr;i&P7vbsO9D9R_`m>&j$@%n0}dsU`Y0V4@TNid@*ZZMj&nC4D&&2w1bV)57B~dv zV{z!HpV+yx<%yQpR>72ZBLt^NMt z>1W^WhRsPb&oNQ(s(0)Vp^brS!3As0Eo%+X#?J45dFH9NUqqD8EfmEG76nlP*P0=v zZ&|CRFz4uIt5Q;JtCm zJKgrqsUK<=Dq3z;w2XTv+WzR&^^IeP?i)Ch^_9eA&1C;~Hoe`lV6VSrufJ_wKUFi; z|IVg)t8@C9>&@4tpFA<&cwouI6_@=2fJFP&;i+e@b z{aZ&SpZ(UcaWmCE^mfsLwf2^^cG@}P|3PE2Y{PihFRaD4ZEGhFP8;TIP09Qs7{?_G zOJPBmEUu#eg=_B?m~Dnd&a5{uxgI9;z%_jL#Ist(`}9kv{#X9L8kwZ@&-ue6qC}}P z?HZJzI7a(eJmSX~k=NrRVoiAdFwD2eB_jp8Pq!=24?+?hzCmXEoe29lXQ^8Sl4MO- zf-n`M4P2H6@o*!W-I`sSIW8v_6UB&SK;rD8_ce$~2FKDG1i>nW%}_QQaWbN-$LHzu z;beqzN`m=87yTuovSVzD8c6yxJTr$?9Eqah3e@hKBdFdvYwdjd!c50PUFUpV=Z~Mc zS=TwM>r`CU2#Iy8=;`UOYdtw%P88wfYr~h%(vAuN%30WDy-O&KHC!dje0AMw6EKWZ zyV6GQaTM4^>P%uCs5A5V#wBtl1MK!-4;m4%hCpQkpQX>@AxODaqq(vG5){C9KmqVs zk$!SjtGkwTYJgzqQT2k*(x_*sVj8a|O&GwU_IF9e3+k&uO%ivIXxc}g=gOL0kzsy1 z*8z?U*r_d6T&tHIG*<>tpWH!*d^0sMcn#?^Cs30eULtXF(&V{I+?XDe$9EOV!;Qni z${Ddl+bMQnoHVLrJt2@~G+Bj+IMsC-GWkKfkn`thfVG}KFR*FvO7UM#@n69n{d9^y z>onkuE516=ef;22GFOBEgq;L!p6$MVf0#DiL0=#om7$F%(+r!hz0K1`!V9sPiI~%v zwfTZI#5YA6L2QvMY>G|*lk9+y{`m*oM2IuHA06E)r1_s<8l0omxj__r_?RJAUa0UsW;{8_wvAZRk(MDhEf|F9 z#X1W2n%q32C2j1ma10c@m!nS=j-(SBrNP zseO&5hD|tf*I`!YrldPocJk}&Jr>^ddy-G-i&h%tbgR-1UIVtEP0p+LoP>PSF_(1yP zK%FuHmHGE$`i>knHvSqO9Gf@V-)NqA_Vu0f#`4LhzH@rIYku7hLWQcF9I5-3VS=ZlcXr1sFW}<|GhzZc z9vW|>X=t@gSYFwmEGr-HS`q%rzVwN_E3o*?f5n$MpO>^3Px7xMl55H)Oi7b@{Ha$B zFY~KfV$d{YqZD0Fqx7Ms?<>ufbP;G=8D>i9e4rN-rOeubIrwdjvuD*AN1N<~V*i^pMUFu_WRq5FAZ>8--*D;uXZ*l-yOCN^HvDU|bV>!xYl zg0=CMwJ~Y4Up;c=$V426SwAZ&$3a$G?UeY5ZQGKTE3JS6+GZa+au*gB#S`t4p&zf3MY_#k2ZCF+j7X4OnKF`kfy$^3t27IU?kG#Z0zwp4QItJ@K9^dFN={+j|YqLFgyU~w{e3(3HJ6dtc~MA`SlXD zdlwKQk;7`|f@h5Txi$Y&dok_0i(Y+t?9gY9(sA8wYvrV8j%fbPsiLwaJy)=13A+58 z8mqlR{0{4tdY0HDmpL854rXws{5FU}D?ns&(iA!WK+gBbp{4DyJ@g-`hm>sZb9Np- z5Cex!f8aLt47**gV7HI8?48nIC~fAVMFY?JJCjHY9Cqf#YFiDkS{XhYXDu=(AcC(d zi+zn|)C$m9ktKHX=TdwO9T!!;e{A!fHvYCbLOZ4t+3UF#StU zf?sGk+6Yc92E!}tRp=K8TueEbu4g7v=eRLxEu8QG!4#KGKJl#{EUt*t_J(|I z$5g>P<#TJd%&lpl!zt|B)xyCQO_WYZlbB1B2PU7L49wcAXHC_~{6dgqduFY|tWNmV z-2$$l;#%vhy^ajIoypRQ3Gd{d*`oSewt6x>aw;a>)1?Qtavy9hJ!sa#wg>Jq+Chhk zvc*GSHGcTwK})*Sk4TSQ6h5X!L0z^D{FWsS4(YORo7&Hw9$dC*NnpzWG&{5mq$di{ zcxKW72lF^G!8Gygmqln}%q(?bRY$*sLZFVhW1%6Zm zKhg(7K3L?1`tjpx_!YX00rOTDejlw-3TzYP5oy355O>p0gWy~N9hRV9PGskY$rSti zd1geY*tKEbP06!z(a8=Jiw;X5S8(McVHh|=OpKs{xE;nVhk;5tat}AqNoeVRBL-_r z%n~%oT^R7!PC+DWp4Dw;4VXA^j@k3=hkYV`^(_6PXZS(2GcMRFO^CRqMR_@?6s~u89ml1>Lzg9G7UAJIup0zelRZsH^)y?zO&C}kS)y=cIX2oT- zRp`rY_}v#De$P<(ak7CCQkCFm#Yj#tTM1gp1~IAzrPByX8KB9a2bw-98x$|$P``_f zBxaZi%||H!TX?_BlgmVyP#+FO>jlZWRl zwPQvYcIs40j?vyL#ks(HtvJVWPk;gby*;p8ymuZp?1sf4&*iThvn2D@k6G>+ zh?z%G9ED`JI$P0t{lMRL{dL!z{m_^VmJ>?g)|u)bIp1^6+4qdu?is)X->U}ubAKPU up7$yNsPB~ly4|b8QVmO&!s0h8zFCpfby0PXef1csTc|;~ZJ4O6ssBF$SLuuZ diff --git a/custom_components/album_slideshow/__pycache__/image_processing.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/image_processing.cpython-314.pyc deleted file mode 100644 index b10e41048c8ed215aaeb626ed8a566c82416fb74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15148 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 U=za~(^bZ7Rx(_yBk6_dOA5FjwBLDyZ diff --git a/custom_components/album_slideshow/__pycache__/number.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/number.cpython-314.pyc deleted file mode 100644 index 9dab212df0453ccafb5bbafd2b994ccbd0fd003c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13314 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 diff --git a/custom_components/album_slideshow/__pycache__/playlist.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/playlist.cpython-314.pyc deleted file mode 100644 index 4e7d519e9c8fad9a6afb0a72850cd1e907a3dd6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8212 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 diff --git a/custom_components/album_slideshow/__pycache__/select.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/select.cpython-314.pyc deleted file mode 100644 index f18dd4d6e66b781b4f5c8cf103e9e947dccb8930..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19468 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 Xx-BE!nNDB(emZ^qo1bAtwEh1Ffu#3c diff --git a/custom_components/album_slideshow/__pycache__/sensor.cpython-314.pyc b/custom_components/album_slideshow/__pycache__/sensor.cpython-314.pyc deleted file mode 100644 index 05660c8d341465c20cf09ec89129557bcda5b6bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10113 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 diff --git a/custom_components/album_slideshow/config_flow.py b/custom_components/album_slideshow/config_flow.py index 9b7f9fb..aefc9ab 100644 --- a/custom_components/album_slideshow/config_flow.py +++ b/custom_components/album_slideshow/config_flow.py @@ -61,12 +61,24 @@ from .const import ( CONF_SYNOLOGY_SPACE, CONF_SYNOLOGY_ALBUM_ID, CONF_SYNOLOGY_IMAGE_SIZE, + CONF_SYNOLOGY_PASSPHRASE, + CONF_SYNOLOGY_FAVORITE, + CONF_SYNOLOGY_SELECTION, DEFAULT_SYNOLOGY_IMAGE_SIZE, SYNOLOGY_SPACE_PERSONAL, SYNOLOGY_SPACE_SHARED, SYNOLOGY_IMAGE_SMALL, SYNOLOGY_IMAGE_MEDIUM, SYNOLOGY_IMAGE_LARGE, + CONF_NEXTCLOUD_URL, + CONF_NEXTCLOUD_USERNAME, + CONF_NEXTCLOUD_PASSWORD, + CONF_NEXTCLOUD_FOLDER, + CONF_NEXTCLOUD_RECURSIVE, + CONF_NEXTCLOUD_IMAGE_SIZE, + DEFAULT_NEXTCLOUD_IMAGE_SIZE, + NEXTCLOUD_IMAGE_PREVIEW, + NEXTCLOUD_IMAGE_ORIGINAL, DEFAULT_REVERSE_GEOCODE, PROVIDER_GOOGLE_SHARED, PROVIDER_LOCAL_FOLDER, @@ -75,6 +87,7 @@ from .const import ( PROVIDER_PHOTOPRISM, PROVIDER_ICLOUD, PROVIDER_SYNOLOGY, + PROVIDER_NEXTCLOUD, DEFAULT_RECURSIVE, ) @@ -129,6 +142,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._syn_device_id: str | None = None self._syn_space: str = SYNOLOGY_SPACE_PERSONAL self._syn_albums: dict[str, str] = {} + # option key -> {"album_id": id|None, "passphrase": str|None} + self._syn_album_meta: dict[str, dict[str, Any]] = {} + # id(str) -> name maps for the composite category multi-selects. + self._syn_people: dict[str, str] = {} + self._syn_places: dict[str, str] = {} + self._syn_tags: dict[str, str] = {} + self._syn_subjects: dict[str, str] = {} @staticmethod @callback @@ -147,7 +167,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ``__init__`` raises (the symptom is a 500 when the user clicks Configure). """ - if config_entry.data.get(CONF_PROVIDER) == PROVIDER_LOCAL_FOLDER: + if config_entry.data.get(CONF_PROVIDER) in ( + PROVIDER_LOCAL_FOLDER, + PROVIDER_NEXTCLOUD, + ): return LocalFolderOptionsFlow() return _NoOptionsFlow() @@ -166,6 +189,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_icloud() if self._provider == PROVIDER_SYNOLOGY: return await self.async_step_synology() + if self._provider == PROVIDER_NEXTCLOUD: + return await self.async_step_nextcloud() return await self.async_step_google_shared() schema = vol.Schema( @@ -177,6 +202,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)", PROVIDER_ICLOUD: "iCloud Shared Album", PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)", + PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder, full metadata)", PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)", }) } @@ -699,9 +725,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: await client.async_login(otp_code=otp or None) - albums = await client.async_list_albums() + # Albums and category browsing live only in the Personal space + # (there is no Shared Space album/category API). For the Shared + # Space, validate access up front so a permission problem + # surfaces here, not later. + if space == SYNOLOGY_SPACE_SHARED: + albums = people = places = tags = subjects = [] + await client.async_collect_assets(None) + else: + albums = await client.async_list_albums() + people = await client.async_list_people() + places = await client.async_list_places() + tags = await client.async_list_tags() + subjects = await client.async_list_subjects() except syn_api.SynologyOtpRequired: errors["otp_code"] = "synology_otp_required" + except syn_api.SynologyPermissionError: + errors["base"] = "synology_shared_unavailable" except Exception: # noqa: BLE001 - any failure means bad URL/creds errors["base"] = "synology_cannot_connect" else: @@ -712,10 +752,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # A trusted-device token is captured only on the OTP login; # store it so future logins skip the 2FA prompt. self._syn_device_id = client.captured_device_id - self._syn_albums = { - str(a["id"]): (a.get("name") or str(a["id"])) - for a in albums - if a.get("id") is not None + # Key each album by a synthetic value so an own album and a + # shared-with-me album that happen to share a numeric id don't + # collide. Track album_id + passphrase per option. + self._syn_albums = {} + self._syn_album_meta = {} + for a in albums: + if a.get("id") is None: + continue + shared = bool(a.get("shared")) + key = f"{'shared' if shared else 'own'}:{a['id']}" + label = a.get("name") or str(a["id"]) + self._syn_albums[key] = f"{label} (shared)" if shared else label + self._syn_album_meta[key] = { + "album_id": None if shared else a["id"], + "passphrase": a.get("passphrase") if shared else None, + } + self._syn_people = { + str(p["id"]): p["name"] for p in people if p.get("id") is not None + } + self._syn_places = { + str(p["id"]): p["name"] for p in places if p.get("id") is not None + } + self._syn_tags = { + str(t["id"]): t["name"] for t in tags if t.get("id") is not None + } + self._syn_subjects = { + str(s["id"]): s["name"] for s in subjects if s.get("id") is not None } await client.async_logout() return await self.async_step_synology_select() @@ -745,7 +808,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_synology_select( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Pick a Synology album (or the whole space) and image size.""" + """Build a composite Synology selection and finish the entry. + + Like the Immich/PhotoPrism providers: tick any mix of favorites, + albums, people, places, tags and subjects. Synology has no OR across + categories, so each member is queried on its own and merged. An empty + selection means the whole space. + """ errors: dict[str, str] = {} if user_input is not None: @@ -753,13 +822,42 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): size = user_input.get( CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE ) - album_id = user_input.get("album") - if album_id in (None, "", "__all__") or album_id not in self._syn_albums: - album_id = "" + favorites = bool(user_input.get("favorites")) + album_ids: list[Any] = [] + passphrases: list[str] = [] + for key in user_input.get("albums", []) or []: + meta = self._syn_album_meta.get(key) + if not meta: + continue + if meta.get("passphrase"): + passphrases.append(meta["passphrase"]) + elif meta.get("album_id") is not None: + album_ids.append(meta["album_id"]) + + def _ids(field: str, valid: dict[str, str]) -> list[int]: + out: list[int] = [] + for v in user_input.get(field, []) or []: + if v in valid: + try: + out.append(int(v)) + except (TypeError, ValueError): + pass + return out + + selection = { + "favorites": favorites, + "album_ids": album_ids, + "passphrases": passphrases, + "person_ids": _ids("people", self._syn_people), + "geocoding_ids": _ids("places", self._syn_places), + "tag_ids": _ids("tags", self._syn_tags), + "concept_ids": _ids("subjects", self._syn_subjects), + } + sel_id = json.dumps(selection, sort_keys=True) unique = ( f"{DOMAIN}:{PROVIDER_SYNOLOGY}:{self._syn_url}:" - f"{self._syn_space}:{album_id or 'all'}" + f"{self._syn_space}:{sel_id}" ) await self.async_set_unique_id(unique) self._abort_if_unique_id_configured() @@ -769,44 +867,128 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_SYNOLOGY_USERNAME: self._syn_username, CONF_SYNOLOGY_PASSWORD: self._syn_password, CONF_SYNOLOGY_SPACE: self._syn_space, + CONF_SYNOLOGY_SELECTION: sel_id, CONF_SYNOLOGY_IMAGE_SIZE: size, CONF_ALBUM_NAME: name, } - if album_id: - data[CONF_SYNOLOGY_ALBUM_ID] = album_id if self._syn_device_id: data[CONF_SYNOLOGY_DEVICE_ID] = self._syn_device_id return self.async_create_entry(title=name, data=data) - album_options = [ - selector.SelectOptionDict(value="__all__", label="All photos in this space") - ] + [ - selector.SelectOptionDict(value=aid, label=aname) - for aid, aname in self._syn_albums.items() - ] + def _multi(options: dict[str, str]): + return selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict(value=v, label=l) + for v, l in options.items() + ], + multiple=True, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value=False, + ) + ) + + fields: dict[Any, Any] = {vol.Required(CONF_ALBUM_NAME): str} + # Favorites, albums and subjects are Personal-space concepts. + if self._syn_space == SYNOLOGY_SPACE_PERSONAL: + fields[vol.Optional("favorites", default=False)] = ( + selector.BooleanSelector() + ) + if self._syn_albums: + fields[vol.Optional("albums")] = _multi(self._syn_albums) + if self._syn_people: + fields[vol.Optional("people")] = _multi(self._syn_people) + if self._syn_places: + fields[vol.Optional("places")] = _multi(self._syn_places) + if self._syn_tags: + fields[vol.Optional("tags")] = _multi(self._syn_tags) + if self._syn_subjects: + fields[vol.Optional("subjects")] = _multi(self._syn_subjects) + fields[ + vol.Optional( + CONF_SYNOLOGY_IMAGE_SIZE, default=DEFAULT_SYNOLOGY_IMAGE_SIZE + ) + ] = vol.In( + { + SYNOLOGY_IMAGE_LARGE: "Large (best for slideshow)", + SYNOLOGY_IMAGE_MEDIUM: "Medium", + SYNOLOGY_IMAGE_SMALL: "Small (thumbnail, fastest)", + } + ) + return self.async_show_form( + step_id="synology_select", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_nextcloud( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Collect and validate a Nextcloud WebDAV folder + app password.""" + errors: dict[str, str] = {} + + if user_input is not None: + name = user_input[CONF_ALBUM_NAME].strip() + url = user_input[CONF_NEXTCLOUD_URL].strip() + username = user_input[CONF_NEXTCLOUD_USERNAME].strip() + password = user_input.get(CONF_NEXTCLOUD_PASSWORD) or "" + folder = (user_input.get(CONF_NEXTCLOUD_FOLDER) or "").strip() + recursive = bool(user_input.get(CONF_NEXTCLOUD_RECURSIVE, False)) + size = user_input.get( + CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE + ) + + from . import nextcloud as nc_api + + client = nc_api.NextcloudClient( + self.hass, url, username, password, folder + ) + try: + await client.async_validate() + except Exception: # noqa: BLE001 - any failure means bad URL/creds/folder + errors["base"] = "nextcloud_cannot_connect" + else: + await self.async_set_unique_id( + f"{DOMAIN}:{PROVIDER_NEXTCLOUD}:{client.base_url}:" + f"{username}:{client.folder}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_PROVIDER: PROVIDER_NEXTCLOUD, + CONF_NEXTCLOUD_URL: client.base_url, + CONF_NEXTCLOUD_USERNAME: username, + CONF_NEXTCLOUD_PASSWORD: password, + CONF_NEXTCLOUD_FOLDER: client.folder, + CONF_NEXTCLOUD_RECURSIVE: recursive, + CONF_NEXTCLOUD_IMAGE_SIZE: size, + CONF_ALBUM_NAME: name, + }, + ) + schema = vol.Schema( { vol.Required(CONF_ALBUM_NAME): str, - vol.Optional("album", default="__all__"): selector.SelectSelector( - selector.SelectSelectorConfig( - options=album_options, - mode=selector.SelectSelectorMode.DROPDOWN, - custom_value=False, - ) + vol.Required(CONF_NEXTCLOUD_URL): str, + vol.Required(CONF_NEXTCLOUD_USERNAME): str, + vol.Required(CONF_NEXTCLOUD_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + vol.Optional(CONF_NEXTCLOUD_FOLDER, default=""): str, + vol.Optional(CONF_NEXTCLOUD_RECURSIVE, default=False): ( + selector.BooleanSelector() ), vol.Optional( - CONF_SYNOLOGY_IMAGE_SIZE, default=DEFAULT_SYNOLOGY_IMAGE_SIZE + CONF_NEXTCLOUD_IMAGE_SIZE, default=DEFAULT_NEXTCLOUD_IMAGE_SIZE ): vol.In( { - SYNOLOGY_IMAGE_LARGE: "Large (best for slideshow)", - SYNOLOGY_IMAGE_MEDIUM: "Medium", - SYNOLOGY_IMAGE_SMALL: "Small (thumbnail, fastest)", + NEXTCLOUD_IMAGE_PREVIEW: "Preview (smoothest slideshow)", + NEXTCLOUD_IMAGE_ORIGINAL: "Original (full quality, slower)", } ), } ) return self.async_show_form( - step_id="synology_select", data_schema=schema, errors=errors + step_id="nextcloud", data_schema=schema, errors=errors ) diff --git a/custom_components/album_slideshow/const.py b/custom_components/album_slideshow/const.py index 84cadc0..b2f9c0f 100644 --- a/custom_components/album_slideshow/const.py +++ b/custom_components/album_slideshow/const.py @@ -58,6 +58,28 @@ PROVIDER_IMMICH = "immich" PROVIDER_PHOTOPRISM = "photoprism" PROVIDER_ICLOUD = "icloud" PROVIDER_SYNOLOGY = "synology" +PROVIDER_NEXTCLOUD = "nextcloud" + +# Nextcloud (authenticated WebDAV folder) provider. Points at any folder in a +# user's files and lists it over WebDAV. Auth is HTTP Basic with a username + +# app password (Settings > Security > Devices & sessions); the app password is +# stored so the coordinator can re-list on each refresh and is sent server-side +# only, never reaching the browser. +CONF_NEXTCLOUD_URL = "nextcloud_url" +CONF_NEXTCLOUD_USERNAME = "nextcloud_username" +CONF_NEXTCLOUD_PASSWORD = "nextcloud_password" +CONF_NEXTCLOUD_FOLDER = "nextcloud_folder" +CONF_NEXTCLOUD_RECURSIVE = "nextcloud_recursive" +CONF_NEXTCLOUD_IMAGE_SIZE = "nextcloud_image_size" + +# ``preview`` uses the core/preview thumbnail endpoint (smoother, smaller); +# ``original`` fetches the real file straight off the WebDAV collection. +NEXTCLOUD_IMAGE_PREVIEW = "preview" +NEXTCLOUD_IMAGE_ORIGINAL = "original" +NEXTCLOUD_IMAGE_SIZE_OPTIONS = [NEXTCLOUD_IMAGE_PREVIEW, NEXTCLOUD_IMAGE_ORIGINAL] +DEFAULT_NEXTCLOUD_IMAGE_SIZE = NEXTCLOUD_IMAGE_PREVIEW +# Long edge (px) requested from the core/preview endpoint for preview quality. +NEXTCLOUD_PREVIEW_PX = 1920 # Synology Photos (direct API) provider. Talks to a DSM Photos package over its # entry.cgi web API. The account password is stored so the coordinator can @@ -70,6 +92,21 @@ CONF_SYNOLOGY_DEVICE_ID = "synology_device_id" CONF_SYNOLOGY_SPACE = "synology_space" CONF_SYNOLOGY_ALBUM_ID = "synology_album_id" CONF_SYNOLOGY_IMAGE_SIZE = "synology_image_size" +# Passphrase for an album that was shared with the configured account. Present +# only when the chosen source is a shared-with-me album; such albums are +# reachable by passphrase rather than by album id. +CONF_SYNOLOGY_PASSPHRASE = "synology_passphrase" +# When True, the source is the account's Favorites (favorited photos) rather +# than the whole space or a specific album. +CONF_SYNOLOGY_FAVORITE = "synology_favorite" +# Composite selection: a client-side union of any mix of albums, people, +# places, tags, subjects and favorites. Synology has no OR across categories, +# so each selected member is queried on its own and the results are merged +# (see the Immich/PhotoPrism composite). Stored as a JSON object: +# ``{"favorites": bool, "album_ids": [...], "passphrases": [...], +# "person_ids": [...], "geocoding_ids": [...], "tag_ids": [...], +# "concept_ids": [...]}``; an empty composite means the whole space. +CONF_SYNOLOGY_SELECTION = "synology_selection" # Personal ("My Photos") vs shared ("Shared Space") library. SYNOLOGY_SPACE_PERSONAL = "personal" diff --git a/custom_components/album_slideshow/coordinator.py b/custom_components/album_slideshow/coordinator.py index b5e5a7b..8fb05b2 100644 --- a/custom_components/album_slideshow/coordinator.py +++ b/custom_components/album_slideshow/coordinator.py @@ -51,8 +51,20 @@ from .const import ( CONF_SYNOLOGY_SPACE, CONF_SYNOLOGY_ALBUM_ID, CONF_SYNOLOGY_IMAGE_SIZE, + CONF_SYNOLOGY_PASSPHRASE, + CONF_SYNOLOGY_FAVORITE, + CONF_SYNOLOGY_SELECTION, DEFAULT_SYNOLOGY_IMAGE_SIZE, SYNOLOGY_SPACE_PERSONAL, + CONF_NEXTCLOUD_URL, + CONF_NEXTCLOUD_USERNAME, + CONF_NEXTCLOUD_PASSWORD, + CONF_NEXTCLOUD_FOLDER, + CONF_NEXTCLOUD_RECURSIVE, + CONF_NEXTCLOUD_IMAGE_SIZE, + DEFAULT_NEXTCLOUD_IMAGE_SIZE, + NEXTCLOUD_IMAGE_ORIGINAL, + NEXTCLOUD_PREVIEW_PX, DEFAULT_REVERSE_GEOCODE, DOMAIN, PROVIDER_GOOGLE_SHARED, @@ -62,6 +74,7 @@ from .const import ( PROVIDER_PHOTOPRISM, PROVIDER_ICLOUD, PROVIDER_SYNOLOGY, + PROVIDER_NEXTCLOUD, ) from .store import SlideshowStore @@ -420,6 +433,11 @@ _NOMINATIM_TIMEOUT_S = 20 _EXIF_BATCH_SAVE = 25 _GEOCODE_BATCH_SAVE = 10 +# Cap a single Nextcloud enrichment download. EXIF/IPTC/XMP live in the first +# blocks of the file, but we read the whole thing since Pillow needs a complete +# image; this bounds memory for pathological files. 64 MB matches the camera. +_NEXTCLOUD_ENRICH_MAX_BYTES = 64 * 1024 * 1024 + # Inserted between background-enrichment iterations so the event loop # stays responsive on the fast path (items that are already scanned and # need zero work). @@ -674,53 +692,90 @@ def _read_local_exif(path: Path) -> dict[str, Any]: try: with Image.open(path) as img: - exif = img.getexif() - - if exif: - dt_raw = exif.get(_EXIF_TAG_DATETIME_ORIGINAL) or exif.get( - _EXIF_TAG_DATETIME - ) - offset_raw = exif.get(_EXIF_TAG_OFFSET_TIME_ORIGINAL) - parsed = _parse_exif_datetime(dt_raw, offset_raw) - if parsed is not None: - out["captured_at"] = parsed - - # Description can come from IPTC / XMP even when the file has no - # EXIF IFD, so this runs regardless of ``exif`` being present. - description = _read_photo_description(img, exif) - if description: - out["description"] = description - - if not exif: - return out - - gps = None - try: - gps = exif.get_ifd(_EXIF_TAG_GPS_IFD) or None - except Exception: - gps = None - if gps: - lat = _gps_to_decimal( - gps.get(_EXIF_GPS_LAT), gps.get(_EXIF_GPS_LAT_REF) - ) - lon = _gps_to_decimal( - gps.get(_EXIF_GPS_LON), gps.get(_EXIF_GPS_LON_REF) - ) - if lat is not None and lon is not None: - # Null Island guard: GPS chips and some editors stamp - # ``(0, 0)`` when the fix is invalid. Treat that as no - # location rather than dropping every such photo onto - # the equator off the African coast. - if abs(lat) < 1e-6 and abs(lon) < 1e-6: - return out - out["latitude"] = lat - out["longitude"] = lon + _read_exif_from_image(img, out) except Exception as err: _LOGGER.debug("EXIF: failed to read %s: %s", path, err) return out +def _read_exif_from_image(img: Any, out: dict[str, Any]) -> None: + """Fill ``out`` with capture date / description / GPS from an open image. + + Shared by ``_read_local_exif`` (opens from a filesystem path) and + ``_read_exif_from_bytes`` (opens from downloaded bytes, e.g. the + Nextcloud provider) - both hand this an already-``Image.open``'d image + plus a dict pre-seeded with a fallback ``captured_at``. + """ + exif = img.getexif() + + if exif: + dt_raw = exif.get(_EXIF_TAG_DATETIME_ORIGINAL) or exif.get( + _EXIF_TAG_DATETIME + ) + offset_raw = exif.get(_EXIF_TAG_OFFSET_TIME_ORIGINAL) + parsed = _parse_exif_datetime(dt_raw, offset_raw) + if parsed is not None: + out["captured_at"] = parsed + + # Description can come from IPTC / XMP even when the file has no EXIF + # IFD, so this runs regardless of ``exif`` being present. + description = _read_photo_description(img, exif) + if description: + out["description"] = description + + if not exif: + return + + gps = None + try: + gps = exif.get_ifd(_EXIF_TAG_GPS_IFD) or None + except Exception: + gps = None + if gps: + lat = _gps_to_decimal(gps.get(_EXIF_GPS_LAT), gps.get(_EXIF_GPS_LAT_REF)) + lon = _gps_to_decimal(gps.get(_EXIF_GPS_LON), gps.get(_EXIF_GPS_LON_REF)) + if lat is not None and lon is not None: + # Null Island guard: GPS chips and some editors stamp ``(0, 0)`` + # when the fix is invalid. Treat that as no location rather than + # dropping every such photo onto the equator off the African coast. + if abs(lat) < 1e-6 and abs(lon) < 1e-6: + return + out["latitude"] = lat + out["longitude"] = lon + + +def _read_exif_from_bytes( + data: bytes, mtime_fallback_ms: int | None +) -> dict[str, Any]: + """Read EXIF metadata from already-downloaded image bytes. + + Same return shape as ``_read_local_exif``, for providers (Nextcloud) + whose files live on a remote server rather than the local filesystem - + the caller downloads the file once for enrichment, regardless of which + quality is used for display. ``mtime_fallback_ms`` takes the place of + the filesystem mtime fallback (e.g. the WebDAV ``Last-Modified`` date). + """ + out: dict[str, Any] = {} + if isinstance(mtime_fallback_ms, int): + out["captured_at"] = mtime_fallback_ms + + try: + from PIL import Image + except Exception: # pragma: no cover - Pillow ships with HA core + return out + + try: + import io + + with Image.open(io.BytesIO(data)) as img: + _read_exif_from_image(img, out) + except Exception as err: + _LOGGER.debug("EXIF: failed to read image bytes: %s", err) + + return out + + def _format_nominatim_location(payload: dict[str, Any]) -> str | None: """Turn a Nominatim reverse-geocode response into a short label. @@ -951,6 +1006,8 @@ class AlbumCoordinator(DataUpdateCoordinator): data = await self._update_icloud() elif self.provider == PROVIDER_SYNOLOGY: data = await self._update_synology() + elif self.provider == PROVIDER_NEXTCLOUD: + data = await self._update_nextcloud() else: raise UpdateFailed(f"Unsupported provider: {self.provider}") except UpdateFailed: @@ -964,7 +1021,7 @@ class AlbumCoordinator(DataUpdateCoordinator): raise items = data.get("items") or [] - if self.provider in (PROVIDER_LOCAL_FOLDER, PROVIDER_IMMICH) and items: + if self.provider in (PROVIDER_LOCAL_FOLDER, PROVIDER_IMMICH, PROVIDER_NEXTCLOUD) and items: # Carry forward EXIF/geocode metadata for items we've already # scanned this session; new items get filled in by the # background worker below. @@ -1541,6 +1598,9 @@ class AlbumCoordinator(DataUpdateCoordinator): device_id = self.entry.data.get(CONF_SYNOLOGY_DEVICE_ID) space = self.entry.data.get(CONF_SYNOLOGY_SPACE, SYNOLOGY_SPACE_PERSONAL) album_id = self.entry.data.get(CONF_SYNOLOGY_ALBUM_ID) + passphrase = self.entry.data.get(CONF_SYNOLOGY_PASSPHRASE) + favorite_only = bool(self.entry.data.get(CONF_SYNOLOGY_FAVORITE)) + selection_raw = self.entry.data.get(CONF_SYNOLOGY_SELECTION) size = self.entry.data.get( CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE ) @@ -1557,7 +1617,23 @@ class AlbumCoordinator(DataUpdateCoordinator): ) try: await client.async_login() - photos = await client.async_collect_assets(album_id or None) + if selection_raw: + # Composite selection (albums + people + places + tags + + # subjects + favorites), merged client-side. + try: + selection = json.loads(selection_raw) + except (TypeError, ValueError): + selection = {} + photos = await client.async_collect_composite(selection) + else: + # Legacy single-source entries (favorites / one album / all). + photos = await client.async_collect_assets( + album_id or None, + passphrase=passphrase or None, + favorite_only=favorite_only, + ) + except syn_api.SynologyPermissionError as err: + raise UpdateFailed(str(err)) from err except Exception as err: raise UpdateFailed(f"Error querying Synology Photos: {err}") from err @@ -1577,10 +1653,19 @@ class AlbumCoordinator(DataUpdateCoordinator): continue unit_id, cache_key = ref meta = syn_api.parse_photo_meta(p) + # Items pulled from a shared-with-me album carry their own + # passphrase (composite path); fall back to the single-album + # passphrase for legacy entries. + item_pp = p.get("_passphrase") or passphrase or None items.append( MediaItem( url=syn_api.build_thumbnail_url( - client.base_url, unit_id, cache_key, size, space + client.base_url, + unit_id, + cache_key, + size, + space, + passphrase=item_pp, ), width=meta.get("width"), height=meta.get("height"), @@ -1605,6 +1690,143 @@ class AlbumCoordinator(DataUpdateCoordinator): "items": items, } + async def _update_nextcloud(self) -> dict[str, Any]: + """List photos from an authenticated Nextcloud WebDAV folder. + + The PROPFIND listing carries filename/size/content-type/mtime but no + EXIF, so capture date, GPS and description are filled in afterwards by + the background enrichment worker (one original-file download per photo - + Nextcloud has no metadata-only endpoint the way Immich does). The app + password is sent server-side only via the coordinator's image headers. + """ + from . import nextcloud as nc_api + + url = self.entry.data.get(CONF_NEXTCLOUD_URL) + username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME) + password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD) + folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or "" + recursive = bool(self.entry.data.get(CONF_NEXTCLOUD_RECURSIVE, False)) + size = self.entry.data.get( + CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE + ) + if not url or not username or not password: + raise UpdateFailed("Nextcloud provider is missing URL or credentials") + + client = nc_api.NextcloudClient(self.hass, url, username, password, folder) + try: + photos = await client.async_list_photos(recursive=recursive) + except Exception as err: + raise UpdateFailed(f"Error listing Nextcloud folder: {err}") from err + + if not photos: + raise UpdateFailed("No images found in the Nextcloud folder") + + # The camera fetches image bytes server-side with this Basic-auth + # header, so the app password never appears in the browser URL. + self.image_request_headers = dict(client.image_headers) + + items: list[MediaItem] = [] + for p in photos: + href = p.get("href") + if not href: + continue + if size != NEXTCLOUD_IMAGE_ORIGINAL and p.get("file_id"): + display_url = nc_api.build_preview_url( + client.base_url, p["file_id"], NEXTCLOUD_PREVIEW_PX + ) + else: + display_url = href + items.append( + MediaItem( + url=display_url, + width=None, + height=None, + mime_type=p.get("content_type"), + filename=p.get("filename"), + uploaded_at=p.get("mtime_ms"), + byte_size=p.get("size"), + source_id=p.get("file_id") or href, + ) + ) + + return { + "title": self.entry.title, + "items": items, + } + + async def _enrich_nextcloud_item(self, item: MediaItem) -> None: + """Download one Nextcloud photo's original bytes and read its EXIF. + + Nextcloud's WebDAV folder has no metadata-only endpoint (unlike + Immich's per-asset detail call), so enrichment costs one full-file + download per photo regardless of the display quality configured. + """ + from . import nextcloud as nc_api + from urllib.parse import quote + + url = self.entry.data.get(CONF_NEXTCLOUD_URL) + username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME) + password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD) + folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or "" + if not username or not password or not url: + item.exif_scanned = True + return + + # Reconstruct the original-file URL: for preview items the display url + # is the preview endpoint, so fall back to the folder href by filename. + original_url = None + if isinstance(item.url, str) and "/remote.php/dav/files/" in item.url: + original_url = item.url + elif item.filename: + client = nc_api.NextcloudClient(self.hass, url, username, password, folder) + original_url = client.dav_root + quote(item.filename) + if not original_url: + item.exif_scanned = True + return + + headers = { + "Authorization": nc_api.basic_auth_header(username, password) + } + session = async_get_clientsession(self.hass) + try: + async with async_timeout.timeout(30): + async with session.get(original_url, headers=headers) as resp: + resp.raise_for_status() + chunks: list[bytes] = [] + total = 0 + async for chunk in resp.content.iter_chunked(64 * 1024): + total += len(chunk) + if total > _NEXTCLOUD_ENRICH_MAX_BYTES: + _LOGGER.debug( + "Nextcloud: %s exceeded %d byte enrichment cap; skipping", + item.filename, _NEXTCLOUD_ENRICH_MAX_BYTES, + ) + item.exif_scanned = True + return + chunks.append(chunk) + data = b"".join(chunks) + except asyncio.CancelledError: + raise + except Exception as err: # noqa: BLE001 + _LOGGER.debug( + "Nextcloud: failed to download %s for enrichment: %s", + item.filename, err, + ) + item.exif_scanned = True + return + + info = await self.hass.async_add_executor_job( + _read_exif_from_bytes, data, item.uploaded_at + ) + if "captured_at" in info: + item.captured_at = info["captured_at"] + if "description" in info: + item.description = info["description"] + if "latitude" in info and "longitude" in info: + item.latitude = info["latitude"] + item.longitude = info["longitude"] + item.exif_scanned = True + async def _enrich_immich_item(self, item: MediaItem) -> None: """Fetch one Immich asset's detail and fill location/description.""" from . import immich as immich_api @@ -1677,6 +1899,24 @@ class AlbumCoordinator(DataUpdateCoordinator): self.async_set_updated_data(data) continue + if self.provider == PROVIDER_NEXTCLOUD: + try: + await self._enrich_nextcloud_item(item) + except asyncio.CancelledError: + raise + except Exception as err: # noqa: BLE001 + _LOGGER.debug("Nextcloud enrich error: %s", err) + item.exif_scanned = True + scanned_since_save += 1 + self._enrich_progress["exif_done"] = ( + self._enrich_progress.get("exif_done", 0) + 1 + ) + if scanned_since_save >= _EXIF_BATCH_SAVE: + scanned_since_save = 0 + await self._save_cached_items(data) + self.async_set_updated_data(data) + continue + url = item.url if not url.startswith("file://"): item.exif_scanned = True diff --git a/custom_components/album_slideshow/manifest.json b/custom_components/album_slideshow/manifest.json index e3d5ebf..5a5b013 100644 --- a/custom_components/album_slideshow/manifest.json +++ b/custom_components/album_slideshow/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/eyalgal/album_slideshow/issues", "requirements": ["Pillow"], - "version": "1.5.0" + "version": "1.6.0" } diff --git a/custom_components/album_slideshow/strings.json b/custom_components/album_slideshow/strings.json index 3f42f14..cf34134 100644 --- a/custom_components/album_slideshow/strings.json +++ b/custom_components/album_slideshow/strings.json @@ -87,7 +87,7 @@ }, "synology": { "title": "Synology Photos", - "description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.", + "description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Choose Personal for your own photos and albums (this is also where albums shared with you appear); choose Shared Space only if your NAS has the shared team library enabled. Tip: use a dedicated Photos account rather than an admin login.", "data": { "synology_url": "DSM URL", "synology_username": "Username", @@ -98,12 +98,30 @@ }, "synology_select": { "title": "Synology source", - "description": "Pick an album or show the whole library, and choose the image quality.", + "description": "Choose what to show. Tick any mix of favorites, albums, people, places, tags and subjects; they are combined into one slideshow. Leave everything empty to show all photos in this space.", "data": { "album_name": "Album name", - "album": "Album", + "favorites": "Favorites", + "albums": "Albums", + "people": "People", + "places": "Places", + "tags": "Tags", + "subjects": "Subjects", "synology_image_size": "Image quality" } + }, + "nextcloud": { + "title": "Nextcloud folder", + "description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.", + "data": { + "album_name": "Album name", + "nextcloud_url": "Server URL", + "nextcloud_username": "Username", + "nextcloud_password": "App password", + "nextcloud_folder": "Folder path (optional)", + "nextcloud_recursive": "Include subfolders", + "nextcloud_image_size": "Image quality" + } } }, "error": { @@ -122,13 +140,15 @@ "invalid_icloud_url": "That does not look like an iCloud Shared Album link.", "icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.", "synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.", - "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue." + "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.", + "synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.", + "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path." } }, "options": { "step": { "init": { - "title": "Local Folder options", + "title": "Location & privacy options", "description": "Reverse geocoding sends your photos' EXIF GPS coordinates to the public OpenStreetMap Nominatim service to look up a human-readable place name. Coordinates are rounded to ~100 m before lookup and cached on disk. Turn this off to keep coordinates entirely local; the latitude and longitude attributes still work either way.", "data": { "reverse_geocode": "Reverse-geocode EXIF GPS coordinates via OpenStreetMap" diff --git a/custom_components/album_slideshow/translations/en.json b/custom_components/album_slideshow/translations/en.json index 3f42f14..cf34134 100644 --- a/custom_components/album_slideshow/translations/en.json +++ b/custom_components/album_slideshow/translations/en.json @@ -87,7 +87,7 @@ }, "synology": { "title": "Synology Photos", - "description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.", + "description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Choose Personal for your own photos and albums (this is also where albums shared with you appear); choose Shared Space only if your NAS has the shared team library enabled. Tip: use a dedicated Photos account rather than an admin login.", "data": { "synology_url": "DSM URL", "synology_username": "Username", @@ -98,12 +98,30 @@ }, "synology_select": { "title": "Synology source", - "description": "Pick an album or show the whole library, and choose the image quality.", + "description": "Choose what to show. Tick any mix of favorites, albums, people, places, tags and subjects; they are combined into one slideshow. Leave everything empty to show all photos in this space.", "data": { "album_name": "Album name", - "album": "Album", + "favorites": "Favorites", + "albums": "Albums", + "people": "People", + "places": "Places", + "tags": "Tags", + "subjects": "Subjects", "synology_image_size": "Image quality" } + }, + "nextcloud": { + "title": "Nextcloud folder", + "description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.", + "data": { + "album_name": "Album name", + "nextcloud_url": "Server URL", + "nextcloud_username": "Username", + "nextcloud_password": "App password", + "nextcloud_folder": "Folder path (optional)", + "nextcloud_recursive": "Include subfolders", + "nextcloud_image_size": "Image quality" + } } }, "error": { @@ -122,13 +140,15 @@ "invalid_icloud_url": "That does not look like an iCloud Shared Album link.", "icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.", "synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.", - "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue." + "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.", + "synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.", + "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path." } }, "options": { "step": { "init": { - "title": "Local Folder options", + "title": "Location & privacy options", "description": "Reverse geocoding sends your photos' EXIF GPS coordinates to the public OpenStreetMap Nominatim service to look up a human-readable place name. Coordinates are rounded to ~100 m before lookup and cached on disk. Turn this off to keep coordinates entirely local; the latitude and longitude attributes still work either way.", "data": { "reverse_geocode": "Reverse-geocode EXIF GPS coordinates via OpenStreetMap" diff --git a/custom_components/album_slideshow/www/album-slideshow-card.js b/custom_components/album_slideshow/www/album-slideshow-card.js index b2f5000..fd8dc47 100644 --- a/custom_components/album_slideshow/www/album-slideshow-card.js +++ b/custom_components/album_slideshow/www/album-slideshow-card.js @@ -26,7 +26,7 @@ * tap_action: none # none | more-info */ -const VERSION = "1.5.0"; +const VERSION = "1.6.0"; const ANIMATED_TRANSITIONS = [ "fade", diff --git a/custom_components/midea_ac_lan/__init__.py b/custom_components/midea_ac_lan/__init__.py index d3b3b54..9ed3e3c 100644 --- a/custom_components/midea_ac_lan/__init__.py +++ b/custom_components/midea_ac_lan/__init__.py @@ -32,13 +32,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from midealocal.device import DeviceType, MideaDevice, ProtocolVersion from midealocal.devices import device_selector +from midealocal.discover import discover from .const import ( ALL_PLATFORM, CONF_ACCOUNT, CONF_KEY, + CONF_MAC, CONF_MODEL, CONF_REFRESH_INTERVAL, + CONF_SN, CONF_SUBTYPE, DEVICES, DOMAIN, @@ -76,7 +79,7 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non dev.set_refresh_interval(refresh_interval) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ARG001 +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # ruff:ignore[unused-function-argument] """Set up midea_lan component when load this integration. Returns @@ -91,13 +94,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: "dict", device_entities["entities"], ).items(): + attribute_key = ( + attribute_name + if isinstance(attribute_name, str) + else attribute_name.value + ) if ( attribute.get("type") in EXTRA_SWITCH - and attribute_name.value not in attributes + and attribute_key not in attributes ): - attributes.append(attribute_name.value) + attributes.append(attribute_key) - def service_set_attribute(service: Any) -> None: # noqa: ANN401 + def service_set_attribute(service: Any) -> None: # ruff:ignore[any-type] """Set service attribute func.""" device_id: int = service.data["device_id"] attr = service.data["attribute"] @@ -126,7 +134,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: attr, ) - def service_send_command(service: Any) -> None: # noqa: ANN401 + def service_send_command(service: Any) -> None: # ruff:ignore[any-type] """Send command to service func.""" device_id = service.data.get("device_id") cmd_type = service.data.get("cmd_type") @@ -204,6 +212,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b subtype = config_entry.data.get(CONF_SUBTYPE, 0) protocol: ProtocolVersion = ProtocolVersion(config_entry.data[CONF_PROTOCOL]) customize: str = config_entry.options.get(CONF_CUSTOMIZE, "") + mac: str | None = config_entry.data.get(CONF_MAC) + serial_number: str | None = config_entry.data.get(CONF_SN) if protocol == ProtocolVersion.V3 and (key == "" or token == ""): _LOGGER.error("For V3 devices, the key and the token is required") return False @@ -223,6 +233,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b model, subtype, customize, + mac, + serial_number, ) # hass core version < 2024.3 else: @@ -238,6 +250,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b model=model, subtype=subtype, customize=customize, + mac=mac, + serial_number=serial_number, ) if device: if refresh_interval is not None: @@ -306,6 +320,80 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> _LOGGER.debug("Migration to configuration version 2 successful") + # 2.1 -> 2.2: backfill mac address and serial number for existing devices + entry_version_2 = 2 + if config_entry.version == entry_version_2 and config_entry.minor_version == 1: + _LOGGER.debug("Migrating configuration from version 2.1") + + if await _async_backfill_mac_and_sn(hass, config_entry): + if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 3): + hass.config_entries.async_update_entry(config_entry, minor_version=2) + else: + config_entry.minor_version = 2 + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.debug("Migration to configuration version 2.2 successful") + else: + _LOGGER.debug( + "Device %s did not respond to discovery;" + " mac/serial number migration will be retried on next start", + config_entry.data.get(CONF_DEVICE_ID), + ) + + return True + + +async def _async_backfill_mac_and_sn( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> bool: + """Best-effort discovery to backfill mac address and serial number. + + Returns + ------- + True if the entry already had the data, has no IP/device id to look up, + or was successfully updated. False if the device did not answer + discovery and the migration should be retried on a later start. + + """ + if config_entry.data.get(CONF_TYPE) == CONF_ACCOUNT: + return True + if config_entry.data.get(CONF_MAC) or config_entry.data.get(CONF_SN): + return True + # Honor an IP override set via the options flow (e.g. after a DHCP change), + # mirroring async_setup_entry; the data IP may be stale. + ip_address = config_entry.options.get(CONF_IP_ADDRESS) + if ip_address is None: + ip_address = config_entry.data.get(CONF_IP_ADDRESS) + device_id = config_entry.data.get(CONF_DEVICE_ID) + if ip_address is None or device_id is None: + return True + try: + found_devices = await hass.async_add_executor_job( + lambda: discover(ip_address=ip_address), + ) + except Exception: + # Best-effort migration: any discovery failure (socket error, malformed + # reply from a legacy device, parse error) must not break entry setup. + # Retry on the next start instead. + _LOGGER.debug( + "Discovery for device %s failed; mac/serial number migration" + " will be retried on next start", + device_id, + exc_info=True, + ) + return False + device = found_devices.get(int(device_id)) + if device is None: + return False + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + CONF_MAC: device.get(CONF_MAC), + CONF_SN: device.get(CONF_SN), + }, + ) return True diff --git a/custom_components/midea_ac_lan/binary_sensor.py b/custom_components/midea_ac_lan/binary_sensor.py index f956d4d..3eb8314 100644 --- a/custom_components/midea_ac_lan/binary_sensor.py +++ b/custom_components/midea_ac_lan/binary_sensor.py @@ -30,9 +30,15 @@ async def async_setup_entry( "dict", MIDEA_DEVICES[device.device_type]["entities"], ).items(): - if config["type"] == Platform.BINARY_SENSOR and entity_key in extra_sensors: - sensor = MideaBinarySensor(device, entity_key) - binary_sensors.append(sensor) + if config["type"] != Platform.BINARY_SENSOR or entity_key not in extra_sensors: + continue + required_attribute = config.get("required_attribute") + if ( + required_attribute is not None + and required_attribute not in device.attributes + ): + continue + binary_sensors.append(MideaBinarySensor(device, entity_key)) async_add_entities(binary_sensors) @@ -41,10 +47,10 @@ class MideaBinarySensor(MideaEntity, BinarySensorEntity): @property def device_class(self) -> BinarySensorDeviceClass | None: - """Return device class.""" + """Device class of the binary sensor.""" return cast("BinarySensorDeviceClass", self._config.get("device_class")) @property def is_on(self) -> bool: - """Return true if sensor state is on.""" + """Whether the binary sensor is on.""" return cast("bool", self._device.get_attribute(self._entity_key)) diff --git a/custom_components/midea_ac_lan/climate.py b/custom_components/midea_ac_lan/climate.py index 6024ba9..db11bc3 100644 --- a/custom_components/midea_ac_lan/climate.py +++ b/custom_components/midea_ac_lan/climate.py @@ -1,5 +1,6 @@ """Midea Climate entries.""" +import json import logging from typing import Any, ClassVar, cast @@ -27,6 +28,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_CUSTOMIZE, CONF_DEVICE_ID, CONF_SWITCHES, MAJOR_VERSION, @@ -179,15 +181,15 @@ class MideaClimate(MideaEntity, ClimateEntity): """Midea Climate extra state attributes.""" return cast("dict", self._device.attributes) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Climate turn on.""" self._device.set_attribute(attr="power", value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Climate turn off.""" self._device.set_attribute(attr="power", value=False) - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea Climate set temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -241,7 +243,7 @@ class MideaClimate(MideaEntity, ClimateEntity): elif old_mode == PRESET_BOOST: self._device.set_attribute(attr="boost_mode", value=False) - def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002 + def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Climate update state.""" if not self.hass: _LOGGER.warning( @@ -250,7 +252,7 @@ class MideaClimate(MideaEntity, ClimateEntity): type(self), ) return - self.schedule_update_ha_state() + self.schedule_update_if_running() class MideaACClimate(MideaClimate): @@ -266,7 +268,9 @@ class MideaACClimate(MideaClimate): ) -> None: """Midea AC Climate entity init.""" super().__init__(device, entity_key) - self._attr_hvac_modes = [ + # fixed positional map: device "mode" int -> HVACMode. DO NOT reorder, + # the device mode value indexes into this list. + self._mode_index = [ HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, @@ -274,6 +278,14 @@ class MideaACClimate(MideaClimate): HVACMode.HEAT, HVACMode.FAN_ONLY, ] + self._attr_hvac_modes = list(self._mode_index) + # customize overrides parsed once; B5 capabilities read dynamically + # (they arrive after the first refresh, so hvac_modes/supported_features + # are computed lazily — priority: customize > B5 > defaults) + self._customize_swing: bool | None = None + self._customize_hvac_modes: list[HVACMode] | None = None + self._customize_preset_modes: list[str] | None = None + self._parse_capability_customize(config_entry) self._fan_speeds: dict[str, int] = { FAN_SILENT: 20, FAN_LOW: 40, @@ -304,6 +316,176 @@ class MideaACClimate(MideaClimate): and "indoor_humidity" in config_entry.options["sensors"] ) + def _parse_capability_customize(self, config_entry: ConfigEntry) -> None: + """Parse the swing / hvac_modes customize overrides (highest priority). + + ``{"swing": false, "hvac_modes": ["off", "cool", "dry", "fan_only"]}`` + """ + customize = config_entry.options.get(CONF_CUSTOMIZE, "") + if not customize: + return + try: + params = json.loads(customize) + except (ValueError, TypeError): + return + if not isinstance(params, dict): + return + if "swing" in params: + self._customize_swing = bool(params["swing"]) + modes = params.get("hvac_modes") + if isinstance(modes, list): + valid = {mode.value: mode for mode in self._mode_index} + wanted: list[HVACMode] = [] + for name in modes: + hvac = valid.get(str(name)) + if hvac is not None and hvac not in wanted: + wanted.append(hvac) + if HVACMode.OFF not in wanted: + wanted.insert(0, HVACMode.OFF) + if any(hvac != HVACMode.OFF for hvac in wanted): + self._customize_hvac_modes = wanted + presets = params.get("preset_modes") + if isinstance(presets, list): + valid_presets = { + PRESET_NONE, + PRESET_COMFORT, + PRESET_ECO, + PRESET_BOOST, + PRESET_SLEEP, + PRESET_AWAY, + } + wanted_p: list[str] = [] + for name in presets: + preset = str(name) + if preset in valid_presets and preset not in wanted_p: + wanted_p.append(preset) + if PRESET_NONE not in wanted_p: + wanted_p.insert(0, PRESET_NONE) + # honor even an empty / none-only list (user wants no presets) + self._customize_preset_modes = wanted_p + + def _capability_swing(self) -> bool: + """Whether swing is available (customize > B5 capability > default). + + Returns: + ``True`` when the swing control should be exposed. + + """ + if self._customize_swing is not None: + return self._customize_swing + caps = getattr(self._device, "capabilities", {}) + if "swing_vertical" in caps or "swing_horizontal" in caps: + return bool(caps.get("swing_vertical") or caps.get("swing_horizontal")) + return True + + @property + def hvac_modes(self) -> list[HVACMode]: + """hvac_modes: customize > B5 capabilities > default full set. + + Read dynamically so capabilities decoded after the first refresh are + reflected (they are not yet available when the entity is created). + """ + if self._customize_hvac_modes is not None: + return self._customize_hvac_modes + caps = getattr(self._device, "capabilities", {}) + if not caps: + return list(self._mode_index) + modes = [HVACMode.OFF] + if caps.get("auto_mode"): + modes.append(HVACMode.AUTO) + if caps.get("cool_mode"): + modes.append(HVACMode.COOL) + if caps.get("dry_mode"): + modes.append(HVACMode.DRY) + if caps.get("heat_mode"): + modes.append(HVACMode.HEAT) + # fan-only is always available on AC devices + modes.append(HVACMode.FAN_ONLY) + return modes + + @property + def preset_modes(self) -> list[str]: + """preset_modes: customize > B5 capabilities > default full set. + + B5 only declares eco and turbo (and heat implies away). comfort and + sleep have no capability flag, so they are kept unless overridden via + the customize ``preset_modes`` option. + """ + all_presets = [ + PRESET_NONE, + PRESET_COMFORT, + PRESET_ECO, + PRESET_BOOST, + PRESET_SLEEP, + PRESET_AWAY, + ] + if self._customize_preset_modes is not None: + return self._customize_preset_modes + caps = getattr(self._device, "capabilities", {}) + if not caps: + return all_presets + keep = { + PRESET_NONE: True, + PRESET_COMFORT: True, + PRESET_ECO: bool(caps.get("eco")), + PRESET_BOOST: bool(caps.get("turbo_cool") or caps.get("turbo_heat")), + PRESET_SLEEP: True, + PRESET_AWAY: bool(caps.get("heat_mode")), + } + return [preset for preset in all_presets if keep[preset]] + + @property + def supported_features(self) -> ClimateEntityFeature: + """Midea AC Climate supported features.""" + features = super().supported_features + if not self._capability_swing(): + features &= ~ClimateEntityFeature.SWING_MODE + # drop the preset control entirely when no real preset is available + if not any(preset != PRESET_NONE for preset in self.preset_modes): + features &= ~ClimateEntityFeature.PRESET_MODE + return features + + @property + def hvac_mode(self) -> HVACMode: + """Midea AC Climate hvac mode (device mode int -> fixed map).""" + if self._device.get_attribute("power"): + mode = cast("int", self._device.get_attribute("mode")) + return self._mode_index[mode] + return HVACMode.OFF + + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Midea AC Climate set hvac mode (via the fixed map index).""" + if hvac_mode == HVACMode.OFF: + self.turn_off() + else: + self._device.set_attribute( + attr="mode", + value=self._mode_index.index(hvac_mode), + ) + + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] + """Midea AC Climate set temperature (raw mode via the fixed map). + + ``hvac_modes`` is a filtered subset here, so the raw protocol mode must + come from the fixed ``_mode_index`` and not from the displayed list. + """ + if ATTR_TEMPERATURE not in kwargs: + return + temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2 + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + if hvac_mode == HVACMode.OFF: + self.turn_off() + else: + try: + mode = self._mode_index.index(hvac_mode.lower()) if hvac_mode else None + self._device.set_target_temperature( + target_temperature=temperature, + mode=mode, + zone=None, + ) + except ValueError: + _LOGGER.exception("Error setting temperature with: %s", kwargs) + @property def fan_mode(self) -> str: """Midea AC Climate fan mode.""" @@ -320,6 +502,28 @@ class MideaACClimate(MideaClimate): return str(FAN_LOW) return str(FAN_SILENT) + @property + def fan_modes(self) -> list[str] | None: + """fan_modes restricted to the speeds the device reports (B5). + + Falls back to the full set when no capability is reported. + """ + caps = getattr(self._device, "capabilities", {}) + if not caps: + return list(self._fan_speeds.keys()) + cap_by_fan = { + FAN_SILENT: "fan_silent", + FAN_LOW: "fan_low", + FAN_MEDIUM: "fan_medium", + FAN_HIGH: "fan_high", + FAN_FULL_SPEED: "fan_custom", + FAN_AUTO: "fan_auto", + } + modes = [ + name for name in self._fan_speeds if caps.get(cap_by_fan.get(name, "")) + ] + return modes or list(self._fan_speeds.keys()) + @property def target_temperature_step(self) -> float: """Midea AC Climate target temperature step.""" @@ -337,7 +541,7 @@ class MideaACClimate(MideaClimate): @property def current_humidity(self) -> float | None: - """Return the current indoor humidity, or None if unavailable.""" + """Current indoor humidity, or None if unavailable.""" # fix error humidity, disable indoor_humidity in web UI # https://github.com/wuwentao/midea_ac_lan/pull/641 if not self._indoor_humidity_enabled: @@ -357,6 +561,22 @@ class MideaACClimate(MideaClimate): self._device.get_attribute(ACAttributes.outdoor_temperature), ) + @property + def min_temp(self) -> float: + """Midea AC Climate min temperature (from device capability).""" + value = self._device.get_attribute(ACAttributes.min_temperature) + if value is None: + return TEMPERATURE_MIN + return cast("float", value) + + @property + def max_temp(self) -> float: + """Midea AC Climate max temperature (from device capability).""" + value = self._device.get_attribute(ACAttributes.max_temperature) + if value is None: + return TEMPERATURE_MAX + return cast("float", value) + def set_fan_mode(self, fan_mode: str) -> None: """Midea AC Climate set fan mode.""" fan_speed = self._fan_speeds.get(fan_mode) @@ -572,11 +792,11 @@ class MideaC3Climate(MideaClimate): self._temperature(False)[self._zone], ) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea C3 Climate turn on.""" self._device.set_attribute(attr=self._power_attr, value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea C3 Climate turn off.""" self._device.set_attribute(attr=self._power_attr, value=False) @@ -605,7 +825,7 @@ class MideaC3Climate(MideaClimate): """Midea C3 Climate current temperature.""" return None - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea C3 Climate set temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -681,7 +901,7 @@ class MideaFBClimate(MideaClimate): self._device.get_attribute(FBAttributes.current_temperature), ) - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea FB Climate set temperature.""" if ATTR_TEMPERATURE not in kwargs: return diff --git a/custom_components/midea_ac_lan/config_flow.py b/custom_components/midea_ac_lan/config_flow.py index 456f641..b5a5a8a 100644 --- a/custom_components/midea_ac_lan/config_flow.py +++ b/custom_components/midea_ac_lan/config_flow.py @@ -70,13 +70,17 @@ else: from .const import ( CONF_ACCOUNT, CONF_KEY, + CONF_MAC, CONF_MODEL, CONF_REFRESH_INTERVAL, CONF_SERVER, + CONF_SN, CONF_SUBTYPE, + DEVICES, DOMAIN, EXTRA_CONTROL, EXTRA_SENSOR, + supports_model, ) from .midea_devices import MIDEA_DEVICES @@ -105,7 +109,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg] """ VERSION = 2 - MINOR_VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """MideaLanConfigFlow class.""" @@ -140,7 +144,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg] record_file = storage_path.joinpath(f"{data[CONF_DEVICE_ID]!s}.json") save_json(str(record_file), data) - def _load_device_config(self, device_id: str) -> Any: # noqa: ANN401 + def _load_device_config(self, device_id: str) -> Any: # ruff:ignore[any-type] """Load device config from json file with device id. Returns @@ -798,6 +802,8 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg] CONF_SUBTYPE: user_input[CONF_SUBTYPE], CONF_TOKEN: user_input[CONF_TOKEN], CONF_KEY: user_input[CONF_KEY], + CONF_MAC: device.get(CONF_MAC), + CONF_SN: device.get(CONF_SN), } # save device json config when adding new device self._save_device_config(data) @@ -937,13 +943,38 @@ class MideaLanOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title="", data=user_input) sensors = {} switches = {} + device_id = self._config_entry.data.get(CONF_DEVICE_ID) + device = ( + self.hass.data.get(DOMAIN, {}).get(DEVICES, {}).get(device_id) + if device_id is not None + else None + ) + selected_attributes = set( + self._config_entry.options.get(CONF_SENSORS, []), + ) | set(self._config_entry.options.get(CONF_SWITCHES, [])) for attribute, attribute_config in cast( "dict", MIDEA_DEVICES[cast("int", self._device_type)]["entities"], ).items(): + if not supports_model( + self._config_entry.data.get(CONF_MODEL), + attribute_config, + ): + continue attribute_name = ( attribute if isinstance(attribute, str) else attribute.value ) + required_attribute = attribute_config.get("required_attribute") + if ( + required_attribute is not None + and ( + device is None + or required_attribute not in device.attributes + or device.get_attribute(required_attribute) is None + ) + and attribute_name not in selected_attributes + ): + continue if attribute_config.get("type") in EXTRA_SENSOR: sensors[attribute_name] = attribute_config.get("name") elif attribute_config.get( diff --git a/custom_components/midea_ac_lan/const.py b/custom_components/midea_ac_lan/const.py index 9a21b78..2299e29 100644 --- a/custom_components/midea_ac_lan/const.py +++ b/custom_components/midea_ac_lan/const.py @@ -1,6 +1,7 @@ """Const for Midea Lan.""" from enum import IntEnum +from typing import Any, cast from homeassistant.const import Platform @@ -14,10 +15,19 @@ CONF_SUBTYPE = "subtype" CONF_ACCOUNT = "account" CONF_SERVER = "server" CONF_REFRESH_INTERVAL = "refresh_interval" +CONF_MAC = "mac" +CONF_SN = "sn" EXTRA_SENSOR = [Platform.SENSOR, Platform.BINARY_SENSOR] -EXTRA_SWITCH = [Platform.SWITCH, Platform.LOCK, Platform.SELECT, Platform.NUMBER] +EXTRA_SWITCH = [ + Platform.SWITCH, + Platform.LOCK, + Platform.SELECT, + Platform.NUMBER, + Platform.TIME, +] EXTRA_CONTROL = [ + Platform.BUTTON, Platform.CLIMATE, Platform.WATER_HEATER, Platform.FAN, @@ -28,6 +38,18 @@ EXTRA_CONTROL = [ ALL_PLATFORM = EXTRA_SENSOR + EXTRA_CONTROL +def supports_model(model: object, config: dict[str, Any]) -> bool: + """Return if the entity config applies to the device model. + + Returns + ------- + True if the entity is available for the device model. + + """ + models = config.get("models") + return not models or str(model) in cast("list[str]", models) + + class FanSpeed(IntEnum): """FanSpeed reference values.""" diff --git a/custom_components/midea_ac_lan/diagnostics.py b/custom_components/midea_ac_lan/diagnostics.py index 3ecd6f3..3cf1151 100644 --- a/custom_components/midea_ac_lan/diagnostics.py +++ b/custom_components/midea_ac_lan/diagnostics.py @@ -19,7 +19,7 @@ TO_REDACT = {CONF_TOKEN, CONF_KEY} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, # noqa: ARG001 + hass: HomeAssistant, # ruff:ignore[unused-function-argument] entry: ConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry. diff --git a/custom_components/midea_ac_lan/fan.py b/custom_components/midea_ac_lan/fan.py index 5f6bde3..d059d5c 100644 --- a/custom_components/midea_ac_lan/fan.py +++ b/custom_components/midea_ac_lan/fan.py @@ -23,7 +23,13 @@ from midealocal.devices.x40 import DeviceAttributes as X40Attributes from midealocal.devices.x40 import MideaX40Device from .const import DEVICES, DOMAIN -from .midea_devices import MIDEA_DEVICES +from .midea_devices import ( + FRESH_AIR_EXHAUST, + FRESH_AIR_EXHAUST_MODE, + FRESH_AIR_EXHAUST_POWER, + FRESH_AIR_EXHAUST_SPEED, + MIDEA_DEVICES, +) from .midea_entity import MideaEntity _LOGGER = logging.getLogger(__name__) @@ -45,6 +51,12 @@ async def async_setup_entry( "dict", MIDEA_DEVICES[device.device_type]["entities"], ).items(): + required_attribute = config.get("required_attribute") + if ( + required_attribute is not None + and required_attribute not in device.attributes + ): + continue if config["type"] == Platform.FAN and ( config.get("default") or entity_key in extra_switches ): @@ -100,7 +112,7 @@ class MideaFan(MideaEntity, FanEntity): """Midea Fan fan speed.""" return cast("int", self._device.get_attribute("fan_speed")) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Fan turn off.""" self._device.set_attribute(attr="power", value=False) @@ -117,7 +129,7 @@ class MideaFan(MideaEntity, FanEntity): """Midea Fan percentage.""" if not self.fan_speed: return None - return int(round(self.fan_speed * self.percentage_step)) # noqa: RUF046 + return int(round(self.fan_speed * self.percentage_step)) # ruff:ignore[unnecessary-cast-to-int] def set_percentage(self, percentage: int) -> None: """Midea Fan set percentage.""" @@ -131,7 +143,7 @@ class MideaFan(MideaEntity, FanEntity): else: await self.hass.async_add_executor_job(self.set_percentage, percentage) - def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002 + def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Fan update state.""" if not self.hass: _LOGGER.warning( @@ -140,7 +152,7 @@ class MideaFan(MideaEntity, FanEntity): type(self), ) return - self.schedule_update_ha_state() + self.schedule_update_if_running() class MideaFAFan(MideaFan): @@ -163,7 +175,7 @@ class MideaFAFan(MideaFan): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs: Any, # noqa: ANN401, ARG002 + **kwargs: Any, # ruff:ignore[any-type, unused-method-argument] ) -> None: """Midea FA Fan turn on.""" fan_speed = int(percentage / self.percentage_step + 0.5) if percentage else None @@ -189,7 +201,7 @@ class MideaB6Fan(MideaFan): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs: Any, # noqa: ANN401, ARG002 + **kwargs: Any, # ruff:ignore[any-type, unused-method-argument] ) -> None: """Midea B6 Fan turn on.""" fan_speed = int(percentage / self.percentage_step + 0.5) if percentage else None @@ -204,6 +216,7 @@ class MideaACFreshAirFan(MideaFan): def __init__(self, device: MideaACDevice, entity_key: str) -> None: """Midea AC Fresh Air Fan entity init.""" super().__init__(device, entity_key) + self._is_exhaust = entity_key == FRESH_AIR_EXHAUST self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -214,47 +227,89 @@ class MideaACFreshAirFan(MideaFan): @property def preset_modes(self) -> list[str] | None: """Midea AC Fan preset modes.""" + if self._is_exhaust: + return cast( + "list[str]", + getattr(self._device, "fresh_air_exhaust_fan_speeds", []), + ) return cast("list", self._device.fresh_air_fan_speeds) @property def is_on(self) -> bool: """Midea AC Fan is on.""" - return cast("bool", self._device.get_attribute(ACAttributes.fresh_air_power)) + attribute = ( + FRESH_AIR_EXHAUST_POWER + if self._is_exhaust + else ACAttributes.fresh_air_power + ) + return cast("bool", self._device.get_attribute(attribute)) @property def fan_speed(self) -> int: """Midea AC Fan fan speed.""" - return cast("int", self._device.get_attribute(ACAttributes.fresh_air_fan_speed)) + attribute = ( + FRESH_AIR_EXHAUST_SPEED + if self._is_exhaust + else ACAttributes.fresh_air_fan_speed + ) + return cast("int", self._device.get_attribute(attribute)) def turn_on( self, - percentage: int | None = None, # noqa: ARG002 - preset_mode: str | None = None, # noqa: ARG002 - **kwargs: Any, # noqa: ANN401, ARG002 + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, # ruff:ignore[any-type, unused-method-argument] ) -> None: """Midea AC Fan tun on.""" - self._device.set_attribute(attr=ACAttributes.fresh_air_power, value=True) + if preset_mode is not None: + self.set_preset_mode(preset_mode) + return + if percentage is not None: + self.set_percentage(percentage) + return + attribute = ( + FRESH_AIR_EXHAUST_POWER + if self._is_exhaust + else ACAttributes.fresh_air_power + ) + self._device.set_attribute(attr=attribute, value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea AC Fan turn off.""" - self._device.set_attribute(attr=ACAttributes.fresh_air_power, value=False) + attribute = ( + FRESH_AIR_EXHAUST_POWER + if self._is_exhaust + else ACAttributes.fresh_air_power + ) + self._device.set_attribute(attr=attribute, value=False) def set_percentage(self, percentage: int) -> None: """Midea AC Fan set percentage.""" fan_speed = int(percentage / self.percentage_step + 0.5) + attribute = ( + FRESH_AIR_EXHAUST_SPEED + if self._is_exhaust + else ACAttributes.fresh_air_fan_speed + ) self._device.set_attribute( - attr=ACAttributes.fresh_air_fan_speed, + attr=attribute, value=fan_speed, ) def set_preset_mode(self, preset_mode: str) -> None: """Midea AC Fan set preset mode.""" - self._device.set_attribute(attr=ACAttributes.fresh_air_mode, value=preset_mode) + attribute = ( + FRESH_AIR_EXHAUST_MODE if self._is_exhaust else ACAttributes.fresh_air_mode + ) + self._device.set_attribute(attr=attribute, value=preset_mode) @property def preset_mode(self) -> str | None: """Midea AC Fan preset mode.""" - return cast("str", self._device.get_attribute(attr=ACAttributes.fresh_air_mode)) + attribute = ( + FRESH_AIR_EXHAUST_MODE if self._is_exhaust else ACAttributes.fresh_air_mode + ) + return cast("str", self._device.get_attribute(attr=attribute)) class MideaCEFan(MideaFan): @@ -274,9 +329,9 @@ class MideaCEFan(MideaFan): def turn_on( self, - percentage: int | None = None, # noqa: ARG002 - preset_mode: str | None = None, # noqa: ARG002 - **kwargs: Any, # noqa: ANN401, ARG002 + percentage: int | None = None, # ruff:ignore[unused-method-argument] + preset_mode: str | None = None, # ruff:ignore[unused-method-argument] + **kwargs: Any, # ruff:ignore[any-type, unused-method-argument] ) -> None: """Midea CE Fan turn on.""" self._device.set_attribute(attr=CEAttributes.power, value=True) @@ -308,13 +363,13 @@ class MideaX40Fan(MideaFan): def turn_on( self, - percentage: int | None = None, # noqa: ARG002 - preset_mode: str | None = None, # noqa: ARG002 - **kwargs: Any, # noqa: ANN401, ARG002 + percentage: int | None = None, # ruff:ignore[unused-method-argument] + preset_mode: str | None = None, # ruff:ignore[unused-method-argument] + **kwargs: Any, # ruff:ignore[any-type, unused-method-argument] ) -> None: """Midea X40 Fan turn on.""" self._device.set_attribute(attr=X40Attributes.fan_speed, value=1) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea X40 Fan turn off.""" self._device.set_attribute(attr=X40Attributes.fan_speed, value=0) diff --git a/custom_components/midea_ac_lan/humidifier.py b/custom_components/midea_ac_lan/humidifier.py index e12cc35..5326d4d 100644 --- a/custom_components/midea_ac_lan/humidifier.py +++ b/custom_components/midea_ac_lan/humidifier.py @@ -92,15 +92,15 @@ class MideaHumidifier(MideaEntity, HumidifierEntity): """Midea Humidifier is on.""" return cast("bool", self._device.get_attribute(attr="power")) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Humidifier turn on.""" self._device.set_attribute(attr="power", value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Humidifier turn off.""" self._device.set_attribute(attr="power", value=False) - def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002 + def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Humidifier update state.""" if not self.hass: _LOGGER.warning( @@ -109,7 +109,7 @@ class MideaHumidifier(MideaEntity, HumidifierEntity): type(self), ) return - self.schedule_update_ha_state() + self.schedule_update_if_running() class MideaA1Humidifier(MideaHumidifier): diff --git a/custom_components/midea_ac_lan/light.py b/custom_components/midea_ac_lan/light.py index 00f27a6..e8eed2d 100644 --- a/custom_components/midea_ac_lan/light.py +++ b/custom_components/midea_ac_lan/light.py @@ -153,7 +153,7 @@ class MideaLight(MideaEntity, LightEntity): """Midea Light effect.""" return cast("str", self._device.get_attribute(X13Attributes.effect)) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea Light turn on.""" if not self.is_on: self._device.set_attribute(attr=X13Attributes.power, value=True) @@ -168,11 +168,11 @@ class MideaLight(MideaEntity, LightEntity): if key == ATTR_EFFECT: self._device.set_attribute(attr=X13Attributes.effect, value=value) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Light turn off.""" self._device.set_attribute(attr=X13Attributes.power, value=False) - def update_state(self, status: Any) -> None: # noqa: ANN401,ARG002 + def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Light update state.""" if not self.hass: _LOGGER.warning( @@ -181,4 +181,4 @@ class MideaLight(MideaEntity, LightEntity): type(self), ) return - self.schedule_update_ha_state() + self.schedule_update_if_running() diff --git a/custom_components/midea_ac_lan/lock.py b/custom_components/midea_ac_lan/lock.py index e66f974..49518ff 100644 --- a/custom_components/midea_ac_lan/lock.py +++ b/custom_components/midea_ac_lan/lock.py @@ -38,17 +38,17 @@ class MideaLock(MideaEntity, LockEntity): @property def is_locked(self) -> bool: - """Return true if state is locked.""" + """Whether the lock is locked.""" return cast("bool", self._device.get_attribute(self._entity_key)) - def lock(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def lock(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Lock the lock.""" self._device.set_attribute(attr=self._entity_key, value=True) - def unlock(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def unlock(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Unlock the lock.""" self._device.set_attribute(attr=self._entity_key, value=False) - def open(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def open(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Open the lock.""" self.unlock() diff --git a/custom_components/midea_ac_lan/manifest.json b/custom_components/midea_ac_lan/manifest.json index 52f6ee4..5a373e7 100644 --- a/custom_components/midea_ac_lan/manifest.json +++ b/custom_components/midea_ac_lan/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/wuwentao/midea_ac_lan/issues", "loggers": ["midealocal"], - "requirements": ["midea-local==6.8.0"], - "version": "0.6.12" + "requirements": ["midea-local==6.11.0"], + "version": "0.7.0" } diff --git a/custom_components/midea_ac_lan/midea_devices.py b/custom_components/midea_ac_lan/midea_devices.py index 170c43b..4d5f53c 100644 --- a/custom_components/midea_ac_lan/midea_devices.py +++ b/custom_components/midea_ac_lan/midea_devices.py @@ -8,8 +8,12 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, + REVOLUTIONS_PER_MINUTE, Platform, + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfTemperature, UnitOfTime, @@ -50,6 +54,11 @@ from midealocal.devices.x26 import DeviceAttributes as X26Attributes from midealocal.devices.x34 import DeviceAttributes as X34Attributes from midealocal.devices.x40 import DeviceAttributes as X40Attributes +FRESH_AIR_EXHAUST = "fresh_air_exhaust" +FRESH_AIR_EXHAUST_MODE = "fresh_air_exhaust_mode" +FRESH_AIR_EXHAUST_POWER = "fresh_air_exhaust_power" +FRESH_AIR_EXHAUST_SPEED = "fresh_air_exhaust_speed" + """ Entity Naming Rule: @@ -384,6 +393,29 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "name": "Fresh Air", "icon": "mdi:fan", }, + ACAttributes.fresh_air_mode: { + "type": Platform.SELECT, + "required_attribute": ACAttributes.fresh_air_mode, + "translation_key": "fresh_air_mode", + "name": "Fresh Air Speed", + "icon": "mdi:fan-chevron-up", + "options": "fresh_air_fan_speeds", + }, + FRESH_AIR_EXHAUST: { + "type": Platform.FAN, + "translation_key": "fresh_air_exhaust", + "name": "Fresh Air Exhaust", + "icon": "mdi:fan-reverse", + "required_attribute": FRESH_AIR_EXHAUST_POWER, + }, + FRESH_AIR_EXHAUST_MODE: { + "type": Platform.SELECT, + "translation_key": "fresh_air_exhaust_mode", + "name": "Fresh Air Exhaust Speed", + "icon": "mdi:fan-chevron-down", + "options": "fresh_air_exhaust_fan_speeds", + "required_attribute": FRESH_AIR_EXHAUST_POWER, + }, ACAttributes.aux_heating: { "type": Platform.SWITCH, "translation_key": "aux_heating", @@ -578,6 +610,125 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "name": "Error Code", "icon": "mdi:alert-box", }, + # group 1: compressor and refrigerant circuit + ACAttributes.compressor_frequency: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.compressor_frequency, + "translation_key": "compressor_frequency", + "name": "Compressor Frequency", + "device_class": SensorDeviceClass.FREQUENCY, + "unit": UnitOfFrequency.HERTZ, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.target_compressor_frequency: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.target_compressor_frequency, + "translation_key": "target_compressor_frequency", + "name": "Target Compressor Frequency", + "device_class": SensorDeviceClass.FREQUENCY, + "unit": UnitOfFrequency.HERTZ, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.compressor_current: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.compressor_current, + "translation_key": "compressor_current", + "name": "Compressor Current", + "device_class": SensorDeviceClass.CURRENT, + "unit": UnitOfElectricCurrent.AMPERE, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.compressor_voltage: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.compressor_voltage, + "translation_key": "compressor_voltage", + "name": "Compressor Voltage", + "device_class": SensorDeviceClass.VOLTAGE, + "unit": UnitOfElectricPotential.VOLT, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.indoor_coil_temperature: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.indoor_coil_temperature, + "translation_key": "indoor_coil_temperature", + "name": "Indoor Coil Temperature (T1)", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.evaporator_temperature: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.evaporator_temperature, + "translation_key": "evaporator_temperature", + "name": "Evaporator Temperature (T2)", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.condenser_temperature: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.condenser_temperature, + "translation_key": "condenser_temperature", + "name": "Condenser Temperature (T3)", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.outdoor_ambient_temperature: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.outdoor_ambient_temperature, + "translation_key": "outdoor_ambient_temperature", + "name": "Outdoor Ambient Temperature (T4)", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.discharge_pipe_temperature: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.discharge_pipe_temperature, + "translation_key": "discharge_pipe_temperature", + "name": "Discharge Pipe Temperature (TP)", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + }, + # group 2: indoor fan and condensate pump + ACAttributes.indoor_fan_speed: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.indoor_fan_speed, + "translation_key": "indoor_fan_speed", + "name": "Indoor Fan Speed", + "icon": "mdi:fan", + "unit": REVOLUTIONS_PER_MINUTE, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.target_indoor_fan_speed: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.target_indoor_fan_speed, + "translation_key": "target_indoor_fan_speed", + "name": "Target Indoor Fan Speed", + "icon": "mdi:fan-clock", + "unit": REVOLUTIONS_PER_MINUTE, + "state_class": SensorStateClass.MEASUREMENT, + }, + ACAttributes.water_pump_running: { + "type": Platform.BINARY_SENSOR, + "required_attribute": ACAttributes.water_pump_running, + "translation_key": "water_pump_running", + "name": "Water Pump Running", + "icon": "mdi:water-pump", + "device_class": BinarySensorDeviceClass.RUNNING, + }, + # group 7: real time compressor power + ACAttributes.compressor_power: { + "type": Platform.SENSOR, + "required_attribute": ACAttributes.compressor_power, + "translation_key": "compressor_power", + "name": "Compressor Power", + "device_class": SensorDeviceClass.POWER, + "unit": UnitOfPower.WATT, + "state_class": SensorStateClass.MEASUREMENT, + }, ACAttributes.wind_lr_angle: { "type": Platform.SELECT, "translation_key": "wind_lr_angle", @@ -592,6 +743,13 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "options": "wind_ud_angles", "icon": "mdi:pan-vertical", }, + ACAttributes.rate_select: { + "type": Platform.SELECT, + "translation_key": "rate_select", + "name": "Power Rate Limit", + "options": "rate_selects", + "icon": "mdi:lightning-bolt-circle", + }, ACAttributes.fan_speed: { "type": Platform.NUMBER, "translation_key": "fan_speed_percent", @@ -2432,12 +2590,82 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "name": "Storage", "icon": "mdi:repeat-variant", }, + "start": { + "type": Platform.BUTTON, + "translation_key": "start", + "name": "Start", + "icon": "mdi:play", + "set_message": "e1_start", + "available_power_attribute": E1Attributes.power, + "models": ["7600024L"], + "default": True, + }, E1Attributes.mode: { "type": Platform.SENSOR, "translation_key": "mode", "name": "Working Mode", "icon": "mdi:dishwasher", }, + "mode_select": { + "type": Platform.SELECT, + "attribute": E1Attributes.mode, + "translation_key": "wash_mode", + "name": "Wash Mode", + "options_dict": "modes", + "options_codes_by_model": { + "7600024L": [13, 4, 8, 6, 2, 11, 10], + }, + "set_message": "e1_work_mode", + "available_power_attribute": E1Attributes.power, + "icon": "mdi:dishwasher", + "default": True, + }, + "estimated_energy_consumption": { + "type": Platform.SENSOR, + "translation_key": "estimated_energy_consumption", + "name": "Estimated Energy Consumption", + "icon": "mdi:lightning-bolt", + "device_class": SensorDeviceClass.ENERGY, + "unit": UnitOfEnergy.KILO_WATT_HOUR, + "state_class": SensorStateClass.TOTAL_INCREASING, + "estimate": { + "kind": "energy", + "values": { + "Germ": 0.765, + "ECO Wash": 0.99, + "Strong Wash": 1.28, + "Hour Wash": 0.91, + "Soak Wash": 0.02, + "Self Clean": 1.524, + "Fruit Wash": 1.625, + }, + }, + "models": ["7600024L"], + "default": True, + }, + "estimated_water_consumption": { + "type": Platform.SENSOR, + "translation_key": "estimated_water_consumption", + "name": "Estimated Water Consumption", + "icon": "mdi:water", + "device_class": SensorDeviceClass.WATER, + "unit": UnitOfVolume.LITERS, + "state_class": SensorStateClass.TOTAL_INCREASING, + "estimate": { + "kind": "water", + "values": { + "Germ": 9.9, + "ECO Wash": 10.4, + "Strong Wash": 13.9, + "Hour Wash": 10.4, + "Soak Wash": 3.4, + "Self Clean": 10.3, + "Fruit Wash": 13.3, + }, + }, + "models": ["7600024L"], + "default": True, + }, E1Attributes.error_code: { "type": Platform.SENSOR, "translation_key": "error_code", @@ -2536,6 +2764,18 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "name": "Whole Tank Heating", "icon": "mdi:restore", }, + E2Attributes.sterilization: { + "type": Platform.SWITCH, + "translation_key": "sterilization", + "name": "Sterilization", + "icon": "mdi:bacteria", + }, + E2Attributes.memory: { + "type": Platform.SWITCH, + "translation_key": "memory", + "name": "Memo U", + "icon": "mdi:brain", + }, }, }, 0xE3: { @@ -2951,6 +3191,176 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = { "unit": UnitOfVolume.LITERS, "state_class": SensorStateClass.TOTAL_INCREASING, }, + # Soft water machine (water softener) entities + EDAttributes.soften: { + "type": Platform.SWITCH, + "translation_key": "soften", + "name": "Softening", + "icon": "mdi:water-outline", + }, + EDAttributes.cl_sterilization: { + "type": Platform.SWITCH, + "translation_key": "cl_sterilization", + "name": "CL Sterilization", + "icon": "mdi:bacteria", + }, + EDAttributes.leak_water_protection: { + "type": Platform.SWITCH, + "translation_key": "leak_water_protection", + "name": "Leak Water Protection", + "icon": "mdi:water-alert", + }, + EDAttributes.water_way: { + "type": Platform.SWITCH, + "translation_key": "water_way", + "name": "Water Way", + "icon": "mdi:pipe", + }, + EDAttributes.regeneration: { + "type": Platform.SWITCH, + "translation_key": "regeneration", + "name": "Regeneration", + "icon": "mdi:refresh", + }, + EDAttributes.velocity: { + "type": Platform.SENSOR, + "translation_key": "velocity", + "name": "Velocity", + "icon": "mdi:speedometer", + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.soft_available: { + "type": Platform.SENSOR, + "translation_key": "soft_available", + "name": "Soft Water Available", + "icon": "mdi:water-check", + "unit": UnitOfVolume.LITERS, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.left_salt: { + "type": Platform.SENSOR, + "translation_key": "left_salt", + "name": "Left Salt", + "icon": "mdi:shaker", + "unit": PERCENTAGE, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.remaining_days: { + "type": Platform.SENSOR, + "translation_key": "remaining_days", + "name": "Remaining Days", + "icon": "mdi:calendar-clock", + "unit": UnitOfTime.DAYS, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.water_hardness: { + "type": Platform.NUMBER, + "translation_key": "water_hardness", + "name": "Water Hardness", + "icon": "mdi:water", + "min": 0, + "max": 65535, + "step": 1, + }, + EDAttributes.flushing_days: { + "type": Platform.NUMBER, + "translation_key": "flushing_days", + "name": "Flushing Days", + "icon": "mdi:water", + "unit": UnitOfTime.DAYS, + "min": 0, + "max": 99, + "step": 1, + }, + EDAttributes.timing_regeneration_hour: { + "type": Platform.TIME, + "translation_key": "timing_regeneration", + "name": "Timing Regeneration", + "icon": "mdi:clock-outline", + }, + EDAttributes.regeneration_left_seconds: { + "type": Platform.SENSOR, + "translation_key": "regeneration_left_seconds", + "name": "Regeneration Left Seconds", + "icon": "mdi:timer", + "unit": UnitOfTime.SECONDS, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.use_days: { + "type": Platform.SENSOR, + "translation_key": "use_days", + "name": "Use Days", + "icon": "mdi:calendar", + "unit": UnitOfTime.DAYS, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.salt_setting: { + "type": Platform.SENSOR, + "translation_key": "salt_setting", + "name": "Salt Setting", + "icon": "mdi:shaker-outline", + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.water_consumption_big: { + "type": Platform.SENSOR, + "translation_key": "water_consumption_big", + "name": "Water Consumption", + "icon": "mdi:water-pump", + "unit": UnitOfVolume.LITERS, + "state_class": SensorStateClass.TOTAL_INCREASING, + "suggested_display_precision": 2, + }, + EDAttributes.water_consumption_average: { + "type": Platform.SENSOR, + "translation_key": "water_consumption_average", + "name": "Water Consumption Average", + "icon": "mdi:water", + "unit": UnitOfVolume.LITERS, + "state_class": SensorStateClass.MEASUREMENT, + }, + EDAttributes.leak_water_protection_value: { + "type": Platform.NUMBER, + "translation_key": "leak_water_protection_value", + "name": "Leak Water Protection Value", + "icon": "mdi:water-alert-outline", + "unit": UnitOfVolume.LITERS, + "min": 0, + "max": 2550, + "step": 50, + }, + EDAttributes.leak_water: { + "type": Platform.BINARY_SENSOR, + "translation_key": "leak_water", + "name": "Leak Water", + "icon": "mdi:water-alert", + "device_class": BinarySensorDeviceClass.PROBLEM, + }, + EDAttributes.rsj_stand_by: { + "type": Platform.BINARY_SENSOR, + "translation_key": "rsj_stand_by", + "name": "Stand By", + "icon": "mdi:power-standby", + }, + EDAttributes.error: { + "type": Platform.SENSOR, + "translation_key": "error", + "name": "Error", + "icon": "mdi:alert-circle", + "device_class": SensorDeviceClass.ENUM, + # Soft water machine (deviceKind=9/10, subtype 703) error codes + # Source: weex.js error dictionary "a" + "options": { + 0: "no_error", + 1: "e1_position_not_found", + 2: "e2_photo_sensor_no_signal", + 3: "e3_motor_not_running", + 4: "e4_wrong_position", + 225: "e1_motor_fault", + 229: "e5_communication_fault", + 230: "e6_salt_sensor_fault", + 231: "e7_chlorine_sterilization_fault", + }, + }, }, }, 0xFA: { diff --git a/custom_components/midea_ac_lan/midea_entity.py b/custom_components/midea_ac_lan/midea_entity.py index 2b5c645..f717fcb 100644 --- a/custom_components/midea_ac_lan/midea_entity.py +++ b/custom_components/midea_ac_lan/midea_entity.py @@ -12,6 +12,7 @@ else: from homeassistant.helpers.entity import ( # type: ignore[attr-defined] DeviceInfo, ) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.entity import Entity from midealocal.device import MideaDevice @@ -27,14 +28,18 @@ class MideaEntity(Entity): def __init__(self, device: MideaDevice, entity_key: str) -> None: """Initialize Midea base entity.""" self._device = device - self._device.register_update(self.update_state) self._config = cast( "dict", MIDEA_DEVICES[self._device.device_type]["entities"], )[entity_key] self._entity_key = entity_key self._unique_id = f"{DOMAIN}.{self._device.device_id}_{entity_key}" - self.entity_id = self._unique_id + # Build entity_id with the correct platform domain (sensor.*, switch.*, …) + # instead of the integration domain. Keeps the legacy "_" + # object_id so existing entity_ids are unchanged, while fixing the HA + # wrong-domain deprecation (breaks in HA 2027.5.0). + ha_domain = self._config["type"] + self.entity_id = f"{ha_domain}.{self._device.device_id}_{entity_key}" self._device_name = self._device.name # HA language setting: @@ -43,7 +48,7 @@ class MideaEntity(Entity): # Entity name translation based on hass.config.language # add language in /config/configuration.yaml will disable web UI setting # homeassistant: - # language: zh-Hans # noqa: ERA001 + # language: zh-Hans # ruff:ignore[commented-out-code] # Translating the name and attributes of entities: # https://developers.home-assistant.io/blog/2023/03/27/entity_name_translations/#translating-entity-name @@ -84,13 +89,13 @@ class MideaEntity(Entity): @property def device(self) -> MideaDevice: - """Return device structure.""" + """Underlying Midea device instance.""" return self._device @property def device_info(self) -> DeviceInfo: - """Return device info.""" - return { + """Device registry info for the entity.""" + info: DeviceInfo = { "manufacturer": "Midea", "model": f"{MIDEA_DEVICES[self._device.device_type]['name']} " f"{self._device.model}" @@ -98,32 +103,100 @@ class MideaEntity(Entity): "identifiers": {(DOMAIN, str(self._device.device_id))}, "name": self._device_name, } + if self._device.mac: + info["connections"] = { + (CONNECTION_NETWORK_MAC, format_mac(self._device.mac)), + } + if self._device.serial_number: + info["serial_number"] = self._device.serial_number + return info @property def unique_id(self) -> str: - """Return entity unique id.""" + """Unique id of the entity.""" return self._unique_id @property def should_poll(self) -> bool: - """Return true is integration should poll.""" + """Whether the integration should poll for updates.""" return False @property def available(self) -> bool: - """Return entity availability.""" + """Whether the entity is available.""" return bool(self._device.available) @property def icon(self) -> str: - """Return entity icon.""" + """Icon for the entity.""" return cast("str", self._config.get("icon")) + async def async_added_to_hass(self) -> None: + """Subscribe to device updates once the entity is added to HA. + + Registering the callback here (rather than in ``__init__``) ensures an + entity that is constructed but never added to HA never receives updates, + which avoids the recurring "HASS is None" warnings. + """ + await super().async_added_to_hass() + self._device.register_update(self.update_state) + + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from device updates when the entity is removed from HA.""" + await super().async_will_remove_from_hass() + self._device.unregister_update(self.update_state) + @callback - def update_state(self, status: Any) -> None: # noqa: ANN401 + def schedule_update_if_running(self) -> None: + """Schedule a state write unless HA is shutting down. + + Shared by the base ``update_state`` and by the per-device-type + subclasses that override it (climate, fan, light, water_heater, + humidifier), so the shutdown guard lives in exactly one place and the + main control entities are protected too (issues #798 and #809). + + The caller is responsible for the ``self.hass is None`` guard. + + Raises: + RuntimeError: If ``schedule_update_ha_state()`` fails for a reason + other than the event loop being closed during shutdown. + + """ + if self.hass.is_stopping: + _LOGGER.debug( + "MideaEntity update_state for %s [%s]: HASS is stopping", + self.name, + type(self), + ) + return + + # A device background thread can still deliver an update after the HA + # event loop has been closed during shutdown. Scheduling a state write + # then raises RuntimeError because the loop is already closed (issues + # #798 and #809). The is_stopping guard above covers the common case, + # but a race remains between that check and the schedule call, so also + # guard against the closed loop and swallow the residual RuntimeError. + if self.hass.loop.is_closed(): + return + try: + self.schedule_update_ha_state() + except RuntimeError: + # Only swallow the shutdown race; re-raise any unrelated RuntimeError. + if not self.hass.loop.is_closed(): + raise + _LOGGER.debug( + "Ignoring update for %s: event loop closed during shutdown", + self.name, + ) + + @callback + def update_state(self, status: Any) -> None: # ruff:ignore[any-type] """Update entity state.""" if not self.hass: - _LOGGER.warning( + # Defensive guard for the is_stopping access below. Since the update + # callback is now registered in async_added_to_hass (#869), hass is + # always set on the normal path, so this is debug (like is_stopping). + _LOGGER.debug( "MideaEntity update_state for %s [%s] with status %s: HASS is None", self.name, type(self), @@ -131,14 +204,7 @@ class MideaEntity(Entity): ) return - if self.hass.is_stopping: - _LOGGER.debug( - "MideaEntity update_state for %s [%s] with status %s: HASS is stopping", - self.name, - type(self), - status, - ) + if self._entity_key not in status and "available" not in status: return - if self._entity_key in status or "available" in status: - self.schedule_update_ha_state() + self.schedule_update_if_running() diff --git a/custom_components/midea_ac_lan/number.py b/custom_components/midea_ac_lan/number.py index 9a02165..b2bc8a6 100644 --- a/custom_components/midea_ac_lan/number.py +++ b/custom_components/midea_ac_lan/number.py @@ -46,7 +46,7 @@ class MideaNumber(MideaEntity, NumberEntity): @property def native_min_value(self) -> float: - """Return minimum value.""" + """Minimum value allowed.""" return cast( "float", ( @@ -62,7 +62,7 @@ class MideaNumber(MideaEntity, NumberEntity): @property def native_max_value(self) -> float: - """Return maximum value.""" + """Maximum value allowed.""" return cast( "float", ( @@ -78,7 +78,7 @@ class MideaNumber(MideaEntity, NumberEntity): @property def native_step(self) -> float: - """Return step value.""" + """Step value between allowed values.""" return cast( "float", ( @@ -94,9 +94,9 @@ class MideaNumber(MideaEntity, NumberEntity): @property def native_value(self) -> float: - """Return value.""" + """Native value of the entity.""" return cast("float", self._device.get_attribute(self._entity_key)) - def set_native_value(self, value: Any) -> None: # noqa: ANN401 + def set_native_value(self, value: Any) -> None: # ruff:ignore[any-type] """Set value.""" self._device.set_attribute(self._entity_key, value) diff --git a/custom_components/midea_ac_lan/select.py b/custom_components/midea_ac_lan/select.py index 99a7136..ed41e26 100644 --- a/custom_components/midea_ac_lan/select.py +++ b/custom_components/midea_ac_lan/select.py @@ -1,18 +1,21 @@ """Select for Midea Lan.""" -from typing import cast +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_SWITCHES, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from midealocal.device import MideaDevice -from .const import DEVICES, DOMAIN +from .const import DEVICES, DOMAIN, supports_model from .midea_devices import MIDEA_DEVICES from .midea_entity import MideaEntity +if TYPE_CHECKING: + from midealocal.devices.e1 import MideaE1Device + async def async_setup_entry( hass: HomeAssistant, @@ -28,9 +31,20 @@ async def async_setup_entry( "dict", MIDEA_DEVICES[device.device_type]["entities"], ).items(): - if config["type"] == Platform.SELECT and entity_key in extra_switches: - dev = MideaSelect(device, entity_key) - selects.append(dev) + if ( + config["type"] != Platform.SELECT + or not supports_model(device.model, config) + or (not config.get("default") and entity_key not in extra_switches) + ): + continue + required_attribute = config.get("required_attribute") + if ( + required_attribute is not None + and required_attribute not in device.attributes + ): + continue + dev = MideaSelect(device, entity_key) + selects.append(dev) async_add_entities(selects) @@ -40,18 +54,85 @@ class MideaSelect(MideaEntity, SelectEntity): def __init__(self, device: MideaDevice, entity_key: str) -> None: """Midea select init.""" super().__init__(device, entity_key) + self._attribute_key = self._config.get("attribute", entity_key) self._options_name = self._config.get("options") + self._options_dict_name = self._config.get("options_dict") @property def options(self) -> list[str]: - """Return entity options.""" + """Available options for the entity.""" + if self._options_dict_name: + options = self._get_options_dict() + codes_by_model = self._config.get("options_codes_by_model", {}) + codes = codes_by_model.get(str(self._device.model)) or self._config.get( + "options_codes", + ) + if codes: + return [options[code] for code in codes if code in options] + return list(options.values()) return cast("list", getattr(self._device, self._options_name)) @property - def current_option(self) -> str: - """Return entity current option.""" - return cast("str", self._device.get_attribute(self._entity_key)) + def current_option(self) -> str | None: + """Currently selected option.""" + option = cast("str | None", self._device.get_attribute(self._attribute_key)) + return option if option in self.options else None + + @property + def available(self) -> bool: + """Whether the entity is available.""" + if not super().available: + return False + power_attribute = self._config.get("available_power_attribute") + return not power_attribute or bool(self._device.get_attribute(power_attribute)) def select_option(self, option: str) -> None: """Select entity option.""" - self._device.set_attribute(self._entity_key, option) + if self._config.get("set_message") == "e1_work_mode": + self._select_e1_work_mode(option) + return + self._device.set_attribute(self._attribute_key, option) + + def _get_options_dict(self) -> dict[int, str]: + """Return option dict from the backing midea-local device. + + Returns + ------- + Option labels keyed by the raw mode code. + + """ + return cast("dict[int, str]", getattr(self._device, self._options_dict_name)) + + def _select_e1_work_mode(self, option: str) -> None: + """Set dishwasher work mode via midea-local's public E1 API. + + Raises + ------ + ValueError + If the requested option is not a supported work mode. + + """ + mode = self._get_dict_key_by_value(self._get_options_dict(), option) + if mode is None: + raise ValueError(f"Unsupported dishwasher mode: {option}") + cast("MideaE1Device", self._device).set_work_mode(mode) + + @callback + def update_state(self, status: Any) -> None: # ruff:ignore[any-type] + """Update entity state.""" + super().update_state(status) + power_attribute = self._config.get("available_power_attribute") + if ( + power_attribute + and self.hass + and not self.hass.is_stopping + and (self._attribute_key in status or power_attribute in status) + ): + self.schedule_update_ha_state() + + @staticmethod + def _get_dict_key_by_value(source: dict[int, str], value: str) -> int | None: + for key, item in source.items(): + if item == value: + return key + return None diff --git a/custom_components/midea_ac_lan/sensor.py b/custom_components/midea_ac_lan/sensor.py index 4d1db3f..9648590 100644 --- a/custom_components/midea_ac_lan/sensor.py +++ b/custom_components/midea_ac_lan/sensor.py @@ -11,9 +11,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_SENSORS, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType +from midealocal.device import MideaDevice -from .const import DEVICES, DOMAIN +from .const import DEVICES, DOMAIN, supports_model from .midea_devices import MIDEA_DEVICES from .midea_entity import MideaEntity @@ -32,9 +34,24 @@ async def async_setup_entry( "dict", MIDEA_DEVICES[device.device_type]["entities"], ).items(): - if config["type"] == Platform.SENSOR and entity_key in extra_sensors: - sensor = MideaSensor(device, entity_key) - sensors.append(sensor) + if ( + config["type"] != Platform.SENSOR + or not supports_model(device.model, config) + or (not config.get("default") and entity_key not in extra_sensors) + ): + continue + required_attribute = config.get("required_attribute") + if ( + required_attribute is not None + and required_attribute not in device.attributes + ): + continue + sensor = ( + MideaEstimatedUsageSensor(device, entity_key) + if config.get("estimate") + else MideaSensor(device, entity_key) + ) + sensors.append(sensor) async_add_entities(sensors) @@ -43,25 +60,134 @@ class MideaSensor(MideaEntity, SensorEntity): @property def native_value(self) -> StateType: - """Return entity value.""" - return cast("StateType", self._device.get_attribute(self._entity_key)) + """Native value of the sensor.""" + value = self._device.get_attribute(self._entity_key) + # If an options mapping exists, translate the raw value to its enum key. + options = self._config.get("options") + if options is not None and isinstance(value, int): + # For an enum sensor an unmapped value is not a valid state, so + # report it as unknown (None) instead of leaking the raw int. + return cast("StateType", options.get(value)) + return cast("StateType", value) @property def device_class(self) -> SensorDeviceClass: - """Return device class.""" + """Device class of the sensor.""" return cast("SensorDeviceClass", self._config.get("device_class")) @property def state_class(self) -> SensorStateClass | None: - """Return state state.""" + """State class of the sensor.""" return cast("SensorStateClass | None", self._config.get("state_class")) + @property + def options(self) -> list[str] | None: + """List of possible states for an enum sensor.""" + if self.device_class != SensorDeviceClass.ENUM: + return None + options = self._config.get("options") + return list(options.values()) if options else None + @property def native_unit_of_measurement(self) -> str | None: - """Return unit of measurement.""" + """Unit of measurement for the sensor.""" return cast("str | None", self._config.get("unit")) + @property + def suggested_display_precision(self) -> int | None: + """Suggested number of decimal digits for display.""" + return cast("int | None", self._config.get("suggested_display_precision")) + @property def capability_attributes(self) -> dict[str, Any] | None: - """Return capabilities.""" + """Capability attributes of the sensor.""" + if self.options is not None: + return {"options": self.options} return {"state_class": self.state_class} if self.state_class else {} + + +class MideaEstimatedUsageSensor(MideaSensor, RestoreEntity): + """Represent estimated dishwasher usage accumulated per run. + + The dishwasher does not report actual energy/water usage, so this sensor + accumulates a fixed per-mode estimate (from the product manual) once each + time a wash run *completes*. Completion is detected by the device + ``progress`` attribute reaching ``"Complete"``; a cancelled or errored run + never reaches that state and is therefore not counted. + """ + + _COMPLETE_PROGRESS = "Complete" + _RUNNING_STATUS = "Running" + + def __init__(self, device: MideaDevice, entity_key: str) -> None: + """Initialize estimated usage sensor.""" + super().__init__(device, entity_key) + self._native_value: float = 0.0 + self._last_progress: str | None = None + self._running_mode: str | None = None + + async def async_added_to_hass(self) -> None: + """Register for device updates and restore the accumulated value.""" + await super().async_added_to_hass() + if last_state := await self.async_get_last_state(): + try: + self._native_value = float(last_state.state) + except (TypeError, ValueError): + self._native_value = 0.0 + self._last_progress = cast( + "str | None", + self._device.get_attribute("progress"), + ) + if self._device.get_attribute("status") == self._RUNNING_STATUS: + self._running_mode = cast("str | None", self._device.get_attribute("mode")) + + @property + def native_value(self) -> StateType: + """Accumulated estimated usage.""" + return round(self._native_value, 3) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Estimate metadata for the sensor.""" + estimate = cast("dict[str, Any]", self._config["estimate"]) + return { + "estimate_source": "fixed_per_wash_mode", + "known_modes": list(cast("dict[str, float]", estimate["values"]).keys()), + "last_progress": self._last_progress, + "running_mode": self._running_mode, + } + + def update_state(self, status: Any) -> None: # ruff:ignore[any-type] + """Accumulate the estimate once when a dishwasher run completes.""" + current_status = self._device.get_attribute("status") + current_progress = cast( + "str | None", + self._device.get_attribute("progress"), + ) + current_mode = cast("str | None", self._device.get_attribute("mode")) + + # Remember the mode selected while the machine is actually running so we + # can still attribute usage after it stops reporting the mode on finish. + if current_status == self._RUNNING_STATUS: + self._running_mode = current_mode + + # Count once, on the edge into the "Complete" progress state. A cancel or + # error transition never reaches "Complete", so it is not counted. + if ( + current_progress == self._COMPLETE_PROGRESS + and self._last_progress != self._COMPLETE_PROGRESS + ): + mode = self._running_mode or current_mode + values = cast("dict[str, float]", self._config["estimate"]["values"]) + if mode in values: + self._native_value += values[mode] + self._running_mode = None + + self._last_progress = current_progress + super().update_state(status) + if ( + self.hass + and not self.hass.is_stopping + and ("progress" in status or "status" in status or "mode" in status) + ): + self.schedule_update_ha_state() diff --git a/custom_components/midea_ac_lan/switch.py b/custom_components/midea_ac_lan/switch.py index ef4b6a8..7ca8c8f 100644 --- a/custom_components/midea_ac_lan/switch.py +++ b/custom_components/midea_ac_lan/switch.py @@ -38,13 +38,13 @@ class MideaSwitch(MideaEntity, ToggleEntity): @property def is_on(self) -> bool: - """Return true if switch is on.""" + """Whether the switch is on.""" return cast("bool", self._device.get_attribute(self._entity_key)) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Turn on switch.""" self._device.set_attribute(attr=self._entity_key, value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Turn off switch.""" self._device.set_attribute(attr=self._entity_key, value=False) diff --git a/custom_components/midea_ac_lan/translations/de.json b/custom_components/midea_ac_lan/translations/de.json index caf280f..119dc4f 100644 --- a/custom_components/midea_ac_lan/translations/de.json +++ b/custom_components/midea_ac_lan/translations/de.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cooking" }, + "error_code": { + "name": "Fehlercode" + }, "filter_change_reminder": { "name": "Filter Change Reminder" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Keep Warm" }, + "leak_water": { + "name": "Leckwasser" + }, "lid_status": { "name": "Lid Status" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Middle Compartment Preheating" }, + "top_elec_heat": { + "name": "Elektroheizung oben" + }, + "bottom_elec_heat": { + "name": "Elektroheizung unten" + }, + "mute_effect": { + "name": "Stummschaltung aktiv" + }, + "mute_status": { + "name": "Stummschaltungsstatus" + }, + "multi_terminal": { + "name": "Multi-Terminal" + }, "oilcup_full": { "name": "Oil-cup Full" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Rinse Aid Shortage" }, + "rsj_stand_by": { + "name": "Standby" + }, "salt": { "name": "Salt Shortage" }, "seat_status": { "name": "Seat Status" }, + "smart_grid": { + "name": "Smart Grid" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Wartungserinnerung" + }, + "maintain_warn": { + "name": "Wartungswarnung" + }, + "order1_effect": { + "name": "Zeitplan 1 aktiv" + }, + "order2_effect": { + "name": "Zeitplan 2 aktiv" + }, "status_dhw": { "name": "DHW status" }, @@ -206,6 +248,20 @@ }, "zone2_water_temp_mode": { "name": "Zone2 Water-temperature Mode" + }, + "microcrystal_fresh": { + "name": "Mikrokristall-Frische" + }, + "electronic_smell": { + "name": "Desodorieren und Sterilisieren" + }, + "water_pump_running": { + "name": "Wasserpumpe läuft" + } + }, + "button": { + "start": { + "name": "Start" } }, "climate": { @@ -214,11 +270,24 @@ }, "climate_zone2": { "name": "Zone2 Thermostat" + }, + "climate_key": { + "state_attributes": { + "fan_mode": { + "state": { + "silent": "Leise", + "full": "Voll" + } + } + } } }, "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Frischluft-Abluft" } }, "lock": { @@ -238,6 +307,21 @@ }, "water_temp_level": { "name": "Water Temperature Level" + }, + "fan_speed_percent": { + "name": "Lüftergeschwindigkeit (Prozent)" + }, + "vacation_days": { + "name": "Urlaubstage" + }, + "leak_water_protection_value": { + "name": "Leckschutzwert" + }, + "flushing_days": { + "name": "Regenerationstage" + }, + "water_hardness": { + "name": "Wasserhärte" } }, "select": { @@ -250,6 +334,25 @@ "fan_speed": { "name": "Fan Speed" }, + "fresh_air_exhaust_mode": { + "name": "Frischluft-Abluftgeschwindigkeit", + "state": { + "off": "Aus", + "silent": "Niedrig", + "high": "Hoch", + "full": "Voll" + } + }, + "fresh_air_mode": { + "name": "Frischluftgeschwindigkeit", + "state": { + "off": "Aus", + "low": "Niedrig", + "medium": "Mittel", + "high": "Hoch", + "full": "Voll" + } + }, "mode": { "name": "Mode" }, @@ -259,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Power Rate Limit" + }, "screen_display": { "name": "Screen Display" }, @@ -268,8 +374,38 @@ "tilting_angle": { "name": "Tilting Angle" }, + "wash_mode": { + "name": "Waschmodus" + }, "water_level_set": { "name": "Water Level Setting" + }, + "wind_lr_angle": { + "name": "Luftstrom horizontal", + "state": { + "off": "Aus", + "left": "Links", + "left-mid": "Links-Mitte", + "middle": "Mitte", + "right-mid": "Rechts-Mitte", + "right": "Rechts" + } + }, + "wind_ud_angle": { + "name": "Luftstrom vertikal", + "state": { + "off": "Aus", + "up": "Oben", + "up-mid": "Oben-Mitte", + "middle": "Mitte", + "down-mid": "Unten-Mitte", + "down": "Unten" + } + } + }, + "time": { + "timing_regeneration": { + "name": "Regenerationszeit" } }, "sensor": { @@ -309,15 +445,71 @@ "dehydration_time": { "name": "dehydration time" }, + "dehydration_time_value": { + "name": "Schleuderzeitwert" + }, + "temperature": { + "name": "Temperatur" + }, + "wash_time_value": { + "name": "Waschzeitwert" + }, + "stains": { + "name": "Flecken" + }, + "dirty_degree": { + "name": "Verschmutzungsgrad" + }, "detergent": { "name": "detergent" }, + "intensity": { + "name": "Intensität" + }, + "dryness_level": { + "name": "Trocknungsgrad" + }, + "dry_temperature": { + "name": "Trocknungstemperatur" + }, + "door_warn": { + "name": "Türwarnung" + }, + "ai_switch": { + "name": "KI-Schalter" + }, + "material": { + "name": "Material" + }, + "water_box": { + "name": "Wasserbehälter" + }, "energy_consumption": { "name": "Energy Consumption" }, + "error": { + "name": "Fehler", + "state": { + "no_error": "Kein Fehler", + "e1_position_not_found": "E1 Position nicht gefunden", + "e2_photo_sensor_no_signal": "E2 Fotosensor kein Signal", + "e3_motor_not_running": "E3 Motor läuft nicht", + "e4_wrong_position": "E4 Falsche Position", + "e1_motor_fault": "E1 Motorfehler", + "e5_communication_fault": "E5 Kommunikationsfehler", + "e6_salt_sensor_fault": "E6 Salzsensorfehler", + "e7_chlorine_sterilization_fault": "E7 Chlorsterilisationsfehler" + } + }, "error_code": { "name": "Error Code" }, + "estimated_energy_consumption": { + "name": "Geschätzter Energieverbrauch" + }, + "estimated_water_consumption": { + "name": "Geschätzter Wasserverbrauch" + }, "fan_level": { "name": "Fan level" }, @@ -381,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Verbleibendes Salz" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -417,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Regeneration verbleibende Sekunden" + }, + "remaining_days": { + "name": "Verbleibende Tage" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -429,12 +630,18 @@ "rinse_level": { "name": "rinse level" }, + "salt_setting": { + "name": "Salzeinstellung" + }, "seat_temperature": { "name": "Seat Temperature" }, "soak_time": { "name": "soak time" }, + "soft_available": { + "name": "Verfügbares Weichwasser" + }, "softener": { "name": "softener" }, @@ -492,14 +699,110 @@ "water_consumption": { "name": "Wasserverbrauch" }, + "water_consumption_average": { + "name": "Durchschnittlicher Wasserverbrauch" + }, + "water_consumption_big": { + "name": "Wasserverbrauch" + }, "water_temperature": { "name": "Water Temperature" }, "water_level": { "name": "Water Level" }, + "wind": { + "name": "Wind" + }, + "typeinfo": { + "name": "Typinformation" + }, + "use_days": { + "name": "Nutzungstage" + }, + "elec_heat": { + "name": "Elektroheizung" + }, + "water_pump": { + "name": "Wasserpumpe" + }, + "four_way": { + "name": "Vierwegeventil" + }, + "back_water": { + "name": "Rücklaufwasser" + }, + "sterilize": { + "name": "Sterilisieren" + }, + "disinfection_temperature": { + "name": "Desinfektionstemperatur" + }, + "max_temperature": { + "name": "Maximale Zieltemperatur" + }, + "vacation_start_year": { + "name": "Urlaubsbeginn Jahr" + }, + "vacation_start_month": { + "name": "Urlaubsbeginn Monat" + }, + "vacation_start_day": { + "name": "Urlaubsbeginn Tag" + }, + "auto_sterilize_week": { + "name": "Auto-Sterilisieren Woche" + }, + "auto_sterilize_hour": { + "name": "Auto-Sterilisieren Stunde" + }, + "auto_sterilize_minute": { + "name": "Auto-Sterilisieren Minute" + }, "working_time": { "name": "Working Time" + }, + "humidity": { + "name": "Luftfeuchtigkeit" + }, + "variable_mode": { + "name": "Variabler Modus" + }, + "compressor_frequency": { + "name": "Kompressorfrequenz" + }, + "target_compressor_frequency": { + "name": "Ziel-Kompressorfrequenz" + }, + "compressor_current": { + "name": "Kompressorstrom" + }, + "compressor_voltage": { + "name": "Kompressorspannung" + }, + "compressor_power": { + "name": "Kompressorleistung" + }, + "indoor_coil_temperature": { + "name": "Innenraum-Wärmetauschertemperatur (T1)" + }, + "evaporator_temperature": { + "name": "Verdampfertemperatur (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Außenumgebungstemperatur (T4)" + }, + "discharge_pipe_temperature": { + "name": "Druckleitungstemperatur (TP)" + }, + "indoor_fan_speed": { + "name": "Innenlüftergeschwindigkeit" + }, + "target_indoor_fan_speed": { + "name": "Ziel-Innenlüftergeschwindigkeit" + }, + "velocity": { + "name": "Durchflussgeschwindigkeit" } }, "switch": { @@ -515,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Kindersicherung" + }, + "cl_sterilization": { + "name": "CL-Sterilisation" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -524,12 +833,24 @@ "disinfect": { "name": "Disinfect" }, + "vacation_mode": { + "name": "Urlaubsmodus" + }, "dry": { "name": "Dry" }, + "cold_water_single": { + "name": "Kaltwasser einzeln" + }, + "cold_water_dot": { + "name": "Kaltwasser Punkt" + }, "eco_mode": { "name": "ECO Mode" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -545,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Leckschutz" + }, "light": { "name": "Licht" }, @@ -575,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Regeneration" + }, "water_pump": { "name": "Wasserpumpe" }, + "water_way": { + "name": "Wasserweg" + }, "screen_display": { "name": "Screen Display" }, @@ -593,6 +923,9 @@ "sleep_mode": { "name": "Sleep Mode" }, + "out_silent": { + "name": "Außengerät Leisemodus" + }, "smart_eye": { "name": "Smart Eye" }, @@ -602,12 +935,18 @@ "smelly_sensor": { "name": "Geruchssensor" }, + "soften": { + "name": "Enthärtung" + }, "standby": { "name": "Standby" }, "start": { "name": "Start" }, + "sterilization": { + "name": "Sterilisation" + }, "storage": { "name": "Storage" }, diff --git a/custom_components/midea_ac_lan/translations/en.json b/custom_components/midea_ac_lan/translations/en.json index 8751a3f..d33740d 100644 --- a/custom_components/midea_ac_lan/translations/en.json +++ b/custom_components/midea_ac_lan/translations/en.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cooking" }, + "error_code": { + "name": "Error Code" + }, "filter_change_reminder": { "name": "Filter Change Reminder" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Keep Warm" }, + "leak_water": { + "name": "Leak Water" + }, "lid_status": { "name": "Lid Status" }, @@ -168,6 +174,9 @@ "rinse_aid": { "name": "Rinse Aid Shortage" }, + "rsj_stand_by": { + "name": "Stand By" + }, "salt": { "name": "Salt Shortage" }, @@ -237,11 +246,22 @@ "zone2_room_temp_mode": { "name": "Zone2 Room-temperature Mode" }, + "zone2_water_temp_mode": { + "name": "Zone2 Water-temperature Mode" + }, "microcrystal_fresh": { "name": "Microcrystal Fresh" }, "electronic_smell": { "name": "Deodorizing sterilizing" + }, + "water_pump_running": { + "name": "Water Pump Running" + } + }, + "button": { + "start": { + "name": "Start" } }, "climate": { @@ -265,6 +285,9 @@ "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Fresh Air Exhaust" } }, "lock": { @@ -290,6 +313,15 @@ }, "vacation_days": { "name": "Vacation Days" + }, + "leak_water_protection_value": { + "name": "Leak Water Protection Value" + }, + "flushing_days": { + "name": "Regeneration Days" + }, + "water_hardness": { + "name": "Water Hardness" } }, "select": { @@ -302,6 +334,25 @@ "fan_speed": { "name": "Fan Speed" }, + "fresh_air_exhaust_mode": { + "name": "Fresh Air Exhaust Speed", + "state": { + "off": "Off", + "silent": "Low", + "high": "High", + "full": "Full" + } + }, + "fresh_air_mode": { + "name": "Fresh Air Speed", + "state": { + "off": "Off", + "low": "Low", + "medium": "Medium", + "high": "High", + "full": "Full" + } + }, "mode": { "name": "Mode" }, @@ -311,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Power Rate Limit" + }, "screen_display": { "name": "Screen Display" }, @@ -320,6 +374,9 @@ "tilting_angle": { "name": "Tilting Angle" }, + "wash_mode": { + "name": "Wash Mode" + }, "water_level_set": { "name": "Water Level Setting" }, @@ -346,6 +403,11 @@ } } }, + "time": { + "timing_regeneration": { + "name": "Timing Regeneration" + } + }, "sensor": { "bathing_leaving_temperature": { "name": "Bathing Leaving Water Temperature" @@ -386,6 +448,9 @@ "dehydration_time_value": { "name": "Dehydration time value" }, + "temperature": { + "name": "Temperature" + }, "wash_time_value": { "name": "Wash time value" }, @@ -422,9 +487,29 @@ "energy_consumption": { "name": "Energy Consumption" }, + "error": { + "name": "Error", + "state": { + "no_error": "No Error", + "e1_position_not_found": "E1 Position not found", + "e2_photo_sensor_no_signal": "E2 Photo sensor no signal", + "e3_motor_not_running": "E3 Motor not running", + "e4_wrong_position": "E4 Wrong position", + "e1_motor_fault": "E1 Motor fault", + "e5_communication_fault": "E5 Communication fault", + "e6_salt_sensor_fault": "E6 Salt sensor fault", + "e7_chlorine_sterilization_fault": "E7 Chlorine sterilization fault" + } + }, "error_code": { "name": "Error Code" }, + "estimated_energy_consumption": { + "name": "Estimated Energy Consumption" + }, + "estimated_water_consumption": { + "name": "Estimated Water Consumption" + }, "fan_level": { "name": "Fan level" }, @@ -488,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Left Salt" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -524,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Regeneration Left Seconds" + }, + "remaining_days": { + "name": "Remaining Days" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -536,12 +630,18 @@ "rinse_level": { "name": "Rinse level" }, + "salt_setting": { + "name": "Salt Setting" + }, "seat_temperature": { "name": "Seat Temperature" }, "soak_time": { "name": "Soak time" }, + "soft_available": { + "name": "Soft Water Available" + }, "softener": { "name": "Softener" }, @@ -587,6 +687,9 @@ "tvoc": { "name": "TVOC" }, + "wash_level": { + "name": "Rinse count" + }, "wash_strength": { "name": "Wash strength" }, @@ -596,6 +699,12 @@ "water_consumption": { "name": "Water Consumption" }, + "water_consumption_average": { + "name": "Water Consumption Average" + }, + "water_consumption_big": { + "name": "Water Consumption" + }, "water_temperature": { "name": "Water Temperature" }, @@ -608,6 +717,9 @@ "typeinfo": { "name": "Type Info" }, + "use_days": { + "name": "Use Days" + }, "elec_heat": { "name": "Electric Heat" }, @@ -655,6 +767,42 @@ }, "variable_mode": { "name": "Variable Mode" + }, + "compressor_frequency": { + "name": "Compressor Frequency" + }, + "target_compressor_frequency": { + "name": "Target Compressor Frequency" + }, + "compressor_current": { + "name": "Compressor Current" + }, + "compressor_voltage": { + "name": "Compressor Voltage" + }, + "compressor_power": { + "name": "Compressor Power" + }, + "indoor_coil_temperature": { + "name": "Indoor Coil Temperature (T1)" + }, + "evaporator_temperature": { + "name": "Evaporator Temperature (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Outdoor Ambient Temperature (T4)" + }, + "discharge_pipe_temperature": { + "name": "Discharge Pipe Temperature (TP)" + }, + "indoor_fan_speed": { + "name": "Indoor Fan Speed" + }, + "target_indoor_fan_speed": { + "name": "Target Indoor Fan Speed" + }, + "velocity": { + "name": "Velocity" } }, "switch": { @@ -670,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Child Lock" + }, + "cl_sterilization": { + "name": "CL Sterilization" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -694,6 +848,9 @@ "eco_mode": { "name": "ECO Mode" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -709,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Leak Water Protection" + }, "light": { "name": "Light" }, @@ -739,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Regeneration" + }, "water_pump": { "name": "Water Pump" }, + "water_way": { + "name": "Water Way" + }, "screen_display": { "name": "Screen Display" }, @@ -769,12 +935,18 @@ "smelly_sensor": { "name": "Smelly Sensor" }, + "soften": { + "name": "Softening" + }, "standby": { "name": "Standby" }, "start": { "name": "Start" }, + "sterilization": { + "name": "Sterilization" + }, "storage": { "name": "Storage" }, diff --git a/custom_components/midea_ac_lan/translations/es.json b/custom_components/midea_ac_lan/translations/es.json index a8ef2cb..4759631 100644 --- a/custom_components/midea_ac_lan/translations/es.json +++ b/custom_components/midea_ac_lan/translations/es.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cocinando" }, + "error_code": { + "name": "Código de error" + }, "filter_change_reminder": { "name": "Recordatorio de cambio de filtro" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Mantener caliente" }, + "leak_water": { + "name": "Fuga de agua" + }, "lid_status": { "name": "Estado de la tapa" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Precalentamiento del compartimento medio" }, + "top_elec_heat": { + "name": "Calefacción eléctrica superior" + }, + "bottom_elec_heat": { + "name": "Calefacción eléctrica inferior" + }, + "mute_effect": { + "name": "Efecto de silencio" + }, + "mute_status": { + "name": "Estado de silencio" + }, + "multi_terminal": { + "name": "Terminal múltiple" + }, "oilcup_full": { "name": "Depósito de aceite lleno" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Escasez de abrillantador" }, + "rsj_stand_by": { + "name": "En espera" + }, "salt": { "name": "Escasez de sal" }, "seat_status": { "name": "Estado del asiento" }, + "smart_grid": { + "name": "Red inteligente" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Recordatorio de mantenimiento" + }, + "maintain_warn": { + "name": "Advertencia de mantenimiento" + }, + "order1_effect": { + "name": "Programación 1 activa" + }, + "order2_effect": { + "name": "Programación 2 activa" + }, "status_dhw": { "name": "Estado ACS" }, @@ -204,11 +246,22 @@ "zone2_room_temp_mode": { "name": "Zona 2 Modo temperatura ambiente" }, + "zone2_water_temp_mode": { + "name": "Zona 2 Modo temperatura del agua" + }, "microcrystal_fresh": { "name": "Frescura microcristalina" }, "electronic_smell": { "name": "Esterilización desodorizante" + }, + "water_pump_running": { + "name": "Bomba de agua en funcionamiento" + } + }, + "button": { + "start": { + "name": "Iniciar" } }, "climate": { @@ -232,6 +285,9 @@ "fan": { "fresh_air": { "name": "Aire fresco" + }, + "fresh_air_exhaust": { + "name": "Extracción de aire fresco" } }, "lock": { @@ -254,6 +310,18 @@ }, "fan_speed_percent": { "name": "Porcentaje de velocidad del ventilador" + }, + "vacation_days": { + "name": "Días de vacaciones" + }, + "leak_water_protection_value": { + "name": "Valor de protección contra fugas" + }, + "flushing_days": { + "name": "Días de regeneración" + }, + "water_hardness": { + "name": "Dureza del agua" } }, "select": { @@ -266,6 +334,25 @@ "fan_speed": { "name": "Velocidad del ventilador" }, + "fresh_air_exhaust_mode": { + "name": "Velocidad de extracción de aire fresco", + "state": { + "off": "Apagado", + "silent": "Baja", + "high": "Alta", + "full": "Máximo" + } + }, + "fresh_air_mode": { + "name": "Velocidad de aire fresco", + "state": { + "off": "Apagado", + "low": "Baja", + "medium": "Media", + "high": "Alta", + "full": "Máximo" + } + }, "mode": { "name": "Modo" }, @@ -275,6 +362,9 @@ "oscillation_mode": { "name": "Modo de oscilación" }, + "rate_select": { + "name": "Límite de tasa de potencia" + }, "screen_display": { "name": "Visualización de pantalla" }, @@ -284,6 +374,9 @@ "tilting_angle": { "name": "Ángulo de inclinación" }, + "wash_mode": { + "name": "Modo de lavado" + }, "water_level_set": { "name": "Ajuste de nivel de agua" }, @@ -310,6 +403,11 @@ } } }, + "time": { + "timing_regeneration": { + "name": "Regeneración programada" + } + }, "sensor": { "bathing_leaving_temperature": { "name": "Temperatura de salida del agua de baño" @@ -350,6 +448,9 @@ "dehydration_time_value": { "name": "Valor del tiempo de centrifugado" }, + "temperature": { + "name": "Temperatura" + }, "wash_time_value": { "name": "Valor del tiempo de lavado" }, @@ -386,9 +487,29 @@ "energy_consumption": { "name": "Consumo de energía" }, + "error": { + "name": "Error", + "state": { + "no_error": "Sin error", + "e1_position_not_found": "E1 Posición no encontrada", + "e2_photo_sensor_no_signal": "E2 Sensor foto sin señal", + "e3_motor_not_running": "E3 Motor no funciona", + "e4_wrong_position": "E4 Posición incorrecta", + "e1_motor_fault": "E1 Fallo del motor", + "e5_communication_fault": "E5 Fallo de comunicación", + "e6_salt_sensor_fault": "E6 Fallo sensor de sal", + "e7_chlorine_sterilization_fault": "E7 Fallo esterilización cloro" + } + }, "error_code": { "name": "Código de error" }, + "estimated_energy_consumption": { + "name": "Consumo estimado de energía" + }, + "estimated_water_consumption": { + "name": "Consumo estimado de agua" + }, "fan_level": { "name": "Nivel del ventilador" }, @@ -452,6 +573,9 @@ "keep_warm_time": { "name": "Tiempo de mantener caliente" }, + "left_salt": { + "name": "Sal restante" + }, "middle_compartment_remaining": { "name": "Tiempo restante compartimento medio" }, @@ -488,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Temperatura establecida del frigorífico" }, + "regeneration_left_seconds": { + "name": "Segundos restantes de regeneración" + }, + "remaining_days": { + "name": "Días restantes" + }, "right_flex_zone_actual_temp": { "name": "Temperatura real de zona flexible derecha" }, @@ -500,12 +630,18 @@ "rinse_level": { "name": "Nivel de aclarado" }, + "salt_setting": { + "name": "Configuración de sal" + }, "seat_temperature": { "name": "Temperatura del asiento" }, "soak_time": { "name": "Tiempo de remojo" }, + "soft_available": { + "name": "Agua suave disponible" + }, "softener": { "name": "Suavizante" }, @@ -551,6 +687,9 @@ "tvoc": { "name": "COVT" }, + "wash_level": { + "name": "Recuento de aclarados" + }, "wash_strength": { "name": "Potencia de lavado" }, @@ -560,12 +699,66 @@ "water_consumption": { "name": "Consumo de agua" }, + "water_consumption_average": { + "name": "Consumo promedio de agua" + }, + "water_consumption_big": { + "name": "Consumo de agua" + }, "water_temperature": { "name": "Temperatura del agua" }, "water_level": { "name": "Nivel de agua" }, + "wind": { + "name": "Viento" + }, + "typeinfo": { + "name": "Información de tipo" + }, + "use_days": { + "name": "Días de uso" + }, + "elec_heat": { + "name": "Calefacción eléctrica" + }, + "water_pump": { + "name": "Bomba de agua" + }, + "four_way": { + "name": "Válvula de cuatro vías" + }, + "back_water": { + "name": "Retorno de agua" + }, + "sterilize": { + "name": "Esterilizar" + }, + "disinfection_temperature": { + "name": "Temperatura de desinfección" + }, + "max_temperature": { + "name": "Temperatura objetivo máxima" + }, + "vacation_start_year": { + "name": "Año de inicio de vacaciones" + }, + "vacation_start_month": { + "name": "Mes de inicio de vacaciones" + }, + "vacation_start_day": { + "name": "Día de inicio de vacaciones" + }, + "auto_sterilize_week": { + "name": "Semana de esterilización automática" + }, + "auto_sterilize_hour": { + "name": "Hora de esterilización automática" + }, + "auto_sterilize_minute": { + "name": "Minuto de esterilización automática" + }, "working_time": { "name": "Tiempo de funcionamiento" }, @@ -574,6 +767,42 @@ }, "variable_mode": { "name": "Modo variable" + }, + "compressor_frequency": { + "name": "Frecuencia del compresor" + }, + "target_compressor_frequency": { + "name": "Frecuencia objetivo del compresor" + }, + "compressor_current": { + "name": "Corriente del compresor" + }, + "compressor_voltage": { + "name": "Voltaje del compresor" + }, + "compressor_power": { + "name": "Potencia del compresor" + }, + "indoor_coil_temperature": { + "name": "Temperatura del serpentín interior (T1)" + }, + "evaporator_temperature": { + "name": "Temperatura del evaporador (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Temperatura ambiente exterior (T4)" + }, + "discharge_pipe_temperature": { + "name": "Temperatura del tubo de descarga (TP)" + }, + "indoor_fan_speed": { + "name": "Velocidad del ventilador interior" + }, + "target_indoor_fan_speed": { + "name": "Velocidad objetivo del ventilador interior" + }, + "velocity": { + "name": "Velocidad" } }, "switch": { @@ -589,6 +818,12 @@ "breezeless": { "name": "Sin corrientes" }, + "child_lock": { + "name": "Bloqueo infantil" + }, + "cl_sterilization": { + "name": "Esterilización CL" + }, "comfort_mode": { "name": "Modo confort" }, @@ -598,6 +833,9 @@ "disinfect": { "name": "Desinfectar" }, + "vacation_mode": { + "name": "Modo vacaciones" + }, "dry": { "name": "Secado" }, @@ -610,6 +848,9 @@ "eco_mode": { "name": "Modo ECO" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "ACS Rápido" }, @@ -625,6 +866,9 @@ "indirect_wind": { "name": "Viento indirecto" }, + "leak_water_protection": { + "name": "Protección contra fugas" + }, "light": { "name": "Luz" }, @@ -655,9 +899,15 @@ "prompt_tone": { "name": "Tono de aviso" }, + "regeneration": { + "name": "Regeneración" + }, "water_pump": { "name": "Bomba de agua" }, + "water_way": { + "name": "Vía de agua" + }, "screen_display": { "name": "Visualización de pantalla" }, @@ -673,6 +923,9 @@ "sleep_mode": { "name": "Modo sueño" }, + "out_silent": { + "name": "Modo silencioso exterior" + }, "smart_eye": { "name": "Ojo inteligente" }, @@ -682,12 +935,18 @@ "smelly_sensor": { "name": "Sensor de olores" }, + "soften": { + "name": "Suavizado" + }, "standby": { "name": "En espera" }, "start": { "name": "Iniciar" }, + "sterilization": { + "name": "Esterilización" + }, "storage": { "name": "Almacenamiento" }, diff --git a/custom_components/midea_ac_lan/translations/fr.json b/custom_components/midea_ac_lan/translations/fr.json index 94b2b23..381e96a 100644 --- a/custom_components/midea_ac_lan/translations/fr.json +++ b/custom_components/midea_ac_lan/translations/fr.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cooking" }, + "error_code": { + "name": "Code d'erreur" + }, "filter_change_reminder": { "name": "Filter Change Reminder" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Keep Warm" }, + "leak_water": { + "name": "Fuite d'eau" + }, "lid_status": { "name": "Lid Status" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Middle Compartment Preheating" }, + "top_elec_heat": { + "name": "Chauffage électrique supérieur" + }, + "bottom_elec_heat": { + "name": "Chauffage électrique inférieur" + }, + "mute_effect": { + "name": "Effet silencieux" + }, + "mute_status": { + "name": "État silencieux" + }, + "multi_terminal": { + "name": "Multi-terminal" + }, "oilcup_full": { "name": "Oil-cup Full" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Rinse Aid Shortage" }, + "rsj_stand_by": { + "name": "Veille" + }, "salt": { "name": "Salt Shortage" }, "seat_status": { "name": "Seat Status" }, + "smart_grid": { + "name": "Réseau intelligent" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Rappel d'entretien" + }, + "maintain_warn": { + "name": "Avertissement d'entretien" + }, + "order1_effect": { + "name": "Programmation 1 active" + }, + "order2_effect": { + "name": "Programmation 2 active" + }, "status_dhw": { "name": "DHW status" }, @@ -206,6 +248,20 @@ }, "zone2_water_temp_mode": { "name": "Zone2 Water-temperature Mode" + }, + "microcrystal_fresh": { + "name": "Fraîcheur microcristal" + }, + "electronic_smell": { + "name": "Désodorisation stérilisation" + }, + "water_pump_running": { + "name": "Pompe à eau en marche" + } + }, + "button": { + "start": { + "name": "Démarrer" } }, "climate": { @@ -214,11 +270,24 @@ }, "climate_zone2": { "name": "Zone2 Thermostat" + }, + "climate_key": { + "state_attributes": { + "fan_mode": { + "state": { + "silent": "Silencieux", + "full": "Maximum" + } + } + } } }, "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Extraction d'air frais" } }, "lock": { @@ -238,6 +307,21 @@ }, "water_temp_level": { "name": "Water Temperature Level" + }, + "fan_speed_percent": { + "name": "Pourcentage de vitesse du ventilateur" + }, + "vacation_days": { + "name": "Jours de vacances" + }, + "leak_water_protection_value": { + "name": "Valeur de protection contre les fuites" + }, + "flushing_days": { + "name": "Jours de régénération" + }, + "water_hardness": { + "name": "Dureté de l'eau" } }, "select": { @@ -250,6 +334,25 @@ "fan_speed": { "name": "Fan Speed" }, + "fresh_air_exhaust_mode": { + "name": "Vitesse d'extraction d'air frais", + "state": { + "off": "Arrêt", + "silent": "Faible", + "high": "Élevée", + "full": "Maximum" + } + }, + "fresh_air_mode": { + "name": "Vitesse d'air frais", + "state": { + "off": "Arrêt", + "low": "Faible", + "medium": "Moyenne", + "high": "Élevée", + "full": "Maximum" + } + }, "mode": { "name": "Mode" }, @@ -259,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Power Rate Limit" + }, "screen_display": { "name": "Screen Display" }, @@ -268,8 +374,38 @@ "tilting_angle": { "name": "Tilting Angle" }, + "wash_mode": { + "name": "Mode de lavage" + }, "water_level_set": { "name": "Water Level Setting" + }, + "wind_lr_angle": { + "name": "Flux d'air horizontal", + "state": { + "off": "Arrêt", + "left": "Gauche", + "left-mid": "Centre-gauche", + "middle": "Centre", + "right-mid": "Centre-droite", + "right": "Droite" + } + }, + "wind_ud_angle": { + "name": "Flux d'air vertical", + "state": { + "off": "Arrêt", + "up": "Haut", + "up-mid": "Centre-haut", + "middle": "Centre", + "down-mid": "Centre-bas", + "down": "Bas" + } + } + }, + "time": { + "timing_regeneration": { + "name": "Régénération programmée" } }, "sensor": { @@ -309,15 +445,71 @@ "dehydration_time": { "name": "dehydration time" }, + "dehydration_time_value": { + "name": "Valeur du temps d'essorage" + }, + "temperature": { + "name": "Température" + }, + "wash_time_value": { + "name": "Valeur du temps de lavage" + }, + "stains": { + "name": "Taches" + }, + "dirty_degree": { + "name": "Degré de saleté" + }, "detergent": { "name": "detergent" }, + "intensity": { + "name": "Intensité" + }, + "dryness_level": { + "name": "Niveau de séchage" + }, + "dry_temperature": { + "name": "Température de séchage" + }, + "door_warn": { + "name": "Alerte de porte" + }, + "ai_switch": { + "name": "Interrupteur IA" + }, + "material": { + "name": "Matériau" + }, + "water_box": { + "name": "Réservoir d'eau" + }, "energy_consumption": { "name": "Energy Consumption" }, + "error": { + "name": "Erreur", + "state": { + "no_error": "Aucune erreur", + "e1_position_not_found": "E1 Position introuvable", + "e2_photo_sensor_no_signal": "E2 Capteur photo sans signal", + "e3_motor_not_running": "E3 Moteur ne tourne pas", + "e4_wrong_position": "E4 Position incorrecte", + "e1_motor_fault": "E1 Panne moteur", + "e5_communication_fault": "E5 Panne de communication", + "e6_salt_sensor_fault": "E6 Panne capteur de sel", + "e7_chlorine_sterilization_fault": "E7 Panne stérilisation chlore" + } + }, "error_code": { "name": "Error Code" }, + "estimated_energy_consumption": { + "name": "Consommation d'énergie estimée" + }, + "estimated_water_consumption": { + "name": "Consommation d'eau estimée" + }, "fan_level": { "name": "Fan level" }, @@ -381,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Sel restant" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -409,7 +604,7 @@ "name": "Realtime Power" }, "pmv": { - "name": "PMV" + "name": "Indice PMV" }, "refrigerator_actual_temp": { "name": "Refrigerator Actual Temperature" @@ -417,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Secondes restantes de régénération" + }, + "remaining_days": { + "name": "Jours restants" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -429,12 +630,18 @@ "rinse_level": { "name": "rinse level" }, + "salt_setting": { + "name": "Réglage du sel" + }, "seat_temperature": { "name": "Seat Temperature" }, "soak_time": { "name": "soak time" }, + "soft_available": { + "name": "Eau douce disponible" + }, "softener": { "name": "softener" }, @@ -492,14 +699,110 @@ "water_consumption": { "name": "Water Consumption" }, + "water_consumption_average": { + "name": "Consommation moyenne d'eau" + }, + "water_consumption_big": { + "name": "Consommation d'eau" + }, "water_temperature": { "name": "Water Temperature" }, "water_level": { "name": "Water Level" }, + "wind": { + "name": "Vent" + }, + "typeinfo": { + "name": "Informations de type" + }, + "use_days": { + "name": "Jours d'utilisation" + }, + "elec_heat": { + "name": "Chauffage électrique" + }, + "water_pump": { + "name": "Pompe à eau" + }, + "four_way": { + "name": "Vanne quatre voies" + }, + "back_water": { + "name": "Retour d'eau" + }, + "sterilize": { + "name": "Stériliser" + }, + "disinfection_temperature": { + "name": "Température de désinfection" + }, + "max_temperature": { + "name": "Température cible maximale" + }, + "vacation_start_year": { + "name": "Année de début des vacances" + }, + "vacation_start_month": { + "name": "Mois de début des vacances" + }, + "vacation_start_day": { + "name": "Jour de début des vacances" + }, + "auto_sterilize_week": { + "name": "Semaine de stérilisation automatique" + }, + "auto_sterilize_hour": { + "name": "Heure de stérilisation automatique" + }, + "auto_sterilize_minute": { + "name": "Minute de stérilisation automatique" + }, "working_time": { "name": "Working Time" + }, + "humidity": { + "name": "Humidité" + }, + "variable_mode": { + "name": "Mode variable" + }, + "compressor_frequency": { + "name": "Fréquence du compresseur" + }, + "target_compressor_frequency": { + "name": "Fréquence cible du compresseur" + }, + "compressor_current": { + "name": "Courant du compresseur" + }, + "compressor_voltage": { + "name": "Tension du compresseur" + }, + "compressor_power": { + "name": "Puissance du compresseur" + }, + "indoor_coil_temperature": { + "name": "Température de la batterie intérieure (T1)" + }, + "evaporator_temperature": { + "name": "Température de l'évaporateur (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Température ambiante extérieure (T4)" + }, + "discharge_pipe_temperature": { + "name": "Température du tuyau de refoulement (TP)" + }, + "indoor_fan_speed": { + "name": "Vitesse du ventilateur intérieur" + }, + "target_indoor_fan_speed": { + "name": "Vitesse cible du ventilateur intérieur" + }, + "velocity": { + "name": "Vitesse" } }, "switch": { @@ -515,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Verrouillage enfant" + }, + "cl_sterilization": { + "name": "Stérilisation CL" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -524,12 +833,24 @@ "disinfect": { "name": "Disinfect" }, + "vacation_mode": { + "name": "Mode vacances" + }, "dry": { "name": "Dry" }, + "cold_water_single": { + "name": "Eau froide simple" + }, + "cold_water_dot": { + "name": "Point d'eau froide" + }, "eco_mode": { "name": "ECO Mode" }, + "memory": { + "name": "Mémo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -545,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Protection contre les fuites" + }, "light": { "name": "Light" }, @@ -575,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Régénération" + }, "water_pump": { "name": "Pompe à eau" }, + "water_way": { + "name": "Voie d'eau" + }, "screen_display": { "name": "Screen Display" }, @@ -593,6 +923,9 @@ "sleep_mode": { "name": "Sleep Mode" }, + "out_silent": { + "name": "Mode silencieux extérieur" + }, "smart_eye": { "name": "Smart Eye" }, @@ -602,17 +935,23 @@ "smelly_sensor": { "name": "Smelly Sensor" }, + "soften": { + "name": "Adoucissement" + }, "standby": { "name": "Standby" }, "start": { "name": "Start" }, + "sterilization": { + "name": "Stérilisation" + }, "storage": { "name": "Storage" }, "swing": { - "name": "swing" + "name": "Swing" }, "swing_horizontal": { "name": "Swing Horizontal" @@ -621,10 +960,10 @@ "name": "Swing Vertical" }, "self_clean": { - "name": "Self Clean" + "name": "Auto-nettoyage" }, "sound": { - "name": "Sound" + "name": "Son" }, "tbh": { "name": "TBH" diff --git a/custom_components/midea_ac_lan/translations/hu.json b/custom_components/midea_ac_lan/translations/hu.json index 404b158..9c6265e 100644 --- a/custom_components/midea_ac_lan/translations/hu.json +++ b/custom_components/midea_ac_lan/translations/hu.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cooking" }, + "error_code": { + "name": "Hibakód" + }, "filter_change_reminder": { "name": "Filter Change Reminder" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Keep Warm" }, + "leak_water": { + "name": "Vízszivárgás" + }, "lid_status": { "name": "Lid Status" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Middle Compartment Preheating" }, + "top_elec_heat": { + "name": "Felső elektromos fűtés" + }, + "bottom_elec_heat": { + "name": "Alsó elektromos fűtés" + }, + "mute_effect": { + "name": "Némítás hatása" + }, + "mute_status": { + "name": "Némítás állapota" + }, + "multi_terminal": { + "name": "Több terminál" + }, "oilcup_full": { "name": "Oil-cup Full" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Rinse Aid Shortage" }, + "rsj_stand_by": { + "name": "Készenlét" + }, "salt": { "name": "Salt Shortage" }, "seat_status": { "name": "Seat Status" }, + "smart_grid": { + "name": "Okos hálózat" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Karbantartási emlékeztető" + }, + "maintain_warn": { + "name": "Karbantartási figyelmeztetés" + }, + "order1_effect": { + "name": "1. ütemezés aktív" + }, + "order2_effect": { + "name": "2. ütemezés aktív" + }, "status_dhw": { "name": "DHW status" }, @@ -206,6 +248,20 @@ }, "zone2_water_temp_mode": { "name": "Zone2 Water-temperature Mode" + }, + "microcrystal_fresh": { + "name": "Mikrokristályos frissítés" + }, + "electronic_smell": { + "name": "Szagtalanító sterilizálás" + }, + "water_pump_running": { + "name": "Vízszivattyú működik" + } + }, + "button": { + "start": { + "name": "Indítás" } }, "climate": { @@ -214,11 +270,24 @@ }, "climate_zone2": { "name": "Zone2 Thermostat" + }, + "climate_key": { + "state_attributes": { + "fan_mode": { + "state": { + "silent": "Csendes", + "full": "Teljes" + } + } + } } }, "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Frisslevegő elszívás" } }, "lock": { @@ -238,6 +307,21 @@ }, "water_temp_level": { "name": "Water Temperature Level" + }, + "fan_speed_percent": { + "name": "Ventilátor sebesség százalék" + }, + "vacation_days": { + "name": "Vakáció napok" + }, + "leak_water_protection_value": { + "name": "Vízszivárgás védelem értéke" + }, + "flushing_days": { + "name": "Regenerálási napok" + }, + "water_hardness": { + "name": "Víz keménység" } }, "select": { @@ -250,6 +334,25 @@ "fan_speed": { "name": "Fan Speed" }, + "fresh_air_exhaust_mode": { + "name": "Frisslevegő elszívás sebessége", + "state": { + "off": "Ki", + "silent": "Alacsony", + "high": "Magas", + "full": "Teljes" + } + }, + "fresh_air_mode": { + "name": "Frisslevegő sebessége", + "state": { + "off": "Ki", + "low": "Alacsony", + "medium": "Közepes", + "high": "Magas", + "full": "Teljes" + } + }, "mode": { "name": "Mode" }, @@ -259,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Power Rate Limit" + }, "screen_display": { "name": "Screen Display" }, @@ -268,8 +374,38 @@ "tilting_angle": { "name": "Tilting Angle" }, + "wash_mode": { + "name": "Mosási mód" + }, "water_level_set": { "name": "Water Level Setting" + }, + "wind_lr_angle": { + "name": "Légáramlás vízszintes", + "state": { + "off": "Ki", + "left": "Bal", + "left-mid": "Bal-közép", + "middle": "Közép", + "right-mid": "Jobb-közép", + "right": "Jobb" + } + }, + "wind_ud_angle": { + "name": "Légáramlás függőleges", + "state": { + "off": "Ki", + "up": "Fel", + "up-mid": "Fel-közép", + "middle": "Közép", + "down-mid": "Le-közép", + "down": "Le" + } + } + }, + "time": { + "timing_regeneration": { + "name": "Időzített regenerálás" } }, "sensor": { @@ -309,15 +445,71 @@ "dehydration_time": { "name": "dehydration time" }, + "dehydration_time_value": { + "name": "Víztelenítési idő értéke" + }, + "temperature": { + "name": "Hőmérséklet" + }, + "wash_time_value": { + "name": "Mosási idő értéke" + }, + "stains": { + "name": "Foltok" + }, + "dirty_degree": { + "name": "Szennyezettség mértéke" + }, "detergent": { "name": "detergent" }, + "intensity": { + "name": "Intenzitás" + }, + "dryness_level": { + "name": "Szárazsági szint" + }, + "dry_temperature": { + "name": "Szárítási hőmérséklet" + }, + "door_warn": { + "name": "Ajtó figyelmeztetés" + }, + "ai_switch": { + "name": "AI kapcsoló" + }, + "material": { + "name": "Anyag" + }, + "water_box": { + "name": "Víztartály" + }, "energy_consumption": { "name": "Energy Consumption" }, + "error": { + "name": "Hiba", + "state": { + "no_error": "Nincs hiba", + "e1_position_not_found": "E1 Pozíció nem található", + "e2_photo_sensor_no_signal": "E2 Fotóérzékelő nincs jel", + "e3_motor_not_running": "E3 Motor nem fut", + "e4_wrong_position": "E4 Hibás pozíció", + "e1_motor_fault": "E1 Motor hiba", + "e5_communication_fault": "E5 Kommunikációs hiba", + "e6_salt_sensor_fault": "E6 Sóérzékelő hiba", + "e7_chlorine_sterilization_fault": "E7 Klór sterilizációs hiba" + } + }, "error_code": { "name": "Error Code" }, + "estimated_energy_consumption": { + "name": "Becsült energiafogyasztás" + }, + "estimated_water_consumption": { + "name": "Becsült vízfogyasztás" + }, "fan_level": { "name": "Fan level" }, @@ -381,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Maradék só" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -417,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Regenerálás hátralévő másodperc" + }, + "remaining_days": { + "name": "Hátralévő napok" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -429,12 +630,18 @@ "rinse_level": { "name": "rinse level" }, + "salt_setting": { + "name": "Só beállítása" + }, "seat_temperature": { "name": "Seat Temperature" }, "soak_time": { "name": "soak time" }, + "soft_available": { + "name": "Elérhető lágy víz" + }, "softener": { "name": "softener" }, @@ -492,14 +699,110 @@ "water_consumption": { "name": "Water Consumption" }, + "water_consumption_average": { + "name": "Átlagos vízfogyasztás" + }, + "water_consumption_big": { + "name": "Vízfogyasztás" + }, "water_temperature": { "name": "Water Temperature" }, "water_level": { "name": "Water Level" }, + "wind": { + "name": "Szél" + }, + "typeinfo": { + "name": "Típusinformáció" + }, + "use_days": { + "name": "Használati napok" + }, + "elec_heat": { + "name": "Elektromos fűtés" + }, + "water_pump": { + "name": "Vízszivattyú" + }, + "four_way": { + "name": "Négyjáratú szelep" + }, + "back_water": { + "name": "Visszatérő víz" + }, + "sterilize": { + "name": "Sterilizálás" + }, + "disinfection_temperature": { + "name": "Fertőtlenítési hőmérséklet" + }, + "max_temperature": { + "name": "Maximális cél hőmérséklet" + }, + "vacation_start_year": { + "name": "Vakáció kezdő év" + }, + "vacation_start_month": { + "name": "Vakáció kezdő hónap" + }, + "vacation_start_day": { + "name": "Vakáció kezdő nap" + }, + "auto_sterilize_week": { + "name": "Automatikus sterilizálás hét" + }, + "auto_sterilize_hour": { + "name": "Automatikus sterilizálás óra" + }, + "auto_sterilize_minute": { + "name": "Automatikus sterilizálás perc" + }, "working_time": { "name": "Working Time" + }, + "humidity": { + "name": "Páratartalom" + }, + "variable_mode": { + "name": "Változó mód" + }, + "compressor_frequency": { + "name": "Kompresszor frekvencia" + }, + "target_compressor_frequency": { + "name": "Cél kompresszor frekvencia" + }, + "compressor_current": { + "name": "Kompresszor áramerősség" + }, + "compressor_voltage": { + "name": "Kompresszor feszültség" + }, + "compressor_power": { + "name": "Kompresszor teljesítmény" + }, + "indoor_coil_temperature": { + "name": "Beltéri hőcserélő hőmérséklet (T1)" + }, + "evaporator_temperature": { + "name": "Párologtató hőmérséklet (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Kültéri környezeti hőmérséklet (T4)" + }, + "discharge_pipe_temperature": { + "name": "Nyomócső hőmérséklet (TP)" + }, + "indoor_fan_speed": { + "name": "Beltéri ventilátor sebesség" + }, + "target_indoor_fan_speed": { + "name": "Cél beltéri ventilátor sebesség" + }, + "velocity": { + "name": "Sebesség" } }, "switch": { @@ -515,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Gyermekzár" + }, + "cl_sterilization": { + "name": "CL sterilizálás" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -524,12 +833,24 @@ "disinfect": { "name": "Disinfect" }, + "vacation_mode": { + "name": "Vakáció mód" + }, "dry": { "name": "Dry" }, + "cold_water_single": { + "name": "Hideg víz egyszeri" + }, + "cold_water_dot": { + "name": "Hideg víz pont" + }, "eco_mode": { "name": "ECO Mode" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -545,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Védelem a vízszivárgás ellen" + }, "light": { "name": "Light" }, @@ -575,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Regenerálás" + }, "water_pump": { "name": "Vízszivattyú" }, + "water_way": { + "name": "Vízi út" + }, "screen_display": { "name": "Screen Display" }, @@ -593,6 +923,9 @@ "sleep_mode": { "name": "Sleep Mode" }, + "out_silent": { + "name": "Kültéri csendes mód" + }, "smart_eye": { "name": "Smart Eye" }, @@ -602,12 +935,18 @@ "smelly_sensor": { "name": "Smelly Sensor" }, + "soften": { + "name": "Lágyítás" + }, "standby": { "name": "Standby" }, "start": { "name": "Start" }, + "sterilization": { + "name": "Sterilizálás" + }, "storage": { "name": "Storage" }, diff --git a/custom_components/midea_ac_lan/translations/ru.json b/custom_components/midea_ac_lan/translations/ru.json index 846f7d6..309a974 100644 --- a/custom_components/midea_ac_lan/translations/ru.json +++ b/custom_components/midea_ac_lan/translations/ru.json @@ -93,6 +93,9 @@ "cooking": { "name": "Приготовление" }, + "error_code": { + "name": "Код ошибки" + }, "filter_change_reminder": { "name": "Требуется замена фильтра" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Поддержание температуры" }, + "leak_water": { + "name": "Протечка воды" + }, "lid_status": { "name": "Крышка" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Middle Compartment Preheating" }, + "top_elec_heat": { + "name": "Верхний электронагрев" + }, + "bottom_elec_heat": { + "name": "Нижний электронагрев" + }, + "mute_effect": { + "name": "Эффект тихого режима" + }, + "mute_status": { + "name": "Статус тихого режима" + }, + "multi_terminal": { + "name": "Мультитерминал" + }, "oilcup_full": { "name": "Oil-cup Full" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Нехватка ополаскивателя" }, + "rsj_stand_by": { + "name": "Ожидание" + }, "salt": { "name": "Соль" }, "seat_status": { "name": "Сиденье" }, + "smart_grid": { + "name": "Умная сеть" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Напоминание об обслуживании" + }, + "maintain_warn": { + "name": "Предупреждение об обслуживании" + }, + "order1_effect": { + "name": "Расписание 1 активно" + }, + "order2_effect": { + "name": "Расписание 2 активно" + }, "status_dhw": { "name": "DHW status" }, @@ -206,6 +248,20 @@ }, "zone2_water_temp_mode": { "name": "Zone2 Water-temperature Mode" + }, + "microcrystal_fresh": { + "name": "Микрокристаллическая свежесть" + }, + "electronic_smell": { + "name": "Дезодорация и стерилизация" + }, + "water_pump_running": { + "name": "Работа водяного насоса" + } + }, + "button": { + "start": { + "name": "Старт" } }, "climate": { @@ -214,11 +270,24 @@ }, "climate_zone2": { "name": "Zone2 Thermostat" + }, + "climate_key": { + "state_attributes": { + "fan_mode": { + "state": { + "silent": "Тихий", + "full": "Полная" + } + } + } } }, "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Вытяжка свежего воздуха" } }, "lock": { @@ -238,6 +307,21 @@ }, "water_temp_level": { "name": "Water Temperature Level" + }, + "fan_speed_percent": { + "name": "Скорость вентилятора (%)" + }, + "vacation_days": { + "name": "Дни отпуска" + }, + "leak_water_protection_value": { + "name": "Значение защиты от протечек" + }, + "flushing_days": { + "name": "Дни регенерации" + }, + "water_hardness": { + "name": "Жёсткость воды" } }, "select": { @@ -250,6 +334,25 @@ "fan_speed": { "name": "Скорость" }, + "fresh_air_exhaust_mode": { + "name": "Скорость вытяжки свежего воздуха", + "state": { + "off": "Выкл", + "silent": "Низкая", + "high": "Высокая", + "full": "Максимальная" + } + }, + "fresh_air_mode": { + "name": "Скорость свежего воздуха", + "state": { + "off": "Выкл", + "low": "Низкая", + "medium": "Средняя", + "high": "Высокая", + "full": "Максимальная" + } + }, "mode": { "name": "Режим" }, @@ -259,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Ограничение мощности" + }, "screen_display": { "name": "Screen Display" }, @@ -268,8 +374,38 @@ "tilting_angle": { "name": "Угол поворота" }, + "wash_mode": { + "name": "Режим мойки" + }, "water_level_set": { "name": "Water Level Setting" + }, + "wind_lr_angle": { + "name": "Горизонтальный поток воздуха", + "state": { + "off": "Выкл", + "left": "Влево", + "left-mid": "Влево-центр", + "middle": "Центр", + "right-mid": "Вправо-центр", + "right": "Вправо" + } + }, + "wind_ud_angle": { + "name": "Вертикальный поток воздуха", + "state": { + "off": "Выкл", + "up": "Вверх", + "up-mid": "Вверх-центр", + "middle": "Центр", + "down-mid": "Вниз-центр", + "down": "Вниз" + } + } + }, + "time": { + "timing_regeneration": { + "name": "Регенерация по таймеру" } }, "sensor": { @@ -312,6 +448,9 @@ "dehydration_time_value": { "name": "Dehydration time value" }, + "temperature": { + "name": "Температура" + }, "wash_time_value": { "name": "Wash time value" }, @@ -348,9 +487,29 @@ "energy_consumption": { "name": "Энергопотребление" }, + "error": { + "name": "Ошибка", + "state": { + "no_error": "Нет ошибок", + "e1_position_not_found": "E1 Позиция не найдена", + "e2_photo_sensor_no_signal": "E2 Датчик не даёт сигнал", + "e3_motor_not_running": "E3 Двигатель не работает", + "e4_wrong_position": "E4 Неправильная позиция", + "e1_motor_fault": "E1 Неисправность двигателя", + "e5_communication_fault": "E5 Ошибка связи", + "e6_salt_sensor_fault": "E6 Неисправность датчика соли", + "e7_chlorine_sterilization_fault": "E7 Неисправность хлорной стерилизации" + } + }, "error_code": { "name": "Код ошибки" }, + "estimated_energy_consumption": { + "name": "Расчётное энергопотребление" + }, + "estimated_water_consumption": { + "name": "Расчётный расход воды" + }, "fan_level": { "name": "Fan level" }, @@ -414,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Остаток соли" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -450,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Оставшиеся секунды регенерации" + }, + "remaining_days": { + "name": "Оставшиеся дни" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -462,12 +630,18 @@ "rinse_level": { "name": "Уровень ополаскивателя" }, + "salt_setting": { + "name": "Настройка соли" + }, "seat_temperature": { "name": "Температура сиденья" }, "soak_time": { "name": "Время замачивания" }, + "soft_available": { + "name": "Доступная мягкая вода" + }, "softener": { "name": "Умягчитель" }, @@ -513,6 +687,9 @@ "tvoc": { "name": "TVOC" }, + "wash_level": { + "name": "Количество полосканий" + }, "wash_strength": { "name": "Wash strength" }, @@ -522,14 +699,110 @@ "water_consumption": { "name": "Water Consumption" }, + "water_consumption_average": { + "name": "Средний расход воды" + }, + "water_consumption_big": { + "name": "Потребление воды" + }, "water_temperature": { "name": "Температура воды" }, "water_level": { "name": "Уровень воды" }, + "wind": { + "name": "Поток воздуха" + }, + "typeinfo": { + "name": "Информация о типе" + }, + "use_days": { + "name": "Дни использования" + }, + "elec_heat": { + "name": "Электронагрев" + }, + "water_pump": { + "name": "Водяной насос" + }, + "four_way": { + "name": "Четырёхходовой клапан" + }, + "back_water": { + "name": "Обратная вода" + }, + "sterilize": { + "name": "Стерилизация" + }, + "disinfection_temperature": { + "name": "Температура дезинфекции" + }, + "max_temperature": { + "name": "Максимальная целевая температура" + }, + "vacation_start_year": { + "name": "Год начала отпуска" + }, + "vacation_start_month": { + "name": "Месяц начала отпуска" + }, + "vacation_start_day": { + "name": "День начала отпуска" + }, + "auto_sterilize_week": { + "name": "День недели автостерилизации" + }, + "auto_sterilize_hour": { + "name": "Час автостерилизации" + }, + "auto_sterilize_minute": { + "name": "Минута автостерилизации" + }, "working_time": { "name": "Время работы" + }, + "humidity": { + "name": "Влажность" + }, + "variable_mode": { + "name": "Переменный режим" + }, + "compressor_frequency": { + "name": "Частота компрессора" + }, + "target_compressor_frequency": { + "name": "Целевая частота компрессора" + }, + "compressor_current": { + "name": "Ток компрессора" + }, + "compressor_voltage": { + "name": "Напряжение компрессора" + }, + "compressor_power": { + "name": "Мощность компрессора" + }, + "indoor_coil_temperature": { + "name": "Температура внутреннего теплообменника (T1)" + }, + "evaporator_temperature": { + "name": "Температура испарителя (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Наружная температура окружающей среды (T4)" + }, + "discharge_pipe_temperature": { + "name": "Температура нагнетательной трубы (TP)" + }, + "indoor_fan_speed": { + "name": "Скорость внутреннего вентилятора" + }, + "target_indoor_fan_speed": { + "name": "Целевая скорость внутреннего вентилятора" + }, + "velocity": { + "name": "Скорость" } }, "switch": { @@ -545,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Блокировка от детей" + }, + "cl_sterilization": { + "name": "CL-стерилизация" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -554,12 +833,24 @@ "disinfect": { "name": "Дезинфекция" }, + "vacation_mode": { + "name": "Режим отпуска" + }, "dry": { "name": "Dry" }, + "cold_water_single": { + "name": "Холодная вода (однократно)" + }, + "cold_water_dot": { + "name": "Холодная вода (порционно)" + }, "eco_mode": { "name": "Эко-режим" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -575,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Защита от протечек" + }, "light": { "name": "Свет" }, @@ -605,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Регенерация" + }, "water_pump": { "name": "Водяной насос" }, + "water_way": { + "name": "Водный путь" + }, "screen_display": { "name": "Screen Display" }, @@ -623,6 +923,9 @@ "sleep_mode": { "name": "Sleep Mode" }, + "out_silent": { + "name": "Тихий режим наружного блока" + }, "smart_eye": { "name": "Smart Eye" }, @@ -632,12 +935,18 @@ "smelly_sensor": { "name": "Smelly Sensor" }, + "soften": { + "name": "Умягчение" + }, "standby": { "name": "Ожидание" }, "start": { "name": "Старт" }, + "sterilization": { + "name": "Стерилизация" + }, "storage": { "name": "Storage" }, diff --git a/custom_components/midea_ac_lan/translations/sk.json b/custom_components/midea_ac_lan/translations/sk.json index 066adcc..8b074a9 100644 --- a/custom_components/midea_ac_lan/translations/sk.json +++ b/custom_components/midea_ac_lan/translations/sk.json @@ -93,6 +93,9 @@ "cooking": { "name": "Cooking" }, + "error_code": { + "name": "Kód chyby" + }, "filter_change_reminder": { "name": "Filter Change Reminder" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "Keep Warm" }, + "leak_water": { + "name": "Únik vody" + }, "lid_status": { "name": "Lid Status" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "Middle Compartment Preheating" }, + "top_elec_heat": { + "name": "Horné elektrické vykurovanie" + }, + "bottom_elec_heat": { + "name": "Dolné elektrické vykurovanie" + }, + "mute_effect": { + "name": "Účinok stíšenia" + }, + "mute_status": { + "name": "Stav stíšenia" + }, + "multi_terminal": { + "name": "Viacero terminálov" + }, "oilcup_full": { "name": "Oil-cup Full" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "Rinse Aid Shortage" }, + "rsj_stand_by": { + "name": "Pohotovostný režim" + }, "salt": { "name": "Salt Shortage" }, "seat_status": { "name": "Seat Status" }, + "smart_grid": { + "name": "Inteligentná sieť" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "Pripomienka údržby" + }, + "maintain_warn": { + "name": "Upozornenie na údržbu" + }, + "order1_effect": { + "name": "Plán 1 aktívny" + }, + "order2_effect": { + "name": "Plán 2 aktívny" + }, "status_dhw": { "name": "DHW status" }, @@ -206,6 +248,20 @@ }, "zone2_water_temp_mode": { "name": "Zone2 Water-temperature Mode" + }, + "microcrystal_fresh": { + "name": "Mikrokryštalická sviežosť" + }, + "electronic_smell": { + "name": "Dezodorácia a sterilizácia" + }, + "water_pump_running": { + "name": "Vodné čerpadlo beží" + } + }, + "button": { + "start": { + "name": "Štart" } }, "climate": { @@ -214,11 +270,24 @@ }, "climate_zone2": { "name": "Zone2 Thermostat" + }, + "climate_key": { + "state_attributes": { + "fan_mode": { + "state": { + "silent": "Tichý", + "full": "Plný" + } + } + } } }, "fan": { "fresh_air": { "name": "Fresh Air" + }, + "fresh_air_exhaust": { + "name": "Odvod čerstvého vzduchu" } }, "lock": { @@ -238,6 +307,21 @@ }, "water_temp_level": { "name": "Water Temperature Level" + }, + "fan_speed_percent": { + "name": "Percento rýchlosti ventilátora" + }, + "vacation_days": { + "name": "Dni dovolenky" + }, + "leak_water_protection_value": { + "name": "Hodnota ochrany proti úniku vody" + }, + "flushing_days": { + "name": "Dni regenerácie" + }, + "water_hardness": { + "name": "Tvrdosť vody" } }, "select": { @@ -250,6 +334,25 @@ "fan_speed": { "name": "Fan Speed" }, + "fresh_air_exhaust_mode": { + "name": "Rýchlosť odvodu čerstvého vzduchu", + "state": { + "off": "Vypnuté", + "silent": "Nízka", + "high": "Vysoká", + "full": "Plná" + } + }, + "fresh_air_mode": { + "name": "Rýchlosť čerstvého vzduchu", + "state": { + "off": "Vypnuté", + "low": "Nízka", + "medium": "Stredná", + "high": "Vysoká", + "full": "Plná" + } + }, "mode": { "name": "Mode" }, @@ -259,6 +362,9 @@ "oscillation_mode": { "name": "Oscillation Mode" }, + "rate_select": { + "name": "Power Rate Limit" + }, "screen_display": { "name": "Screen Display" }, @@ -268,8 +374,38 @@ "tilting_angle": { "name": "Tilting Angle" }, + "wash_mode": { + "name": "Režim prania" + }, "water_level_set": { "name": "Water Level Setting" + }, + "wind_lr_angle": { + "name": "Vodorovné prúdenie vzduchu", + "state": { + "off": "Vypnuté", + "left": "Vľavo", + "left-mid": "Vľavo-stred", + "middle": "Stred", + "right-mid": "Vpravo-stred", + "right": "Vpravo" + } + }, + "wind_ud_angle": { + "name": "Zvislé prúdenie vzduchu", + "state": { + "off": "Vypnuté", + "up": "Hore", + "up-mid": "Hore-stred", + "middle": "Stred", + "down-mid": "Dole-stred", + "down": "Dole" + } + } + }, + "time": { + "timing_regeneration": { + "name": "Časovaná regenerácia" } }, "sensor": { @@ -309,15 +445,71 @@ "dehydration_time": { "name": "dehydration time" }, + "dehydration_time_value": { + "name": "Hodnota času odstreďovania" + }, + "temperature": { + "name": "Teplota" + }, + "wash_time_value": { + "name": "Hodnota času prania" + }, + "stains": { + "name": "Škvrny" + }, + "dirty_degree": { + "name": "Stupeň znečistenia" + }, "detergent": { "name": "detergent" }, + "intensity": { + "name": "Intenzita" + }, + "dryness_level": { + "name": "Úroveň suchosti" + }, + "dry_temperature": { + "name": "Teplota sušenia" + }, + "door_warn": { + "name": "Upozornenie na dvere" + }, + "ai_switch": { + "name": "AI prepínač" + }, + "material": { + "name": "Materiál" + }, + "water_box": { + "name": "Nádržka na vodu" + }, "energy_consumption": { "name": "Energy Consumption" }, + "error": { + "name": "Chyba", + "state": { + "no_error": "Bez chyby", + "e1_position_not_found": "E1 Pozícia nenájdená", + "e2_photo_sensor_no_signal": "E2 Fotosenzor bez signálu", + "e3_motor_not_running": "E3 Motor nebeží", + "e4_wrong_position": "E4 Nesprávna pozícia", + "e1_motor_fault": "E1 Porucha motora", + "e5_communication_fault": "E5 Porucha komunikácie", + "e6_salt_sensor_fault": "E6 Porucha senzora soli", + "e7_chlorine_sterilization_fault": "E7 Porucha chlórovej sterilizácie" + } + }, "error_code": { "name": "Error Code" }, + "estimated_energy_consumption": { + "name": "Odhadovaná spotreba energie" + }, + "estimated_water_consumption": { + "name": "Odhadovaná spotreba vody" + }, "fan_level": { "name": "Fan level" }, @@ -381,6 +573,9 @@ "keep_warm_time": { "name": "Keep Warm Time" }, + "left_salt": { + "name": "Zvyšná soľ" + }, "middle_compartment_remaining": { "name": "Middle Compartment Remaining" }, @@ -417,6 +612,12 @@ "refrigerator_setting_temp": { "name": "Refrigerator Setting Temperature" }, + "regeneration_left_seconds": { + "name": "Zostávajúce sekundy regenerácie" + }, + "remaining_days": { + "name": "Zostávajúce dni" + }, "right_flex_zone_actual_temp": { "name": "Right Flex Zone Actual Temperature" }, @@ -429,12 +630,18 @@ "rinse_level": { "name": "rinse level" }, + "salt_setting": { + "name": "Nastavenie soli" + }, "seat_temperature": { "name": "Seat Temperature" }, "soak_time": { "name": "soak time" }, + "soft_available": { + "name": "Dostupná mäkká voda" + }, "softener": { "name": "softener" }, @@ -492,14 +699,110 @@ "water_consumption": { "name": "Water Consumption" }, + "water_consumption_average": { + "name": "Priemerná spotreba vody" + }, + "water_consumption_big": { + "name": "Spotreba vody" + }, "water_temperature": { "name": "Water Temperature" }, "water_level": { "name": "Water Level" }, + "wind": { + "name": "Vietor" + }, + "typeinfo": { + "name": "Informácie o type" + }, + "use_days": { + "name": "Dni používania" + }, + "elec_heat": { + "name": "Elektrické vykurovanie" + }, + "water_pump": { + "name": "Vodné čerpadlo" + }, + "four_way": { + "name": "Štvorcestný ventil" + }, + "back_water": { + "name": "Spätná voda" + }, + "sterilize": { + "name": "Sterilizácia" + }, + "disinfection_temperature": { + "name": "Teplota dezinfekcie" + }, + "max_temperature": { + "name": "Maximálna cieľová teplota" + }, + "vacation_start_year": { + "name": "Rok začiatku dovolenky" + }, + "vacation_start_month": { + "name": "Mesiac začiatku dovolenky" + }, + "vacation_start_day": { + "name": "Deň začiatku dovolenky" + }, + "auto_sterilize_week": { + "name": "Týždeň automatickej sterilizácie" + }, + "auto_sterilize_hour": { + "name": "Hodina automatickej sterilizácie" + }, + "auto_sterilize_minute": { + "name": "Minúta automatickej sterilizácie" + }, "working_time": { "name": "Working Time" + }, + "humidity": { + "name": "Vlhkosť" + }, + "variable_mode": { + "name": "Variabilný režim" + }, + "compressor_frequency": { + "name": "Frekvencia kompresora" + }, + "target_compressor_frequency": { + "name": "Cieľová frekvencia kompresora" + }, + "compressor_current": { + "name": "Prúd kompresora" + }, + "compressor_voltage": { + "name": "Napätie kompresora" + }, + "compressor_power": { + "name": "Výkon kompresora" + }, + "indoor_coil_temperature": { + "name": "Teplota vnútorného výmenníka (T1)" + }, + "evaporator_temperature": { + "name": "Teplota výparníka (T2)" + }, + "outdoor_ambient_temperature": { + "name": "Vonkajšia teplota okolia (T4)" + }, + "discharge_pipe_temperature": { + "name": "Teplota výtlačného potrubia (TP)" + }, + "indoor_fan_speed": { + "name": "Rýchlosť vnútorného ventilátora" + }, + "target_indoor_fan_speed": { + "name": "Cieľová rýchlosť vnútorného ventilátora" + }, + "velocity": { + "name": "Rýchlosť" } }, "switch": { @@ -515,6 +818,12 @@ "breezeless": { "name": "Breezeless" }, + "child_lock": { + "name": "Detský zámok" + }, + "cl_sterilization": { + "name": "CL sterilizácia" + }, "comfort_mode": { "name": "Comfort Mode" }, @@ -524,12 +833,24 @@ "disinfect": { "name": "Disinfect" }, + "vacation_mode": { + "name": "Režim dovolenky" + }, "dry": { "name": "Dry" }, + "cold_water_single": { + "name": "Studená voda jednorazovo" + }, + "cold_water_dot": { + "name": "Studená voda bodovo" + }, "eco_mode": { "name": "ECO Mode" }, + "memory": { + "name": "Memo U" + }, "fast_dhw": { "name": "Fast DHW" }, @@ -545,6 +866,9 @@ "indirect_wind": { "name": "Indirect Wind" }, + "leak_water_protection": { + "name": "Ochrana proti úniku vody" + }, "light": { "name": "Light" }, @@ -575,9 +899,15 @@ "prompt_tone": { "name": "Prompt Tone" }, + "regeneration": { + "name": "Regenerácia" + }, "water_pump": { "name": "Vodné čerpadlo" }, + "water_way": { + "name": "Vodná cesta" + }, "screen_display": { "name": "Screen Display" }, @@ -593,6 +923,9 @@ "sleep_mode": { "name": "Sleep Mode" }, + "out_silent": { + "name": "Vonkajší tichý režim" + }, "smart_eye": { "name": "Smart Eye" }, @@ -602,12 +935,18 @@ "smelly_sensor": { "name": "Smelly Sensor" }, + "soften": { + "name": "Zmäkčovanie" + }, "standby": { "name": "Standby" }, "start": { "name": "Start" }, + "sterilization": { + "name": "Sterilizácia" + }, "storage": { "name": "Storage" }, diff --git a/custom_components/midea_ac_lan/translations/zh-Hans.json b/custom_components/midea_ac_lan/translations/zh-Hans.json index 8743f2f..1087a0c 100644 --- a/custom_components/midea_ac_lan/translations/zh-Hans.json +++ b/custom_components/midea_ac_lan/translations/zh-Hans.json @@ -93,6 +93,9 @@ "cooking": { "name": "烹饪中" }, + "error_code": { + "name": "错误代码" + }, "filter_change_reminder": { "name": "滤芯更换提醒" }, @@ -126,6 +129,9 @@ "keep_warm": { "name": "保温" }, + "leak_water": { + "name": "漏水报警" + }, "lid_status": { "name": "盖子状态" }, @@ -138,6 +144,21 @@ "middle_compartment_preheating": { "name": "中层预热" }, + "top_elec_heat": { + "name": "顶部电加热" + }, + "bottom_elec_heat": { + "name": "底部电加热" + }, + "mute_effect": { + "name": "静音效果" + }, + "mute_status": { + "name": "静音状态" + }, + "multi_terminal": { + "name": "多终端" + }, "oilcup_full": { "name": "油杯满提示" }, @@ -153,12 +174,33 @@ "rinse_aid": { "name": "漂洗剂不足" }, + "rsj_stand_by": { + "name": "待机状态" + }, "salt": { "name": "软水盐不足" }, "seat_status": { "name": "入座状态" }, + "smart_grid": { + "name": "智能电网" + }, + "eco": { + "name": "ECO" + }, + "maintenance_reminder": { + "name": "维护提醒" + }, + "maintain_warn": { + "name": "维护警告" + }, + "order1_effect": { + "name": "定时1生效" + }, + "order2_effect": { + "name": "定时2生效" + }, "status_dhw": { "name": "DHW状态" }, @@ -212,6 +254,14 @@ }, "electronic_smell": { "name": "净味杀菌" + }, + "water_pump_running": { + "name": "水泵运行" + } + }, + "button": { + "start": { + "name": "启动" } }, "climate": { @@ -235,6 +285,9 @@ "fan": { "fresh_air": { "name": "新风" + }, + "fresh_air_exhaust": { + "name": "排风" } }, "lock": { @@ -257,6 +310,18 @@ }, "fan_speed_percent": { "name": "风速百分比" + }, + "vacation_days": { + "name": "假期天数" + }, + "leak_water_protection_value": { + "name": "漏水保护阈值" + }, + "flushing_days": { + "name": "再生天数" + }, + "water_hardness": { + "name": "进水硬度" } }, "select": { @@ -269,6 +334,25 @@ "fan_speed": { "name": "风速设定" }, + "fresh_air_exhaust_mode": { + "name": "排风风速", + "state": { + "off": "关闭", + "silent": "低速", + "high": "高速", + "full": "强劲" + } + }, + "fresh_air_mode": { + "name": "新风风速", + "state": { + "off": "关闭", + "low": "低速", + "medium": "中速", + "high": "高速", + "full": "强劲" + } + }, "mode": { "name": "模式" }, @@ -278,6 +362,9 @@ "oscillation_mode": { "name": "水平摆头模式" }, + "rate_select": { + "name": "功率限制" + }, "screen_display": { "name": "屏幕显示" }, @@ -287,6 +374,9 @@ "tilting_angle": { "name": "垂直摆头角度" }, + "wash_mode": { + "name": "洗涤模式" + }, "water_level_set": { "name": "水位设定" }, @@ -313,6 +403,11 @@ } } }, + "time": { + "timing_regeneration": { + "name": "定时再生时间" + } + }, "sensor": { "bathing_leaving_temperature": { "name": "淋浴出水温度" @@ -353,6 +448,9 @@ "dehydration_time_value": { "name": "脱水时间值" }, + "temperature": { + "name": "目标温度" + }, "wash_time_value": { "name": "洗涤时间值" }, @@ -362,6 +460,9 @@ "dirty_degree": { "name": "脏度" }, + "detergent": { + "name": "洗涤剂" + }, "intensity": { "name": "强度" }, @@ -386,9 +487,29 @@ "energy_consumption": { "name": "能耗" }, + "error": { + "name": "错误", + "state": { + "no_error": "无故障", + "e1_position_not_found": "E1 一直找不到工作位", + "e2_photo_sensor_no_signal": "E2 光感没有发出信号", + "e3_motor_not_running": "E3 电机不转", + "e4_wrong_position": "E4 工作位不正确", + "e1_motor_fault": "E1 电机故障", + "e5_communication_fault": "E5 通信故障", + "e6_salt_sensor_fault": "E6 盐位传感器故障", + "e7_chlorine_sterilization_fault": "E7 余氯杀菌故障" + } + }, "error_code": { "name": "错误码" }, + "estimated_energy_consumption": { + "name": "预估能耗" + }, + "estimated_water_consumption": { + "name": "预估水耗" + }, "fan_level": { "name": "当前风扇档位" }, @@ -452,6 +573,9 @@ "keep_warm_time": { "name": "保温时间" }, + "left_salt": { + "name": "剩余盐量" + }, "middle_compartment_remaining": { "name": "中层剩余时间" }, @@ -488,6 +612,12 @@ "refrigerator_setting_temp": { "name": "冷藏室设置温度" }, + "regeneration_left_seconds": { + "name": "再生剩余秒数" + }, + "remaining_days": { + "name": "剩余天数" + }, "right_flex_zone_actual_temp": { "name": "右变温区实际温度" }, @@ -500,12 +630,18 @@ "rinse_level": { "name": "漂洗档位" }, + "salt_setting": { + "name": "盐量设置" + }, "seat_temperature": { "name": "当前座温" }, "soak_time": { "name": "浸泡时间" }, + "soft_available": { + "name": "软化水可用量" + }, "softener": { "name": "柔顺剂" }, @@ -560,17 +696,68 @@ "wash_time": { "name": "洗涤时间档位" }, - "detergent": { - "name": "洗涤剂" - }, "water_consumption": { "name": "总耗水量" }, + "water_consumption_average": { + "name": "平均耗水量" + }, + "water_consumption_big": { + "name": "总耗水量" + }, + "water_temperature": { + "name": "水温" + }, "water_level": { "name": "水位" }, - "water_temperature": { - "name": "水温" + "wind": { + "name": "风" + }, + "typeinfo": { + "name": "类型信息" + }, + "use_days": { + "name": "使用天数" + }, + "elec_heat": { + "name": "电加热" + }, + "water_pump": { + "name": "水泵" + }, + "four_way": { + "name": "四通阀" + }, + "back_water": { + "name": "回水" + }, + "sterilize": { + "name": "杀菌" + }, + "disinfection_temperature": { + "name": "消毒温度" + }, + "max_temperature": { + "name": "最高目标温度" + }, + "vacation_start_year": { + "name": "假期开始年" + }, + "vacation_start_month": { + "name": "假期开始月" + }, + "vacation_start_day": { + "name": "假期开始日" + }, + "auto_sterilize_week": { + "name": "自动杀菌星期" + }, + "auto_sterilize_hour": { + "name": "自动杀菌小时" + }, + "auto_sterilize_minute": { + "name": "自动杀菌分钟" }, "working_time": { "name": "工作时间" @@ -580,6 +767,42 @@ }, "variable_mode": { "name": "变温区模式" + }, + "compressor_frequency": { + "name": "压缩机频率" + }, + "target_compressor_frequency": { + "name": "目标压缩机频率" + }, + "compressor_current": { + "name": "压缩机电流" + }, + "compressor_voltage": { + "name": "压缩机电压" + }, + "compressor_power": { + "name": "压缩机功率" + }, + "indoor_coil_temperature": { + "name": "室内盘管温度 (T1)" + }, + "evaporator_temperature": { + "name": "蒸发器温度 (T2)" + }, + "outdoor_ambient_temperature": { + "name": "室外环境温度 (T4)" + }, + "discharge_pipe_temperature": { + "name": "排气管温度 (TP)" + }, + "indoor_fan_speed": { + "name": "室内风机转速" + }, + "target_indoor_fan_speed": { + "name": "目标室内风机转速" + }, + "velocity": { + "name": "流速" } }, "switch": { @@ -595,27 +818,39 @@ "breezeless": { "name": "无风感" }, + "child_lock": { + "name": "童锁" + }, + "cl_sterilization": { + "name": "CL 杀菌" + }, "comfort_mode": { "name": "舒省模式" }, - "cold_water_single": { - "name": "单次零冷水" - }, - "cold_water_dot": { - "name": "点动零冷水" - }, "dhw_power": { "name": "生活热水电源开关" }, "disinfect": { "name": "消毒" }, + "vacation_mode": { + "name": "假期模式" + }, "dry": { "name": "干燥" }, + "cold_water_single": { + "name": "单次零冷水" + }, + "cold_water_dot": { + "name": "点动零冷水" + }, "eco_mode": { "name": "ECO模式" }, + "memory": { + "name": "记忆" + }, "fast_dhw": { "name": "快速生活热水" }, @@ -626,12 +861,14 @@ "name": "防霜冻" }, "heating_power": { - "name": "加热", - "_comments": "E2 加热功率, E6 加热开关" + "name": "加热" }, "indirect_wind": { "name": "防直吹" }, + "leak_water_protection": { + "name": "漏水保护" + }, "light": { "name": "灯光" }, @@ -662,9 +899,15 @@ "prompt_tone": { "name": "提示音" }, + "regeneration": { + "name": "再生" + }, "water_pump": { "name": "水泵" }, + "water_way": { + "name": "水路" + }, "screen_display": { "name": "屏幕显示" }, @@ -692,12 +935,18 @@ "smelly_sensor": { "name": "异味感应" }, + "soften": { + "name": "软化功能" + }, "standby": { "name": "待机" }, "start": { "name": "启动" }, + "sterilization": { + "name": "杀菌" + }, "storage": { "name": "保管开关" }, diff --git a/custom_components/midea_ac_lan/water_heater.py b/custom_components/midea_ac_lan/water_heater.py index 38fd760..4247295 100644 --- a/custom_components/midea_ac_lan/water_heater.py +++ b/custom_components/midea_ac_lan/water_heater.py @@ -151,7 +151,7 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity): """Midea Water Heater target temperature.""" return cast("float", self._device.get_attribute("target_temperature")) - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea Water Heater set temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -170,23 +170,23 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity): return None return cast("list", self._device.preset_modes) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Water Heater turn on.""" self._device.set_attribute(attr="power", value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Water Heater turn off.""" self._device.set_attribute(attr="power", value=False) - async def async_turn_on(self, **kwargs: Any) -> None: # noqa: ANN401 + async def async_turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea Water Heater async turn on.""" await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs)) - async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ANN401 + async def async_turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea Water Heater async off.""" await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs)) - def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002 + def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea Water Heater update state.""" if not self.hass: _LOGGER.warning( @@ -195,7 +195,7 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity): type(self), ) return - self.schedule_update_ha_state() + self.schedule_update_if_running() class MideaE2WaterHeater(MideaWaterHeater): @@ -300,7 +300,7 @@ class MideaC3WaterHeater(MideaWaterHeater): """Midea C3 Water Heater target temperature.""" return cast("float", self._device.get_attribute(C3Attributes.dhw_target_temp)) - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea C3 Water Heater set temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -317,11 +317,11 @@ class MideaC3WaterHeater(MideaWaterHeater): """Midea C3 Water Heater max temperature.""" return cast("float", self._device.get_attribute(C3Attributes.dhw_temp_max)) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea C3 Water Heater turn on.""" self._device.set_attribute(attr=C3Attributes.dhw_power, value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea C3 Water Heater turn off.""" self._device.set_attribute(attr=C3Attributes.dhw_power, value=False) @@ -387,7 +387,7 @@ class MideaE6WaterHeater(MideaWaterHeater): """Midea E6 Water Heater target temperature.""" return cast("float", self._device.get_attribute(self._target_temperature_attr)) - def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401 + def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type] """Midea E6 Water Heater set temperature.""" if ATTR_TEMPERATURE not in kwargs: return @@ -399,7 +399,7 @@ class MideaE6WaterHeater(MideaWaterHeater): """Midea E6 Water Heater min temperature.""" min_temperature = cast( "list[str]", - self._device.get_attribute(E6Attributes.min_temperature), + self._device.get_attribute(E6Attributes.temperature_min), ) return cast( "float", @@ -411,18 +411,18 @@ class MideaE6WaterHeater(MideaWaterHeater): """Midea E6 Water Heater max temperature.""" max_temperature = cast( "list[str]", - self._device.get_attribute(E6Attributes.max_temperature), + self._device.get_attribute(E6Attributes.temperature_max), ) return cast( "float", max_temperature[self._use], ) - def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea E6 Water Heater turn on.""" self._device.set_attribute(attr=self._power_attr, value=True) - def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002 + def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument] """Midea E6 Water Heater turn off.""" self._device.set_attribute(attr=self._power_attr, value=False)