From 31e88692f93b4f33d0bb06127a77919690d73044 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=B6nke=20K=C3=BCper?= Date: Mon, 19 Feb 2024 14:54:21 +0100 Subject: [PATCH] [sungrow] Initial contribution (#15130) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * 0000: Implementation --------- Signed-off-by: Sönke Küper Signed-off-by: Leo Siepel Co-authored-by: Wouter Born Co-authored-by: Eric Bodden Co-authored-by: Leo Siepel --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.modbus.sungrow/NOTICE | 13 + .../README.md | 174 ++++++ .../doc/WiNet-S_Modbus.PNG | Bin 0 -> 67337 bytes .../pom.xml | 27 + .../sungrow/internal/ConversionConstants.java | 42 ++ .../ModbusSungrowBindingConstants.java | 33 ++ .../internal/ModbusSungrowHandlerFactory.java | 55 ++ .../SungrowInverterConfiguration.java | 26 + .../internal/SungrowInverterHandler.java | 185 +++++++ .../internal/SungrowInverterRegisters.java | 255 +++++++++ .../main/resources/OH-INF/config/config.xml | 15 + .../resources/OH-INF/i18n/sungrow.properties | 64 +++ .../resources/OH-INF/thing/thing-types.xml | 507 ++++++++++++++++++ .../SungrowInverterRegistersTest.java | 73 +++ bundles/pom.xml | 1 + 17 files changed, 1476 insertions(+) create mode 100644 bundles/org.openhab.binding.modbus.sungrow/NOTICE create mode 100644 bundles/org.openhab.binding.modbus.sungrow/README.md create mode 100644 bundles/org.openhab.binding.modbus.sungrow/doc/WiNet-S_Modbus.PNG create mode 100644 bundles/org.openhab.binding.modbus.sungrow/pom.xml create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ConversionConstants.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowBindingConstants.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowHandlerFactory.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterConfiguration.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterHandler.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegisters.java create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/i18n/sungrow.properties create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.modbus.sungrow/src/test/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegistersTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 2cd8f3d172..3fc7762cb3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -219,6 +219,7 @@ /bundles/org.openhab.binding.modbus.sbc/ @fwolter /bundles/org.openhab.binding.modbus.stiebeleltron/ @pail23 /bundles/org.openhab.binding.modbus.studer/ @giovannimirulla +/bundles/org.openhab.binding.modbus.sungrow/ @soenkekueper /bundles/org.openhab.binding.modbus.sunspec/ @mrbig /bundles/org.openhab.binding.monopriceaudio/ @mlobstein /bundles/org.openhab.binding.mpd/ @stefanroellin diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e0a5201f67..49192aef09 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1086,6 +1086,11 @@ org.openhab.binding.modbus.studer ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.modbus.sungrow + ${project.version} + org.openhab.addons.bundles org.openhab.binding.modbus.sunspec diff --git a/bundles/org.openhab.binding.modbus.sungrow/NOTICE b/bundles/org.openhab.binding.modbus.sungrow/NOTICE new file mode 100644 index 0000000000..38d625e349 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.modbus.sungrow/README.md b/bundles/org.openhab.binding.modbus.sungrow/README.md new file mode 100644 index 0000000000..79a87f2cf3 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/README.md @@ -0,0 +1,174 @@ +# Modbus Sungrow Binding + +This binding integrates the sungrow inverters into openHAB. +It is based on the Sungrow specification "Communication Protocol of Residential Hybrid Inverter V1.0.23", which can be found here: https://github.com/bohdan-s/SunGather/issues/36. + +## Supported Inverters + +As defined within the spec mentioned above the following inverters are supported, but not all are tested yet: + +- SH3K6 +- SH4K6 +- SH5K-20 +- SH5K-V13 +- SH3K6-30 +- SH4K6-30 +- SH5K-30 +- SH3.0RS +- SH3.6RS +- SH4.0RS +- SH5.0RS +- SH6.0RS +- SH5.0RT +- SH6.0RT +- SH8.0RT +- SH10RT + +## Supported Things + +The binding supports only one thing: + +- `sungrow-inverter`: The sungrow inverter + +## Preparation + +The data from the inverter is read via Modbus. So you need to configure a Modbus Serial Slave `serial` or Modbus TCP Slave `tcp` as bridge first. +If you are using a Modbus TCP Slave and the WiNet-S Communication Module please ensure: + +- that you have the correct IP-Address of your WiNet-S Device +- that Modbus is enabled within the Communication Module +- that you've the correct port number +- that the white list is disabled or your openHAB instance IP is listed + +Enabling modbus and whitelist setting can be done in WiNet-S Web-UI as shown below: +WiNet-S Modbus configuration + +## Thing Configuration + +Once you've configured the Modbus TCP Slave or Modbus Serial Slave as Bridge you can configure the Sungrow inverter thing. +You just have to select the configured bridge and optional configure the polling interval. + +### Sungrow Inverter (`sungrow-inverter`) + +| Name | Type | Description | Default | Required | Advanced | +|--------------|---------|--------------------------------------|---------|----------|----------| +| pollInterval | integer | Interval the device is polled in ms. | 5000 | yes | no | + +## Channels + +The `sungrow-inverter` thing has channels that serve the current state of the sungrow inverter, as you are used to from the iSolareCloud Website and App. + +| Channel Type ID | Item Type | Description | Advanced | Channel Group | +|------------------------------------|--------------------------|---------------------------------------|-----------|---------------------| +| sg-internal-temperature | Number:Temperature | Internal Temperature | yes | Overview | +| sg-total-dc-power | Number:Power | Total DC Power | no | Overview | +| sg-phase-a-voltage | Number:ElectricPotential | Phase A Voltage | yes | Overview | +| sg-phase-b-voltage | Number:ElectricPotential | Phase B Voltage | yes | Overview | +| sg-phase-c-voltage | Number:ElectricPotential | Phase C Voltage | yes | Overview | +| sg-daily-pv-generation | Number:Energy | Daily PV Generation | no | Overview | +| sg-total-pv-generation | Number:Energy | Total PV Generation | no | Overview | +| sg-reactive-power | Number:Power | Reactive Power | yes | Overview | +| sg-power-factor | Number:Dimensionless | Power Factor | yes | Overview | +| sg-phase-a-current | Number:ElectricCurrent | Phase A Current | yes | Overview | +| sg-phase-b-current | Number:ElectricCurrent | Phase B Current | yes | Overview | +| sg-phase-c-current | Number:ElectricCurrent | Phase C Current | yes | Overview | +| sg-total-active-power | Number:Power | Total Active Power | no | Overview | +| sg-grid-frequency | Number:Frequency | Grid Frequency | yes | Overview | +| sg-mppt1-voltage | Number:ElectricPotential | MPPT1 Voltage | yes | MPPT Information | +| sg-mppt1-current | Number:ElectricCurrent | MPPT1 Current | yes | MPPT Information | +| sg-mppt2-voltage | Number:ElectricPotential | MPPT2 Voltage | yes | MPPT Information | +| sg-mppt2-current | Number:ElectricCurrent | MPPT2 Current | yes | MPPT Information | +| sg-daily-battery-charge | Number:Energy | Daily Battery Charge | no | Battery Information | +| sg-total-battery-charge | Number:Energy | Total Battery Charge | no | Battery Information | +| sg-battery-voltage | Number:ElectricPotential | Battery Voltage | yes | Battery Information | +| sg-battery-current | Number:ElectricCurrent | Battery Current | yes | Battery Information | +| sg-battery-power | Number:Power | Battery Power | no | Battery Information | +| sg-battery-level | Number:Dimensionless | Battery Level | no | Battery Information | +| sg-battery-healthy | Number:Dimensionless | Battery Healthy | no | Battery Information | +| sg-battery-temperature | Number:Temperature | Battery Temperature | no | Battery Information | +| sg-daily-battery-discharge-energy | Number:Energy | Daily Battery Discharge Energy | no | Battery Information | +| sg-total-battery-discharge-energy | Number:Energy | Total Battery Discharge Energy | no | Battery Information | +| sg-battery-capacity | Number:Energy | Battery Capacity | no | Battery Information | +| sg-daily-charge-energy | Number:Energy | Daily Charge Energy | no | Battery Information | +| sg-total-charge-energy | Number:Energy | Total Charge Energy | no | Battery Information | +| sg-daily-import-energy | Number:Energy | Daily Import Energy | no | Grid Information | +| sg-total-import-energy | Number:Energy | Total Import Energy | no | Grid Information | +| sg-daily-export-energy | Number:Energy | Daily Export Energy | no | Grid Information | +| sg-total-export-energy | Number:Energy | Total Export Energy | no | Grid Information | +| sg-daily-export-power-from-pv | Number:Power | Daily Export Power from PV | no | Grid Information | +| sg-total-export-energy-from-pv | Number:Energy | Total Export Energy from PV | no | Grid Information | +| sg-export-power | Number:Power | Export Power | no | Grid Information | +| sg-load-power | Number:Power | Load Power | no | Load Information | +| sg-daily-direct-energy-consumption | Number:Energy | Daily Direct Energy Consumption | no | Load Information | +| sg-total-direct-energy-consumption | Number:Energy | Total Direct Energy Consumption | no | Load Information | +| sg-self-consumption-today | Number:Dimensionless | Self Consumption Today | no | Load Information | + +## Full Example + +This example shows how to configure a sungrow inverter connected via modbus and uses the most common channels. + +_sungrow.things_ + +```java +Bridge modbus:tcp:sungrowBridge [ host="10.0.0.2", port=502, id=1, enableDiscovery=false ] { + Thing sungrow-inverter sungrowInverter "Sungrow Inverter" [ pollInterval=5000 ] +} +``` + +_sungrow.items_ + +```java +// Groups +Group sungrowInverter "Sungrow Inverter" ["Inverter"] +Group overview "Overview" (sungrowInverter) +Group batteryInformation "Battery information" (sungrowInverter) +Group gridInformation "Grid information" (sungrowInverter) +Group loadInformation "Load information" (sungrowInverter) + +// Overview +Number:Power total_active_power "Total Active Power" (overview) ["Measurement", "Power"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-overview#sg-total-active-power"} +Number:Power total_dc_power "Total DC Power" (overview) ["Measurement", "Power"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-overview#sg-total-dc-power"} +Number:Energy daily_pv_generation "Daily PV Generation" (overview) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-overview#sg-daily-pv-generation"} +Number:Energy total_pv_generation "Total PV Generation" (overview) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-overview#sg-total-pv-generation"} + +// Battery information +Number:Power battery_power "Battery Power" (batteryInformation) ["Measurement", "Power"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-battery-information#sg-battery-power"} +Number:Dimensionless battery_level "Battery Level" (batteryInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-battery-information#sg-battery-level"} +Number:Energy daily_charge_energy "Daily Battery Charge Energy" (batteryInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-battery-information#sg-daily-charge-energy"} +Number:Energy daily_discharge_energy "Daily Battery Discharge Energy" (batteryInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-battery-information#sg-daily-battery-discharge-energy"} + +// Grid information +Number:Power export_power "Export Power" (gridInformation) ["Measurement", "Power"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-grid-information#sg-export-power"} +Number:Energy daily_export_energy "Daily Export Energy" (gridInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-grid-information#sg-daily-export-energy"} +Number:Energy daily_import_energy "Daily Import Energy" (gridInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-grid-information#sg-daily-import-energy"} + +// Load information +Number:Power load_power "Load Power" (loadInformation) ["Measurement", "Power"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-load-information#sg-load-power"} +Number:Energy daily_direct_energy_consumption "Daily Direct Energy Consumption" (loadInformation) ["Measurement", "Energy"] {channel="modbus:sungrow-inverter:sungrowBridge:sungrowInverter:sg-load-information#sg-daily-direct-energy-consumption"} +``` + +_sungrow.sitemap_ + +```perl +sitemap sungrow label="Sungrow Binding" +{ + Frame { + Text item=total_active_power + Text item=total_dc_power + Text item=daily_pv_generation + Text item=total_pv_generation + + Text item=battery_power + Text item=battery_level + Text item=daily_charge_energy + Text item=daily_discharge_energy + + Text item=export_power + Text item=daily_export_energy + Text item=daily_import_energy + + Text item=load_power + Text item=daily_direct_energy_consumption + } +} +``` diff --git a/bundles/org.openhab.binding.modbus.sungrow/doc/WiNet-S_Modbus.PNG b/bundles/org.openhab.binding.modbus.sungrow/doc/WiNet-S_Modbus.PNG new file mode 100644 index 0000000000000000000000000000000000000000..942273fdc0877683f23a072b10b4034af144f853 GIT binary patch literal 67337 zcmdqJhgVZ++dfJgV3alr(h0ndf*>GLr3J+bQWPnI5CoMDMtVtt!$=iSX-W$!MQTJk zfkX!a3PR{DKr{##T1*0jK+4%T^M3F9t@HZ>e&_6^i}mbm_RjO{yIl8mUk@qvwwFW? z$Q|I};Ss%Z`TTVro?m%+cm%Es3j$~4L`yFMKX^m0UpmKAH>f}c1_C~3ZO-!W)Mp-K z-Tnm_|9b!O%}^emBggjt^Ip0B`wt!-ZrPRdXKzG#tg$l-9Jh+T><#K7`r6LWyFw{N zp~Y>Xg99(Ryzy<$Gh~wr!WHqXP+Hiz7_U*alz%--7C3vZ>g*Ne^d|>?{V;UC?}(&^ z*D;rCr)rNsIYKyYb;xqYS5Jp^QY2M|*16ZfaCNM9s&#inSH-(Jq9*AbpgWYNNzOHN zgyTE&xJfX2DL>%Z?|yF}+}%m?dJEB3THE6)mdmCb2EO_JaWyhA{`bmk73KW_aIEx5 zJo!E6w1B|hLyI{XzGtP^6XX9L^1P9^w0L%-PeS6Kak#heXx*Ae*rN|^{Ru)_O?z2W~|4$ZqN->5|6Z_(Hbq9+hjY0agj-6aCcCyrduw2k3clg&IB(|a0D24 zj~L=1P1;88%`PXpgIy7vWyHKfo$vyzFl)UIyy5Q>0@Z#Qgip%7;5B<>H8dI*YKq@ujPWIPw?9Ji427G#WR5tD#2wq~6*;$Uay*ydg@Q<`%2(JSh#mf&NrA+>IaYa)kn zi$x;5kDNWCkFi1AUan4HL4^A!m6E8dcr(T=`WYv)%oL|tN>q2rScamBI-`tSprXA0 zbe=x1AF7FP4%w262|{VJM#D&$I$0Bm&B08NHKmgpw>#Hn*kB#szFUFVt!RwdT3LD} z$elwlh-C;4o*PZat7+}^7-F6VeY(2YBOg*)>(*j`-rGPIAUJFy7Z#xzeoxppUy9$f z4asFKlZk9J!nw`e1G;xhFM@J>T0}E!FE^|ro zUQbjLi_diggPEV~M*4CzE(J=19-En&B~-W_)KO@>-Ie$o!9V8S-|iAGp|N{Ibmwu4 zi>&W)5kA}j3nq`f$E}OA7o-s=BYH+0|F!iX@2W3V)?#7;#0Pb1AqarlpfB}8vY zU!6NzJ>;+yWuYi?{>uJ)&}gZy@)uTnyxiy+uEe*C<)F9$_ena5=SbO}-nAo}pX}hL z*-M64zxm@GELy+!{Ke7NWK#jj>L|80x1W2R$sc_hw&H-4W10SReNP&{MYN9a4aR6f z(asX`u?$~of8mjuZw#y*h5g-x{5g?pXUv=yFAYV|rKhFtZ{f(DG~N=K?fY7s3r#jBu0!-j6+r%^-$w7*V#-j>{?~QW{RB z^kuvu!*IgN8mAQZzO!QhZEotOi2Du9-<+uF&-vyuiz? zNiH|gq^ZfAC*X1tJ7}iy1iON}i4?@Y8cq2$yX~L+u$UGAcGA%c)9((DLj`s8IGh-q zr%&VI5j`MjeE~hu)z#I0jOmGex5^=$*ZF>!_AsAx0Ibt3VQJ{tlJ@ZBE!MvrJA!Mz zz9321*O+d;2C>!l+*TVssV^b37?%o5i-3t5zq;bXU4~5USZY6I6@@LvaJEo}s&@S? zDX=AeTgQvB?iZ_DiMEN+w68CtX6x05M#`);T1Gehjm3Et&ph~@-NM}_j`W&u*PjSt z?`{o%;(q#S>1K5fvPNO)#npQJa5wjoeD6il`_**A`RC1n)6arCwX}o`OX%LRJ>$^LjzJNQlQ~OEnQ5@)ztf~cANavOleN7QssFh*uQFr!X ziN)3IRY9)7FJ1%W_B zRQCroi}T{*{Y<&GspIi=f|eFp-on0eM=E&IUOtuR5(crqU%C|Npz!8X(seJXmn_TU z^Y5ws*O?KxoWv?y`H4>w_N+*eCdmpsf|iAB1R@btC(}ScvIQKs^W!;omJO_oZ}Efb zC_=x!z$TE@+L`_5li7%^YDn#@t?s@b{{#c?7xf)0c&q&zNB>~Y5!vpcJ4XkM+PkM#Q9=9q#CRR~Q* zg}~pNCH~8t_B2z^`4X_w7EvtzuPLeC85+)Y9Vt=eN@g=1aQf1~0Oe7?zS^mr@-7K`1^M)^MKUX&aLRTfyWRHo<~)*l7wX;N?%6PMHmAIcGDYzguQA7pGJhf?XONrly&lFAef#MjPZJ_+I=VZDmu3%g%=q& zocHx zT=3|?QzctYz~g%@>{);OX6fiisGk1^ggx)Hot<<%^Zqc-uX7&0LT#Q*k0896^S97> z^PA9|w_3R=>ySoiH`pm-;bes!a!{+2)zfgm-lL+Lw{wS-HgG$`h;GT`CLy+`eT1C( z^CMEbzyVw$d2TtPG>jZc9tp84Zz$ll-eg4<3ZCTbF(}QPhtVd?RS6#+HSwNqot9n@ zqT|eydf(w!zoJOGMNXJxRHTSh_j|XS3jOGf_SJk1&nCk04BlpRaC`fVr3F*n0(Z7p zA}`08{>hm~y)OibG&spo+Ee#%!buEmHh6ycV~I}Dv$WU(>^}cUg4s*|reZblT^E+u z4Tbg-b$=e#;AAJ=NUavZX&+vE_&h~~7YCEHJ9q^9XSd(m)Rwd!T>(k(Ng8C^E2?6p zt+xbU5F*AtEz59+7kdLhM-m(ME#X4j4sH*``3f3I6qTQ;i!a^%2eo=`SINzPO5-j0 z0pKn%w5eb`%@N)s_phpF$ffUtCX_%9KlPnVaq{h_F262X$wsY&b_v34XDA`oB(H5j=2ddc6yTQ=x! zVSTsGUov_hg~Vz66Ij-wc7tZ(yfYN$0smnF{^71)cs<-bZh+Jb6*(r>+WU~f`U0<+ zm+V#Vc_xmQ64E_ab6;vNX9!sEC@N z3CYN-0}h3;WD(lpao`SV%2U zIJkRzR+x4l#y8Z=N!YKwj*2#2p7MzffBA~F<-}OT0 zc`gxgE22x(P>-^F5VG^v7w;l5uP7}gXI0#ZZsXr7A|F%Ib~wqCn4Mi5C$*yKB%DnO zI08YlRv8p78yn)`5y$-o!9GVMPnvBbu@DkX8%DMsK)H(u;gpAJTjpnGFU1A3_Oa}S zccV&4DCniYZ0OtT4$IIs`F{wF14~=Dc zQfcFVrh08Xr19ckxojzmUG%6t`-jZV32|n(l|>uP%HkUIvaeqKp>CkmNK%Z_PW_te z@>^xplkLVn>Y)4Byn-+=BZLaI>{IcfLO#-@_W`+M@O#qp{lS zcr8B)h)V?(2=#jVDs!luC}*)xLQ*8Nr=TB$;*Gs$*W&KJH2R^)y$_r3h$io@qC%8@ z&-{=rQ&%OI{IN+s_hXVS?O_rt9@Y`*jrA5a?S|&b+(`9~*OAChld2=YW(`({LWad&a+bca84 z7;f2E4{t|~dFG^+Rwg5|10PClN}t5E9=gHeXYRE3G&6O+OY{zVQ&Xnw0C)>M0GLyr z5Zz1dpOU=4E>L zC+S~4VG;e1RnoFsM^77TRT_EsQih1=Xb*;dwq(crGdPJ9YYJ-o48r?bS}gV|?xje5 zBDJ#`Dt`vLRDRt>DHss$5cJ$6$g7BIVVpKEGu!QIe~&;AHDdTqVmwLc4mAmCy5Ul< ziiPhZi(d?-iJrLCDs_z5;@QgnGXmMtAOY&Zvmyex_m|PPpSH4oeC=`)thz=uFWLZt z8~7P1FMH>%IebSFA)M&)RGqsRVT#k%XEJ`0MDGLji&aYA7zs=EnD9sfq{G*Wa|f7b zKhn691=Yq;u(ob8rjOrz*u~Of*aQqeNqE)CQVv9`lvE`&I)A1hkXjs;eZbE01~)cl z@8vN7K)CsQktwgAGZ8|MR*rKsH#fv1?nyV_+DQu0Pl|^}lA<&^n}i{;3e zColLxXNsjEarHD%X)iYqr17}>{*^-B(kO+af?_1)SOp8bbDb|ECS$v}s$B~dmCTkF zlBe-+Lp?baV76a`Uu-=-su;KM2ebsU15e(|3`%8yxQh6;D60;@&T^t)w7tg*D$M#P zM)%}2{>&DVI42U{y_;iRCh z>>HJP;Nf0Euj`yTqQ;Q{nWu#PMnp6H+I!T^lZ=0Z^vYmTDG4lnydz9(5fwhZDy`v* zV|aFC?AuVnpE!I^98pI@f*5*%+9})e3A^N&`Faj+IUZXs24u(u8VF9kmj<$8E;5yE zSx?kan}>1odSFN(w5)`*a<2Yg_ROEw^NI#A7ZNJAkRy}nKv6)D1Pge-J&t*CQDXzl zJ?pOI-+l-%OHz1-cvsZg;^K(~>Bl=|zaupDItvDY$wra(3?YALp>`}!eZxjQZmFh$ z%YVkH@)t5gm2$HuoK1gmNqz`a>HdTVqp)KoGTW7MKTe48I@U^Nhv-iDeJEeFvcQ7m zIA_{ukcv<RcOgALUt`|@pvxt{ zFdCO)&M^^$({%s*(X!MN2gfjx)!#I>y94s1`Aw<$lGfX3Q*Xc6iQ#?`0?RTRZ^mXGf|)lkd6cV=N-1!8Q8PEG%`Hru z)X77zQ@f&H&iUL@Lkv_m542S7vZ`Y6V-Ep3~l z%#H0xumG^9qovRVD9>pQcD73DaQO%U%yM14Z|6}5pa#i38EaE~gb-qZTb;qhw;M@5 zIMd1PmmZzgG%U@EWcTa6T{ujR#LB{cK^kp27xJC7{UujDL#1#ZCX4-Mb3?%`ZBzkA zZlXENN;+4661VEDZz82~qa%s1Nct`-e*17B#rpxVzATS8>8`iuS=zlb=r`<0iG81!MaF$SI-P zLuwIuv`CEik-^gm*dt2eXx?!sHIRe#86KkQ#mu9!-gY zk-}3aHHdbs$PNTaFNVpu!r2O7>@GsEQHr6C5bV%~u2y!4LsJl%oj6=qFt{HAQZ;s2 z-&DbV9?fw~+S2`V$)Q)9-EJNSJxF=@Fiu$2N&HvpQikKhzLu17FJZOg{0r12+UOrR zLq?z+Un(hjk^kn)xdT`8J!}9bY}TcMKdn~@+|fhG*#QAf65tq}148~HZwc-0f~GXotxR3gly*Ebw+YSBznJZtmbZtG{m@j^JE1ZXjpoSM%CDGCJ~v`G?)fw(oSg#Zi2!TT&nhQ7C6;(zX;CW!N@%4`mN*fT} zsJa>$3aKeI`Vno}K8~B-e8?Un=K=)#2x7vz`NBD-fg_=r5F)eQ{*!yCYkg%`n0MNV z1CVHayJtdWZI~0dgDcI5Fc!Z;E6MZhqKT81#=;#-iJot7ww67WPFzBP#J6|E; z`3<)DeJUj~o~e^yb4f1{8NV&8CQYdXs2kYSzcJ@=!LTa9NE*#C zuCMDH?rh0ad6avbI+J%@#wFR&GnE8__EZ@$hq@O}PpDg5tKrf6F}AFZ(lD6tw_vWQ zK}%X$+^$~#OU|^C01^7FsIS9%KejT<9jZA=VGkCuRz6r3auiJ@K814!hx}TB>gQn) z=t>^k21b$i-} zY@_24IJhUAIwMIz)bz?Rq!`gnuvIsrzj^K_z2dVatgYJJTX<)>Ivm+OsnU!x#(8qK zfY?5{5j!L?bU87;Jenz99%b3HV#bW-Ur1QO>}^zc*xb0Kqh=n(SwV69?>oMnx-=g2 zsuJ*ErP#Il4`)k_q$W1z&;&8w3rwpEw;OpYsCpCh@1+GHn6@)*G9;$y&X9f2E}D|!NwB-l+3=+RFvzxo;F1b_D+N;UO1GQ!dvI`;z0j#&(kR^3?b+qhfG z;c%AvRO@Nh3PJ~{+-tobdRt97yjzRA+?rm1R;uA-p?>Jjkl)iTx{0beB_&JgdwoA) zJDuI1WC(D9k&YY$MMgM=(hCFv{+~>&EsR4nu#-Mw39i3xKNifR_CL7LYtte15vwFh z`--X@Io`Mum(HWMK~g<(d{uJC`;s(Lg)j@AaUJ5` z)Q9hYet|s2^EyXidu@8uRLoi5R5DGpI~;2DN{}b8Za*u!unnDSk2M&}3yN9(B|V-Hy9ychpJfzLx&c3S6cd&Ff?!n=>7T6BUAwj27~Y_aQUVJ9#kahY9#7- z-`75pDeP@FF=aQqJgzq^6<;M6AfbkUP1d>@2EMIteRY!>s%E~h(7?g6Tet>mjo#4Q zmohbJ>kHwZoA#9S1oWN(ncjo-_ObZ$9bVe^*O_8i>3^Yv%J99 z)*4m}hJieY)0Tnuy}QNo9L&2^4Oq*Gd6p0~&ZQ?5C`+{xp(M+rNGHUy5_RT}a|-t# z=cxS;Nq_2`!nX|G7yWlltM|GM+Nd>hdO&obihuQfuAGYati+Wu&k#-5?||a;Hn=0x zh5*__XuC&ozW&Y1bL7E&bu|$j!fJlPIV>V_=dD#v6dYQkftd&GP4(vU|BmBdhF<-< zOr699NOBcPQ!dWV&h+q9?W4=%o*!|ZmLf)t=((I{;_H$^ab9X_iBVpys+`BG zq<3ln3kJnKo{Ve!kWUJy!jmWUG4oJx zz+{~wOCiO(t*A~wlIGxwi(oF_U(>~)`j9sJMso=;nnM(9l!$|4N4d_8FHHS`_1tRVTdK87MYS+_<<8#?P0tAVApMdQ1E4|BW>o&;J-@h~xKZ!JUZb~)y$-1f zzc>V$R`V=&(ieL3aa9(MlX?4>|HwkJO50uy2!VG#Eb(q*S^d~RF(I24ZV2Zuajrp8 z8wFfOWUsjFw-S~ryR;Ode`j+#ziLqTQQ_s~81o!G?x*MU+lfj$7ST>5{_5qT43bP8lG^%UJ{LNunLVwd)tBqC(>|FUi-OfJjryc0MQLGosiF z^*z*Sna`;~X%&R(T{)+9UGtEIP(w^aJ*-2O6ubQK>Cvvl4S=475?b-q$DD@yAPbfL zAO84ddcL}oxl}Na(@9#DAoMawK&LC8-;mngJNc$H-zAO>j16`|m4l^QGH)i!jHUdq zoJ39d7i3r!{#&wm<<7n2V;`%UwU2;of$rgAm;S$vNu2irZuF1EDu8C)DaRwuCWrhL z0;d-oZBF@GVB{Q#aTk%r34}rmb{?rhQZL1Mf`u3O;JEDg?uP_^$x`~{Pa8T=zO8r zNx=u4O*?L5^!qcZ-c~wSpCONxo;<>v{2_}==d!>Bgyn<-P>asEz@ttApPX}cwYiH} zk>VIt;wAGifFRxWA%9Xu25MsNC--UxoU*Oty+= zA9Z-0n}X_hUdr|dOW~f_j5kN;c>@4O#EYl=4S*0?o+pFl!SM@)Z|Wr>toy7a#7@%f zt^+!E4ks8bvb#NPsd=wxql=m$PmOH;pq0Y ztwQ4y@F(5kX;G|&-xwhLu|Y-GCa?{@oGxK6(&fm0UlizQ{q3j7;d4Zm`)_K&ChkJ< z=L%=4{qOszr|rE#1IYWCbqLbez(JO;3Tve40+mqHhD=u&2}pLCL)|gEc2G2#1TfM5 z#m3rBr9fl(3OzfRYMwqyz}2I)C(gEMvBtP@*P?^&67>;kLi-3*>lC zT^HdiYDHB)_~_FJZ=z*QE0v>Me*NBU$Sr*kiB;DsG(E0NOoU+2AI=N0a<*t zw`rF}Jq-^)(A`E4cEw5C$*pa1tid86j?ujhKt8C5Ap9s^-J_ zVQ5l_K7dfFVL6zZ$!yUX0AWksKvROnuGo~jIDX`@hyAz8%~v(?q1VUd^$MjB0EJS0 ziUbinsXzvI+Dic+Edj(o{~uPk{a>!_TLH}eKib1uRs)xV=B2JMqM{|}#!#sd-Q zs#TBBB9I5=|MUCL%ESNBME>X8^|{l3%O?MG>e))}-^$9%|1+UyH(LHN&HtP#ZNK}s zKJ!1PejnHTTf+I|Ka;Br{^@5~ihQYr|GHt=84Xai!TzIxj$CTceDreB<{pu2{*QX| zea@lY%7v(Kg_ggsk>yKW#UsH;SjRtV(Dymgy|to%yHrwvf+Ku?HEMbO5thDpl=|m0 zTcB)`-w!3g(K@p~g`bzJTkNmyHgsQ@yMH)m{-30z`>p*vz|rMHI-bG`|NY$Y|6i$6 z|L^tL0zZ9$#%TId*v^lyOCK0Jp&S;ot1c6pc{~_0>+$UPGZp3iR|gy`_kcHx8?wtG zdD^iUE~$Ijr78cE1w1_Q?Jtg4TEsFo29yRbLzT@TzkR2P^A6Y;!r%cb(}cWTr0H`p zrND9GWI;@GL|H){s(nC@lI7+AG0hZ0SE2Is{$*+L^D#U;V*`zVm@r}e^-WA%bSS#t zSYY$3lwWMf?H@kG;<1Km)h*Mhf?e9f64kwtK}UL32-sa$;P>!C73F_!uyW5;gg5;d z_}J84dA|1T+{SQ-0jnX_8HYWOsZC4cIiT3RtBUk@R39YM{7%v7gFElIk z&x<@(a>j3(&93Dw5kil2^aZ&NxDm2;y(Qitd|aVHTL#a1f&uL+8IW#fsc&CvqEvqT zkEDA4MoV}k(yZ{QmxUT}Hwte%+^|fsFMCF#oElxsgzmv;Z>c`!ftUR{A*^n z-26(r2qG57H0%{Pblg(ce{r^?{|t}he<{Grfxvb{|I1;auP8w6bFJls#zyF^+SzHj zc=BA~lCwKxB~g3BF9<2x_W35k!A#^Zp)MqdfDO75F9U@;L}rSoy` zDh&v7s&|u!(kU zD^@>yv}G&zL%$opQM%H+i?Pj3%#58k5v1(_w{d+}+w=}U=ospn=P1G(PBWsZRFD?h zk7HpCW`681U#?@$4HY-)IH~ltId`Aj|O_g%EqXG++= zmNIeU*GWh)98NMQH>-E2EQKxj5*8OLjuEhCn*Cb+N%2*-dpY9TTfuGh_nGe+&7A1b z4}4sU#FuUWUt!be0RAY1S`kj{^jn>nr~;cf=LWkqoqMWh1#tQ8*ZD1m17-P)--Vxb zee3BM!*ZVfB|)qFVIJ}_VZ%M=2vBmkYPz?kfzl*M6Oo~=^69Ner-3=>7(b$}M^A|xkwnuVb z2KCQvC6UMSHf((mJ2CC|PY^I~b`&md3Jnm7qnCW7TFjme>a|Qin;+Dw%%*UyiB^<5 z(xNd4W%l-qAo6D3lIYyBOJ+;?{A`Q=kG*iya3A7ZMHrs7LTywJ#)coj5Kv!yW=cgB ztTVy)OQ?Q^IW+vBf#$93+`3zN5isugg^K<~-5(3Rqjd>cQTH5}#Xo~ceDxh5hTsl> z9X0mO*VF8n*`Dh_QNE{WXBh5NftzW2Mce%?$C)pc=5T-g)|}}Dms`2wWRapHtGSI$ zsCt`)cbK`_(GbxPyOn@quK?e0B+!zC%*0qnfN|!H@a`wq;QkFSM>F;1bOgr83u>s1 zEVfxOcc2!Q=PbH1H^xl8I^vsP;rxI{Eq~6^VtMrGx^?WI3YeA-(6GJ}+zHgP)A_M2{-4|Bd+xxMUU5Cta zGIZV{+Y$1PG|S%=_T5a}Fa-Io+?3Z4x?9cGODcbtlk{$~uIaC#j}F;a7Ha(^zFUd& zTxi%0^Sl&Uii1joeJbDl>{g~PujPIfwH5NQj`%dgCT#I(Km0w*%eWl|T`i&K&ylB+ zbJFDS-o*Tg_@QS(FfCk7Dtof=c8Ae zC(I{;FE6V*v7))(LIExP#L(x;Ic-ra5@<{hcl(?`jH5riy=*^|U{_4MZJc3ovH znWeh!)aP8IBG@tXTfYaE7Fby8R({b;v~uSjJmU7!NGz)2A*#hm$he77rk8L;K<+q1 z(YFzwUcGJRP@qzBw9>?!p#=zK_a-@{fXs4zo%lx2=oz+a#Mc}7si=o4%0GSMUc9Wn zfMnmEQbT}qMG>>l@Ml6ZEZstT69CP5ZSd@MnQUhF(ilM@Th>+9*LUYQ5E7vsG{1yx4uLvLE)IwHWuL(nQe@{jHlB_6Du5 z));38KR+WebXVW#%R9{?w!cVsjm`2*;h~x{o9_VS3V;3F50w&Yl-byf-DK|Owyy@U zd*#f)!@}q;p$9!OeExDB>!6AT@F%T<-oQ4SNK9;KD7T_m-B!!yp78gwu%)`z^~B8x z^smTB@zPN!I##LOw7TXhKhWX%Yh0f1KrhI@_H^V;51>)-bJ2$G2)`NDbaSf^SVg?m z7^rSF51F4Sp76u&MwUwrNFffue7#pUZ=okE?oM+e>Qc|Xh%1Qmy!VagTEp6=2Yauj zVto28AUzYU>mF?#c*nYN%Pm*|GmxxOG6?GZ^2MGwF*Y6-Yzg$cHIA;&&d7=N*@+&v zDVO22x$b8IbH%CpqS_D7ZSc)*ukT9oqsn|R_4mFvnjMrbTV!llCG}|>Xfkn$rJgt5 zeBa(!b${jg1#73WaB&4I)x`CGnSAM>QNvg{bAIl0&XF>lJ9@PKAd~L;WMVVZh0nKj zGQw~pz|XaT?&}{k4Hx$PGVU{yQCH@7E%aF0MWm|-wN>t-iNxGSJH2=Gu-jxQF3ok% zBdoW*arOQ})AM>u3!>FB&=Lr>U)rQgRS2NNO7%D2G0HY0>_o64AifxMMUxet?9;Qe zYtX@9%`7#mMjmlelmm#9u17xaM$`41bx-tvET~?Or0Tw*GTR%gP0XkZBaE}? zMaGc0fMYojs*pZdu?>&?l#iPG89BF%-x~h)PpTbKOTy&*LXq;U;0U z7h|wXs{wDn58C)mmaj!TN;E@KIe|L=NcKfMh1LGXumPva5saR&(XGXxrSgIevY+Uv z!kV2~z;aGqCe87W-RlsM&uai~%x-?s@W995{8-imqQ*#BPdnUfirCIA(hzhOmx~qt zp5J)KZMF`I9O#9ZvRx*An$V@Ob|hyEWO)hZHmV8q!8rxhCvt2r93O9-HWkooO&xsY z2uwVq;$hY6VaNF>)7nqscZh#yns&;7n1!}WVSAc2?S^PqA^2H)-kla~ti#MiE4z6k z+|REW{|iIuqhR>Ci1*BlML*_Wzxa{cHJ%N9%z;*z7%<7*=f{9a^o#7t^3^ z67CZErV#IMLU;G2ZK zxkPBBoJ@HB@hl?V-7V2+Saw-r=*xm;U7aov3ihN&OtjapR(Qpe52h1D+?sq&TUw;I ze43viW4xrwLQ$fnBeEavJ)(;9YT8=o8DNs+h^T4-)z@1%`(7+r%wiLf__<$y4 z-`%etdP3V;sB!W?Z+B+*d)hQCjMjkrr$Et-S1IXO%_|*lyEjX1wwR1GI|R-(>) zIh3FU^INW};w^DvlH2kv^$58)0z)lbn`@h_?CIu8!E={FL?*iLvdt=2yDSNrKEl2~ z#=M6&w#bRJEh$TjBJ+%U(|+$syGwo_1X2CA{6{8nspoNo%lbi1;=KAD%(28qJ;+lP zu|lnM{vPz`VQp;gptI7igW^2ewgtrl&YykTc4l7t+0d1l?P1E0E3dp{mYU3J`C9|} zgod>h?a-9RFzKWi+Me5!T<{HJ)tm64m~$1IRWf{i;s>;+vy0EXaGwa%>6|D~e6Fo@ ze#2MzJL!x4S!T4j(k8Of(t>=>xaDY~*TuTvQ9yAn-yy3buo)3CQ(d3vC)immXpp_F zGY{wgU?{#zaxJImW90 zWA6*leUJQzynFG2;v`!jTvL;B4L|`~l{k-nv42S`xfSpa+bphXDPEa_-sfh_RkHNd z8Sar~ym@KUmFP}nK+8jK z2Vc@2&RHk93bj?-5&rHK0~9itq<(R)$mK|bnNSS~qvv(41HJLgm$*QmnnQXSC_DFT zpEtanT=O!m)q^)SW z5-o=h@Ky8iPmE91>NI>ZAAJ{Lj9zFCJ8gS*qIIsaCp5n@!J?>8EO3fkf11hjsad+= z;6>SzoAYj%OvBOF8l>$YbLV%XY8l&8sTbL619tePC8}PWds7dim%vZ%roJW*F_i4k zB*|pvGPSkX@{JOoRbEE3?d#U4OMMqJ1tG`}w@a^y0@Z?RyPjsJcU1Mpx-EKJ(F!G6 zutjFko3`3D?J}L%Q;O>ihL4wh(o~dxdcwVUKOKR_NXq=$*T<4Vq-8vkIJ;j(tW|V` zTv-8jCcSRedm?tX;;k#DB7Ebyj`(1nc#V@{jTyq!4CCkLH~#V$zu{;4e&fkf(2I3b zy5FK^V3%$=o5H4|au@~NPIf>xnDMJ9?_ESlbUK+g?;v@-2z1r5LZF|+ zd6Xn5jJ9Zm9Q0!SwOcXhdT!kmcZg^FsxC*RWQFn~4hX=p;n^xBPZ>`|@s09)ecSq^ zSoGMB+qnjNeU$1`Z$e2wGG6k2?s$|oLPO`Z*j_WJTb2N*JpIN*G3g4$=@&THYWZ}a zf^Dh?Yg}4k(Zk1(H`j|JRsYLGuL8?>CEs>nMx~@htTY^2(nixe{Y#lt;BoAsp#m~h zpXzd?ba5`T!p8%yDJoKEC*ANe{izVVzstC*N$g&W3GV9I8aT9fr12iGtJpX6^y>~{ zHTsZ!0CAmyj!+l$b=BbB0oF(`!WLE$uKxBEpjRm)nzMRq38uO|lPxMl?x!NlYSHva z;qQOxMm9GS_O1irvDIm%(&(4zFRABS${uD-Uy#!3d~P=Xmv<~mG^E1scNQfZfC!Z^LgclARu0a;w1Hd~4=*`d=U#Ip$ULPWoRTwRADWO|Gy~iBE3!?*gZPv7-Q$p{u=L9iDvb z0|d#SUR{B)o%Ukc`a|}nc`jSNn_tU5@K#X7gF;UrI-tnc9Z0tpwx_Xfaf9Lb&^WNA zCj{Jh-FDc5o38_MyT)+=Qqi~8MI5CiQJs!|G7RWyI~+~}2)7uwJgiZR-_#L0Aq4E< zdPB!g4r4Gmc=RX|AVz=s<8$7hZG!1IsvqIFG z6p=QXk=KwWBwnrsfCICq?h_BFO_xUVWx{T@YAG5Oxx1LV6o8^Sy`7Cc+Od-{ZbFku z;6a_lc)1pn_Rc1`p+EG2Lglwh2<`I2#8q4C0)D&x(Txw$MXBti%%J`9{jJok1DTp` z^{-D5IfHXu$o@}0Bjvby%2%OEA!p5)?@`7`_anBZ=t_Ox@s0RMMMtrrZvf=fE#mZj z{JO8eOpGjUuho}nh_Ezvu75YUr;4=pF=$K-w%t;=GSHB|2$YiTJF71V#c?HJg!;tz zd62K^Qgkf=9*tO-7H!+?Yn}+AwCfbX{FFP9?(<;)KH+|zo8F47c-Sz#_;qBJye2J| zXIN*lwG~oEEXF)Zgcay*&|B>8wye+O4rZQKyW!&;E3DR8C<-<(cgT>&-du=9GhZ0( zdTaE7`u{kVj?X7y1jWiWmN1FMmYDTKij-lX%xv>;Yf^khc@VWN^3uYYJ9&~#i7$>+ zgfk30Rv#6Nyd0KBsz0BSu&+R(Uc+FcX@q=&xcvLIO&{y=*uPse{y+c9Nd5o-sYX&Z zuSN!hC*A{!$+F0kLCsOuBBuYi;AaCN^U&opEA*$g(l&18wYP*=&oQ)Y(((z~w(`a5 zfnbvIRzX5V;LRVEj(!dc%>b8eRZSYZlTOfr49jK9nnr<_fC}&Ja=n`7#?081k2iL1 zb)@Q8nroi+t^8(1En8tn3S$7HU%NaUrI|}4%uU%vx<+1V&O9bR^6+-Z!-rvC=BF;b zUTX^@qD?hjTa`80r1qr)7rMj-Ax60o`gXcwQF9xCv2L$Lf&HDFQv@QY)S**b6h!)!DKS(PO&&7? zDb80-Eck9O_ADqERE|CYC!sm_zXi$T{LuNpPPk^hAFFEy30kGZjZUa9H-Ui^71a-6 z42Lb1J*>aIB(_tsjy}qMqjb% zP~hzH{Oc~~kFrKjCiE%Q!%LRwKBgXr75MzWe*=QPN(oEP|6pE)X!_*$Vd;ez;XD6b zFaUo8M!z~K{xmceeZXH4JE_zsgA zSLWT1wxEnJ!#Gj&=7fZc>4duJ-6-QjEd$LE%yLuU0#t-({%qVb}G}z z2+{=OQo}mVY@VVg#V6V#!U*BXZ3Y{$2xEwmsLS*th|*;rY)q(eQ~Rm0(YVjAiCXS* zmzVz^_TD?H>8yJnMPR2>T{0xDHnP{syQMvx#7q98&D)g*L?4kL^p;EWvz}h-uv}GOgra%_St)%XFvNn z*mAJ@DW^8pnE2uVWJ(KECD<$2CSJW{%Ih3OB4?@!TjC?}sh>{h`DF37lU+T&{D+mU)WwT1)r!(%vO%R};r zrJi9`03Qq+_9lnr=G_EM?XzLTSY6?}&G_NqPI122g=+QSD=8~qg9Sj#rwJ66h4QOg zBQNA1N0qQ&zn-?Ko3J`4R^Yjg<`Uqg71TJ8_f z=K7)HedaexF5BxS;_SrU`|c<5pA>`=8W&{NR^FGusJZK>f&@uEiLwSldH<%g&i`Rq zykCef3;W^E|Ei#Ly#hVTMtQ9qqfl07Ja&_7SWeXjXy_$3I^ zvI8!Ac3iph{?#?_KYEDpH5CT*JO6)L@ag}_(s~;ZHUjl=1keuI>@LuvphB6@zjQq- z#nz-41eXRX0=`40hWo7HP4>nQPw`N&Su|)J7<#aw5SPs}J*Q+A3H}`agIj^7pY!{% zSbF+S0BM`>hmd-$hmkG02Ik+7=`nsFQ%K6@B4{cIB<%%y=3>$^*NPTyZ`IX=QuYe2 zXi&7Wn8>c?o3zIViNGWI#i57}E#oFyyWUbFD>rZ8N(1*@XS+0;a}hc?cND>I$U}@J zcM5Pog^j8&kv43Ly|dVz&`rZnDH;CuorB zkf(b@E-i-4W@_NCDq8iUW!LK7UoKP0>fSqDS6t!mM$G{=dQR%7^ElOg{pro#x}bEn zKQ+zj(^nq~2DKi!a=NKKa#PElm_=`k#sg)@ zg_})T+O+)HF)7)#+3rjig|(DiP+P~0gs%HA(q}**2&7VpO=rqHm`L$g3tr~hHeTv& z4M5O`(ws8uitXh;$Y$oB(e}D&b)BVEiX@zjxMs~3Tk|XQn;~_9Dy*P}(G>4>ptOpg zSsZrCd!o_iB|_riWdDOsn!dkVQU=vN=SPF;!~K@ko;JwaqZfOh@gwSJRGfsz03`-7 zHsG;i|HQ}Ie3t}mDiJw`m$DB!2gjDe9`i#D5c3V3Vf~DrmQ9vj4Jmn48Y$PQYO-+A z%UrqWSk_Mff$YK~sj0sD_dMVN@U=_rjI83MDUX9pG$04oGEMQL6G4Zlr7ce{rOg_~ zdXPfxq3KdoJ9t94#Uh@_g{H5;%JJ}RyD{;-S}7mo;Q0I=&M=l1JVwYoHOKTc%TISWJUB9b<1a^j4E zY}_a-k+(5U6Ox=_{?cK_#;9|P6G~r*qe7$%|_^t zy%oCDQ$WuovJ-DmXr|wsaYHI%!#YrSuZ>biQHr?s;fHuJJaPM@V>QZ@#nDn{NY|jT zQP*XzT%gI*P9#OfL*}S3y5{X$>)c0HAqP>0rG~P<(=GI%qsk-i$W^VxJo>$t6DLt# zuOqxu`ySX_PKeREt?ps_q^ z)&kCircpb|^YvTi+-Pp^gJs;L1)<-!KdKYaC>T$I#ZOzQSJo+a;1>^Q0XB9bxmYAV zf~IQ!VxL}Z52zzZdU}YfmINNu&Lu}UDoh>_hiE!R$B!^i5%&%&tesf!eaDq|8F>tj zO?6$EFkk9ROn97<;%Ig+TT?r;i4en(=>mkEN?@e<-x%qGooOLUyCCAW6)Wx5deF<< z@}2hE-{Y1SFQkDqQyUgnZa6J3MP_o}=a25NSQvb}gRTA0!dKPPyg)K=J@g zX#c{^B6)AlEF0{*p0_PeuRn0&_?Xi=(Wdi7D}maMxVVJUlsDkm6Ss>tJ;kcA@psn( zm4mlk6_%XY+`gXP(kAEavoaIem)HMMIY;~uZ?E+-xBTb)SJZnpd z*>>?Y9OnmAxI%%V+5xd;F|C_B)65l5b%f2=1>>0cjeN(FM^=~Dy9Iqa1VQ9D;rlc+ z+W~}+cy=oh07tN@$`U6F-}uCkH0L)~NT~*Po1D&rJ2umA7LS%6v9uOY4@``D_E#P# z+YSYV=ec;S9a-CcyBmA?1@}Q^{I$%0^=^-p*T7gLe zQT0cDFgC(@i}#DcQ?hE_&nGI*r91eFc_P-rV$o1t7+s<+{0l6Q_9=F2dPZE!cjWk- zdCcdxlSq~*o!tcxR-Zb=zA&>z2{~`XxGzY_b0N! zGOx)TCHm(pEcP9+0JZjj_9u|wgEzYH1%+`B-_JsOtOlCL?d5YkbaL<1`(B8=CL^V2 z3TGHR!*f_NLFDb-BRayiT>;@a%N35yz^%-(JT4p#sl|==auBum zQZL2r7cuu6K(#%Uitg>Yvi`|T5Ys_3MDrnVOm2Mz>UQt)(%8_8E(M$f)D$ zsIoU%3TTM);i1NHD6Drczk-7ZMa8G-oOifn4+8}GuX78t#+_+3k&>>G-X4t!Y6X3f z)}&DZKVtn8HBUmtL=Y_N+xK^b?Mcg~nzofmA;#{AT48C6#gB@E+WInj4j6;d|DGF- zn2~=xZ`3{ySmfTgbh3nrQsJ`n-cKIwrD~Q~J(X|AKrz=(3g&QIjbnl6ngd0j*D4x-?Db9zp+l~D8 zC*0Y&Y+IV$oK=PNwcEn|J3E1DN?&V9J4~rQCBZ$_QouR5r?Q_Lb*Ny4F1WKQ(OzQGi zH2gEQr?Qx}O_u7lJaE$^oc{U~r}T~Ehr@f?tjuZADNpgXgZ<@t@_~=tx8Zw}xShoj zK3i2ya`-GeNmIDp?bZI;qK>ewxu&8bMbSqJm^aV=wiPdb>e2!Nu854eVIw;5zMggLuYG4V{7cx!BPL!$!7PN|mzWZo4q= zj!-XC#*~Laro7si_TT2Bo%RI<;oq62)z}A;R{Ap^E8TVn7bV5DGCAkzvv5uSKgDOn zc!b;#UaR*j6JvmRKir=70vwtq4F95~X$o8Gt6Y%($f^?(q%ZIHu!i9&Mcd@9Dz=G! zV`EJBVM~9p?svwzQuqOx8XyxvcSDZgMxQrlJVu<#-EHl#{mH|UxI#t7j*qowA)}^` zxI^7oQ)o_N_>t%;$&C3PmtBG_>LlbUpm=od;biM45wV4sW53QXI10A8e};3<31^8l z8N&#+e2nK*UrMZNjUIA2dY6$gkZ6i}y0ajo-(U4~^gS;VQS&R(pINmZ5NL3QF(1R7 zNgA?-A9i@gvkYQ}AfAl9Orp0ejx;f(`+_gJxuho8E3*&UQc@(ulRA##z;?9?R5=~( zKf22Z&?!vlB?i8b97>{QjHjrDfsN0ykzJR*N{_(CYZS-8?z$Cn>(djzmiSTsI))|y z>NKOBPUAZs?J;em=*g(SGy-~l%b-&Jykz});za=12t#Lp*{W_SvyEP6%MyQPYN zw4}@X#qJJ`1_0RLZHTwrB)ghZ8$r#S4Omnt`(CuaA1=Bxy*?Ul^4-$vq5(szs6a2T z!Wm(AH&1L!xK=bY-;}x>Cx8(B_N;@1{fzOK@|w>YLG-T!q+NUh=pFM4pS!) zW<5A$A4$m%hGx`q-clabBAPmtD20QrlIkDmMu2?}l0AXm_y08~|81iFek*((2jBOk zL|l8KJpHjfFQmdyN?iw z{zu>MR)7S3RCX8Az4X6S2);dN{kR~Vt9JNn@8_`mp{qm%PSlq-4AJo%sIHh>b^?0M z-d?gpM!W$+uyWa*@7hcb3t8WlT(r(xuo}<;lhBC&>=M$v1g#5Vv(`7mXGT05olc~c z>)UKf>^{`;e~zO$Qbyi{U!_#@j^youZ&WYJ)!5?tX~z}i*wf%RP4DjRE<(bf3jC@ z-$_b+|+`dQ_-2Gx)%le!Kp&fX8e5!nHcwkojuIuYzyT2M|*}EXB{T=SmgB z+l1l2kVzZ`G9`;sXBpn9dc{o?W?skd3Iq4O_6=w;>Qo3#ROSV&@$fX$LR8C}URR9r z*1Elg#m-1oQKqaQBfb5bO6J?5R1C5TAk_R{SBa^`fg>x*XcKkB!d~_D=%e4YXEY>@ zAF}MrbjLbtHv_l{CRrMRSPVHVf+=c*^u-6w9ZTX7vRt7S6M;I46uN-X=t3Q3KU4Bt zX=yRES2;svm*v~js`G=^y8+JZWbtKDtJQ%zp2k#;cGWar(47-PMaq`Zx++)R-U&rm z=x2%sTVb`*+o_?^$99-S|69`t$R3(=|*KSDUmS0{KU3$4|6*|EOvfc01HWooB(KCsdkw>x55OPZ7y;>|CNyC*Bw;g z-194Fc0YJWmj5~laB=Fj@dMl5fR-DtgP!(%VLl^iTGTOPy}X^NLh87vD7(SB6AA?e_DuKtkARvc4(k5*hN2mUtPVs88t< zxVxbZDDDG5yQfALCYO#|+0n8);#$%5#1{hOgeo(2v>bJ!G)|VX+z(ELhMmqhR*s23)Hv2~xS)GhfZiL+!R`+x`Rb}(y2DSk}gm&M?n`y>tdXZSCRdyM_7U>6=da?=iKFxn=l=)O0EqjL_ zIZ!ay=0;x}c$nqC}^dCq-Bm9ex1)eX;6u0;xfa25%tdx)D$F2jY|x`4?W6z=t~R?A@Fb`<73`gp z^PP*AYfg(=(Q0D2h(72v-vdGrFp-~=BkyG}xkZiOfEzq?*Z?&CCE?Ze?l1deG6HY# zOk@T;wyo9mpGy8*72A!t3zXB+{IYEpw<9+_spfS{MO@1U2t7ehE7Jvc-`y&w)0 z&!;9uXcVLEvZP(9GX28Mo%d!c^HfKo&!b!?y%3I3> zEspbkzE1~)Zl`B8fIR&3TVHQ#*&X#FHM?;S-Q)@g%%5Mm4E}IW1@ZmMe}O-;1OG9O z-C3g``}+K;-GWps(e;0yFQS|se^b*aP!HWJ5RpqqK&!Qf!NF)CMfJO(V$>p(2L|RJ zpy_4!qQ}1+BD@M#A9&mrxan`OVjW<2ex7G`aA8B#njJtuBd#vEWWqpSUT{FuB!9KC z6OPGVKThlz$T>efNWcBhrF&l`0)_I-k7@9ig~yOV4eHW*w~3FZoX<=9|ND;s)yV}v zvZyTAHmRAA0JChVoLWbJ7du9&oh&*wZIEDIk1;p}u6aq)n8~QaV(Iqul*tMAU1e1Z zi)6}ko?aWZho;0}rq=NxRW7%9G3;51u2$zzHVd84o@Op`;x8kmpR&R1pt8sRIKK&QyHh6m1|^U=}7wJT&tN;M-qrZl~* zF=N}2BK?)Gxjoj)gbTAbayxiC&L zvrX+*wb#L6*|mv4v%>5n?c$4*m>5WpyhdRlF4l-rUKc2wt}VPf>T@PnXo3GHXi%EF z-C`uOi@JCjke$x zBz}&f6{iA_Ume&}(bNc=T{5#Jh(LC2%8de=C;d(Ix;*ZFqRr+l+}t2Zi%7xaY63mm zpIlgarF6K|9ZcA?VuCp*UYs)-0JzZ7lnVKbowrR#UZi=n>^7G#oft)rX04+$rP+9O zc+{o8hO)J5M6Fe;7ae2*-aZD%!C6;$h$?~CYlO;)t}o57is9i_DEBmGYU)NHD>kJC z6CIeSlk(5Ew_mZ$WHe6}0xDjT_yrk4OyM4V{x)bGLHLkKOdCU;7rCCbJrK*ws{4lZ=`B2nj#&(3r@}_-N&jro901 zZT|?sd@`QhVAFvMQpA4u;6HyGWzf_X-al`tORFpzWT6*R>sSW9p?*P6N3&jD9eHub zA?f`K*o@K9 z>z|DC{IPeMNjhkmd@BpWfSuHn@G-rtooR=OS_zJQE!`G~lC-n)n)1F}fCFazR};4y z1PUchBG{tqID(o>S20*^Lc4xUdscYuNI;#o+qn`LP$yNdnGWmQt1H~Tz&Ao#j^$|M zt#X_oF&pspiQv+bG}9O^|Ee>o@UL?#gz&LoQ0h`OB$XB_8XJM=^!_Hd&u;Kse^U0& zv`%`Om+3m-9`Mer2NX*a#+FNfU7Da^f;Z&0q8CdhqcUg36Rkb9o&Fdp>0TA3y!Tw= zZ_))Mf7t0U+#mIe_4G;Qyy!YjVf0n(e7nmyd!a| z`BX~J@VY#!FewM!&>bm`ZRPw%t}J=qtyPX=CusHrhCW-qL0R#JY0quS-&FtRe>bL{ zO_qT84d)r5uu7z$9kQ1D8}dnvaQbEPpA?@)9Z@sD6`A36rZv&)CDY5`vTlxx1E%(N z;YvAP=B}xL4#=zV&QOPQ+?=Zo-ud~UhpB*rzpM^5` z=2%y35Vv-W^4z~O)5>2{m0FE$ueTea6p%V9J@q5=^+XjL>1t#o%&*I{#=J^%V9Xw~ zx=T%#tLeQxkQ1!-O32osbPw3+73h49j0SOF*5PCy5mh*y&7ML3OM-OR_V9VToCXjL zgC?|l_S6vO`#V{~5SMlxk#hr~`&0V2;OpHTh=JG!>6*al4j%oip~ifVWab^@^jTNO zC9@z%6)^>Lvvck6M`bxWs)YUkIXK~Hie0>CmAj)ZYX(K{ACfN(%smfQ;+e)%?iAYH zvoy4L`7zy^%tsTQ_CNGBpk;FveS35q=QAf7X`#OMx*aUho?(JYO5kjUR@t-z(762f z+)d2p`N({bO!v%n^xXBi35yiqdH*H+M_jxAA8*CecxaO;o}?Wj^<*@wNoKmxFUJ@I zs7C+dLbk7Uau-X#UJ!N&Bngk{=xP4KVl+7eW#NfPQOHA@1_c+Ra4|pzHtVA>%~zXD zUHH}Az*TKJGjjSY-Uj=WL)5JRE79=`l7iK%3j6C{LpR9aC;r<|9tkcE_&(bIZ}O28 z&^#g_sl_LlQmnr|QGCvj&w{@0ZOc8?fhgUOC7(r0tNFB$l~91PAOP494-mcAU!Q0& z)9n-3JF2`zYro&uXX3xg+%RgrDt{HG4 zj5_3!q5)K+Ei6ENI-8MsO28-6Ck4bx8T0_0kCmQ>Bkga>EB6FVe^z!RBv%-2ikJ|b zC_X;@^aqw=&{qePeJl4MDy62I==uWHkHlYUt*}zHbkfPqqKuqHZDRhwrWUjQyfsOw zB^_NBks{!ly0@;_ET!R!vI!dDa6j6UWmwl`hF}nHlI~Hgzh0cb0M+BS61^X6#YAdZ zOp)KO9^Pj?Ft#L!1n(L|JthDo2}Dbx7-Q+OhR=?&fH=949yI;vuF~z^cVOqwBm!#U zr@?|z1G7p4cip*4x~2m(MIT&i6bAbE^+5NNCLo&U94+%O3vKFp&#Yl-IfLd-tr8%Y ziGKV1`WY$ilU}y9;PCl*_F!mhW3=9?0}k1yoFC)a27Z8Jl&#?KAb5TmpoV@jdE>A7 z?PdP1cG&qo3Nr#KCz3c{=zXHKF63g86#X+u%+f1o|BN0{1c`-;HzKDfS^c4NvM7B^|d>!(~ z6WdwMlq9c8SJw$mLgN6qXyxn7LWy2(xXMyix&e2n2#_7TkySCKk&CTKl1&S&djJ#(>TGVDBK@i*444)`NOV=B zwEtOwFT`{V-$ck`mbr4}CnAbx2g|)IdY2x~VQOS)fM_`BD+geg%AHjnZa2~%jsfYL z7N7RF^cur~_|oa#JIjgW3J9ytuSzR(PZRHrDXexda};Bc<&Q-w-mwoo4XwH3U=2|wtD^ucny&uN76}6k6|df6eBgn?)Do#U&EP3#LoG9I9e}AbLfc+nIj`yN%@ztX>|Hn$lZ#I2U6MsUy~54VF0?2 z6HzrWh@LTRJX3N}LcDN``8HnFT5rc{=TY8_Fn> z)O0N1m*+xQQ{BmxQ`P&MR+p+Z$|^!T7&bh+0ZM=6YItZcrBf~6+|hZVRiojmrB~Y6*HZUo>Wg4CN=C#>j0cqKH3NH2t9m~as`awj1F%g zbSn}WvH_^&+ZpTMc#+^M_VQg>PlfvlVo=Z8m5 zU9tXY2S~Y3X~&0Q8K+sw5!n1r0kZt9*ZeI8NZhKH0{B$N2!2;`J!Sw%H49Y0VgaBY zJoNzLvE-1}9OqrhAC@Fk|sFhnCg>Qoii>Mnr_2B4dF^6gmDcDPZveL3?j{kaU2oLci1(Fz1E za!*?aHC6e!a3?%Hs*uhoTlX3C#h7J<*v4Q=>gZET=PKf4QQRkn1&HzNR+6_6%XRQ9Y%g<~T z^is>@x+}_DFcdCExv*G&MI?%U?X}VibM7F3sEBlpKzvW$FY2kzK1FQYAg-J^;g>zW zEQN4Q4r`1$N+~b;&zG>#ej)Z4VlPND+A{4+7c%)gUeEs8Wh?Snq&Zy#y#ONC+_wziy1;n$S$%RwSzWFCMe9rZ#NXCQv|3rNM zXW(B}-0do1ZU%+`dgAo_NO)FtnBC&DQxsTCef7o0XB}PA!HdMvd1Tiks+UDhnf|?0 z?Bk(QtIn*2iR)7fEt1*!cRp>o$~jo!&NM6U^f5LDpD$IGd4h8|5py{6PZ4LC3nsHgzFk*viT6Glf!o!l#GPp;$}*`C6!(v< zD!1UhGAa%!3SiSP@o7+YzSlr~DA@+C_9?qVXHJjUP({E$5A3I zy_4twV85VY+vTY)8$1=Voiw9iSH4fhzG{~NZ{8}NN?Hy-MG3{p>XI3c^uQ{wD~NT# zjKz$#IoogG#&~+eQ>Liwm6`b$=B!PY{6^Yu`3p?#4GFj)O@k_=aV65{Ov4F!C%HdC zL2vHlVCk=mjqu@w`p0U9w<{ClAf-I_fL z%}@>sv#-P)t8Fj}Uj=vKE+s=@_Q%H zj7J_+>X1%n>w?WCtbqLgzE0(K6{9b_&~}P4u_B#Rr*@3($uwu@5e&Gs_GYZB+$?M%jUxNnMzbA5SCVCz^o)UMP88#Luz)fUl_mvJy5 zFJ_FELgZZy)B9v(={ap9AYdh=(gt)-7P-}yF7!Y4zO^DcG}3ICG!^Z9cqIOM(O_CP zw5u$7)s}0$JQYzlvQax7pA>#FV`o|dDW$T^&!zr)(XatFScmXue^^L;;auoIqF0>r zsc0_?7+bTHP@vqODz9y~*z~xkw6gch!e|L=ESNS&D8c3Z`m>xROCkcc`ek^N?!i(e zyZZd#L6zk%o9$!ZZ@r>v>HU;cq<9pq=qNT77YG)91KqaKFzAz$oor*2bQr9p0DbqsHUH;htzA*~c@i8)JROrgA)m=8KGzj~X z)8`z%nNl0ewA1o{bI|OFb-OG!FV+z|5UrkN_$g8}Ljo_4tI$-*)|W=ssv$RnrnAQ< zaL7LYu!h1F>AE^Kk4WX_UxO!}M|3hprXko{yAslFeK{7U(>tYrxRNfb&9!ZpeH8^( z24ohs+B{mBh?<(3)nkiUO7``aB7i2}uFzmlXKG1Y`YCYj(9m-;KRLIWJDkENKshRA zRj1Rf{(AP51yucGb;v9q;G%Qz%3iM|$saW%ogK~Ma%15^HuzIvj)k68=EV4cB~B}P zDVSy!7cxw2$DHCfzTF*pZEJs`*XkQ)hEq?yD8-p9O=k4EbAOXHV~*kIN9%>Wkm%f^VzDG(Iy9HyY^(Eh4E=c_u*^?_?tK zLeMxq&(l6f1e4({ng333h&PXQ9^oDwu;5fm#6{AJ1Su3KG^f4p0H@6RR0-K;y3ZU( z+5+#XY}!VP+1l?5wev$FAXXUNr+zVkNpot^k0H>nJtd|CK*)qywBJOY3;nt?XQZbA zvT@p!Tn8cYkNFQT+qadD$*!jGjHfye93$gBeZ>&WvW>1!w@rh~y6X#1Qng54OY}xG z>x(1Zs=OveKe;lA^tM0I20L7cUK-DdDL5ad&>aQO1tk5J-8O5#I&Mz1j2McrCd#T3 zm3d+***TR^KR||&+hblIj|*drYkMootk>mwRI1rXMmg({y-mxsyvl;$VZBeEOx$m> z&Jzae6%t=|9&qY9a`P4S8#Z?s%Tqg+_o&dIVX7Ssg9$Y;@HgOrO)XO&hllHyg)wLT zX3oJ!^98D8?@)|VbU9#@;U$VA9!4x8zT~DIIgO!W?laq(q9Q5EQuX$8g21Am#TC8L z_{{m3zrl(JW80gz?hri1U%NPVQ2#$Q$96?{*XaqjzwkY1Y*Zqk3}4jK8iMJ@;A0K$+SEU7ehHj>S5ae};q801YUj%|!}bRu-KZ#6SuVmH>T0SzN)jI6 z_CK5_Xq5%;G9YBIHg-$NuXSHp%kl6d<&Xl=OU^P5_l^WlHZ`8&j!ZlFh4mIH^?w#a z&$mtpT|YkAy6{}ycjfZ2O4h(B$~{+?G-5e-NpF zJfKkTE>E?)yHzEMp?JDp#C!93`DaIyD~Gprx+)hL!X7}0r;(;F9(mN*b zFvZAIy2-Rrd|Z0Rtm*k-t>~$7lTU#`B`Q=SY`fhmbFk)@0{uRjYZso5FScXg{u2Y0 zJNsF$JDJ|JWe?=@D}ob4Xjo0Dd8iJpeD6u6RB9PKzx(7Tmq_2>2R{4=9-`f!An%RP z3ecbEoa{dL_(V?IE{J>}brkVDky>oNQ7ZuEC-by?YSFvS#NY*0Idp5^D!6`%UoK2b=kl|+cN))iX%mxT35OT9Y0Le+hy}^ZBddZ; zJgGH)s?zgUq#|G_Qz8EFpvlv-eMPN+S>y^gqxBzI=&K~ch=k9+C7uZov=gqh)Agy{ ze^%P`rFu2-9|jhWFLoem^6_ms_yb;>M`{cO+j}eYN5P&d2z-;FA=jR}e80(FkVVW5 z88z%46DXk597dGPIUQb7X+8iz zcmb|U^vry><LfTQ2kNHJqHifWEPQY6jm zQ{=UUcf)G`ZK`{1bEnj|Z-$)c?ki^4A{C{>T5+9kWW#uUao9ZuJVvKR%ME8XzTyTW zlNj74*<5P?Hf8>mXM!s0r)}|d($YoRyCiU$|Il%CC|)kyX|Dhroofq<2Ri#5 zi-+YbU7U>9vGEC5>9Q~b7R?qDdZKH(!3(9&%#xAoT{!NZFOT+smR@baWoi=C@SB^~ z;NILo(O<QOpWpA1*SPW&OjdzvDqCm!k<~jDZXM+5B>(ob2hkj zP70e{+617$D4w;aUJ$B{S6}oZ5mj%s3Y|8=+Ew(=Op(oZE-nva4TmZVR*Wa7j|V-} z;~Q%C2k(=euJBPM@Y{yz0gpG2*-K}%K~A&lnD>}x)K(S%`RC|U)Xo$)uPz+FvV#25 zP^OtzR2;WCCUlF)bvvMmO{v&6$ALM+A>JOVjVp`Nlt!|mx5R$f5@u3~M2>(@nqpI# z#+5SXL*ek4W1rhpTDI4x3ChT3Pv>@6R-L~#*MQ#?O^w4OmBO&zLCXm@r2J%5lfw+; zv3zYO5?7)8Dr+2JgNTHq*A5-ZvAN zHtMchpjZh}kt^lF@zROPvtp2#?mll~U-j#o)BW+Vd*W+&Q>G&;Y%QphuGt1n!>OBDB1)SU$56u&~v-T7?cAmcQ@-3qHT;bq{%NCZi5DoiLW#3tU?A<^4@5V&`jLfKZaD9J_`1Tx7Hf&MBTcYMHF@vmS%=eV z9tRxCCh6^R>vU()mCF3EVd6s23IPzK8IY7I$X!XsmRq)$+!g?hRa|_gSbqGla5}<9 zX`NAp^5ONLnc2F-+qS6xxijrYYsVksYmOCfNd1_o|;<~(!5XaSK9Mlo# zKmD+Cck}G2sD#x(Q2g3Vr;{D;@K*IPORl_Xwb5Tuha>Jx;76mqFD2g>4A1di$Q-iJgk-%GJgX-c-%yqKd4;eCEdguZET`S zh>w6%;0S)3DVwE)&iVAv==k}i+hUr&WSzo3_2Udo1`{Hv4uS7#}PTQdzH z-g*1bgWbIFIO?X`8Wo?2>qf30UFFTWj%&Sn8?5gYY}f*%SiGy7cn;X0(D6x!{k*B; zl|ICdG+N4O$kK5Zf;dIC`O$=aJm^x}-;C+d(xX9b=&6MIwppBlMFFmPF?2AX7d78f z8XT4g3;DT}VS(+hULm(ZQxz-XHuua>{t(dfpLE5`u8rEu z1_L!!3Twv^k9fG?I*ll^F9LDEfLx5ZOYePmNWeY~_Tjlg5IgoJ{BsnUPYJ>7D zSG#j_Y_x>YL(lO!1qs$U-!0jV+5-^Zh61O0(GV~4>BI#Gogb`+)qK~FqIkq7JvDe1kR8&*DTj2=p+VmhiPI$-Kl73F2N=k5+5 zto*@Wd~&w$`FpvcFK7QQ;x{ufd<#|EkI!z_$Y7*MLY4xu8<#pOhhFq-d;osHe8x*1X%>mW3 z<-@b}^B02-eSnNVS@5X4VtCM4m8^Y@rZTuN>R3{5ya7MJ&_Mus3-IZ+?|O=M zSDg%5eY;g=(+@wdw_4}<`)qc!J8ZYf9&QzxjtLp&J9)CssFtfTP^RkrFCWqsWx-c#p)ccz3ppjm50shHsT_yZ zC`gjZYU)rhBNb&mON?e0G$#DC$!O@-XCUOS^d`*F=3@kYFKwaj5M-rK)JFVre(}(v zqqN#Sqj(HUZ9!2E09{(kgPb z)Cp;j@M(H~slpPL<=W%6`bQ{+O|2^lzpp%^w8|$pLj7lzQ>&(2w?>g4F6pD`>K|r$ zgEhB$q)T!5ZVgUgL-7SFVs%C+ix9|4xxUtlw=zT0zu{QxOKtLOB6fx6vvP5EMY0RN z=gYY!q1e=LZP2>}chSR83S*VMdj4Ohd3^cI5`QylP+c7-UcWk5GFMpCMYNZ|%oT)^ zs*A{M?k=3>DWj3DF8MXQr4gTHAM8c+l7sWkPo}yX_LnNl_L0|mRK|_2RnlAs?6fp(sYv@yBtK*lP|y ze%{!o-Lw$pgIgZs)IyXO98a*uG*22qmPU_zznw23?JJPepv0~6=srv8wL;_12FFQf z8LLa5%^8M-&HdCe)I3u|4ow43JHsK1dR+=O(lvTIDNtu+c6DpN-)AiSOacK|?gXT4 zDp&rM!x;#NjdlFjrC$5qW*IT4*vM&;qoEcBNmiVK^}OGrtkVI5I4S7-Q8^+H*AOW} zkTZuAFIR-E@P>J7T3!IpvS1kYY0cA-MWzP#ma4|K)<`1j4nZk#Bwpl5PSuhW8OB!M zSUYCT~uR-o| z#wmuVD~OE{=JNO`Iei5pW9=7;4b&_l=-})fEW4m4PGZ%Lxo^~jLoJw8h4^19)COG3@8lFiFmtd_K)l>=dnNRBArotQp( zvBq)r*iz@<>@dl1T%7=scE(Mwmg{?6zKEu|8p@6;B<8T=rI*WOJ{Kk_E{vh7J(k}Ueu5lOuzD!Oe_@Fnq9qqLNgW(1tO4VehU+B&t4n8Lqdudh zlm~_Qqftp!E%=0zhQFD1z6Wpr}dY=@CY7swFUdfv>amJ+Wig7hDa2 zg5{$uS6T3oJuBm;P@3n9uoWicAbgMm)j!pbf=01n)mRwu)-|F3xPJSaff5tmaRXb! ztkwUjpaemMb#eCLBeZ7&Qr9;8_n-|T{VH*e6sc%1XIkP6g0?rQNNMYHo7lfDn!6(EK{)C?5E7XDW2l(>>FQu`ENPUy1W^Kg+}tJAAF*c)jM zv$W{?r~=Q+D^~jkTxY{DoDYug3&o+hBLCIVC;Uz03Hc57;V`jU?tuZ^nwI#+561Nw zz@dy*o*PSxvcJq@wo>2c8H15Ht`YP8Ud>wWz5$C_V&B@yhAZ#)`!_2}|FEJNkVl-C z5C_fdGbWtnX!c`e-1jrmOb!p?m5_eKviqC@SkWYzF!WN_OUcGdO&viu(k3uc1lGt3JJCR@-gGmnc(mF z8NA@b%HfWM2nk)=dt|LibL!LXqq5DZu=)pH#+x2IdXc`L%Vp2_UpV{LeGiqYs`LsFftd4 zv00vG4H7i#_l)vwR{bm7gkOO@o(&bd_I`oi_CncY4w%V)<&#EZ>cU_e=9+8mymu6- zOu`Np+(!lNpUg8DJyN-ESK35q!S9@;31aFK(TP(=Pq&{m$~nId_($;R^2Lv9+K~NI z%auqYFt(n**Y7LCXcb2dn)&w0Yr;v%suSypfrTgt0U?BzL~0-t^S`#yiXul%PuIl0f=Gjq>p<}-6B?40;MJsYFk1v$7V6q(9PA}r|EJ%za{B1o5z zyvO)T|J%+yQgUghF%7x18zu^W=f-%^Pg=T5@2^)EMhs&HaDnRnBfp7R25Fz=FGwYl(a5cXToRqk^(sr;^az@q_?CAL;|ic0*5(*x3ov}bC1LDqHe z;6FATA%4r&TusviV@}{@osb6%OZMXAFgt59;}&+MZ~$t^#7IL8@fA_e*mCA3pebs7 zML`r7cuLpiTyy*R=AksqqM!b$@gYzsrJad0&p>Cum)>(8Dk6f{!wkgea^OtN72$|K zNK3P*2QJvk`oEqy8nQF*9KI6rrFA^QCdUdv&?#F+qHAL86m}OMrgk?S3|qV+eCxjQ za1x9%Yhv87qmcHxH_am~p&re`5M!Q!9360xig}Y%?_$kBy9oB<8TDrJ1{K#*5Sd(* z(Y8J(=(%tsOBsC@KQrIzrCzt$dymDk z2@VzTLZ{&#(Mn@-gy+qph3vBsQBZS(Md?@A01B?TSbmpTS-Ro37h0GMW1K zW&)NEH~@pccXCa7hj>!#@0zD`9qe>op4u1iLW|p+Eyo9ZX?nP_9nmqB?nsj2Q&Eu8 z4PEptKvXSP%DZzfFy$E7aRRxpzTzr-d=lFAgSpnaLxeZFWU3TVCW9Vq;kI4SEtXD! z6drb%$D6#Y6Tz+2UB!FVwu)J5ee0tzY8dKFBvcGGwqf3Q&t2y%|MZJk-`S5g=$-M& z;r%qi6pj`8=pDTDQW*hzu$n<*CG?5MEBQ+c(UfDR#@z$`zzSV91dHV*B9Sy18;0J( z;W4)qH!{H@UuKU8S|cv7$AwziE@rvFo%6*mfQ%#Pw~0*{{h2vYGF;r#ICPnVf%Gk}vkJS-g@{jfc|G)5qV&yK%@xv1JH=#80Jy?-euy<(&9 z(lB)4#ztSt>d-X^d_)F5wXv4VOOI9E`1oi;Ko|h%0OfxsU+|dQRQaJ&$k4ulRbJ9W>{VMJ;m9x36xvDyh<~D{t{OYz}~cD zobwwTF~eT9`C@&yrw5Fxp^J4b%`;Ly(>1buTKC2JI<7F(#DF6zw$u#|A^~%p&!hp~ zq$K}m&Tr2H!tA{z(bMMd5_96v$GZaGj#RAn&Ds1CnlN85BkuRR4Bt(6Q>I>TuA|UE zL0nPUL-*xy*x3P>+{5ECORGt-4erdaPSc;q=;@d=S=PH{3W~d2z(S|E(Hu)~eZcb| zQ;it46-YVB9DlXDc%7vHhEvn zv3;i!!OU>R4dv>Tvb*LwytT)n-i(Cvw~}M;^9?Lka)(KaTZiTe;=tp{VP`MhcS%Wc zs!M|2)1lYuxxTmJ)2%q5F#Q>v37nMTBR9B5nOSMZuoyB5(D*PKT-VTM#T{=Zx+qse z?RIoQ#@r{$(*glETOh^vUyuEd$BQ$^zx7z#^qnnVzt8Kw#+$d#)q~9->3M@D#_|5{ z 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.modbus.sungrow/pom.xml b/bundles/org.openhab.binding.modbus.sungrow/pom.xml new file mode 100644 index 0000000000..63dbb502dd --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.binding.modbus.sungrow + + openHAB Add-ons :: Bundles :: Modbus Sungrow Binding + + + + org.openhab.addons.bundles + org.openhab.binding.modbus + ${project.version} + provided + + + + + diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ConversionConstants.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ConversionConstants.java new file mode 100644 index 0000000000..9d7298bd31 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ConversionConstants.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants for converting values. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +final class ConversionConstants { + + private ConversionConstants() { + } + + /** + * Multiplicand for 0.1. + */ + static final BigDecimal DIV_BY_TEN = new BigDecimal(BigInteger.ONE, 1); + + /** + * Value conversion from Celsius to Kelvin. + */ + static final Function CELSIUS_TO_KELVIN = (BigDecimal celsius) -> celsius + .add(new BigDecimal(273.15f)); +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowBindingConstants.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowBindingConstants.java new file mode 100644 index 0000000000..1b1abaeacb --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowBindingConstants.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.modbus.ModbusBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link ModbusSungrowBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class ModbusSungrowBindingConstants { + + /** + * ThingType-ID for Inverter. + */ + public static final ThingTypeUID THING_TYPE_INVERTER = new ThingTypeUID(ModbusBindingConstants.BINDING_ID, + "sungrow-inverter"); +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowHandlerFactory.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowHandlerFactory.java new file mode 100644 index 0000000000..d0a547cd18 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/ModbusSungrowHandlerFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import static org.openhab.binding.modbus.sungrow.internal.ModbusSungrowBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link ModbusSungrowHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.sungrow", service = ThingHandlerFactory.class) +public class ModbusSungrowHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INVERTER); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_INVERTER.equals(thingTypeUID)) { + return new SungrowInverterHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterConfiguration.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterConfiguration.java new file mode 100644 index 0000000000..88965f9c52 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SungrowInverterConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class SungrowInverterConfiguration { + + public int pollInterval; +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterHandler.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterHandler.java new file mode 100644 index 0000000000..5b1d55f793 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterHandler.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.modbus.handler.BaseModbusThingHandler; +import org.openhab.core.io.transport.modbus.AsyncModbusFailure; +import org.openhab.core.io.transport.modbus.AsyncModbusReadResult; +import org.openhab.core.io.transport.modbus.ModbusBitUtilities; +import org.openhab.core.io.transport.modbus.ModbusConstants; +import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode; +import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SungrowInverterHandler} is responsible for reading the modbus values of the + * sungrow inverter. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class SungrowInverterHandler extends BaseModbusThingHandler { + + @NonNullByDefault + private static final class ModbusRequest { + + private final Deque registers; + private final ModbusReadRequestBlueprint blueprint; + + public ModbusRequest(Deque registers, int slaveId) { + this.registers = registers; + this.blueprint = initReadRequest(registers, slaveId); + } + + private ModbusReadRequestBlueprint initReadRequest(Deque registers, int slaveId) { + int firstRegister = registers.getFirst().getRegisterNumber(); + int lastRegister = registers.getLast().getRegisterNumber(); + int length = lastRegister - firstRegister + registers.getLast().getRegisterCount(); + assert length <= ModbusConstants.MAX_REGISTERS_READ_COUNT; + + return new ModbusReadRequestBlueprint( // + slaveId, // + ModbusReadFunctionCode.READ_INPUT_REGISTERS, // + firstRegister - 1, // + length, // + TRIES // + ); + } + } + + private final Logger logger = LoggerFactory.getLogger(SungrowInverterHandler.class); + + private static final int TRIES = 1; + private List modbusRequests = new ArrayList<>(); + + public SungrowInverterHandler(Thing thing) { + super(thing); + } + + /** + * Splits the SungrowInverterRegisters into multiple ModbusRequest, to ensure the max request size. + */ + private List buildRequests() { + final List requests = new ArrayList<>(); + Deque currentRequest = new ArrayDeque<>(); + int currentRequestFirstRegister = 0; + + for (SungrowInverterRegisters channel : SungrowInverterRegisters.values()) { + + if (currentRequest.isEmpty()) { + currentRequest.add(channel); + currentRequestFirstRegister = channel.getRegisterNumber(); + } else { + int sizeWithRegisterAdded = channel.getRegisterNumber() - currentRequestFirstRegister + + channel.getRegisterCount(); + if (sizeWithRegisterAdded > ModbusConstants.MAX_REGISTERS_READ_COUNT) { + requests.add(new ModbusRequest(currentRequest, getSlaveId())); + currentRequest = new ArrayDeque<>(); + + currentRequest.add(channel); + currentRequestFirstRegister = channel.getRegisterNumber(); + } else { + currentRequest.add(channel); + } + } + } + + if (!currentRequest.isEmpty()) { + requests.add(new ModbusRequest(currentRequest, getSlaveId())); + } + logger.debug("Created {} modbus request templates.", requests.size()); + return requests; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType && !this.modbusRequests.isEmpty()) { + for (ModbusRequest request : this.modbusRequests) { + submitOneTimePoll( // + request.blueprint, // + (AsyncModbusReadResult result) -> this.readSuccessful(request, result), // + this::readError // + ); + } + } + } + + @Override + public void modbusInitialize() { + final SungrowInverterConfiguration config = getConfigAs(SungrowInverterConfiguration.class); + + if (config.pollInterval <= 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid poll interval: " + config.pollInterval); + return; + } + + this.updateStatus(ThingStatus.UNKNOWN); + + this.modbusRequests = this.buildRequests(); + + for (ModbusRequest request : modbusRequests) { + registerRegularPoll( // + request.blueprint, // + config.pollInterval, // + 0, // + (AsyncModbusReadResult result) -> this.readSuccessful(request, result), // + this::readError // + ); + } + } + + private void readSuccessful(ModbusRequest request, AsyncModbusReadResult result) { + result.getRegisters().ifPresent(registers -> { + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + + int firstRegister = request.registers.getFirst().getRegisterNumber(); + + for (SungrowInverterRegisters channel : request.registers) { + int index = channel.getRegisterNumber() - firstRegister; + + ModbusBitUtilities.extractStateFromRegisters(registers, index, channel.getType()) + .map(channel::createState).ifPresent(v -> updateState(createChannelUid(channel), v)); + } + }); + } + + private void readError(AsyncModbusFailure error) { + this.logger.debug("Failed to get modbus data", error.getCause()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Failed to retrieve data: " + error.getCause().getMessage()); + } + + private ChannelUID createChannelUid(SungrowInverterRegisters register) { + return new ChannelUID( // + thing.getUID(), // + "sg-" + register.getChannelGroup(), // + "sg-" + register.getChannelName() // + ); + } +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegisters.java b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegisters.java new file mode 100644 index 0000000000..12517febfb --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegisters.java @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import static org.openhab.core.io.transport.modbus.ModbusConstants.ValueType.INT16; +import static org.openhab.core.io.transport.modbus.ModbusConstants.ValueType.INT32_SWAP; +import static org.openhab.core.io.transport.modbus.ModbusConstants.ValueType.UINT16; +import static org.openhab.core.io.transport.modbus.ModbusConstants.ValueType.UINT32_SWAP; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; + +/** + * The {@link SungrowInverterRegisters} is responsible for defining Modbus registers and their units. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public enum SungrowInverterRegisters { + + // the following register numbers are 1-based. They need to be converted before sending them on the wire. + // Registers are duplicate to DAILY_PV_GENERATION / TOTAL_PV_GENERATION + // DAILY_OUTPUT_ENERGY(5003, UINT16, ConversionConstants.DIV_BY_TEN, quantityTypeFactory(Units.KILOWATT_HOUR), + // TOTAL_OUTPUT_ENERGY(5004, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityTypeFactory(Units.KILOWATT_HOUR), + + INTERNAL_TEMPERATURE(5008, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KELVIN), + ConversionConstants.CELSIUS_TO_KELVIN, "overview"), + MPPT1_VOLTAGE(5011, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"), + MPPT1_CURRENT(5012, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), "mppt-information"), + MPPT2_VOLTAGE(5013, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"), + MPPT2_CURRENT(5014, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), "mppt-information"), + TOTAL_DC_POWER(5017, UINT32_SWAP, BigDecimal.ONE, quantityFactory(Units.WATT), "overview"), + PHASE_A_VOLTAGE(5019, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "overview"), + PHASE_B_VOLTAGE(5020, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "overview"), + PHASE_C_VOLTAGE(5021, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "overview"), + + REACTIVE_POWER(5033, INT32_SWAP, BigDecimal.ONE, quantityFactory(Units.VAR), "overview"), + POWER_FACTOR(5035, INT16, new BigDecimal(BigInteger.ONE, 3), DecimalType::new, "overview"), + GRID_FREQUENCY(5036, UINT16, new BigDecimal(BigInteger.ONE, 2), quantityFactory(Units.HERTZ), "overview"), + + /* + * Not working + * EXPORT_LIMIT_MIN(10, 5622, UINT16, quantityTypeFactory(Units.WATT)), + * EXPORT_LIMIT_MAX(10, 5623, UINT16, quantityTypeFactory(Units.WATT)), + * BDC_RATED_POWER(100, 5628, UINT16, quantityTypeFactory(Units.WATT)), + * MAX_CHARGING_CURRENT(1, 5635, UINT16, quantityTypeFactory(Units.AMPERE)), + * MAX_DISCHARGING_CURRENT(1, 5636, UINT16, quantityTypeFactory(Units.AMPERE)), + * PV_POWER_TODAY(1, 6100, UINT16, quantityTypeFactory(Units.WATT)), + * DAILY_PV_ENERGY_YIELDS(1, 6196, UINT16, quantityTypeFactory(Units.KILOWATT_HOUR)), + * MONTHLY_PV_ENERGY_YIELDS(1, 9227, UINT16, quantityTypeFactory(Units.KILOWATT_HOUR)), + */ + + /** + * Registers return invalid values. + * SYSTEM_STATE(13000, UINT16, 1, quantityTypeFactory(Units.ONE)), + * RUNNING_STATE(13001, UINT16, 1, quantityTypeFactory(Units.ONE)), + */ + + DAILY_PV_GENERATION(13002, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "overview"), + TOTAL_PV_GENERATION(13003, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "overview"), + DAILY_EXPORT_ENERGY_FROM_PV(13005, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "grid-information"), + TOTAL_EXPORT_ENERGY_FROM_PV(13006, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, + quantityFactory(Units.KILOWATT_HOUR), "grid-information"), + LOAD_POWER(13008, INT32_SWAP, BigDecimal.ONE, quantityFactory(Units.WATT), "load-information"), + EXPORT_POWER(13010, INT32_SWAP, BigDecimal.ONE, quantityFactory(Units.WATT), "grid-information"), + DAILY_BATTERY_CHARGE(13012, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + TOTAL_BATTERY_CHARGE(13013, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + /* + * Not working + * CO2_REDUCTION(13015, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, tech.units.indriya.unit.Units.KILOGRAM), + */ + DAILY_DIRECT_ENERGY_CONSUMPTION(13017, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "load-information"), + TOTAL_DIRECT_ENERGY_CONSUMPTION(13018, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, + quantityFactory(Units.KILOWATT_HOUR), "load-information"), + BATTERY_VOLTAGE(13020, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "battery-information"), + BATTERY_CURRENT(13021, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), + "battery-information"), + BATTERY_POWER(13022, UINT16, BigDecimal.ONE, quantityFactory(Units.WATT), "battery-information"), + BATTERY_LEVEL(13023, UINT16, ConversionConstants.DIV_BY_TEN, PercentType::new, "battery-information"), + BATTERY_HEALTHY(13024, UINT16, ConversionConstants.DIV_BY_TEN, PercentType::new, "battery-information"), + BATTERY_TEMPERATURE(13025, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KELVIN), + ConversionConstants.CELSIUS_TO_KELVIN, "battery-information"), + DAILY_BATTERY_DISCHARGE_ENERGY(13026, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + TOTAL_BATTERY_DISCHARGE_ENERGY(13027, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, + quantityFactory(Units.KILOWATT_HOUR), "battery-information"), + SELF_CONSUMPTION_TODAY(13029, UINT16, ConversionConstants.DIV_BY_TEN, PercentType::new, "load-information"), + // Not working + // GRID_STATE(13030, UINT16, 1, quantityTypeFactory(Units.ONE, "grid-information"), + PHASE_A_CURRENT(13031, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), "overview"), + PHASE_B_CURRENT(13032, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), "overview"), + PHASE_C_CURRENT(13033, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.AMPERE), "overview"), + TOTAL_ACTIVE_POWER(13034, INT32_SWAP, BigDecimal.ONE, quantityFactory(Units.WATT), "overview"), + DAILY_IMPORT_ENERGY(13036, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "grid-information"), + TOTAL_IMPORT_ENERGY(13037, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "grid-information"), + BATTERY_CAPACITY(13039, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + DAILY_CHARGE_ENERGY(13040, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + TOTAL_CHARGE_ENERGY(13041, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "battery-information"), + // DRM_STATE(13043, UINT16, 1, quantityTypeFactory(Units.ONE, "channelGroup"), + + DAILY_EXPORT_ENERGY(13045, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "grid-information"), + TOTAL_EXPORT_ENERGY(13046, UINT32_SWAP, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.KILOWATT_HOUR), + "grid-information"); + + /* + * Status Registers -not known if working so not implemented yet. + * + * + * INVERTER_ALARM(13050, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * GRID_SIDE_FAULT(13052, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * SYSTEM_FAULT_1(13054, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * SYSTEM_FAULT_2(13056, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * DC_SIDE_FAULT(13058, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * PERMANENT_FAULT(13060, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BDC_SIDE_FAULT(13062, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BDC_SIDE_PERMANENT_FAULT(13064, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BATTERY_FAULT(13066, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BATTERY_ALARM(13068, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BMS_ALARM_1(13070, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BMS_PROTECTION(13072, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BMS_FAULT_1(13074, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BMS_FAULT_2(13076, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE), + * BMS_ALARM_2(13078, UINT32_SWAP, 1, quantityTypeFactory(Units.ONE); + */ + + private final BigDecimal multiplier; + private final int registerNumber; + private final ValueType type; + + private final Function conversion; + private final Function stateFactory; + private final String channelGroup; + + SungrowInverterRegisters(int registerNumber, ValueType type, BigDecimal multiplier, + Function stateFactory, Function conversion, + String channelGroup) { + this.multiplier = multiplier; + this.registerNumber = registerNumber; + this.type = type; + this.conversion = conversion; + this.stateFactory = stateFactory; + this.channelGroup = channelGroup; + } + + SungrowInverterRegisters(int registerNumber, ValueType type, BigDecimal multiplier, + Function stateFactory, String channelGroup) { + this.multiplier = multiplier; + this.registerNumber = registerNumber; + this.type = type; + this.conversion = Function.identity(); + this.stateFactory = stateFactory; + this.channelGroup = channelGroup; + } + + /** + * Creates a Function that creates {@link QuantityType} states with the given {@link Unit}. + * + * @param unit {@link Unit} to be used for the value. + * @return Function for value creation. + */ + private static Function quantityFactory(Unit unit) { + return (BigDecimal value) -> new QuantityType<>(value, unit); + } + + /** + * Returns the modbus register number. + * + * @return modbus register number. + */ + public int getRegisterNumber() { + return registerNumber; + } + + /** + * Returns the {@link ValueType} for the channel. + * + * @return {@link ValueType} for the channel. + */ + public ValueType getType() { + return type; + } + + /** + * Returns the count of registers read to return the value of this register. + * + * @return register count. + */ + public int getRegisterCount() { + return this.type.getBits() / 16; + } + + /** + * Returns the channel group. + * + * @return channel group id. + */ + public String getChannelGroup() { + return channelGroup; + } + + /** + * Returns the channel name. + * + * @return the channel name. + */ + public String getChannelName() { + return this.name().toLowerCase().replace('_', '-'); + } + + /** + * Creates the {@link State} for the given register value. + * + * @param registerValue the value for the channel. + * @return {@link State] for the given value. + */ + public State createState(DecimalType registerValue) { + final BigDecimal scaledValue = registerValue.toBigDecimal().multiply(this.multiplier); + + final BigDecimal convertedValue = conversion.apply(scaledValue); + return this.stateFactory.apply(convertedValue); + } +} diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000..10a87cdb03 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,15 @@ + + + + + + + Time between polling the data in ms. + 5000 + + + + diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/i18n/sungrow.properties b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/i18n/sungrow.properties new file mode 100644 index 0000000000..510aeeb41d --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/i18n/sungrow.properties @@ -0,0 +1,64 @@ +# thing types + +thing-type.modbus.sungrowInverter.label = Sungrow inverter +thing-type.modbus.sungrowInverter.description = Sungrow inverter connected via Modbus. + +# thing types config + +thing-type.config.sungrow.inverter.pollInterval.label = Poll Interval +thing-type.config.sungrow.inverter.pollInterval.description = Time between polling the data in ms. + +# channel groups +channel-group-type.modbus.sg-overview.label = Overview +channel-group-type.modbus.sg-mppt_information.label = MPPT +channel-group-type.modbus.sg-battery_information.label = Battery +channel-group-type.modbus.sg-load_information.label = Load +channel-group-type.modbus.sg-grid_information.label = Grid + +# channel types + +channel-type.modbus.sg-battery_capacity.label = Battery Capacity +channel-type.modbus.sg-battery_current.label = Battery Current +channel-type.modbus.sg-battery_healthy.label = Battery Health (SOH) +channel-type.modbus.sg-battery_level.label = Battery Level (SOC) +channel-type.modbus.sg-battery_power.label = Battery Power +channel-type.modbus.sg-battery_temperature.label = Battery Temperature +channel-type.modbus.sg-battery_voltage.label = Battery Voltage +channel-type.modbus.sg-co2_reduction.label = Co2 Reduction +channel-type.modbus.sg-daily_battery_charge.label = Daily Battery Charging Energy from PV +channel-type.modbus.sg-daily_battery_discharge_energy.label = Daily Battery Discharging Energy +channel-type.modbus.sg-daily_charge_energy.label = Daily Battery Charging Energy +channel-type.modbus.sg-daily_direct_energy_consumption.label = Daily Load Energy Consumption from PV +channel-type.modbus.sg-daily_export_energy.label = Daily Feed-in Energy +channel-type.modbus.sg-daily_export_power_from_pv.label = Daily Feed-in Energy (PV) +channel-type.modbus.sg-daily_import_energy.label = Daily Purchased Energy +channel-type.modbus.sg-daily_pv_generation.label = Daily PV Yield +channel-type.modbus.sg-export_power.label = Total Export Active Power +channel-type.modbus.sg-grid_frequency.label = Grid Frequency +channel-type.modbus.sg-internal_temperature.label = Internal Temperature +channel-type.modbus.sg-load_power.label = Load Power +channel-type.modbus.sg-mppt1_current.label = MPPT1 Current +channel-type.modbus.sg-mppt1_voltage.label = MPPT1 Voltage +channel-type.modbus.sg-mppt2_current.label = MPPT2 Current +channel-type.modbus.sg-mppt2_voltage.label = MPPT2 Voltage +channel-type.modbus.sg-phase_a_current.label = Phase A Current +channel-type.modbus.sg-phase_a_voltage.label = Phase A Voltage +channel-type.modbus.sg-phase_b_current.label = Phase B Current +channel-type.modbus.sg-phase_b_voltage.label = Phase B Voltage +channel-type.modbus.sg-phase_c_current.label = Phase C Current +channel-type.modbus.sg-phase_c_voltage.label = Phase C Voltage +channel-type.modbus.sg-power_factor.label = Total Power Factor +channel-type.modbus.sg-reactive_power.label = Total Reactive Power +channel-type.modbus.sg-running_state.label = Running Status +channel-type.modbus.sg-self_consumption_today.label = Daily Self-consumption Rate +channel-type.modbus.sg-system_state.label = Current Status +channel-type.modbus.sg-total_active_power.label = Total Active Power +channel-type.modbus.sg-total_battery_charge.label = Total Battery Charging Energy +channel-type.modbus.sg-total_battery_discharge_energy.label = Total Battery Discharging Energy +channel-type.modbus.sg-total_charge_energy.label = Total Battery Charging Energy from PV +channel-type.modbus.sg-total_dc_power.label = Total DC Power +channel-type.modbus.sg-total_direct_energy_consumption.label = Total Load Energy Consumption from PV +channel-type.modbus.sg-total_export_energy.label = Total Feed-in Energy +channel-type.modbus.sg-total_export_energy_from_pv.label = Total Feed-in Energy (PV) +channel-type.modbus.sg-total_import_energy.label = Total Purchased Energy +channel-type.modbus.sg-total_pv_generation.label = Total PV Yield diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000..127c10bdd6 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,507 @@ + + + + + + + + + + + Sungrow inverter connected via Modbus. + Inverter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Battery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:Dimensionless + + Energy + + + + Number:Frequency + + + Measurement + Frequency + + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:ElectricPotential + + Energy + + Measurement + Voltage + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:Power + + Battery + + Measurement + Power + + + + + Number:Dimensionless + + Battery + + Measurement + Energy + + + + + Number:Dimensionless + + Battery + + + + Number:Temperature + + Battery + + Measurement + Temperature + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Dimensionless + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:ElectricCurrent + + Energy + + Measurement + Current + + + + + Number:Power + + Energy + + Measurement + Power + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + + Number:Energy + + Energy + + Measurement + Energy + + + + diff --git a/bundles/org.openhab.binding.modbus.sungrow/src/test/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegistersTest.java b/bundles/org.openhab.binding.modbus.sungrow/src/test/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegistersTest.java new file mode 100644 index 0000000000..422bcf9954 --- /dev/null +++ b/bundles/org.openhab.binding.modbus.sungrow/src/test/java/org/openhab/binding/modbus/sungrow/internal/SungrowInverterRegistersTest.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.modbus.sungrow.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.io.transport.modbus.ModbusBitUtilities; +import org.openhab.core.io.transport.modbus.ModbusRegisterArray; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.State; + +/** + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +class SungrowInverterRegistersTest { + @Test + public void testCreatePercentTypeState() { + SungrowInverterRegisters batteryLevelRegister = SungrowInverterRegisters.BATTERY_LEVEL; + + ModbusRegisterArray registers = new ModbusRegisterArray(1000); + Optional value = ModbusBitUtilities.extractStateFromRegisters( // + registers, // + 0, // + batteryLevelRegister.getType() // + ); + assertTrue(value.isPresent()); + DecimalType decimalTypeValue = value.get(); + // Value is not scaled yet + assertEquals(BigDecimal.valueOf(1000), decimalTypeValue.toBigDecimal()); + + State state = batteryLevelRegister.createState(decimalTypeValue); + assertInstanceOf(PercentType.class, state); + assertEquals("100.0", state.toFullString()); + } + + @Test + public void testCreateQuantityTypeState() { + SungrowInverterRegisters mpttVoltage = SungrowInverterRegisters.MPPT1_VOLTAGE; + + ModbusRegisterArray registers = new ModbusRegisterArray(1234); + Optional value = ModbusBitUtilities.extractStateFromRegisters( // + registers, // + 0, // + mpttVoltage.getType() // + ); + assertTrue(value.isPresent()); + DecimalType decimalTypeValue = value.get(); + // Value is not scaled yet + assertEquals(BigDecimal.valueOf(1234), decimalTypeValue.toBigDecimal()); + + State state = mpttVoltage.createState(decimalTypeValue); + assertInstanceOf(QuantityType.class, state); + assertEquals("123.4 V", state.toFullString()); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index d6ec5faad6..5299fc2507 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -249,6 +249,7 @@ org.openhab.binding.modbus.e3dc org.openhab.binding.modbus.sbc org.openhab.binding.modbus.studer + org.openhab.binding.modbus.sungrow org.openhab.binding.modbus.sunspec org.openhab.binding.modbus.stiebeleltron org.openhab.binding.modbus.helioseasycontrols -- 2.47.3