From 596b261d4738557392e1c1dfdc92dcd69bbaa46b Mon Sep 17 00:00:00 2001
From: Andrew Fiddian-Green > statusFuture;
+
+ public PilightDeviceDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, AUTODISCOVERY_SEARCH_TIME_SEC);
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+ }
+
+ @Override
+ protected void startScan() {
+ if (pilightBridgeHandler != null) {
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+
+ configFuture.thenAcceptBoth(statusFuture, (config, allStatus) -> {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ config.getDevices().forEach((deviceId, device) -> {
+ if (this.pilightBridgeHandler != null) {
+ final Optional
+ +
+ ## Supported Things This binding supports the following thing types: @@ -24,6 +28,7 @@ Additionally the binding have two types of bridge things which correspond to ava * *Telldus Core Bridge* - Oldest API, used by USB devices. `telldus-core` * *Telldus Live Bridge* - Telldus Cloud service, all devices with online access. `telldus-live` +* *Telldus Local Bridge* - Telldus Local API, Tellstick Net v2/Tellstick ZNet Lite v1/v2. `telldus-local` ***Switchbased sensors workaround*** @@ -32,11 +37,12 @@ Additionally the binding have two types of bridge things which correspond to ava ## Discovery -Devices which is added to *Telldus Core* and *Telldus Live* can be discovered by openHAB. +Devices which is added to *Telldus Core*, *Telldus Live* and *Telldus Local* can be discovered by openHAB. When you add this binding it will try to discover the *Telldus Core Bridge*. If it is installed correct its devices will show up. -If you want to use the *Telldus Live* its bridge, *Telldus Live bridge* need to be added manually. + +If you want to use the *Telldus Live* or *Telldus Local*, their bridges, *Telldus Live bridge* or *Tellstick Local*, needs to be added manually. ## Binding Configuration @@ -54,13 +60,13 @@ Use the option `repeat` for that. Default resend count is 2. ### Bridges -Depending on your tellstick device type there is different ways of using this binding. -The binding implements two different API: +Depending on your Tellstick device type there is different ways of using this binding. +The binding implements three different APIs: **1)** *Telldus Core* which is a local only interface supported by USB based device.zLdAyfmGv+0upD05jBfiJcO5C-2=$%Su!$R((O&aEc|1ob#l3MO5T`I1XN!^4*PhS=1&RX5JTR-srdD(2c&UzCPfG*R2 zUd=9JXR?y}rKP8qPJr@8sJ*YG;Zo_V&B?EgdJfa^xluPF1)377_W8YhY@XUVJuVm@ zxe*AePdb`aj_=2<{8~MGj 2wWfuGe;cwc5$@3-Onkt*F2rEYc*`WxBYQg#^ZgX2vzUoB$W2Tg>ki{zC+; zTxxbsE_~f%X!EeHbMjEw*rYf}9Q6a>z8n0?73H;wT=248iTl8wh|!kFjM0Vz0N@0n zb2jr#H%5-7{YmSVGt;JjDqR}&1@%$g?dSEY0TgQTh_;ixLsH+2$UxIn<|EQi&uPOx zCgaHVDgtn#0mQOlZK`VFYW+ixjva+54&^J)+WDs#t*UDw{{;6Z! 0(@`faM}?SERW}tPiNmcMpRXG)Ea;anbiMA!8VpJ{4_cN5o}XS~t>($J zlr8TyimA;gPYek=AD$UYUrNOLD$q3-cVk4MsTx@7-80}c^kRHtLu1LpzOpYss4@I? zQh35oy>?Ojp1jz8Ap)N_HU+`+L;#+~b4LpPMraxG%Nbx|FZXlr*wkosqn!0s$=H?S zVeE%08^XrbE&iht6Q0~*y|}OUwyHi^8Ar!k(S0Bq?0?KBKl4f8)d#Bw2H2x>ud1Cj zEFOThy@b-bc<@}>R6(r77>dzo7#+8sZoZcK tCme3Y~7;8ShW~Y+H3F_9g?v}#9iN+uflKS+N zN*-oi{>DOpLqs_Fp9HY(Ru(MgQ4ln(ZM&6k+CDn9s_mQ0HZgQa-%QZ{d^V?$EBa-^ zE~Q&q38DKA8_trf1p#9OgOwT6U@DZnx(2n>zUg|7s}e&mvnaNhi2q>)pv8`?thOBV zYuhmUOxQg${dA!y7KjjIXmk|p Ri2N+|SIW<=b#7eiamJZ9l{!S?m%6rp+5azE{ ^AN-fLZb5`cLxV!DQIEDQ08VCrWYdkR|sisLf$6Ug|ipptB0{ zeGVLI?a_jlgSs?TL%#pHFW 1@}y z@RV3kV-?w`kfJyxHitP)n=IeAddlUkt~eE-5wdc)6qh;S!)1fIoG}eq_E?083}b#; z!f)RcqktyzkyPvY^=`OiAD+^-@t3wH7Wsyo!V-^8BohKN@23ZYP9ztHrDR2p@~a zE94%rbOWhY?3lW&l8Pe5f@cJU?`?H_k_}&v-ke=Rsk;0YkDXj)TCjbgEsxxuZ9(r6 zQzRv)dnfhIK!KaFJHlNArwmK+XSIDA^%~L?Z{>UY-}f%zXTG7oG_{%{6^r^?Ii%5| z`HXGCn~F9#Sy1H~0J>B%et264E3GF9LHm3s`H{|GEUgY4m(?kYWWsCeFO=xj%iNfL z8|H62 CaS4iXH`g$%-kfMZ`oE$g_`@QB v2L +M4-Yd DFOJ8B_dRsre}V^yq_q2k64;rv`OHG-7LY=038IhoUO zs-P#%;&XXz%-a*8rmG#Ayw)`}G3t!rbK*)X-!gW(&70QnlanwiImyVQX^>e^E;TjQ zKmVnAJ>4`d2a?PN*kNmHYuB-GNm5Rd`}{N>YU(FRSB!F)aEU(`{mc)XR;2pMBpqXj zL`K7?WhKnp9>v?7A|a}R=rTfXCNbC@aZfiK32##*iW@wl3HKJGRrPgc>)PI&wU^o3 z_KLX%uro*GzUNoJH>!F^j<{myt?QF2iQadH(FXL|0s;a*-U&tWCESN)=Q(DY(p>RL zMsetuetk2RmN%zM^bOs9RUSMw5x~YyQEog`n7HC9$hKwC163s|aZNg)kNeX1@mY4Z z%PZI3Dwk)Kifu*Oltf1=@5UEshs5RZ7&+Yd+o`?NWTKF+LR8c(+x7+6?G(B+h-Z;( z#*EnKMN`5AHshp^5^JZZAz{gvY7MPEcoUM|+EMOwktd0z&!GtzEJf$*aK+CWI#=Rx z>I?&3)P2&$-5fqiVTcO_detj&&TK@@q8r%*Mg5QVVS#R6Uj%|sVFtkEr|_tfUf`V! zQrq4+vlYcWVq|0PU2Ml?^|>8dq|{B6aFKzkBrHS#AUZk*5Dn{h2}Jef7(~39=wKc` z24X4FJ21GGOVHmn1l8wD0x$h37u#gDvnk=~w6stk?NdrMQRy7p4% {N&>GWJRaQpBo+GIc4qNq%^9_VQBgV_qxvwLq3@VN-_Ev* zo1L&hcF~ka_V*w6e|}JZ;?_VVpNz chu z%&;SJvY;?)mPA|ht?f#E*aYAUjfcBohy6lUhkbc@btkB0>&tFd19MH%x~U(BD{iWq zM1j^w%;DaTK`xwfQ*#BRY}jN-yqc|9{wbeIh~51QWf9gCh{5K$n$caqhg!a>o*q2Y zw0)$rjNC83M8x+Bmc 0{^2sLF{ BvIQ&QPU96FJawH#FuLNA?;^3tQI*j@Kqs{#(X^~ zr?Td-clgVsFV*? d(+8j$WCRn|9|*HioCYLvQVp@`_&d4v&4Yv{`>7?#6dmNB<2fsbW|y+gc#09)6#~ zQ>t25(A)R}`XSOOBP+479CJ$NjGsL#CgbaRJK<(n8QzC{<5Z _#bv2kkOvRnApU4%~~nW@A(9rA`2sFdT;d%0L_*~jMN zpUR%g!su`jk2hx??GU}-v*DVFZm 0?1#MIWI?5WBH@-!NJ zrCB3@lY%_i{HzZunSEP|$ZG&i+NQOKHY56!(zsEFS>=}@)3Xtls `!2hZZlcl0R4HBx7A7yFAttT& zxUru(_4rzq*EbX8QtXU|(qi6CXsX;%`^**;IF*2^6C7aF!vI9Rw!lD@xWAtcAb^Mm z%mCLicZEoq1sA|HlWN;XcjpuLem7H+4A+3Mxw%95r=7?x4?miSmDHB~ bYKR`(Mq3?ShOy5gC~EQ!R~ST@ zb$u}^quWoS1fH#2IVfn N92^xL@uf#Yb{zq>c7B6N~Z~tTPn-tOR ztM?fcO`Q=L@k*MxrYmBE=2&od45{N#@DBqQkJfx&v3~j_jG+O8lORelJXs3LGzIt0 z1nz@}BJ3+N#hrtdY?uqwSn)+Z70Rh>4_>srNfYh4dY?tn)KL I4f1?CNRmJmC9#VRO7%2&@%?pkX-TV5 zK(V@7HJgP&?GS?Q%%KU_M| 6Rtn{=RHIPXLkJSTU*rCNz0 zHbO`M@52A3##bU05fgiT#7;8uG#XQWsS4Wq!>Q2lgyae1G}4Qvj&ix1bY$T6;#>Vk z9oAKxPRwFS;Z^2Hp5-SFvchVK*{zpiR2H!Y93^h9l9W+n+KZ+4!f3Ex2ozXP$K!fn zN+|j?VvW=n57aJi{{VH&6lrz{)vK2BW{PgV-p*Fluazn22=q%BN+Tj#j`7(z&5Z3E zq2;0kvl0wEqbb=6yHcb1>?Vn(YWT$cR!gi~D0~f#*iMv Xpq|IICqNd+?Lzu<`s|Ul!?MsR4#bK zEOl%>339?Up9d^jIH8}VKyN$NA~?e{@2CbhE+mF4t4zDtF>p~c*K^DDr_(w28Z|rJ zG25S+L6-~fW_u_*MW%TxTB`<3itH`{U5=q08f14PU=bxj9^Cy#2ChV4CC_(T42Hr3 zf?~B!tT=Y{$h5$)pt|=)Bvh$iRoL8BAdaC;s~OnxFBT6v=yqu&5jv5fBpdY7%j<5- zkoIiHDIrQv8{y;Zp`%$qy9r|6mS;UT`FL#G4olxH%%7%@yS`W?JcGw?dXjs7KF&oV z$NseHJ|vX8hJDO!gX+?CaK{*lZqbB1Y`4|;NFOeX9)gC}t(UQ0yc3BP%|h@k(zmXO zr|RywN3#wDjBt!wkrLgeX 9Q*)xi^HI^=q&G8~eP@TwPQCQq=m*^GC7oDYO6%)tlrtb+=rT<+epd)ms_Nil)< zV(XAzR(Typ^X(d7Er}|Kf4|EG{MBNC4~E75-@c1~42S+^NDw n z&CRs4p_Yr)7L`9P`U14+4+L_@4#EV>c3OP7lOM1Dal!o{e;032gX=^HxC8P*GhHYZ z>i=!{c(+w_l@CMcDyV`5h|TEcP#)#{+b~|?L%zoWPjTqrs}8K>ezW8#o9usr`3Unh zWt>e{PQ=8`2ztR{79reGrTq^l(c(|rl<{4C>#V4?c-xIqUYVzVQaBdh+KWHGQxbBD ztXgl6sr? q!(3{El2HBBpQ#djeBRQ7L`cijzU9~9=Iz~os?lue zYpwWj>WU+Q`xBmgok9+AE0d)ie}%54@4kPu1ow03=L+J$syc<4b8?TZxCbwW;-2|4 zt~E06CQ*qp?<#p(&JvVkL_82j?h+F?^)!jxmS 0c8j_@}=CzIUj8w>*BB zP{;bnk_|R?L``eFBI`y#XF>5IBk3)Zc^!UH%aF*jh(-cMyt23a;slE9^=mT5*8l^^ zoi&E)lf7hIN6>D%AtP> X$*9FsW`2tJ z`MFck0oPFYW^@nD%1^o#)@jY6h=d?%v-esuQb?G|sp#ZGiRg?)NZWTV2_|zSn(3EB zNE;0L6m}mdYle>Doh!z0bSu;l1O(0#wF!j 9z z=MBYov%;_r!zh*UO}*kV?HjziI?dWPw=AT_s0*-FJGf_J*pX;3ycI1BpuR<8mDHA& z+%OHP>5ISTb qylaM^y`HJ3AiTKx-xH@B;INSJXCJq(+cO+hVt{nyu2h-ob#I Izt!)M%}!8=>nDFYL7sF&4)aAeCC^-|XftkKx+!opccjF- zz+eY Sz)B)Fk zdy$NdcBd%$ {B>Q!A-BuA`vIUIZEF8emQ+JvF9hX{~tZ{S!7^Aa?=% zj%<(l5ITK3oZ?6u%`#zz&_f)T sA}IH|sNO! L?Xg8a?U6Z}j?UX^;gG*;| zMrSEB|NVDNQEghv6(7oE-@wm~U4xw0fDbZP(iV;lF6c3E?s%tq0 7DMr(cqPx8X&N{Q}hkOl!+e zv^L4D*SvzM6=?xs*u+H+ZTGd=v{_Sj(Fp;1pfdIF!Sop&`FsxwmREiyws#ENPFaGM zAVHo-;U~z5v~q!HO)W}*qXYS3NPMV%=L+p$9_K}6{#xft5jD@k{cAuxZhc=*#zN_s zT;lj;Q=*~F3q2=#XX517O^o-KnIObPCd^Vt<(a-mjv>`A?RXjHbOeJ$NEC1Bm&d~B z%qOu))F$U}Ug}aPt!TaP?NopHEiPAq+b@fN#G|U=LA@4nT)G$=kqieRbo;hc4_TiM z9)Ws`0h3g4OJc}#1icS*>+1yv>#)vGnWpi0$LKSpk6U#In-8Jv!=wqF+wBeJDQa9Y z_?}WJYC<}!@%}0^o|JI1U5(nerSCABxY^=xsj~_|@{ PE(gk32w2imNM`i*RDA9PrXto;DNB JAL>d>T*H_JAe#pA z 1! zY*P42D#Tn}Noy*&IPrVFBgQiNTQaDEJZqcz_jC|DR 6V`Eii&oh=}ID^1`->+^o^koM|I~Aq%c>QUU#ry@6tkhmpZnHQuol1_^V0 zZy<@yR%DL$`_6Pp>lvw5=DkI46d)p68>OcWg)E-~cF8a+)S_ICwZ>9PKa>L29@-6W z(vH9%D7;9J7HS}C3xS~-$yy=b*!`Gy(am(v!;9rx@QlPO-Q_YeqcS@$I(wj{O>dQ+ z{M?HEdzMN4%Q2i*r!{Uz)z0zTb DF zO65<8PJ60Hj#QN!<56-A^-#$K;-rMKR>fuyyuZ4`Ug1Zb66Kw@jPrs9)7Ohr^2?jL z&J0b>Ld{R9Sz~v3)9g&o -@B}jrV7~ z_WXKqbVwNqpGUKrsdsj1#?{F4r+$9tpq3sMkGqT=_ vgrY*T&(^SSiULN zRADR94=WJ*tWpQA@)?sYTSeM_Lz6)v?{Tjpnw)|jM9s(H`}#`cEs-bMMw2HQ;CF(1 zutbr2O#Nf1!Cc)1od_;r+Es5F0UQhiOyhjF; literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickBindingConstants.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickBindingConstants.java index a8690ec827a66..1f62da26158ee 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickBindingConstants.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickBindingConstants.java @@ -14,10 +14,7 @@ import static org.openhab.core.library.unit.MetricPrefix.*; -import java.util.Collections; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.measure.Unit; import javax.measure.quantity.Angle; @@ -65,6 +62,7 @@ public class TellstickBindingConstants { public static final String DEVICE_ISDIMMER = "dimmer"; public static final String BRIDGE_TELLDUS_CORE = "telldus-core"; public static final String BRIDGE_TELLDUS_LIVE = "telldus-live"; + public static final String BRIDGE_TELLDUS_LOCAL = "telldus-local"; public static final String DEVICE_SENSOR = "sensor"; public static final String DEVICE_WINDSENSOR = "windsensor"; public static final String DEVICE_RAINSENSOR = "rainsensor"; @@ -82,6 +80,7 @@ public class TellstickBindingConstants { public static final ThingTypeUID TELLDUSBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE_TELLDUS_CORE); public static final ThingTypeUID TELLDUSCOREBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE_TELLDUS_CORE); public static final ThingTypeUID TELLDUSLIVEBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE_TELLDUS_LIVE); + public static final ThingTypeUID TELLDUSLOCALBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE_TELLDUS_LOCAL); // List of all Channel ids public static final String CHANNEL_DIMMER = "dimmer"; public static final String CHANNEL_STATE = "state"; @@ -97,13 +96,11 @@ public class TellstickBindingConstants { public static final String CHANNEL_AMPERE = "ampere"; public static final String CHANNEL_LUX = "lux"; - public static final Set SUPPORTED_BRIDGE_THING_TYPES_UIDS = Collections.unmodifiableSet( - Stream.of(TELLDUSCOREBRIDGE_THING_TYPE, TELLDUSLIVEBRIDGE_THING_TYPE).collect(Collectors.toSet())); - public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(DIMMER_THING_TYPE, SWITCH_THING_TYPE, SENSOR_THING_TYPE, RAINSENSOR_THING_TYPE, - WINDSENSOR_THING_TYPE, POWERSENSOR_THING_TYPE).collect(Collectors.toSet())); - public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream - .of(DIMMER_THING_TYPE, SWITCH_THING_TYPE, SENSOR_THING_TYPE, RAINSENSOR_THING_TYPE, WINDSENSOR_THING_TYPE, - POWERSENSOR_THING_TYPE, TELLDUSCOREBRIDGE_THING_TYPE, TELLDUSLIVEBRIDGE_THING_TYPE) - .collect(Collectors.toSet())); + public static final Set SUPPORTED_BRIDGE_THING_TYPES_UIDS = Set.of(TELLDUSCOREBRIDGE_THING_TYPE, + TELLDUSLIVEBRIDGE_THING_TYPE); + public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(DIMMER_THING_TYPE, + SWITCH_THING_TYPE, SENSOR_THING_TYPE, RAINSENSOR_THING_TYPE, WINDSENSOR_THING_TYPE, POWERSENSOR_THING_TYPE); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(DIMMER_THING_TYPE, SWITCH_THING_TYPE, + SENSOR_THING_TYPE, RAINSENSOR_THING_TYPE, WINDSENSOR_THING_TYPE, POWERSENSOR_THING_TYPE, + TELLDUSCOREBRIDGE_THING_TYPE, TELLDUSLIVEBRIDGE_THING_TYPE, TELLDUSLOCALBRIDGE_THING_TYPE); } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java index 3897fcee9bd65..266e7c8bd3d3f 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java @@ -16,19 +16,24 @@ import java.util.Hashtable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.tellstick.internal.core.TelldusCoreBridgeHandler; import org.openhab.binding.tellstick.internal.discovery.TellstickDiscoveryService; import org.openhab.binding.tellstick.internal.handler.TelldusBridgeHandler; import org.openhab.binding.tellstick.internal.handler.TelldusDevicesHandler; import org.openhab.binding.tellstick.internal.live.TelldusLiveBridgeHandler; +import org.openhab.binding.tellstick.internal.local.TelldusLocalBridgeHandler; import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; 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.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,11 +42,18 @@ * handlers. * * @author Jarle Hjortland - Initial contribution + * @author Jan Gustafsson - Adding support for local API */ @Component(service = ThingHandlerFactory.class, configurationPid = "binding.tellstick") public class TellstickHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(TellstickHandlerFactory.class); private TellstickDiscoveryService discoveryService = null; + private final HttpClient httpClient; + + @Activate + public TellstickHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -68,6 +80,10 @@ protected ThingHandler createHandler(Thing thing) { TelldusLiveBridgeHandler handler = new TelldusLiveBridgeHandler((Bridge) thing); registerDeviceDiscoveryService(handler); return handler; + } else if (thing.getThingTypeUID().equals(TELLDUSLOCALBRIDGE_THING_TYPE)) { + TelldusLocalBridgeHandler handler = new TelldusLocalBridgeHandler((Bridge) thing, httpClient); + registerDeviceDiscoveryService(handler); + return handler; } else if (supportsThingType(thing.getThingTypeUID())) { return new TelldusDevicesHandler(thing); } else { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLiveConfiguration.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLiveConfiguration.java index 32528e7c11593..e334c0b9fa9fc 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLiveConfiguration.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLiveConfiguration.java @@ -14,7 +14,7 @@ /** * Configuration class for {@link TellstickBridge} bridge used to connect to the - * Tellus Live service. + * Telldus Live service. * * @author Jarle Hjortland - Initial contribution */ diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLocalConfiguration.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLocalConfiguration.java new file mode 100644 index 0000000000000..61e315ad0b9e7 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/conf/TelldusLocalConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.conf; + +/** + * Configuration class for {@link TellstickBridge} bridge used to connect to the + * Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TelldusLocalConfiguration { + public String ipAddress; + public String accessToken; + public long refreshInterval; +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java index 424e805fdc35a..59ab0403784ec 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java @@ -22,6 +22,8 @@ import org.openhab.binding.tellstick.internal.live.xml.LiveDataType; import org.openhab.binding.tellstick.internal.live.xml.TellstickNetDevice; import org.openhab.binding.tellstick.internal.live.xml.TellstickNetSensor; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalDeviceDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorDTO; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; @@ -141,6 +143,14 @@ private ThingUID getThingUID(Bridge bridge, Device device) { thingUID = new ThingUID(TellstickBindingConstants.SWITCH_THING_TYPE, bridge.getUID(), device.getUUId()); } + } else if (device instanceof TellstickLocalDeviceDTO) { + if ((((TellstickLocalDeviceDTO) device).getMethods() & JNA.CLibrary.TELLSTICK_DIM) > 0) { + thingUID = new ThingUID(TellstickBindingConstants.DIMMER_THING_TYPE, bridge.getUID(), + device.getUUId()); + } else { + thingUID = new ThingUID(TellstickBindingConstants.SWITCH_THING_TYPE, bridge.getUID(), + device.getUUId()); + } } break; default: @@ -163,7 +173,7 @@ private ThingTypeUID findSensorType(Device device) { } else { sensorThingId = TellstickBindingConstants.SENSOR_THING_TYPE; } - } else { + } else if (device instanceof TellstickNetSensor) { TellstickNetSensor sensor = (TellstickNetSensor) device; if (sensor.isSensorOfType(LiveDataType.WINDAVERAGE) || sensor.isSensorOfType(LiveDataType.WINDDIRECTION) || sensor.isSensorOfType(LiveDataType.WINDGUST)) { @@ -175,6 +185,18 @@ private ThingTypeUID findSensorType(Device device) { } else { sensorThingId = TellstickBindingConstants.SENSOR_THING_TYPE; } + } else { + TellstickLocalSensorDTO sensor = (TellstickLocalSensorDTO) device; + if (sensor.isSensorOfType(LiveDataType.WINDAVERAGE) || sensor.isSensorOfType(LiveDataType.WINDDIRECTION) + || sensor.isSensorOfType(LiveDataType.WINDGUST)) { + sensorThingId = TellstickBindingConstants.WINDSENSOR_THING_TYPE; + } else if (sensor.isSensorOfType(LiveDataType.RAINRATE) || sensor.isSensorOfType(LiveDataType.RAINTOTAL)) { + sensorThingId = TellstickBindingConstants.RAINSENSOR_THING_TYPE; + } else if (sensor.isSensorOfType(LiveDataType.WATT)) { + sensorThingId = TellstickBindingConstants.POWERSENSOR_THING_TYPE; + } else { + sensorThingId = TellstickBindingConstants.SENSOR_THING_TYPE; + } } return sensorThingId; } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java index b46412e426539..e1b57874cc7e5 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java @@ -23,12 +23,16 @@ import org.openhab.binding.tellstick.internal.live.xml.DataTypeValue; import org.openhab.binding.tellstick.internal.live.xml.TellstickNetSensor; import org.openhab.binding.tellstick.internal.live.xml.TellstickNetSensorEvent; +import org.openhab.binding.tellstick.internal.local.dto.LocalDataTypeValueDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorEventDTO; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DateTimeType; 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.SIUnits; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -109,9 +113,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } if (command instanceof RefreshType) { - getBridge().getHandler().handleCommand(channelUID, command); - refreshDevice(dev); - return; + Bridge bridge = getBridge(); + if (bridge != null) { + TelldusBridgeHandler localBridgeHandler = (TelldusBridgeHandler) bridge.getHandler(); + if (localBridgeHandler != null) { + localBridgeHandler.handleCommand(channelUID, command); + refreshDevice(dev); + return; + } + } } if (channelUID.getId().equals(CHANNEL_DIMMER) || channelUID.getId().equals(CHANNEL_STATE)) { try { @@ -123,9 +133,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } catch (TellstickException e) { logger.debug("Failed to send command to tellstick", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (Exception e) { - logger.error("Failed to send command to tellstick", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } else { logger.warn("Setting of channel {} not possible. Read-only", channelUID); @@ -159,8 +166,9 @@ public void initialize() { if (repeatCount != null) { resend = repeatCount.intValue(); } - if (getBridge() != null) { - bridgeStatusChanged(getBridge().getStatusInfo()); + Bridge bridge = getBridge(); + if (bridge != null) { + bridgeStatusChanged(bridge.getStatusInfo()); } } @@ -169,31 +177,34 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { logger.debug("device: {} bridgeStatusChanged: {}", deviceId, bridgeStatusInfo); if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { try { - TelldusBridgeHandler tellHandler = (TelldusBridgeHandler) getBridge().getHandler(); - logger.debug("Init bridge for {}, bridge:{}", deviceId, tellHandler); - if (tellHandler != null) { - this.bridgeHandler = tellHandler; - this.bridgeHandler.registerDeviceStatusListener(this); - Configuration config = editConfiguration(); - Device dev = getDevice(tellHandler, deviceId); - if (dev != null) { - if (dev.getName() != null) { - config.put(TellstickBindingConstants.DEVICE_NAME, dev.getName()); - } - if (dev.getProtocol() != null) { - config.put(TellstickBindingConstants.DEVICE_PROTOCOL, dev.getProtocol()); - } - if (dev.getModel() != null) { - config.put(TellstickBindingConstants.DEVICE_MODEL, dev.getModel()); - } - updateConfiguration(config); + Bridge localBridge = getBridge(); + if (localBridge != null) { + TelldusBridgeHandler telldusBridgeHandler = (TelldusBridgeHandler) localBridge.getHandler(); + logger.debug("Init bridge for {}, bridge:{}", deviceId, telldusBridgeHandler); + if (telldusBridgeHandler != null) { + this.bridgeHandler = telldusBridgeHandler; + this.bridgeHandler.registerDeviceStatusListener(this); + Configuration config = editConfiguration(); + Device dev = getDevice(telldusBridgeHandler, deviceId); + if (dev != null) { + if (dev.getName() != null) { + config.put(TellstickBindingConstants.DEVICE_NAME, dev.getName()); + } + if (dev.getProtocol() != null) { + config.put(TellstickBindingConstants.DEVICE_PROTOCOL, dev.getProtocol()); + } + if (dev.getModel() != null) { + config.put(TellstickBindingConstants.DEVICE_MODEL, dev.getModel()); + } + updateConfiguration(config); - updateStatus(ThingStatus.ONLINE); - } else { - logger.warn( - "Could not find {}, please make sure it is defined and that telldus service is running", - deviceId); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + updateStatus(ThingStatus.ONLINE); + } else { + logger.warn( + "Could not find {}, please make sure it is defined and that telldus service is running", + deviceId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } } } } catch (Exception e) { @@ -240,6 +251,10 @@ private void updateSensorStates(Device dev) { for (DataTypeValue type : ((TellstickNetSensor) dev).getData()) { updateSensorDataState(type); } + } else if (dev instanceof TellstickLocalSensorDTO) { + for (LocalDataTypeValueDTO type : ((TellstickLocalSensorDTO) dev).getData()) { + updateSensorDataState(type); + } } } @@ -260,6 +275,9 @@ public void onDeviceStateChanged(Bridge bridge, Device device, TellstickEvent ev } else if (event instanceof TellstickNetSensorEvent) { TellstickNetSensorEvent sensorevent = (TellstickNetSensorEvent) event; updateSensorDataState(sensorevent.getDataTypeValue()); + } else if (event instanceof TellstickLocalSensorEventDTO) { + TellstickLocalSensorEventDTO sensorevent = (TellstickLocalSensorEventDTO) event; + updateSensorDataState(sensorevent.getDataTypeValue()); } else if (event instanceof TellstickSensorEvent) { TellstickSensorEvent sensorevent = (TellstickSensorEvent) event; updateSensorDataState(sensorevent.getDataType(), sensorevent.getData()); @@ -340,6 +358,46 @@ private void updateSensorDataState(DataTypeValue dataType) { } } + private void updateSensorDataState(LocalDataTypeValueDTO dataType) { + switch (dataType.getName()) { + case HUMIDITY: + updateState(humidityChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), HUMIDITY_UNIT)); + break; + case TEMPERATURE: + updateState(tempChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), SIUnits.CELSIUS)); + break; + case RAINRATE: + updateState(rainRateChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), RAIN_UNIT)); + break; + case RAINTOTAL: + updateState(raintTotChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), RAIN_UNIT)); + break; + case WINDAVERAGE: + updateState(windAverageChannel, + new QuantityType<>(new BigDecimal(dataType.getValue()), WIND_SPEED_UNIT_MS)); + break; + case WINDDIRECTION: + updateState(windDirectionChannel, + new QuantityType<>(new BigDecimal(dataType.getValue()), WIND_DIRECTION_UNIT)); + break; + case WINDGUST: + updateState(windGuestChannel, + new QuantityType<>(new BigDecimal(dataType.getValue()), WIND_SPEED_UNIT_MS)); + break; + case WATT: + if (dataType.getScale() == 5) { + updateState(ampereChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), ELECTRIC_UNIT)); + } else if (dataType.getScale() == 2) { + updateState(wattChannel, new QuantityType<>(new BigDecimal(dataType.getValue()), Units.WATT)); + } + break; + case LUMINATION: + updateState(luxChannel, new QuantityType<>(new DecimalType(dataType.getValue()), LUX_UNIT)); + break; + default: + } + } + private void updateDeviceState(Device device) { if (device != null) { logger.debug("Updating state of {} {} ({}) id: {}", device.getDeviceType(), device.getName(), diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/LiveDataType.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/LiveDataType.java index 67dd3f95c91c8..9340acf11340b 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/LiveDataType.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/LiveDataType.java @@ -20,11 +20,11 @@ public enum LiveDataType { HUMIDITY("humidity"), TEMPERATURE("temp"), - WINDAVERAGE("windaverage"), - WINDDIRECTION("winddirection"), - WINDGUST("windgust"), - RAINRATE("rainrate"), - RAINTOTAL("rainttotal"), + WINDAVERAGE("wavg"), + WINDDIRECTION("wdir"), + WINDGUST("wgust"), + RAINRATE("rrate"), + RAINTOTAL("rtot"), WATT("watt"), LUMINATION("lum"), UNKOWN("unkown"); diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java new file mode 100644 index 0000000000000..d1a7dd4a09b1f --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.tellstick.internal.conf.TelldusLocalConfiguration; +import org.openhab.binding.tellstick.internal.handler.DeviceStatusListener; +import org.openhab.binding.tellstick.internal.handler.TelldusBridgeHandler; +import org.openhab.binding.tellstick.internal.handler.TelldusDeviceController; +import org.openhab.binding.tellstick.internal.handler.TelldusDevicesHandler; +import org.openhab.binding.tellstick.internal.local.dto.LocalDataTypeValueDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalDeviceDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalDevicesDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorEventDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalSensorsDTO; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tellstick.device.TellstickDeviceEvent; +import org.tellstick.device.TellstickException; +import org.tellstick.device.iface.Device; + +/** + * {@link TelldusLocalBridgeHandler} is the handler for Telldus Local API (Tellstick ZNET v1/v2) and connects it + * to the framework. All {@link TelldusDevicesHandler}s use the + * {@link TelldusLocalDeviceController} to execute the actual commands. + * + * @author Jan Gustafsson- Initial contribution + */ +public class TelldusLocalBridgeHandler extends BaseBridgeHandler implements TelldusBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(TelldusLocalBridgeHandler.class); + + private TellstickLocalDevicesDTO deviceList = null; + private TellstickLocalSensorsDTO sensorList = null; + private TelldusLocalDeviceController controller = null; + private List deviceStatusListeners = Collections.synchronizedList(new ArrayList<>()); + private final HttpClient httpClient; + private ScheduledFuture> pollingJob; + /** + * Use cache for refresh command to not update again when call is made within 10 seconds of previous call. + */ + private final ExpiringCache refreshCache = new ExpiringCache<>(Duration.ofSeconds(10), + this::refreshDeviceList); + + public TelldusLocalBridgeHandler(Bridge bridge, HttpClient httpClient) { + super(bridge); + this.httpClient = httpClient; + } + + @Override + public void initialize() { + TelldusLocalConfiguration configuration = getConfigAs(TelldusLocalConfiguration.class); + this.controller = new TelldusLocalDeviceController(configuration, httpClient); + pollingJob = scheduler.scheduleWithFixedDelay(this::refreshDeviceList, 11, configuration.refreshInterval, + TimeUnit.MILLISECONDS); + updateStatus(ThingStatus.UNKNOWN); + } + + @Override + public void dispose() { + if (pollingJob != null) { + pollingJob.cancel(true); + } + if (this.controller != null) { + this.controller.dispose(); + } + deviceList = null; + sensorList = null; + super.dispose(); + } + + private boolean refreshDeviceList() { + try { + updateDevices(deviceList); + updateSensors(sensorList); + updateStatus(ThingStatus.ONLINE); + return true; + } catch (TellstickException | InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + return false; + } + + private synchronized void updateDevices(TellstickLocalDevicesDTO previouslist) + throws TellstickException, InterruptedException { + TellstickLocalDevicesDTO newList = controller + .callRestMethod(TelldusLocalDeviceController.HTTP_LOCAL_API_DEVICES, TellstickLocalDevicesDTO.class); + logger.debug("Device list {}", newList.getDevices()); + if (newList.getDevices() != null) { + if (previouslist == null) { + for (TellstickLocalDeviceDTO device : newList.getDevices()) { + device.setUpdated(true); + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + listener.onDeviceAdded(getThing(), device); + } + } + } + this.deviceList = newList; + } else { + for (TellstickLocalDeviceDTO device : newList.getDevices()) { + int index = previouslist.getDevices().indexOf(device); + logger.debug("Device:{} found at {}", device, index); + if (index >= 0) { + TellstickLocalDeviceDTO orgDevice = previouslist.getDevices().get(index); + if (device.getState() != orgDevice.getState()) { + orgDevice.setState(device.getState()); + orgDevice.setStatevalue(device.getStatevalue()); + orgDevice.setUpdated(true); + } + } else { + logger.debug("New Device - Adding:{}", device); + previouslist.getDevices().add(device); + device.setUpdated(true); + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + listener.onDeviceAdded(getThing(), device); + } + } + } + } + } + + for (TellstickLocalDeviceDTO device : deviceList.getDevices()) { + if (device.isUpdated()) { + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + listener.onDeviceStateChanged(getThing(), device, + new TellstickDeviceEvent(device, null, null, null, System.currentTimeMillis())); + } + } + device.setUpdated(false); + } + } + } + } + + private synchronized void updateSensors(TellstickLocalSensorsDTO previouslist) + throws TellstickException, InterruptedException { + TellstickLocalSensorsDTO newList = controller + .callRestMethod(TelldusLocalDeviceController.HTTP_LOCAL_API_SENSORS, TellstickLocalSensorsDTO.class); + logger.debug("Updated sensors:{}", newList.getSensors()); + if (newList.getSensors() != null) { + if (previouslist == null) { + this.sensorList = newList; + for (TellstickLocalSensorDTO sensor : sensorList.getSensors()) { + sensor.setUpdated(true); + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + listener.onDeviceAdded(getThing(), sensor); + } + } + } + } else { + for (TellstickLocalSensorDTO sensor : previouslist.getSensors()) { + sensor.setUpdated(false); + } + + for (TellstickLocalSensorDTO sensor : newList.getSensors()) { + int index = this.sensorList.getSensors().indexOf(sensor); + if (index >= 0) { + TellstickLocalSensorDTO orgSensor = this.sensorList.getSensors().get(index); + orgSensor.setData(sensor.getData()); + orgSensor.setUpdated(true); + sensor.setUpdated(true); + } else { + this.sensorList.getSensors().add(sensor); + sensor.setUpdated(true); + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + listener.onDeviceAdded(getThing(), sensor); + } + } + } + } + } + for (TellstickLocalSensorDTO sensor : sensorList.getSensors()) { + if (sensor.getData() != null && sensor.isUpdated()) { + synchronized (deviceStatusListeners) { + for (DeviceStatusListener listener : deviceStatusListeners) { + for (LocalDataTypeValueDTO type : sensor.getData()) { + listener.onDeviceStateChanged(getThing(), sensor, + new TellstickLocalSensorEventDTO(sensor.getId(), type.getValue(), type, + sensor.getProtocol(), sensor.getModel(), System.currentTimeMillis())); + } + } + } + sensor.setUpdated(false); + } + } + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + refreshCache.getValue(); + } + } + + @Override + public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) { + if (deviceStatusListener == null) { + throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener."); + } + return deviceStatusListeners.add(deviceStatusListener); + } + + @Override + public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) { + return deviceStatusListeners.remove(deviceStatusListener); + } + + private Device getDevice(String id, List devices) { + for (Device device : devices) { + if (device.getId() == Integer.valueOf(id)) { + return device; + } + } + return null; + } + + private Device getSensor(String id, List sensors) { + for (Device sensor : sensors) { + if (sensor.getId() == Integer.valueOf(id)) { + return sensor; + } + } + return null; + } + + @Override + public Device getDevice(String serialNumber) { + return getDevice(serialNumber, getDevices()); + } + + private List getDevices() { + if (deviceList == null) { + refreshDeviceList(); + } + return deviceList.getDevices(); + } + + @Override + public Device getSensor(String deviceUUId) { + Device result = null; + if (sensorList != null) { + result = getSensor(deviceUUId, sensorList.getSensors()); + } + return result; + } + + @Override + public void rescanTelldusDevices() { + this.deviceList = null; + this.sensorList = null; + refreshDeviceList(); + } + + @Override + public TelldusDeviceController getController() { + return controller; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java new file mode 100644 index 0000000000000..425f76e29ae26 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local; + +import java.math.BigDecimal; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.tellstick.internal.TelldusBindingException; +import org.openhab.binding.tellstick.internal.conf.TelldusLocalConfiguration; +import org.openhab.binding.tellstick.internal.handler.TelldusDeviceController; +import org.openhab.binding.tellstick.internal.local.dto.TelldusLocalResponseDTO; +import org.openhab.binding.tellstick.internal.local.dto.TellstickLocalDeviceDTO; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tellstick.JNA; +import org.tellstick.device.TellstickDevice; +import org.tellstick.device.TellstickDeviceEvent; +import org.tellstick.device.TellstickException; +import org.tellstick.device.TellstickSensorEvent; +import org.tellstick.device.iface.Device; +import org.tellstick.device.iface.DeviceChangeListener; +import org.tellstick.device.iface.SensorListener; +import org.tellstick.device.iface.SwitchableDevice; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * {@link TelldusLocalDeviceController} handles the communication with Telldus Local API (Tellstick ZNET v1/v2) + * This controller uses JSON based Rest API to communicate with Telldus Local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TelldusLocalDeviceController implements DeviceChangeListener, SensorListener, TelldusDeviceController { + private final Logger logger = LoggerFactory.getLogger(TelldusLocalDeviceController.class); + private long lastSend = 0; + public static final long DEFAULT_INTERVAL_BETWEEN_SEND_SEC = 250; + static final int REQUEST_TIMEOUT_MS = 5000; + private final HttpClient httpClient; + private final Gson gson = new Gson(); + private String localApiUrl; + private String authorizationHeader = "Bearer "; + static final String HTTP_LOCAL_API = "api/"; + static final String HTTP_LOCAL_API_DEVICES = HTTP_LOCAL_API + "devices/list?supportedMethods=19&includeIgnored=0"; + static final String HTTP_LOCAL_API_SENSORS = HTTP_LOCAL_API + + "sensors/list?includeValues=1&includeScale=1&includeUnit=1&includeIgnored=0"; + static final String HTTP_LOCAL_API_SENSOR_INFO = HTTP_LOCAL_API + "sensor/info"; + static final String HTTP_LOCAL_API_DEVICE_DIM = HTTP_LOCAL_API + "device/dim?id=%d&level=%d"; + static final String HTTP_LOCAL_API_DEVICE_TURNOFF = HTTP_LOCAL_API + "device/turnOff?id=%d"; + static final String HTTP_LOCAL_DEVICE_TURNON = HTTP_LOCAL_API + "device/turnOn?id=%d"; + private static final int MAX_RETRIES = 3; + + public TelldusLocalDeviceController(TelldusLocalConfiguration configuration, HttpClient httpClient) { + this.httpClient = httpClient; + localApiUrl = "http://" + configuration.ipAddress + "/"; + authorizationHeader = authorizationHeader + configuration.accessToken; + } + + @Override + public void dispose() { + } + + @Override + public void handleSendEvent(Device device, int resendCount, boolean isdimmer, Command command) + throws TellstickException { + logger.debug("Send {} to {}", command, device); + try { + if (device instanceof TellstickLocalDeviceDTO) { + if (command == OnOffType.ON) { + turnOn(device); + } else if (command == OnOffType.OFF) { + turnOff(device); + } else if (command instanceof PercentType) { + dim(device, (PercentType) command); + } else if (command instanceof IncreaseDecreaseType) { + increaseDecrease(device, ((IncreaseDecreaseType) command)); + } + } else if (device instanceof SwitchableDevice) { + if (command == OnOffType.ON) { + if (isdimmer) { + logger.trace("Turn off first in case it is allready on"); + turnOff(device); + } + turnOn(device); + } else if (command == OnOffType.OFF) { + turnOff(device); + } + } else { + logger.warn("Cannot send to {}", device); + } + } catch (InterruptedException e) { + logger.debug("OH is shut-down."); + } + } + + private void increaseDecrease(Device dev, IncreaseDecreaseType increaseDecreaseType) + throws TellstickException, InterruptedException { + String strValue = ((TellstickDevice) dev).getData(); + double value = 0; + if (strValue != null) { + value = Double.valueOf(strValue); + } + int percent = (int) Math.round((value / 255) * 100); + if (IncreaseDecreaseType.INCREASE == increaseDecreaseType) { + percent = Math.min(percent + 10, 100); + } else if (IncreaseDecreaseType.DECREASE == increaseDecreaseType) { + percent = Math.max(percent - 10, 0); + } + dim(dev, new PercentType(percent)); + } + + private void dim(Device dev, PercentType command) throws TellstickException, InterruptedException { + double value = command.doubleValue(); + + // 0 means OFF and 100 means ON + if (value == 0 && dev instanceof TellstickLocalDeviceDTO) { + turnOff(dev); + } else if (value == 100 && dev instanceof TellstickLocalDeviceDTO) { + turnOn(dev); + } else if (dev instanceof TellstickLocalDeviceDTO + && (((TellstickLocalDeviceDTO) dev).getMethods() & JNA.CLibrary.TELLSTICK_DIM) > 0) { + long tdVal = Math.round((value / 100) * 255); + TelldusLocalResponseDTO response = callRestMethod( + String.format(HTTP_LOCAL_API_DEVICE_DIM, dev.getId(), tdVal), TelldusLocalResponseDTO.class); + handleResponse((TellstickLocalDeviceDTO) dev, response); + } else { + throw new TelldusBindingException("Cannot send DIM to " + dev); + } + } + + private void turnOff(Device dev) throws TellstickException, InterruptedException { + if (dev instanceof TellstickLocalDeviceDTO) { + TelldusLocalResponseDTO response = callRestMethod(String.format(HTTP_LOCAL_API_DEVICE_TURNOFF, dev.getId()), + TelldusLocalResponseDTO.class); + handleResponse((TellstickLocalDeviceDTO) dev, response); + } else { + throw new TelldusBindingException("Cannot send OFF to " + dev); + } + } + + private void handleResponse(TellstickLocalDeviceDTO device, TelldusLocalResponseDTO response) + throws TellstickException { + if (response == null || (response.getStatus() == null && response.getError() == null)) { + throw new TelldusBindingException("No response " + response); + } else if (response.getError() != null) { + device.setUpdated(true); + throw new TelldusBindingException("Error " + response.getError()); + } else if (!response.getStatus().trim().equals("success")) { + throw new TelldusBindingException("Response " + response.getStatus()); + } + } + + private void turnOn(Device dev) throws TellstickException, InterruptedException { + if (dev instanceof TellstickLocalDeviceDTO) { + TelldusLocalResponseDTO response = callRestMethod(String.format(HTTP_LOCAL_DEVICE_TURNON, dev.getId()), + TelldusLocalResponseDTO.class); + handleResponse((TellstickLocalDeviceDTO) dev, response); + } else { + throw new TelldusBindingException("Cannot send ON to " + dev); + } + } + + @Override + public State calcState(Device dev) { + TellstickLocalDeviceDTO device = (TellstickLocalDeviceDTO) dev; + State st = null; + + switch (device.getState()) { + case JNA.CLibrary.TELLSTICK_TURNON: + st = OnOffType.ON; + break; + case JNA.CLibrary.TELLSTICK_TURNOFF: + st = OnOffType.OFF; + break; + case JNA.CLibrary.TELLSTICK_DIM: + BigDecimal dimValue = new BigDecimal(device.getStatevalue()); + if (dimValue.intValue() == 0) { + st = OnOffType.OFF; + } else if (dimValue.intValue() >= 255) { + st = OnOffType.ON; + } else { + st = OnOffType.ON; + } + break; + default: + logger.warn("Could not handle {} for {}", device.getState(), device); + } + + return st; + } + + @Override + public BigDecimal calcDimValue(Device device) { + BigDecimal dimValue = BigDecimal.ZERO; + switch (((TellstickLocalDeviceDTO) device).getState()) { + case JNA.CLibrary.TELLSTICK_TURNON: + dimValue = new BigDecimal(100); + break; + case JNA.CLibrary.TELLSTICK_TURNOFF: + break; + case JNA.CLibrary.TELLSTICK_DIM: + dimValue = new BigDecimal(((TellstickLocalDeviceDTO) device).getStatevalue()); + dimValue = dimValue.multiply(new BigDecimal(100)); + dimValue = dimValue.divide(new BigDecimal(255), 0, BigDecimal.ROUND_HALF_UP); + break; + default: + logger.warn("Could not handle {} for {}", (((TellstickLocalDeviceDTO) device).getState()), device); + } + return dimValue; + } + + public long getLastSend() { + return lastSend; + } + + public void setLastSend(long currentTimeMillis) { + lastSend = currentTimeMillis; + } + + @Override + public void onRequest(TellstickSensorEvent newDevices) { + setLastSend(newDevices.getTimestamp()); + } + + @Override + public void onRequest(TellstickDeviceEvent newDevices) { + setLastSend(newDevices.getTimestamp()); + } + + T callRestMethod(String uri, Class response) throws TelldusLocalException, InterruptedException { + T resultObj = null; + try { + for (int i = 0; i < MAX_RETRIES; i++) { + try { + resultObj = innerCallRest(localApiUrl + uri, response); + break; + } catch (TimeoutException e) { + logger.warn("TimeoutException error in get"); + } + } + } catch (JsonSyntaxException e) { + throw new TelldusLocalException(e); + } catch (ExecutionException e) { + throw new TelldusLocalException(e); + } + return resultObj; + } + + private T innerCallRest(String uri, Class json) + throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException { + logger.trace("HTTP GET: {}", uri); + + Request request = httpClient.newRequest(uri).method(HttpMethod.GET); + request.header("Authorization", authorizationHeader); + + ContentResponse response = request.send(); + String content = response.getContentAsString(); + logger.trace("API response: {}", content); + + return gson.fromJson(content, json); + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalException.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalException.java new file mode 100644 index 0000000000000..f9da540621220 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalException.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.tellstick.device.TellstickException; + +/** + * {@link TelldusLocalException} is used when there is exception communicating with Telldus local API. + * This exception extends the Telldus Core exception. + * + * @author Jan Gustafsson - Initial contribution + */ +@NonNullByDefault +public class TelldusLocalException extends TellstickException { + + public TelldusLocalException(Exception source) { + super(null, 0); + this.initCause(source); + } + + private static final long serialVersionUID = 3067179547449454711L; + + @Override + public @NonNull String getMessage() { + Throwable throwable = getCause(); + if (throwable != null) { + String localMessage = throwable.getMessage(); + if (localMessage != null) { + return localMessage; + } + } + return ""; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/LocalDataTypeValueDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/LocalDataTypeValueDTO.java new file mode 100644 index 0000000000000..63bab55c26e31 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/LocalDataTypeValueDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import org.openhab.binding.tellstick.internal.live.xml.LiveDataType; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class LocalDataTypeValueDTO { + + private String name; + private int scale; + private String value; + + public LiveDataType getName() { + return LiveDataType.fromName(name); + } + + public void setName(String name) { + this.name = name; + } + + public int getScale() { + return scale; + } + + public void setScale(int scale) { + this.scale = scale; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TelldusLocalResponseDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TelldusLocalResponseDTO.java new file mode 100644 index 0000000000000..9f6d7726bdebe --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TelldusLocalResponseDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TelldusLocalResponseDTO { + + private String error; + private String status; + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDeviceDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDeviceDTO.java new file mode 100644 index 0000000000000..7289dcd999478 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDeviceDTO.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import org.tellstick.device.iface.Device; +import org.tellstick.enums.DeviceType; + +import com.google.gson.annotations.SerializedName; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TellstickLocalDeviceDTO implements Device { + + @SerializedName("id") + private int deviceId; + private int methods; + private String name; + private int state; + private String statevalue; + private String type; + private String protocol; + private String model; + private boolean updated; + + public void setUpdated(boolean b) { + this.updated = b; + } + + public boolean isUpdated() { + return updated; + } + + @Override + public int getId() { + return deviceId; + } + + public void setId(int deviceId) { + this.deviceId = deviceId; + } + + public int getMethods() { + return methods; + } + + public void setMethods(int methods) { + this.methods = methods; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getUUId() { + return Integer.toString(deviceId); + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getModel() { + return model; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.DEVICE; + } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + public String getStatevalue() { + return statevalue; + } + + public void setStatevalue(String statevalue) { + this.statevalue = statevalue; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDevicesDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDevicesDTO.java new file mode 100644 index 0000000000000..275768de1388f --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalDevicesDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TellstickLocalDevicesDTO { + + @SerializedName("device") + private List devices = null; + + public List getDevices() { + return devices; + } + + public void setDevices(List devices) { + this.devices = devices; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorDTO.java new file mode 100644 index 0000000000000..f8087fb928877 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorDTO.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import java.util.List; + +import org.openhab.binding.tellstick.internal.live.xml.LiveDataType; +import org.tellstick.device.iface.Device; +import org.tellstick.enums.DeviceType; + +import com.google.gson.annotations.SerializedName; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TellstickLocalSensorDTO implements Device { + + private int battery; + private boolean updated; + private List data = null; + @SerializedName("id") + private int deviceId; + private String model; + private String name; + private String protocol; + private int sensorId; + + public int getBattery() { + return battery; + } + + public void setBattery(int battery) { + this.battery = battery; + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + @Override + public int getId() { + return deviceId; + } + + public void setId(int id) { + this.deviceId = id; + } + + @Override + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public void setUpdated(boolean b) { + this.updated = b; + } + + public boolean isUpdated() { + return updated; + } + + public boolean isSensorOfType(LiveDataType type) { + boolean res = false; + if (data != null) { + for (LocalDataTypeValueDTO val : data) { + if (val.getName() == type) { + res = true; + break; + } + } + } + return res; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.SENSOR; + } + + public int getSensorId() { + return sensorId; + } + + public void setSensorId(int sensorId) { + this.sensorId = sensorId; + } + + @Override + public String getUUId() { + return Integer.toString(deviceId); + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorEventDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorEventDTO.java new file mode 100644 index 0000000000000..39e6744e9e034 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorEventDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import org.openhab.binding.tellstick.internal.TellstickRuntimeException; +import org.tellstick.device.TellstickSensorEvent; +import org.tellstick.device.iface.TellstickEvent; +import org.tellstick.enums.DataType; + +/** + * This class is used for events for the telldus live sensors. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TellstickLocalSensorEventDTO extends TellstickSensorEvent implements TellstickEvent { + + private LocalDataTypeValueDTO dataType; + + public TellstickLocalSensorEventDTO(int sensorId, String data, LocalDataTypeValueDTO dataValue, String protocol, + String model, long timeStamp) { + super(sensorId, data, null, protocol, model, timeStamp); + this.dataType = dataValue; + } + + public LocalDataTypeValueDTO getDataTypeValue() { + return dataType; + } + + @Override + public DataType getDataType() { + throw new TellstickRuntimeException("Should not call this method"); + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorsDTO.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorsDTO.java new file mode 100644 index 0000000000000..ff6e64f9607e7 --- /dev/null +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/dto/TellstickLocalSensorsDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.tellstick.internal.local.dto; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * Class used to deserialize JSON from Telldus local API. + * + * @author Jan Gustafsson - Initial contribution + */ +public class TellstickLocalSensorsDTO { + + @SerializedName("sensor") + private List sensors = null; + + public List getSensors() { + return sensors; + } + + public void setSensors(List sensors) { + this.sensors = sensors; + } +} diff --git a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/bridge.xml index da1ca16a0d627..11f8e2b091fc7 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/bridge.xml @@ -6,7 +6,7 @@ - This bridge represents the telldus center on a local computer. +This bridge represents the Telldus center on a local computer. @@ -25,7 +25,7 @@ - +This bridge represents the telldus live cloud service. +This bridge represents the Telldus live cloud service. @@ -34,12 +34,10 @@ The private key from telldus - credentials The public key from telldus - @@ -48,7 +46,7 @@credentials The openauth token. The openauth token secret. -+ The refresh interval in ms which is used to poll Telldus Live. @@ -57,4 +55,27 @@+ + diff --git a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/devices.xml b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/devices.xml index 3e2fa9f1bc596..f51e8f2d9d7f3 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/devices.xml +++ b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/devices.xml @@ -9,6 +9,7 @@This bridge represents the Telldus local API. + ++ + ++ + +The local IP address of the Tellstick. +network-address ++ + +The access token. ++ + +The refresh interval in ms which is used to poll Telldus local API. + +60000 +diff --git a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/sensor.xml b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/sensor.xml index 4a598a43244ec..5e1820371b896 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/sensor.xml +++ b/bundles/org.openhab.binding.tellstick/src/main/resources/OH-INF/thing/sensor.xml @@ -8,6 +8,7 @@ + @@ -91,14 +92,14 @@ + Number:Length The current rain rate -+ @@ -126,9 +127,9 @@ Number:Length Total rain -+ From 0082cd11aeb19ff560f0292fdcfe2a917508af22 Mon Sep 17 00:00:00 2001 From: leoguiders <30723929+leoguiders@users.noreply.github.com> Date: Thu, 18 Feb 2021 14:07:31 +0100 Subject: [PATCH 013/118] [modbus.sunspec] Fix decimal number handling for inverter channel types (#10195) Signed-off-by: Jan Philipp Giel Number:Power - -Current kWatt -+ + Current power +--- .../main/resources/OH-INF/thing/inverter-channel-types.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml index fa0506bbc02a8..0ee3ccb91824a 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/OH-INF/thing/inverter-channel-types.xml @@ -20,14 +20,14 @@ Number:ElectricPotential This phase's AC voltage relative to the next phase -+ Number:ElectricPotential This phase's AC voltage relative to N line -+ From 1c5f0d17971b7f2e83e397370c263e347762a992 Mon Sep 17 00:00:00 2001 From: Pantastisch <45369229+Pantastisch@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:35:23 +0100 Subject: [PATCH 014/118] [icloud] Add german translation (#10164) Signed-off-by: Panagiotis Doulgeris panagiotis.doulgeris@outlook.com Signed-off-by: Pantastisch <45369229+Pantastisch@users.noreply.github.com> --- .../OH-INF/i18n/iCloud_de.properties | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties diff --git a/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties b/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties new file mode 100644 index 0000000000000..2b6beab311a3f --- /dev/null +++ b/bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud_de.properties @@ -0,0 +1,34 @@ +# Binding +icloud.binding.name=iCloud Binding +icloud.binding.description=Die Apple iCloud wird genutzt, um Daten wie den Ladezustand oder den Standort von einem oder mehreren Apple Geräten zu erhalten, die mit einem iCloud Account verknüpft sind. + +# Account Thing +icloud.account-thing.label=iCloud Account +icloud.account-thing.description=Der iCloud Account (Bridge) repräsentiert einen iCloud Account. Du benötigst mehrere iCloud Account Bridges, um mehrere iCloud Accounts zu verwalten. + +icloud.account-thing.parameter.apple-id.label=Apple-ID +icloud.account-thing.parameter.apple-id.description=Apple-ID (E-Mail Adresse), um Zugriff zur iCloud zu erhalten. +icloud.account-thing.parameter.password.label=Passwort +icloud.account-thing.parameter.password.description=Passwort der Apple-ID, um Zugriff zur iCloud zu erhalten. +icloud.account-thing.parameter.refresh.label=Aktualisierungszeit in Minuten +icloud.account-thing.parameter.refresh.description=Zeit in der die iCloud Informationen aktualisiert werden sollen. + +icloud.account-thing.property.owner=Besitzer + +# Device Thing +icloud.device-thing.label=iCloud Gerät +icloud.device-thing.description=Das iCloud Gerät (Thing) repräsentiert ein mit der iCloud verknüpftes Apple Gerät, wie zum Beispiel ein iPhone. Es muss mit einem iCloud Account (Bridge) verknüpft werden, um Aktualisierungen zu erhalten. Mehrere iCloud Geräte können mit einem iCloud Account (Bridge) verknüpft werden. + +icloud.device-thing.parameter.id.label=Geräte-ID + +icloud.device-thing.channel.battery-status.label=Ladezustand +icloud.device-thing.channel.battery-status.state.not-charging=Lädt nicht +icloud.device-thing.channel.battery-status.state.charged=Aufgeladen +icloud.device-thing.channel.battery-status.state.charging=Lädt +icloud.device-thing.channel.battery-status.state.unknown=Unbekannt +icloud.device-thing.channel.find-my-phone.label=Wo ist? +icloud.device-thing.channel.location.label=Standort +icloud.device-thing.channel.location-accuracy=Standort Genauigkeit +icloud.device-thing.channel.location-last-update=Letztes Standort Update + +icloud.device-thing.property.device-name=Gerätename From a9f440dba22d649d3b1e149adfc70d7817c69dbd Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green -Date: Fri, 19 Feb 2021 19:16:26 +0000 Subject: [PATCH 015/118] [hue] Eliminate NPE in #9985 (#10199) * [hue] extra null check Signed-off-by: Andrew Fiddian-Green --- .../internal/discovery/HueBridgeDiscoveryParticipant.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java index d5bb4ce498354..7018c62917b3a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeDiscoveryParticipant.java @@ -119,9 +119,11 @@ public long getRemovalGracePeriodSeconds(RemoteDevice device) { try { Configuration conf = configAdmin.getConfiguration("binding.hue"); Dictionary properties = conf.getProperties(); - Object property = properties.get(HueBindingConstants.REMOVAL_GRACE_PERIOD); - if (property != null) { - removalGracePeriodSeconds = Long.parseLong(property.toString()); + if (properties != null) { + Object property = properties.get(HueBindingConstants.REMOVAL_GRACE_PERIOD); + if (property != null) { + removalGracePeriodSeconds = Long.parseLong(property.toString()); + } } } catch (IOException | IllegalStateException | NumberFormatException e) { // fall through to pre-initialised (default) value From fd1c96677e37352283875f25755ab510b4f7dffa Mon Sep 17 00:00:00 2001 From: Daniel Weber <25605184+fruggy83@users.noreply.github.com> Date: Sat, 20 Feb 2021 17:13:28 +0100 Subject: [PATCH 016/118] [enocean] Improved device discovery and added SMACK capability (#10157) * Added SMACK teach in * Teached in devices can be teach out on a repeated teach in * Improved detection of RPS devices, device types can be better distinguished now * Bugfixes for discovery fallback to GenericThings * Responses to message requests are send automatically now, no need for linking SEND_COMMAND channel Fixes #10156 Signed-off-by: Daniel Weber --- bundles/org.openhab.binding.enocean/README.md | 19 +- .../internal/EnOceanBindingConstants.java | 24 +-- .../internal/EnOceanHandlerFactory.java | 6 +- .../config/EnOceanActuatorConfig.java | 2 +- .../internal/config/EnOceanBaseConfig.java | 8 + .../internal/config/EnOceanBridgeConfig.java | 8 +- .../EnOceanChannelTransformationConfig.java | 9 +- .../EnOceanDeviceDiscoveryService.java | 169 +++++++++++++----- .../enocean/internal/eep/A5_02/A5_02.java | 4 - .../enocean/internal/eep/A5_04/A5_04.java | 3 - .../enocean/internal/eep/A5_07/A5_07.java | 3 - .../enocean/internal/eep/A5_08/A5_08.java | 3 - .../enocean/internal/eep/A5_10/A5_10.java | 3 - .../enocean/internal/eep/A5_20/A5_20_04.java | 2 +- .../internal/eep/Base/PTM200Message.java | 8 +- .../internal/eep/Base/UTEResponse.java | 5 +- .../Base/_4BSTeachInVariation3Response.java | 6 +- .../internal/eep/Base/_RPSMessage.java | 2 + .../enocean/internal/eep/D2_05/D2_05_00.java | 3 +- .../enocean/internal/eep/EEPFactory.java | 151 ++++++++++++---- .../binding/enocean/internal/eep/EEPType.java | 101 ++++++++--- .../enocean/internal/eep/F6_01/F6_01_01.java | 9 +- .../enocean/internal/eep/F6_02/F6_02_01.java | 26 ++- .../enocean/internal/eep/F6_02/F6_02_02.java | 13 +- .../enocean/internal/eep/F6_05/F6_05_02.java | 9 +- .../enocean/internal/eep/F6_10/F6_10_00.java | 8 +- .../eep/F6_10/F6_10_00_EltakoFPE.java | 6 + .../enocean/internal/eep/F6_10/F6_10_01.java | 9 +- .../internal/eep/Generic/GenericEEP.java | 5 +- .../handler/EnOceanBaseActuatorHandler.java | 73 ++++---- .../handler/EnOceanBaseSensorHandler.java | 20 ++- .../handler/EnOceanBaseThingHandler.java | 17 +- .../handler/EnOceanBridgeHandler.java | 156 ++++++++++++---- .../handler/EnOceanClassicDeviceHandler.java | 2 +- .../internal/messages/ESP3PacketFactory.java | 26 +++ .../internal/messages/EventMessage.java | 65 +++++++ .../enocean/internal/messages/Response.java | 2 +- .../{ => Responses}/BaseResponse.java | 3 +- .../{ => Responses}/RDBaseIdResponse.java | 3 +- .../Responses/RDLearnedClientsResponse.java | 61 +++++++ .../{ => Responses}/RDRepeaterResponse.java | 3 +- .../{ => Responses}/RDVersionResponse.java | 3 +- .../Responses/SMACKTeachInResponse.java | 65 +++++++ .../enocean/internal/messages/SAMessage.java | 64 +++++++ .../transceiver/EnOceanESP3Transceiver.java | 19 +- .../transceiver/EnOceanTransceiver.java | 90 +++++++--- .../internal/transceiver/EventListener.java | 23 +++ .../internal/transceiver/PacketListener.java | 2 +- .../internal/transceiver/TeachInListener.java | 21 +++ .../resources/OH-INF/thing/CentralCommand.xml | 2 +- .../resources/OH-INF/thing/ClassicDevice.xml | 2 +- .../resources/OH-INF/thing/GenericThing.xml | 2 +- .../OH-INF/thing/HeatRecoveryVentilation.xml | 2 +- .../OH-INF/thing/MeasurementSwitch.xml | 2 +- .../resources/OH-INF/thing/Rollershutter.xml | 2 +- .../resources/OH-INF/thing/Thermostat.xml | 2 +- .../main/resources/OH-INF/thing/bridge.xml | 15 +- .../main/resources/OH-INF/thing/channels.xml | 7 - 58 files changed, 1064 insertions(+), 314 deletions(-) create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java rename bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/{ => Responses}/BaseResponse.java (85%) rename bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/{ => Responses}/RDBaseIdResponse.java (91%) create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java rename bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/{ => Responses}/RDRepeaterResponse.java (93%) rename bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/{ => Responses}/RDVersionResponse.java (94%) create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java create mode 100644 bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java diff --git a/bundles/org.openhab.binding.enocean/README.md b/bundles/org.openhab.binding.enocean/README.md index e6ff99b0a1686..38600c86d2e4b 100644 --- a/bundles/org.openhab.binding.enocean/README.md +++ b/bundles/org.openhab.binding.enocean/README.md @@ -126,7 +126,21 @@ The corresponding channels are created dynamically, too. If the actuator supports UTE teach-in, the corresponding thing can be created and paired automatically. First you have to **start the discovery scan for a gateway**. Then press the teach-in button of the actuator. -If the EEP of the actuator is known, the binding sends an UTE teach-in response with a new SenderId and creates a new thing with its channels. +If the EEP of the actuator is known, the binding sends an UTE teach-in response with a new SenderId and creates a new thing with its channels. + +This binding supports so called smart acknowlegde (SMACK) devices too. +Before you can pair a SMACK device you have to configure your gateway bridge as a SMACK postmaster. +If this option is enabled you can pair up to 20 SMACK devices with your gateway. + +Communication between your gateway and a SMACK device is handled through mailboxes. +A mailbox is created for each paired SMACK device and deleted after teach out. +You can see the paired SMACK devices and their mailbox index in the gateway properties. +SMACK devices send periodically status updates followed by a response request. +Whenever such a request is received a `requestAnswer` event is triggered for channel `statusRequestEvent`. +Afterwards you have 100ms time to recalculate your items states and update them. +A message with the updated item states is built, put into the corresponding mailbox and automatically sent upon request of the device. +Pairing and unpairing can be done through a discovery scan. +The corresponding thing of an unpaired device gets disabled, you have to delete it manually if you want to. If the actuator does not support UTE teach-ins, you have to create, configure and choose the right EEP of the thing manually. It is important to link the teach-in channel of this thing to a switch item. @@ -158,6 +172,8 @@ If you change the SenderId of your thing, you have to pair again the thing with | | espVersion | ESP Version of gateway | ESP3, ESP2 | | | rs485 | If gateway is directly connected to a RS485 bus the BaseId is set to 0x00 | true, false | | rs485BaseId | Override BaseId 0x00 if your bus contains a telegram duplicator (FTD14 for ex) | 4 byte hex value | +| | enableSmack | Enables SMACK pairing and handling of SMACK messages | true, false | +| | sendTeachOuts | Defines if a repeated teach in request should be answered with a learned in or teach out response | true, false | | pushButton | receivingEEPId | EEP used for receiving msg | F6_01_01, D2_03_0A | | | enoceanId | EnOceanId of device this thing belongs to | hex value as string | | rockerSwitch | receivingEEPId | | F6_02_01, F6_02_02 | @@ -300,6 +316,7 @@ The channels of a thing are determined automatically based on the chosen EEP. | rssi | Number | Received Signal Strength Indication (dBm) of last received message | | repeatCount | Number | Number of repeaters involved in the transmission of the telegram | | lastReceived | DateTime | Date and time the last telegram was received | +| statusRequestEvent | Trigger | Emits event 'requestAnswer' | Items linked to bi-directional actuators (actuator sends status messages back) should always disable the `autoupdate`. This is especially true for Eltako rollershutter, as their position is calculated out of the current position and the moving time. diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java index 4ae4bf2240bca..c6b3814c5edbc 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanBindingConstants.java @@ -177,7 +177,7 @@ public class EnOceanBindingConstants { public static final String CHANNEL_WAKEUPCYCLE = "wakeUpCycle"; public static final String CHANNEL_SERVICECOMMAND = "serviceCommand"; public static final String CHANNEL_STATUS_REQUEST_EVENT = "statusRequestEvent"; - public static final String CHANNEL_SEND_COMMAND = "sendCommand"; + public static final String VIRTUALCHANNEL_SEND_COMMAND = "sendCommand"; public static final String CHANNEL_VENTILATIONOPERATIONMODE = "ventilationOperationMode"; public static final String CHANNEL_FIREPLACESAFETYMODE = "fireplaceSafetyMode"; @@ -293,7 +293,8 @@ public class EnOceanBindingConstants { Map.entry(CHANNEL_INDOORAIRANALYSIS, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_INDOORAIRANALYSIS), CoreItemFactory.STRING)), - Map.entry(CHANNEL_SETPOINT, + Map.entry( + CHANNEL_SETPOINT, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SETPOINT), CoreItemFactory.NUMBER)), Map.entry(CHANNEL_CONTACT, @@ -444,13 +445,6 @@ public class EnOceanBindingConstants { new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SERVICECOMMAND), CoreItemFactory.NUMBER)), - Map.entry(CHANNEL_STATUS_REQUEST_EVENT, - new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_STATUS_REQUEST_EVENT), null, - "", false, true)), - Map.entry(CHANNEL_SEND_COMMAND, - new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_SEND_COMMAND), - CoreItemFactory.SWITCH)), - Map.entry(CHANNEL_VENTILATIONOPERATIONMODE, new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_VENTILATIONOPERATIONMODE), CoreItemFactory.STRING)), @@ -527,6 +521,10 @@ public class EnOceanBindingConstants { CoreItemFactory.NUMBER + ItemUtil.EXTENSION_SEPARATOR + Dimensionless.class.getSimpleName())), + Map.entry(CHANNEL_STATUS_REQUEST_EVENT, + new EnOceanChannelDescription(new ChannelTypeUID(BINDING_ID, CHANNEL_STATUS_REQUEST_EVENT), null, + "", false, true)), + Map.entry(CHANNEL_REPEATERMODE, new EnOceanChannelDescription( new ChannelTypeUID(BINDING_ID, CHANNEL_REPEATERMODE), CoreItemFactory.STRING))); @@ -536,11 +534,8 @@ public class EnOceanBindingConstants { public static final String REPEATERMODE_LEVEL_2 = "LEVEL2"; // Bridge config properties - public static final String SENDERID = "senderId"; public static final String PATH = "path"; - public static final String HOST = "host"; - public static final String RS485 = "rs485"; - public static final String NEXTSENDERID = "nextSenderId"; + public static final String PARAMETER_NEXT_SENDERID = "nextSenderId"; // Bridge properties public static final String PROPERTY_BASE_ID = "Base ID"; @@ -551,13 +546,12 @@ public class EnOceanBindingConstants { public static final String PROPERTY_DESCRIPTION = "Description"; // Thing properties - public static final String PROPERTY_ENOCEAN_ID = "enoceanId"; + public static final String PROPERTY_SENDINGENOCEAN_ID = "SendingEnoceanId"; // Thing config parameter public static final String PARAMETER_SENDERIDOFFSET = "senderIdOffset"; public static final String PARAMETER_SENDINGEEPID = "sendingEEPId"; public static final String PARAMETER_RECEIVINGEEPID = "receivingEEPId"; - public static final String PARAMETER_EEPID = "eepId"; public static final String PARAMETER_BROADCASTMESSAGES = "broadcastMessages"; public static final String PARAMETER_ENOCEANID = "enoceanId"; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java index 3ac9ea809cced..bbc82fff8d2b9 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/EnOceanHandlerFactory.java @@ -28,6 +28,7 @@ import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingManager; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; @@ -60,6 +61,9 @@ public class EnOceanHandlerFactory extends BaseThingHandlerFactory { @Reference ItemChannelLinkRegistry itemChannelLinkRegistry; + @Reference + ThingManager thingManager; + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); @@ -96,7 +100,7 @@ protected void removeHandler(ThingHandler thingHandler) { } private void registerDeviceDiscoveryService(EnOceanBridgeHandler handler) { - EnOceanDeviceDiscoveryService discoveryService = new EnOceanDeviceDiscoveryService(handler); + EnOceanDeviceDiscoveryService discoveryService = new EnOceanDeviceDiscoveryService(handler, thingManager); discoveryService.activate(); this.discoveryServiceRegs.put(handler.getThing().getUID(), bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java index 461e2e0c95d5d..eb9291abcafca 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanActuatorConfig.java @@ -19,7 +19,7 @@ public class EnOceanActuatorConfig extends EnOceanBaseConfig { public int channel; - public int senderIdOffset = -1; + public Integer senderIdOffset = null; public String manufacturerId; public String teachInType; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java index 49a0cdae330df..100d83a3b8633 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBaseConfig.java @@ -17,15 +17,23 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.util.HexUtils; /** * * @author Daniel Weber - Initial contribution */ +@NonNullByDefault public class EnOceanBaseConfig { + /** + * EnOceanId of the physical device + */ public String enoceanId; + /** + * EEP used/send by physical device + */ public List receivingEEPId = new ArrayList<>(); public boolean receivingSIGEEP = false; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java index baad63a36ed47..6e6bc671a4461 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanBridgeConfig.java @@ -46,10 +46,16 @@ public static ESPVersion getESPVersion(String espVersion) { public boolean rs485; public String rs485BaseId; - public int nextSenderId = 0; + public Integer nextSenderId; + + public boolean enableSmack; + public boolean sendTeachOuts; public EnOceanBridgeConfig() { espVersion = "ESP3"; + sendTeachOuts = false; + enableSmack = true; + nextSenderId = null; } public ESPVersion getESPVersion() { diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java index 36471ec37be02..81c701f4579e4 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/config/EnOceanChannelTransformationConfig.java @@ -12,12 +12,19 @@ */ package org.openhab.binding.enocean.internal.config; +import org.openhab.core.config.core.Configuration; + /** * * @author Daniel Weber - Initial contribution */ -public class EnOceanChannelTransformationConfig { +public class EnOceanChannelTransformationConfig extends Configuration { public String transformationType; public String transformationFunction; + + public EnOceanChannelTransformationConfig() { + put("transformationType", ""); + put("transformationFunction", ""); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java index 030e601e576bf..0373c474ba586 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/discovery/EnOceanDeviceDiscoveryService.java @@ -25,9 +25,14 @@ import org.openhab.binding.enocean.internal.messages.BasePacket; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; -import org.openhab.binding.enocean.internal.transceiver.PacketListener; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; +import org.openhab.binding.enocean.internal.messages.Responses.SMACKTeachInResponse; +import org.openhab.binding.enocean.internal.transceiver.TeachInListener; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingManager; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.util.HexUtils; @@ -39,15 +44,16 @@ * * @author Daniel Weber - Initial contribution */ - -public class EnOceanDeviceDiscoveryService extends AbstractDiscoveryService implements PacketListener { +public class EnOceanDeviceDiscoveryService extends AbstractDiscoveryService implements TeachInListener { private final Logger logger = LoggerFactory.getLogger(EnOceanDeviceDiscoveryService.class); private EnOceanBridgeHandler bridgeHandler; + private ThingManager thingManager; - public EnOceanDeviceDiscoveryService(EnOceanBridgeHandler bridgeHandler) { + public EnOceanDeviceDiscoveryService(EnOceanBridgeHandler bridgeHandler, ThingManager thingManager) { super(null, 60, false); this.bridgeHandler = bridgeHandler; + this.thingManager = thingManager; } /** @@ -102,72 +108,139 @@ public void packetReceived(BasePacket packet) { } String enoceanId = HexUtils.bytesToHex(eep.getSenderId()); - ThingTypeUID thingTypeUID = eep.getThingTypeUID(); - ThingUID thingUID = new ThingUID(thingTypeUID, bridgeHandler.getThing().getUID(), enoceanId); - - int senderIdOffset = 0; - boolean broadcastMessages = true; - - // check for bidirectional communication => do not use broadcast in this case - if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] - & UTEResponse.CommunicationType_MASK) == UTEResponse.CommunicationType_MASK) { - broadcastMessages = false; - } - - // if ute => send response if needed - if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] & UTEResponse.ResponseNeeded_MASK) == 0) { - logger.info("Sending UTE response to {}", enoceanId); - senderIdOffset = sendTeachInResponse(msg, enoceanId); - } - - // if 4BS teach in variation 3 => send response - if ((eep instanceof _4BSMessage) && ((_4BSMessage) eep).isTeachInVariation3Supported()) { - logger.info("Sending 4BS teach in variation 3 response to {}", enoceanId); - senderIdOffset = sendTeachInResponse(msg, enoceanId); - } - DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUID) - .withRepresentationProperty(enoceanId).withBridge(bridgeHandler.getThing().getUID()); + bridgeHandler.getThing().getThings().stream() + .filter(t -> t.getConfiguration().getProperties().getOrDefault(PARAMETER_ENOCEANID, EMPTYENOCEANID) + .toString().equals(enoceanId)) + .findFirst().ifPresentOrElse(t -> { + // If repeated learn is not allowed => send teach out + // otherwise do nothing + if (bridgeHandler.sendTeachOuts()) { + sendTeachOutResponse(msg, enoceanId, t); + thingManager.setEnabled(t.getUID(), false); + } + }, () -> { + Integer senderIdOffset = null; + boolean broadcastMessages = true; + + // check for bidirectional communication => do not use broadcast in this case + if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] + & UTEResponse.CommunicationType_MASK) == UTEResponse.CommunicationType_MASK) { + broadcastMessages = false; + } + + if (msg.getRORG() == RORG.UTE && (msg.getPayload(1, 1)[0] & UTEResponse.ResponseNeeded_MASK) == 0) { + // if ute => send response if needed + logger.debug("Sending UTE response to {}", enoceanId); + senderIdOffset = sendTeachInResponse(msg, enoceanId); + if (senderIdOffset == null) { + return; + } + } else if ((eep instanceof _4BSMessage) && ((_4BSMessage) eep).isTeachInVariation3Supported()) { + // if 4BS teach in variation 3 => send response + logger.debug("Sending 4BS teach in variation 3 response to {}", enoceanId); + senderIdOffset = sendTeachInResponse(msg, enoceanId); + if (senderIdOffset == null) { + return; + } + } + + createDiscoveryResult(eep, broadcastMessages, senderIdOffset); + }); + } - eep.addConfigPropertiesTo(discoveryResultBuilder); - discoveryResultBuilder.withProperty(PARAMETER_BROADCASTMESSAGES, broadcastMessages); - discoveryResultBuilder.withProperty(PARAMETER_ENOCEANID, enoceanId); + @Override + public void eventReceived(EventMessage event) { + if (event.getEventMessageType() == EventMessageType.SA_CONFIRM_LEARN) { + EEP eep = EEPFactory.buildEEPFromTeachInSMACKEvent(event); + if (eep == null) { + return; + } - if (senderIdOffset > 0) { - // advance config with new device id - discoveryResultBuilder.withProperty(PARAMETER_SENDERIDOFFSET, senderIdOffset); + SMACKTeachInResponse response = EEPFactory.buildResponseFromSMACKTeachIn(event, + bridgeHandler.sendTeachOuts()); + if (response != null) { + bridgeHandler.sendMessage(response, null); + + if (response.isTeachIn()) { + // SenderIdOffset will be determined during Thing init + createDiscoveryResult(eep, false, -1); + } else if (response.isTeachOut()) { + // disable already teached in thing + bridgeHandler.getThing().getThings().stream() + .filter(t -> t.getConfiguration().getProperties() + .getOrDefault(PARAMETER_ENOCEANID, EMPTYENOCEANID).toString() + .equals(HexUtils.bytesToHex(eep.getSenderId()))) + .findFirst().ifPresentOrElse(t -> { + thingManager.setEnabled(t.getUID(), false); + logger.info("Disable thing with id {}", t.getUID()); + }, () -> { + logger.info("Thing for EnOceanId {} already deleted", + HexUtils.bytesToHex(eep.getSenderId())); + }); + } + } } - - thingDiscovered(discoveryResultBuilder.build()); - - // As we only support sensors to be teached in, we do not need to send a teach in response => 4bs - // bidirectional teach in proc is not supported yet - // this is true except for UTE teach in => we always have to send a response here } - private int sendTeachInResponse(ERP1Message msg, String enoceanId) { - int offset; + private Integer sendTeachInResponse(ERP1Message msg, String enoceanId) { // get new sender Id - offset = bridgeHandler.getNextSenderId(enoceanId); - if (offset > 0) { + Integer offset = bridgeHandler.getNextSenderId(enoceanId); + if (offset != null) { byte[] newSenderId = bridgeHandler.getBaseId(); newSenderId[3] += offset; // send response - EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, newSenderId); + EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, newSenderId, true); if (response != null) { bridgeHandler.sendMessage(response.getERP1Message(), null); - logger.info("Teach in response for {} with new senderId {} (= offset {}) sent", enoceanId, + logger.debug("Teach in response for {} with new senderId {} (= offset {}) sent", enoceanId, HexUtils.bytesToHex(newSenderId), offset); } else { logger.warn("Teach in response for enoceanId {} not supported!", enoceanId); } + } else { + logger.warn("Could not get new SenderIdOffset"); } return offset; } + private void sendTeachOutResponse(ERP1Message msg, String enoceanId, Thing thing) { + byte[] senderId = bridgeHandler.getBaseId(); + senderId[3] += (byte) thing.getConfiguration().getProperties().getOrDefault(PARAMETER_SENDERIDOFFSET, 0); + + // send response + EEP response = EEPFactory.buildResponseEEPFromTeachInERP1(msg, senderId, false); + if (response != null) { + bridgeHandler.sendMessage(response.getERP1Message(), null); + logger.debug("Teach out response for thing {} with EnOceanId {} sent", thing.getUID().getId(), enoceanId); + } else { + logger.warn("Teach out response for enoceanId {} not supported!", enoceanId); + } + } + + protected void createDiscoveryResult(EEP eep, boolean broadcastMessages, Integer senderIdOffset) { + String enoceanId = HexUtils.bytesToHex(eep.getSenderId()); + ThingTypeUID thingTypeUID = eep.getThingTypeUID(); + ThingUID thingUID = new ThingUID(thingTypeUID, bridgeHandler.getThing().getUID(), enoceanId); + + DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUID) + .withRepresentationProperty(PARAMETER_ENOCEANID).withProperty(PARAMETER_ENOCEANID, enoceanId) + .withProperty(PARAMETER_BROADCASTMESSAGES, broadcastMessages) + .withBridge(bridgeHandler.getThing().getUID()); + + eep.addConfigPropertiesTo(discoveryResultBuilder); + + if (senderIdOffset != null) { + // advance config with new device id + discoveryResultBuilder.withProperty(PARAMETER_SENDERIDOFFSET, senderIdOffset); + } + + thingDiscovered(discoveryResultBuilder.build()); + } + @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { // we just want teach in msg, so return zero here return 0; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java index 30c8522f7c2d2..957296c15f37f 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_02/A5_02.java @@ -20,7 +20,6 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; /** * @@ -51,9 +50,6 @@ protected int getUnscaledTemperatureValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } double scaledTemp = getScaledMin() - (((getUnscaledMin() - getUnscaledTemperatureValue()) * (getScaledMin() - getScaledMax())) diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java index fa4241caf9548..355d6365b356e 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_04/A5_04.java @@ -62,9 +62,6 @@ protected int getUnscaledHumidityValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_TEMPERATURE)) { double scaledTemp = getScaledTemperatureMin() diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java index ac78bbf02e832..56f074f799ae9 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_07/A5_07.java @@ -53,9 +53,6 @@ protected State getSupplyVoltage(int value) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_ILLUMINATION)) { return getIllumination(); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java index 5f6a54c806dd1..cf471e7fd7136 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_08/A5_08.java @@ -71,9 +71,6 @@ protected int getUnscaledIlluminationValue() { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } if (channelId.equals(CHANNEL_TEMPERATURE)) { double scaledTemp = getScaledTemperatureMin() diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java index 99def10649e15..465ce91d2528f 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_10/A5_10.java @@ -40,9 +40,6 @@ public A5_10(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_FANSPEEDSTAGE: diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java index 1c54b86f78c95..058a34b760163 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/A5_20/A5_20_04.java @@ -169,7 +169,7 @@ private byte getSer(Function getCurrentStateFunc) { @Override protected void convertFromCommandImpl(String channelId, String channelTypeId, Command command, Function getCurrentStateFunc, Configuration config) { - if (CHANNEL_SEND_COMMAND.equals(channelId) && (command.equals(OnOffType.ON))) { + if (VIRTUALCHANNEL_SEND_COMMAND.equals(channelId)) { byte db3 = getPos(getCurrentStateFunc); byte db2 = getTsp(getCurrentStateFunc); byte db1 = (byte) (0x00 | getMc(getCurrentStateFunc) | getWuc(getCurrentStateFunc)); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java index ce476f4f85237..ead041877797c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/PTM200Message.java @@ -55,9 +55,6 @@ protected void convertFromCommandImpl(String channelId, String channelTypeId, Co @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_GENERAL_SWITCHING: @@ -77,4 +74,9 @@ protected State convertToStateImpl(String channelId, String channelTypeId, return UnDefType.UNDEF; } + + @Override + public boolean isValidForTeachIn() { + return false; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java index 41cdcb1a88462..21dba840f65b3 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/UTEResponse.java @@ -27,11 +27,12 @@ public class UTEResponse extends _VLDMessage { public static final byte ResponseNeeded_MASK = 0x40; public static final byte TeachIn_NotSpecified = 0x20; - public UTEResponse(ERP1Message packet) { + public UTEResponse(ERP1Message packet, boolean teachIn) { int dataLength = packet.getPayload().length - ESP3_SENDERID_LENGTH - ESP3_RORG_LENGTH - ESP3_STATUS_LENGTH; setData(packet.getPayload(ESP3_RORG_LENGTH, dataLength)); - bytes[0] = (byte) 0x91; // bidirectional communication, teach in accepted, teach in response + bytes[0] = (byte) (teachIn ? 0x91 : 0xA1); // bidirectional communication, teach in accepted or teach out, teach + // in response setStatus((byte) 0x80); setSuppressRepeating(true); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java index 96d1200b0c4eb..651a786af8d72 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_4BSTeachInVariation3Response.java @@ -23,11 +23,11 @@ */ public class _4BSTeachInVariation3Response extends _4BSMessage { - public _4BSTeachInVariation3Response(ERP1Message packet) { + public _4BSTeachInVariation3Response(ERP1Message packet, boolean teachIn) { byte[] payload = packet.getPayload(ESP3_RORG_LENGTH, RORG._4BS.getDataLength()); - payload[3] = (byte) 0xF0; // telegram with EEP number and Manufacturer ID, - // EEP supported, Sender ID stored, Response + payload[3] = (byte) (teachIn ? 0xF0 : 0xD0); // telegram with EEP number and Manufacturer ID, + // EEP supported, Sender ID stored or deleted, Response setData(payload); setDestinationId(packet.getSenderId()); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java index 4f6afd303d39e..3dd44acc26e6d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Base/_RPSMessage.java @@ -51,4 +51,6 @@ public EEP setStatus(byte status) { return this; } + + public abstract boolean isValidForTeachIn(); } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java index c6d54180a628a..018b3ea06335b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/D2_05/D2_05_00.java @@ -98,7 +98,8 @@ protected byte getChannel() { @Override public void addConfigPropertiesTo(DiscoveryResultBuilder discoveredThingResultBuilder) { - discoveredThingResultBuilder.withProperty(PARAMETER_EEPID, getEEPType().getId()); + discoveredThingResultBuilder.withProperty(PARAMETER_SENDINGEEPID, getEEPType().getId()) + .withProperty(PARAMETER_RECEIVINGEEPID, getEEPType().getId()); } @Override diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java index 63e9c0fcf3f43..c84a1a753f7f7 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPFactory.java @@ -15,18 +15,24 @@ import static org.openhab.binding.enocean.internal.messages.ESP3Packet.*; import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; import org.openhab.binding.enocean.internal.eep.Base.UTEResponse; import org.openhab.binding.enocean.internal.eep.Base._4BSMessage; import org.openhab.binding.enocean.internal.eep.Base._4BSTeachInVariation3Response; +import org.openhab.binding.enocean.internal.eep.Base._RPSMessage; import org.openhab.binding.enocean.internal.eep.D5_00.D5_00_01; import org.openhab.binding.enocean.internal.eep.F6_01.F6_01_01; import org.openhab.binding.enocean.internal.eep.F6_02.F6_02_01; +import org.openhab.binding.enocean.internal.eep.F6_05.F6_05_02; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_00; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_00_EltakoFPE; import org.openhab.binding.enocean.internal.eep.F6_10.F6_10_01; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; +import org.openhab.binding.enocean.internal.messages.Responses.SMACKTeachInResponse; import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,8 +51,9 @@ public static EEP createEEP(EEPType eepType) { if (cl == null) { throw new IllegalArgumentException("Message " + eepType + " not implemented"); } - return cl.newInstance(); - } catch (IllegalAccessException | InstantiationException e) { + return cl.getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { throw new IllegalArgumentException(e); } } @@ -69,6 +76,21 @@ public static EEP buildEEP(EEPType eepType, ERP1Message packet) { } } + private static EEPType getGenericEEPTypeFor(byte rorg) { + logger.info("Received unsupported EEP teach in, trying to fallback to generic thing"); + RORG r = RORG.getRORG(rorg); + if (r == RORG._4BS) { + logger.info("Fallback to 4BS generic thing"); + return EEPType.Generic4BS; + } else if (r == RORG.VLD) { + logger.info("Fallback to VLD generic thing"); + return EEPType.GenericVLD; + } else { + logger.info("Fallback not possible"); + return null; + } + } + public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { if (!msg.getIsTeachIn() && !(msg.getRORG() == RORG.RPS)) { return null; @@ -77,38 +99,48 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { switch (msg.getRORG()) { case RPS: try { - EEP result = new F6_01_01(msg); - if (result.isValid()) { // check if t21 is set, nu not set, and data == 0x10 or 0x00 + _RPSMessage result = new F6_10_00(msg); + if (result.isValidForTeachIn()) { + return result; + } + } catch (Exception e) { + } + + try { + _RPSMessage result = new F6_10_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } try { - EEP result = new F6_02_01(msg); - if (result.isValid()) { // check if highest bit is not set + _RPSMessage result = new F6_02_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } try { - EEP result = new F6_10_00(msg); - if (result.isValid()) { + _RPSMessage result = new F6_05_02(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } + try { - EEP result = new F6_10_00_EltakoFPE(msg); - if (result.isValid()) { // check if data == 0x10 or 0x00 + _RPSMessage result = new F6_01_01(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { } + try { - EEP result = new F6_10_01(msg); - if (result.isValid()) { + _RPSMessage result = new F6_10_00_EltakoFPE(msg); + if (result.isValidForTeachIn()) { return result; } } catch (Exception e) { @@ -120,8 +152,8 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { case _4BS: { int db_0 = msg.getPayload()[4]; if ((db_0 & _4BSMessage.LRN_Type_Mask) == 0) { // Variation 1 - logger.info("Received 4BS Teach In variation 1 without EEP"); - return null; + logger.info("Received 4BS Teach In variation 1 without EEP, fallback to generic thing"); + return buildEEP(EEPType.Generic4BS, msg); } byte db_3 = msg.getPayload()[1]; @@ -132,19 +164,21 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { int type = ((db_3 & 0b11) << 5) + ((db_2 & 0xFF) >>> 3); int manufId = ((db_2 & 0b111) << 8) + (db_1 & 0xff); - logger.info("Received 4BS Teach In with EEP A5-{}-{} and manufacturerID {}", + logger.debug("Received 4BS Teach In with EEP A5-{}-{} and manufacturerID {}", HexUtils.bytesToHex(new byte[] { (byte) func }), HexUtils.bytesToHex(new byte[] { (byte) type }), HexUtils.bytesToHex(new byte[] { (byte) manufId })); EEPType eepType = EEPType.getType(RORG._4BS, func, type, manufId); if (eepType == null) { - logger.debug("Received unsupported EEP teach in, fallback to generic thing"); - eepType = EEPType.Generic4BS; + eepType = getGenericEEPTypeFor(RORG._4BS.getValue()); } - return buildEEP(eepType, msg); + if (eepType != null) { + return buildEEP(eepType, msg); + } } + break; case UTE: { byte[] payload = msg.getPayload(); @@ -161,38 +195,58 @@ public static EEP buildEEPFromTeachInERP1(ERP1Message msg) { EEPType eepType = EEPType.getType(RORG.getRORG(rorg), func, type, manufId); if (eepType == null) { - logger.info("Received unsupported EEP teach in, fallback to generic thing"); - RORG r = RORG.getRORG(rorg); - if (r == RORG._4BS) { - eepType = EEPType.Generic4BS; - } else if (r == RORG.VLD) { - eepType = EEPType.GenericVLD; - } else { - return null; - } + eepType = getGenericEEPTypeFor(rorg); } - return buildEEP(eepType, msg); + if (eepType != null) { + return buildEEP(eepType, msg); + } } - case Unknown: - case VLD: - case MSC: - case SIG: + break; + default: return null; } return null; } - public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] senderId) { + public static EEP buildEEPFromTeachInSMACKEvent(EventMessage event) { + if (event.getEventMessageType() != EventMessageType.SA_CONFIRM_LEARN) { + return null; + } + + byte[] payload = event.getPayload(); + byte manufIdMSB = payload[2]; + byte manufIdLSB = payload[3]; + int manufId = ((manufIdMSB & 0b111) << 8) + (manufIdLSB & 0xff); + + byte rorg = payload[4]; + int func = payload[5] & 0xFF; + int type = payload[6] & 0xFF; + + byte[] senderId = Arrays.copyOfRange(payload, 12, 12 + 4); + + logger.debug("Received SMACK Teach In with EEP {}-{}-{} and manufacturerID {}", + HexUtils.bytesToHex(new byte[] { (byte) rorg }), HexUtils.bytesToHex(new byte[] { (byte) func }), + HexUtils.bytesToHex(new byte[] { (byte) type }), HexUtils.bytesToHex(new byte[] { (byte) manufId })); + + EEPType eepType = EEPType.getType(RORG.getRORG(rorg), func, type, manufId); + if (eepType == null) { + eepType = getGenericEEPTypeFor(rorg); + } + + return createEEP(eepType).setSenderId(senderId); + } + + public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] senderId, boolean teachIn) { switch (msg.getRORG()) { case UTE: - EEP result = new UTEResponse(msg); + EEP result = new UTEResponse(msg, teachIn); result.setSenderId(senderId); return result; case _4BS: - result = new _4BSTeachInVariation3Response(msg); + result = new _4BSTeachInVariation3Response(msg, teachIn); result.setSenderId(senderId); return result; @@ -200,4 +254,31 @@ public static EEP buildResponseEEPFromTeachInERP1(ERP1Message msg, byte[] sender return null; } } + + public static SMACKTeachInResponse buildResponseFromSMACKTeachIn(EventMessage event, boolean sendTeachOuts) { + SMACKTeachInResponse response = new SMACKTeachInResponse(); + + byte priority = event.getPayload()[1]; + if ((priority & 0b1001) == 0b1001) { + logger.debug("gtw is already postmaster"); + if (sendTeachOuts) { + logger.debug("Repeated learn is not allow hence send teach out"); + response.setTeachOutResponse(); + } else { + logger.debug("Send a repeated learn in"); + response.setRepeatedTeachInResponse(); + } + } else if ((priority & 0b100) == 0) { + logger.debug("no place for further mailbox"); + response.setNoPlaceForFurtherMailbox(); + } else if ((priority & 0b10) == 0) { + logger.debug("rssi is not good enough"); + response.setBadRSSI(); + } else if ((priority & 0b1) == 0b1) { + logger.debug("gtw is candidate for postmaster => teach in"); + response.setTeachIn(); + } + + return response; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java index 451c853303fdd..26c461ae8257c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/EEPType.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.enocean.internal.EnOceanChannelDescription; +import org.openhab.binding.enocean.internal.config.EnOceanChannelTransformationConfig; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_01; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_02; import org.openhab.binding.enocean.internal.eep.A5_02.A5_02_03; @@ -164,15 +165,9 @@ public enum EEPType { UTEResponse(RORG.UTE, 0, 0, false, UTEResponse.class, null), _4BSTeachInVariation3Response(RORG._4BS, 0, 0, false, _4BSTeachInVariation3Response.class, null), - GenericRPS(RORG.RPS, 0xFF, 0xFF, false, GenericRPS.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD), - Generic4BS(RORG._4BS, 0xFF, 0xFF, false, Generic4BS.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD, CHANNEL_VIBRATION), - GenericVLD(RORG.VLD, 0xFF, 0xFF, false, GenericVLD.class, THING_TYPE_GENERICTHING, CHANNEL_GENERIC_SWITCH, - CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_DIMMER, CHANNEL_GENERIC_NUMBER, CHANNEL_GENERIC_STRING, - CHANNEL_GENERIC_COLOR, CHANNEL_GENERIC_TEACHINCMD), + GenericRPS(RORG.RPS, 0xFF, 0xFF, false, GenericRPS.class, THING_TYPE_GENERICTHING), + Generic4BS(RORG._4BS, 0xFF, 0xFF, false, Generic4BS.class, THING_TYPE_GENERICTHING, CHANNEL_VIBRATION), + GenericVLD(RORG.VLD, 0xFF, 0xFF, false, GenericVLD.class, THING_TYPE_GENERICTHING), PTM200(RORG.RPS, 0x00, 0x00, false, PTM200Message.class, null, CHANNEL_GENERAL_SWITCHING, CHANNEL_ROLLERSHUTTER, CHANNEL_CONTACT), @@ -391,8 +386,8 @@ public enum EEPType { // UniversalCommand(RORG._4BS, 0x3f, 0x7f, false, A5_3F_7F_Universal.class, THING_TYPE_UNIVERSALACTUATOR, // CHANNEL_GENERIC_ROLLERSHUTTER, CHANNEL_GENERIC_LIGHT_SWITCHING, CHANNEL_GENERIC_DIMMER, CHANNEL_TEACHINCMD), - EltakoFSB(RORG._4BS, 0x3f, 0x7f, false, "EltakoFSB", 0, A5_3F_7F_EltakoFSB.class, THING_TYPE_ROLLERSHUTTER, 0, - new Hashtable () { + EltakoFSB(RORG._4BS, 0x3f, 0x7f, false, false, "EltakoFSB", 0, A5_3F_7F_EltakoFSB.class, THING_TYPE_ROLLERSHUTTER, + 0, new Hashtable () { private static final long serialVersionUID = 1L; { put(CHANNEL_ROLLERSHUTTER, new Configuration()); @@ -404,10 +399,10 @@ public enum EEPType { } }), - Thermostat(RORG._4BS, 0x20, 0x04, false, A5_20_04.class, THING_TYPE_THERMOSTAT, CHANNEL_VALVE_POSITION, + Thermostat(RORG._4BS, 0x20, 0x04, false, true, A5_20_04.class, THING_TYPE_THERMOSTAT, CHANNEL_VALVE_POSITION, CHANNEL_BUTTON_LOCK, CHANNEL_DISPLAY_ORIENTATION, CHANNEL_TEMPERATURE_SETPOINT, CHANNEL_TEMPERATURE, CHANNEL_FEED_TEMPERATURE, CHANNEL_MEASUREMENT_CONTROL, CHANNEL_FAILURE_CODE, CHANNEL_WAKEUPCYCLE, - CHANNEL_SERVICECOMMAND, CHANNEL_STATUS_REQUEST_EVENT, CHANNEL_SEND_COMMAND), + CHANNEL_SERVICECOMMAND), SwitchWithEnergyMeasurment_00(RORG.VLD, 0x01, 0x00, true, D2_01_00.class, THING_TYPE_MEASUREMENTSWITCH, CHANNEL_GENERAL_SWITCHING, CHANNEL_TOTALUSAGE), @@ -512,23 +507,36 @@ public enum EEPType { private boolean supportsRefresh; + private boolean requestsResponse; + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, String... channelIds) { this(rorg, func, type, supportsRefresh, eepClass, thingTypeUID, -1, channelIds); } + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, + Class extends EEP> eepClass, ThingTypeUID thingTypeUID, String... channelIds) { + this(rorg, func, type, supportsRefresh, requestsResponse, eepClass, thingTypeUID, -1, channelIds); + } + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, String... channelIds) { - this(rorg, func, type, supportsRefresh, manufactorSuffix, manufId, eepClass, thingTypeUID, 0, channelIds); + this(rorg, func, type, supportsRefresh, false, manufactorSuffix, manufId, eepClass, thingTypeUID, 0, + channelIds); } EEPType(RORG rorg, int func, int type, boolean supportsRefresh, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { - this(rorg, func, type, supportsRefresh, "", 0, eepClass, thingTypeUID, command, channelIds); + this(rorg, func, type, supportsRefresh, false, "", 0, eepClass, thingTypeUID, command, channelIds); } - EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { + this(rorg, func, type, supportsRefresh, requestsResponse, "", 0, eepClass, thingTypeUID, command, channelIds); + } + + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, String manufactorSuffix, + int manufId, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, int command, String... channelIds) { this.rorg = rorg; this.func = func; this.type = type; @@ -538,24 +546,18 @@ public enum EEPType { this.manufactorSuffix = manufactorSuffix; this.manufactorId = manufId; this.supportsRefresh = supportsRefresh; + this.requestsResponse = requestsResponse; for (String id : channelIds) { this.channelIdsWithConfig.put(id, new Configuration()); this.supportedChannels.put(id, CHANNELID2CHANNELDESCRIPTION.get(id)); } - this.channelIdsWithConfig.put(CHANNEL_RSSI, new Configuration()); - this.supportedChannels.put(CHANNEL_RSSI, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_RSSI)); - - this.channelIdsWithConfig.put(CHANNEL_REPEATCOUNT, new Configuration()); - this.supportedChannels.put(CHANNEL_REPEATCOUNT, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_REPEATCOUNT)); - - this.channelIdsWithConfig.put(CHANNEL_LASTRECEIVED, new Configuration()); - this.supportedChannels.put(CHANNEL_LASTRECEIVED, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_LASTRECEIVED)); + addDefaultChannels(); } - EEPType(RORG rorg, int func, int type, boolean supportsRefresh, String manufactorSuffix, int manufId, - Class extends EEP> eepClass, ThingTypeUID thingTypeUID, int command, + EEPType(RORG rorg, int func, int type, boolean supportsRefresh, boolean requestsResponse, String manufactorSuffix, + int manufId, Class extends EEP> eepClass, ThingTypeUID thingTypeUID, int command, Hashtable channelConfigs) { this.rorg = rorg; this.func = func; @@ -567,11 +569,46 @@ public enum EEPType { this.manufactorSuffix = manufactorSuffix; this.manufactorId = manufId; this.supportsRefresh = supportsRefresh; + this.requestsResponse = requestsResponse; for (String id : channelConfigs.keySet()) { this.supportedChannels.put(id, CHANNELID2CHANNELDESCRIPTION.get(id)); } + addDefaultChannels(); + } + + private void addDefaultChannels() { + + if (THING_TYPE_GENERICTHING.equals(this.thingTypeUID)) { + this.channelIdsWithConfig.put(CHANNEL_GENERIC_SWITCH, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_SWITCH, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_SWITCH)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_ROLLERSHUTTER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_ROLLERSHUTTER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_ROLLERSHUTTER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_DIMMER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_DIMMER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_DIMMER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_NUMBER, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_NUMBER, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_NUMBER)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_STRING, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_STRING, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_STRING)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_COLOR, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_COLOR, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_COLOR)); + + this.channelIdsWithConfig.put(CHANNEL_GENERIC_TEACHINCMD, new EnOceanChannelTransformationConfig()); + this.supportedChannels.put(CHANNEL_GENERIC_TEACHINCMD, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_GENERIC_TEACHINCMD)); + } + this.channelIdsWithConfig.put(CHANNEL_RSSI, new Configuration()); this.supportedChannels.put(CHANNEL_RSSI, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_RSSI)); @@ -580,6 +617,12 @@ public enum EEPType { this.channelIdsWithConfig.put(CHANNEL_LASTRECEIVED, new Configuration()); this.supportedChannels.put(CHANNEL_LASTRECEIVED, CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_LASTRECEIVED)); + + if (requestsResponse) { + this.channelIdsWithConfig.put(CHANNEL_STATUS_REQUEST_EVENT, new Configuration()); + this.supportedChannels.put(CHANNEL_STATUS_REQUEST_EVENT, + CHANNELID2CHANNELDESCRIPTION.get(CHANNEL_STATUS_REQUEST_EVENT)); + } } public Class extends EEP> getEEPClass() { @@ -602,6 +645,10 @@ public boolean getSupportsRefresh() { return supportsRefresh; } + public boolean getRequstesResponse() { + return requestsResponse; + } + public Map GetSupportedChannels() { return Collections.unmodifiableMap(supportedChannels); } @@ -614,7 +661,7 @@ public boolean isChannelSupported(Channel channel) { } public boolean isChannelSupported(String channelId, String channelTypeId) { - return supportedChannels.containsKey(channelId) + return supportedChannels.containsKey(channelId) || VIRTUALCHANNEL_SEND_COMMAND.equals(channelId) || supportedChannels.values().stream().anyMatch(c -> c.channelTypeUID.getId().equals(channelTypeId)); } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java index ce19528af36f0..b69aa8d4d6386 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_01/F6_01_01.java @@ -34,9 +34,6 @@ public F6_01_01(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } return getBit(bytes[0], 4) ? CommonTriggerEvents.PRESSED : CommonTriggerEvents.RELEASED; } @@ -45,4 +42,10 @@ protected String convertToEventImpl(String channelId, String channelTypeId, Stri protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && !getBit(bytes[0], 7); } + + @Override + public boolean isValidForTeachIn() { + // just treat press as teach in, ignore release + return t21 && !nu && bytes[0] == 0x10; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java index 2af97fdc06f67..8152c5e90a30a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_01.java @@ -55,9 +55,6 @@ public F6_02_01(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } if (t21 && nu) { byte dir1 = channelId.equals(CHANNEL_ROCKERSWITCH_CHANNELA) ? A0 : B0; @@ -112,11 +109,6 @@ protected State convertToStateImpl(String channelId, String channelTypeId, // this method is used by the classic device listener channels to convert an rocker switch message into an // appropriate item update State currentState = getCurrentStateFunc.apply(channelId); - - if (!isValid()) { - return UnDefType.UNDEF; - } - if (t21 && nu) { EnOceanChannelVirtualRockerSwitchConfig c = config.as(EnOceanChannelVirtualRockerSwitchConfig.class); byte dir1 = c.getChannel() == Channel.ChannelA ? A0 : B0; @@ -179,4 +171,22 @@ private State inverse(UpDownType currentState) { protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && !getBit(bytes[0], 7); } + + @Override + public boolean isValidForTeachIn() { + if (t21) { + // just treat press as teach in => DB0.4 has to be set + if (!getBit(bytes[0], 4)) { + return false; + } + // DB0.7 is never set for rocker switch message + if (getBit(bytes[0], 7)) { + return false; + } + } else { + return false; + } + + return true; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java index 29b643cb01a1a..76e169785171a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_02/F6_02_02.java @@ -52,9 +52,6 @@ public F6_02_02(ERP1Message packet) { @Override protected String convertToEventImpl(String channelId, String channelTypeId, String lastEvent, Configuration config) { - if (!isValid()) { - return null; - } if (t21 && nu) { byte dir1 = channelId.equals(CHANNEL_ROCKERSWITCH_CHANNELA) ? AI : BI; @@ -109,11 +106,6 @@ protected State convertToStateImpl(String channelId, String channelTypeId, // this method is used by the classic device listener channels to convert an rocker switch message into an // appropriate item update State currentState = getCurrentStateFunc.apply(channelId); - - if (!isValid()) { - return UnDefType.UNDEF; - } - if (t21 && nu) { EnOceanChannelVirtualRockerSwitchConfig c = config.as(EnOceanChannelVirtualRockerSwitchConfig.class); byte dir1 = c.getChannel() == Channel.ChannelA ? AI : BI; @@ -171,4 +163,9 @@ private State inverse(OnOffType currentState) { private State inverse(UpDownType currentState) { return currentState == UpDownType.UP ? UpDownType.DOWN : UpDownType.UP; } + + @Override + public boolean isValidForTeachIn() { + return false; // Never treat a message as F6-02-02, let user decide which orientation of rocker switch is used + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java index 50855ab2e92cc..4fafc5b68ac6e 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_05/F6_05_02.java @@ -44,9 +44,6 @@ public F6_05_02(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } switch (channelId) { case CHANNEL_SMOKEDETECTION: @@ -62,4 +59,10 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && (bytes[0] == ALARM_OFF || bytes[0] == ALARM_ON || bytes[0] == ENERGY_LOW); } + + @Override + public boolean isValidForTeachIn() { + // just treat the first message with ALARM_ON as teach in + return !t21 && !nu && bytes[0] == ALARM_ON; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java index 43fc0f75344d4..498a807458fdb 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00.java @@ -47,9 +47,6 @@ public F6_10_00(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } byte data = (byte) (bytes[0] & 0xF0); @@ -82,4 +79,9 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && getBit(bytes[0], 7) && getBit(bytes[0], 6); } + + @Override + public boolean isValidForTeachIn() { + return t21 && !nu && getBit(bytes[0], 7) && getBit(bytes[0], 6); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java index 8a381ea9e1600..ee9cd485f9c9b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_00_EltakoFPE.java @@ -61,4 +61,10 @@ protected boolean validateData(byte[] bytes) { // FPE just sends 0b00010000 or 0b00000000 value, so we apply mask 0b11101111 return super.validateData(bytes) && ((bytes[0] & (byte) 0xEF) == (byte) 0x00); } + + @Override + public boolean isValidForTeachIn() { + // just treat CLOSED as teach in + return bytes[0] == CLOSED; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java index 51ed9306212f0..3773cafe49c8d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/F6_10/F6_10_01.java @@ -47,9 +47,6 @@ public F6_10_01(ERP1Message packet) { @Override protected State convertToStateImpl(String channelId, String channelTypeId, Function getCurrentStateFunc, Configuration config) { - if (!isValid()) { - return UnDefType.UNDEF; - } byte data = (byte) (bytes[0] & 0x0F); @@ -82,4 +79,10 @@ protected State convertToStateImpl(String channelId, String channelTypeId, protected boolean validateData(byte[] bytes) { return super.validateData(bytes) && getBit(bytes[0], 6) && getBit(bytes[0], 3) && getBit(bytes[0], 2); } + + @Override + public boolean isValidForTeachIn() { + return !getBit(bytes[0], 7) && getBit(bytes[0], 6) && !getBit(bytes[0], 5) && !getBit(bytes[0], 4) + && getBit(bytes[0], 3) && getBit(bytes[0], 2); + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java index 178ae890b8689..d930e8206400b 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/eep/Generic/GenericEEP.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.enocean.internal.eep.Generic; -import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.PARAMETER_EEPID; +import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import static org.openhab.binding.enocean.internal.messages.ESP3Packet.*; import java.lang.reflect.InvocationTargetException; @@ -161,6 +161,7 @@ protected boolean validateData(byte[] bytes) { @Override public void addConfigPropertiesTo(DiscoveryResultBuilder discoveredThingResultBuilder) { - discoveredThingResultBuilder.withProperty(PARAMETER_EEPID, getEEPType().getId()); + discoveredThingResultBuilder.withProperty(PARAMETER_SENDINGEEPID, getEEPType().getId()) + .withProperty(PARAMETER_RECEIVINGEEPID, getEEPType().getId()); } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java index 590769153791d..be2398ac0cf6c 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseActuatorHandler.java @@ -28,6 +28,7 @@ import org.openhab.binding.enocean.internal.eep.EEPType; import org.openhab.binding.enocean.internal.messages.BasePacket; import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -68,8 +69,8 @@ public EnOceanBaseActuatorHandler(Thing thing, ItemChannelLinkRegistry itemChann * @param senderIdOffset to be validated * @return true if senderIdOffset is between ]0;128[ and is not used yet */ - private boolean validateSenderIdOffset(int senderIdOffset) { - if (senderIdOffset == -1) { + private boolean validateSenderIdOffset(Integer senderIdOffset) { + if (senderIdOffset == null) { return true; } @@ -157,26 +158,24 @@ boolean validateConfig() { } private boolean initializeIdForSending() { - // Generic things are treated as actuator things, however to support also generic sensors one can define a - // senderIdOffset of -1 - // TODO: seperate generic actuators from generic sensors? - String thingTypeId = this.getThing().getThingTypeUID().getId(); - String genericThingTypeId = THING_TYPE_GENERICTHING.getId(); - - if (getConfiguration().senderIdOffset == -1 && thingTypeId.equals(genericThingTypeId)) { - return true; - } - EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); if (bridgeHandler == null) { return false; } - // if senderIdOffset is not set (=> defaults to -1) or set to -1, the next free senderIdOffset is determined - if (getConfiguration().senderIdOffset == -1) { + // Generic things are treated as actuator things, however to support also generic sensors one can omit + // senderIdOffset + // TODO: seperate generic actuators from generic sensors? + if ((getConfiguration().senderIdOffset == null + && THING_TYPE_GENERICTHING.equals(this.getThing().getThingTypeUID()))) { + return true; + } + + // if senderIdOffset is not set, the next free senderIdOffset is determined + if (getConfiguration().senderIdOffset == null) { Configuration updateConfig = editConfiguration(); getConfiguration().senderIdOffset = bridgeHandler.getNextSenderId(thing); - if (getConfiguration().senderIdOffset == -1) { + if (getConfiguration().senderIdOffset == null) { configurationErrorDescription = "Could not get a free sender Id from Bridge"; return false; } @@ -185,12 +184,10 @@ private boolean initializeIdForSending() { } byte[] baseId = bridgeHandler.getBaseId(); - baseId[3] = (byte) ((baseId[3] & 0xFF) + getConfiguration().senderIdOffset); + baseId[3] = (byte) ((baseId[3] + getConfiguration().senderIdOffset) & 0xFF); this.senderId = baseId; - - this.updateProperty(PROPERTY_ENOCEAN_ID, HexUtils.bytesToHex(this.senderId)); + this.updateProperty(PROPERTY_SENDINGENOCEAN_ID, HexUtils.bytesToHex(this.senderId)); bridgeHandler.addSender(getConfiguration().senderIdOffset, thing); - return true; } @@ -203,6 +200,22 @@ private void refreshStates() { } } + @Override + protected void sendRequestResponse() { + sendMessage(VIRTUALCHANNEL_SEND_COMMAND, VIRTUALCHANNEL_SEND_COMMAND, OnOffType.ON, null); + } + + protected void sendMessage(String channelId, String channelTypeId, Command command, Configuration channelConfig) { + EEP eep = EEPFactory.createEEP(sendingEEPType); + if (eep.convertFromCommand(channelId, channelTypeId, command, id -> getCurrentState(id), channelConfig) + .hasData()) { + BasePacket msg = eep.setSenderId(senderId).setDestinationId(destinationId) + .setSuppressRepeating(getConfiguration().suppressRepeating).getERP1Message(); + + getBridgeHandler().sendMessage(msg, null); + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { // We must have a valid sendingEEPType and sender id to send commands @@ -237,16 +250,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { Configuration channelConfig = channel.getConfiguration(); - - EEP eep = EEPFactory.createEEP(sendingEEPType); - if (eep.convertFromCommand(channelId, channelTypeId, command, id -> getCurrentState(id), channelConfig) - .hasData()) { - BasePacket msg = eep.setSenderId(senderId).setDestinationId(destinationId) - .setSuppressRepeating(getConfiguration().suppressRepeating).getERP1Message(); - - getBridgeHandler().sendMessage(msg, null); - } - + sendMessage(channelId, channelTypeId, command, channelConfig); } catch (IllegalArgumentException e) { logger.warn("Exception while sending telegram!", e); } @@ -254,11 +258,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void handleRemoval() { - if (getConfiguration().senderIdOffset > 0) { - EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); - if (bridgeHandler != null) { + + EnOceanBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler != null) { + if (getConfiguration().senderIdOffset != null && getConfiguration().senderIdOffset > 0) { bridgeHandler.removeSender(getConfiguration().senderIdOffset); } + + if (bridgeHandler.isSmackClient(this.thing)) { + logger.warn("Removing smack client (ThingId: {}) without teach out!", this.thing.getUID().getId()); + } } super.handleRemoval(); diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java index d533c1e558c4a..b4fed2ccb4ad7 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseSensorHandler.java @@ -19,8 +19,11 @@ import java.util.Comparator; import java.util.Hashtable; import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import org.apache.commons.lang3.NotImplementedException; import org.openhab.binding.enocean.internal.config.EnOceanBaseConfig; import org.openhab.binding.enocean.internal.eep.EEP; import org.openhab.binding.enocean.internal.eep.EEPFactory; @@ -57,6 +60,8 @@ public class EnOceanBaseSensorHandler extends EnOceanBaseThingHandler implements protected final Hashtable receivingEEPTypes = new Hashtable<>(); + protected ScheduledFuture> responseFuture = null; + public EnOceanBaseSensorHandler(Thing thing, ItemChannelLinkRegistry itemChannelLinkRegistry) { super(thing, itemChannelLinkRegistry); } @@ -104,7 +109,7 @@ boolean validateConfig() { } @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { return Long.parseLong(config.enoceanId, 16); } @@ -129,6 +134,10 @@ protected Predicate channelFilter(EEPType eepType, byte[] senderId) { }; } + protected void sendRequestResponse() { + throw new NotImplementedException("Sensor cannot send responses"); + } + @Override public void packetReceived(BasePacket packet) { ERP1Message msg = (ERP1Message) packet; @@ -175,6 +184,15 @@ public void packetReceived(BasePacket packet) { break; } }); + + if (receivingEEPType.getRequstesResponse()) { + // fire trigger for receive + triggerChannel(prepareAnswer, "requestAnswer"); + // Send response after 100ms + if (responseFuture == null || responseFuture.isDone()) { + responseFuture = scheduler.schedule(this::sendRequestResponse, 100, TimeUnit.MILLISECONDS); + } + } } } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java index 20ccfa80b5062..f561db01859cf 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBaseThingHandler.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.enocean.internal.handler; +import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; + import java.util.AbstractMap.SimpleEntry; import java.util.Collection; import java.util.Hashtable; @@ -63,18 +65,25 @@ public abstract class EnOceanBaseThingHandler extends ConfigStatusThingHandler { private ItemChannelLinkRegistry itemChannelLinkRegistry; + protected @NonNull ChannelUID prepareAnswer; + public EnOceanBaseThingHandler(Thing thing, ItemChannelLinkRegistry itemChannelLinkRegistry) { super(thing); this.itemChannelLinkRegistry = itemChannelLinkRegistry; + prepareAnswer = new ChannelUID(thing.getUID(), CHANNEL_STATUS_REQUEST_EVENT); } - @SuppressWarnings("null") @Override public void initialize() { logger.debug("Initializing enocean base thing handler."); this.gateway = null; // reset gateway in case we change the bridge this.config = null; - initializeThing((getBridge() == null) ? null : getBridge().getStatus()); + Bridge bridge = getBridge(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "A bridge is required"); + } else { + initializeThing(bridge.getStatus()); + } } private void initializeThing(ThingStatus bridgeStatus) { @@ -143,6 +152,10 @@ protected void updateChannels() { String channelId = entry.getKey(); EnOceanChannelDescription cd = entry.getValue().GetSupportedChannels().get(channelId); + if (cd == null) { + return; + } + // if we do not need to auto create channel => skip if (!cd.autoCreate) { return; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java index c888f6a60be2d..e793366f26698 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanBridgeHandler.java @@ -15,30 +15,35 @@ import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import java.io.IOException; -import java.math.BigDecimal; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.openhab.binding.enocean.internal.EnOceanConfigStatusMessage; import org.openhab.binding.enocean.internal.config.EnOceanBaseConfig; import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig; +import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig.ESPVersion; import org.openhab.binding.enocean.internal.messages.BasePacket; -import org.openhab.binding.enocean.internal.messages.BaseResponse; import org.openhab.binding.enocean.internal.messages.ESP3PacketFactory; -import org.openhab.binding.enocean.internal.messages.RDBaseIdResponse; -import org.openhab.binding.enocean.internal.messages.RDRepeaterResponse; -import org.openhab.binding.enocean.internal.messages.RDVersionResponse; import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.binding.enocean.internal.messages.Response.ResponseType; +import org.openhab.binding.enocean.internal.messages.Responses.BaseResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDBaseIdResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDLearnedClientsResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDLearnedClientsResponse.LearnedClient; +import org.openhab.binding.enocean.internal.messages.Responses.RDRepeaterResponse; +import org.openhab.binding.enocean.internal.messages.Responses.RDVersionResponse; import org.openhab.binding.enocean.internal.transceiver.EnOceanESP2Transceiver; import org.openhab.binding.enocean.internal.transceiver.EnOceanESP3Transceiver; import org.openhab.binding.enocean.internal.transceiver.EnOceanTransceiver; import org.openhab.binding.enocean.internal.transceiver.PacketListener; import org.openhab.binding.enocean.internal.transceiver.ResponseListener; import org.openhab.binding.enocean.internal.transceiver.ResponseListenerIgnoringTimeouts; +import org.openhab.binding.enocean.internal.transceiver.TeachInListener; import org.openhab.binding.enocean.internal.transceiver.TransceiverErrorListener; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.status.ConfigStatusMessage; @@ -76,9 +81,12 @@ public class EnOceanBridgeHandler extends ConfigStatusBridgeHandler implements T private byte[] baseId = null; private Thing[] sendingThings = new Thing[128]; - private int nextSenderId = 0; private SerialPortManager serialPortManager; + private boolean smackAvailable = false; + private boolean sendTeachOuts = true; + private Set smackClients = Set.of(); + public EnOceanBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) { super(bridge); this.serialPortManager = serialPortManager; @@ -157,13 +165,6 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SerialPortManager could not be found"); } else { - Object devId = getConfig().get(NEXTSENDERID); - if (devId != null) { - nextSenderId = ((BigDecimal) devId).intValue(); - } else { - nextSenderId = 0; - } - if (connectorTask == null || connectorTask.isDone()) { connectorTask = scheduler.scheduleWithFixedDelay(new Runnable() { @Override @@ -187,9 +188,12 @@ private synchronized void initTransceiver() { switch (c.getESPVersion()) { case ESP2: transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager); + smackAvailable = false; + sendTeachOuts = false; break; case ESP3: transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager); + sendTeachOuts = c.sendTeachOuts; break; default: break; @@ -200,6 +204,7 @@ private synchronized void initTransceiver() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread..."); transceiver.StartReceiving(scheduler); + logger.info("EnOceanSerialTransceiver RX thread up and running"); if (c.rs485) { if (c.rs485BaseId != null && !c.rs485BaseId.isEmpty()) { @@ -238,6 +243,28 @@ public void responseReceived(RDBaseIdResponse response) { } } }); + + if (c.getESPVersion() == ESPVersion.ESP3) { + logger.debug("set postmaster mailboxes"); + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_POSTMASTER((byte) (c.enableSmack ? 20 : 0)), + new ResponseListenerIgnoringTimeouts () { + + @Override + public void responseReceived(BaseResponse response) { + + logger.debug("received response for postmaster mailboxes"); + if (response.isOK()) { + updateProperty("Postmaster mailboxes:", + Integer.toString(c.enableSmack ? 20 : 0)); + smackAvailable = c.enableSmack; + refreshProperties(); + } else { + updateProperty("Postmaster mailboxes:", "Not supported"); + smackAvailable = false; + } + } + }); + } } logger.debug("request version info"); @@ -283,7 +310,7 @@ public Collection getConfigStatus() { Collection configStatusMessages = new LinkedList<>(); // The serial port must be provided - String path = (String) getThing().getConfiguration().get(PATH); + String path = getThing().getConfiguration().as(EnOceanBridgeConfig.class).path; if (path == null || path.isEmpty()) { configStatusMessages.add(ConfigStatusMessage.Builder.error(PATH) .withMessageKeySuffix(EnOceanConfigStatusMessage.PORT_MISSING.getMessageKey()).withArguments(PATH) @@ -297,30 +324,33 @@ public byte[] getBaseId() { return baseId.clone(); } - public int getNextSenderId(Thing sender) { - // TODO: change id to enoceanId + public boolean isSmackClient(Thing sender) { + return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId); + } + + public Integer getNextSenderId(Thing sender) { return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId); } - public int getNextSenderId(String senderId) { - if (nextSenderId != 0 && sendingThings[nextSenderId] == null) { - int result = nextSenderId; - Configuration config = getConfig(); - config.put(NEXTSENDERID, null); - updateConfiguration(config); - nextSenderId = 0; + public Integer getNextSenderId(String enoceanId) { + EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class); - return result; + if (config.nextSenderId != null && sendingThings[config.nextSenderId] == null) { + Configuration c = this.editConfiguration(); + c.put(PARAMETER_NEXT_SENDERID, null); + updateConfiguration(c); + + return config.nextSenderId; } - for (byte i = 1; i < sendingThings.length; i++) { + for (int i = 1; i < sendingThings.length; i++) { if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId - .equalsIgnoreCase(senderId)) { + .equalsIgnoreCase(enoceanId)) { return i; } } - return -1; + return null; } public boolean existsSender(int id, Thing sender) { @@ -345,7 +375,7 @@ public void sendMessage(BasePacket message, ResponseListene } public void addPacketListener(PacketListener listener) { - addPacketListener(listener, listener.getSenderIdToListenTo()); + addPacketListener(listener, listener.getEnOceanIdToListenTo()); } public void addPacketListener(PacketListener listener, long senderIdToListenTo) { @@ -355,7 +385,7 @@ public void addPacketListener(PacketListener listener, long senderIdToListenTo) } public void removePacketListener(PacketListener listener) { - removePacketListener(listener, listener.getSenderIdToListenTo()); + removePacketListener(listener, listener.getEnOceanIdToListenTo()); } public void removePacketListener(PacketListener listener, long senderIdToListenTo) { @@ -364,12 +394,74 @@ public void removePacketListener(PacketListener listener, long senderIdToListenT } } - public void startDiscovery(PacketListener teachInListener) { + public void startDiscovery(TeachInListener teachInListener) { transceiver.startDiscovery(teachInListener); + + if (smackAvailable) { + // activate smack teach in + logger.debug("activate smack teach in"); + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true), + new ResponseListenerIgnoringTimeouts () { + + @Override + public void responseReceived(BaseResponse response) { + + if (response.isOK()) { + logger.debug("Smack teach in activated"); + } + } + }); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + } + } } public void stopDiscovery() { transceiver.stopDiscovery(); + + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null); + refreshProperties(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + } + } + + private void refreshProperties() { + if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) { + + logger.debug("request learned smack clients"); + try { + transceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS, + new ResponseListenerIgnoringTimeouts () { + + @Override + public void responseReceived(RDLearnedClientsResponse response) { + + logger.debug("received response for learned smack clients"); + if (response.isValid() && response.isOK()) { + LearnedClient[] clients = response.getLearnedClients(); + updateProperty("Learned smart ack clients", Integer.toString(clients.length)); + updateProperty("Smart ack clients", + Arrays.stream(clients) + .map(x -> String.format("%s (MB Idx: %d)", + HexUtils.bytesToHex(x.clientId), x.mailboxIndex)) + .collect(Collectors.joining(", "))); + smackClients = Arrays.stream(clients).map(x -> HexUtils.bytesToHex(x.clientId)) + .collect(Collectors.toSet()); + } + } + }); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Smack packet could not be send: " + e.getMessage()); + + } + } } @Override @@ -378,4 +470,8 @@ public void ErrorOccured(Throwable exception) { transceiver = null; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage()); } + + public boolean sendTeachOuts() { + return sendTeachOuts; + } } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java index 412cd9ad8d8fe..08a1e499e82d4 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/handler/EnOceanClassicDeviceHandler.java @@ -71,7 +71,7 @@ void initializeConfig() { } @Override - public long getSenderIdToListenTo() { + public long getEnOceanIdToListenTo() { return 0; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java index 73551cdc83bbe..f410d6e1aba8a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/ESP3PacketFactory.java @@ -13,8 +13,10 @@ package org.openhab.binding.enocean.internal.messages; import org.openhab.binding.enocean.internal.EnOceanBindingConstants; +import org.openhab.binding.enocean.internal.Helper; import org.openhab.binding.enocean.internal.messages.BasePacket.ESPPacketType; import org.openhab.binding.enocean.internal.messages.CCMessage.CCMessageType; +import org.openhab.binding.enocean.internal.messages.SAMessage.SAMessageType; import org.openhab.core.library.types.StringType; /** @@ -42,6 +44,28 @@ public static BasePacket CO_WR_REPEATER(StringType level) { } } + public static BasePacket SA_WR_LEARNMODE(boolean activate) { + return new SAMessage(SAMessageType.SA_WR_LEARNMODE, + new byte[] { SAMessageType.SA_WR_LEARNMODE.getValue(), (byte) (activate ? 1 : 0), 0, 0, 0, 0, 0 }); + } + + public final static BasePacket SA_RD_LEARNEDCLIENTS = new SAMessage(SAMessageType.SA_RD_LEARNEDCLIENTS); + + public static BasePacket SA_RD_MAILBOX_STATUS(byte[] clientId, byte[] controllerId) { + return new SAMessage(SAMessageType.SA_RD_MAILBOX_STATUS, + Helper.concatAll(new byte[] { SAMessageType.SA_RD_MAILBOX_STATUS.getValue() }, clientId, controllerId)); + } + + public static BasePacket SA_WR_POSTMASTER(byte mailboxes) { + return new SAMessage(SAMessageType.SA_WR_POSTMASTER, + new byte[] { SAMessageType.SA_WR_POSTMASTER.getValue(), mailboxes }); + } + + public static BasePacket SA_WR_CLIENTLEARNRQ(byte manu1, byte manu2, byte rorg, byte func, byte type) { + return new SAMessage(SAMessageType.SA_WR_CLIENTLEARNRQ, + new byte[] { SAMessageType.SA_WR_CLIENTLEARNRQ.getValue(), manu1, manu2, rorg, func, type }); + } + public static BasePacket BuildPacket(int dataLength, int optionalDataLength, byte packetType, byte[] payload) { ESPPacketType type = ESPPacketType.getPacketType(packetType); @@ -50,6 +74,8 @@ public static BasePacket BuildPacket(int dataLength, int optionalDataLength, byt return new Response(dataLength, optionalDataLength, payload); case RADIO_ERP1: return new ERP1Message(dataLength, optionalDataLength, payload); + case EVENT: + return new EventMessage(dataLength, optionalDataLength, payload); default: return null; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java new file mode 100644 index 0000000000000..4cacceba47052 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/EventMessage.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages; + +import java.util.stream.Stream; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class EventMessage extends BasePacket { + + public enum EventMessageType { + UNKNOWN((byte) 0x00, 1), + SA_RECLAIM_NOT_SUCCESSFUL((byte) 0x01, 1), + SA_CONFIRM_LEARN((byte) 0x02, 17), + SA_LEARN_ACK((byte) 0x03, 4), + CO_READY((byte) 0x04, 2), + CO_EVENT_SECUREDEVICES((byte) 0x05, 6), + CO_DUTYCYCLE_LIMIT((byte) 0x06, 2), + CO_TRANSMIT_FAILED((byte) 0x07, 2); + + private byte value; + private int dataLength; + + EventMessageType(byte value, int dataLength) { + this.value = value; + this.dataLength = dataLength; + } + + public byte getValue() { + return this.value; + } + + public int getDataLength() { + return dataLength; + } + + public static EventMessageType getEventMessageType(byte value) { + return Stream.of(EventMessageType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } + + private EventMessageType type; + + EventMessage(int dataLength, int optionalDataLength, byte[] payload) { + super(dataLength, optionalDataLength, ESPPacketType.EVENT, payload); + + type = EventMessageType.getEventMessageType(payload[0]); + } + + public EventMessageType getEventMessageType() { + return type; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java index 2c8a0699fe366..e7b8f2094d30a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Response.java @@ -57,7 +57,7 @@ public static ResponseType getResponsetype(byte value) { protected ResponseType responseType; protected boolean _isValid = false; - protected Response(int dataLength, int optionalDataLength, byte[] payload) { + public Response(int dataLength, int optionalDataLength, byte[] payload) { super(dataLength, optionalDataLength, ESPPacketType.RESPONSE, payload); try { diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java similarity index 85% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java index 63de3f5cdd984..56a4ccbbf869d 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/BaseResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/BaseResponse.java @@ -10,9 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; /** * diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java similarity index 91% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java index e2220c053f863..b2a8a0da72ea2 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDBaseIdResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDBaseIdResponse.java @@ -10,9 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; /** * diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java new file mode 100644 index 0000000000000..481915212bd31 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDLearnedClientsResponse.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages.Responses; + +import java.util.Arrays; + +import org.openhab.binding.enocean.internal.Helper; +import org.openhab.binding.enocean.internal.messages.Response; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class RDLearnedClientsResponse extends Response { + + public class LearnedClient { + public byte[] clientId; + public byte[] controllerId; + public int mailboxIndex; + } + + LearnedClient[] learnedClients; + + public RDLearnedClientsResponse(Response response) { + this(response.getPayload().length, response.getOptionalPayload().length, + Helper.concatAll(response.getPayload(), response.getOptionalPayload())); + } + + RDLearnedClientsResponse(int dataLength, int optionalDataLength, byte[] payload) { + super(dataLength, optionalDataLength, payload); + + if (payload == null || ((payload.length - 1) % 9) != 0) { + return; + } else { + _isValid = true; + } + + learnedClients = new LearnedClient[(payload.length - 1) / 9]; + for (int i = 0; i < learnedClients.length; i++) { + LearnedClient client = new LearnedClient(); + client.clientId = Arrays.copyOfRange(payload, 1 + i * 9, 1 + i * 9 + 4); + client.controllerId = Arrays.copyOfRange(payload, 5 + i * 9, 5 + i * 9 + 4); + client.mailboxIndex = payload[9 + i * 9] & 0xFF; + learnedClients[i] = client; + } + } + + public LearnedClient[] getLearnedClients() { + return learnedClients; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java similarity index 93% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java index cecc623b86dcb..440068ba4dcfd 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDRepeaterResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDRepeaterResponse.java @@ -10,11 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*; import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.library.types.StringType; /** diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java similarity index 94% rename from bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java rename to bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java index 1374c56e01970..daedc9aa9ae01 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/RDVersionResponse.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/RDVersionResponse.java @@ -10,11 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.enocean.internal.messages; +package org.openhab.binding.enocean.internal.messages.Responses; import java.util.Arrays; import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.util.HexUtils; /** diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java new file mode 100644 index 0000000000000..a827922d300fb --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/Responses/SMACKTeachInResponse.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages.Responses; + +import org.openhab.binding.enocean.internal.messages.Response; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class SMACKTeachInResponse extends Response { + + // set response time to 250ms + static final byte RESPONSE_TIME_HVALUE = 0; + static final byte RESPONSE_TIME_LVALUE = (byte) 0xFA; + + static final byte TEACH_IN = 0x00; + static final byte TEACH_OUT = 0x20; + static final byte REPEATED_TEACH_IN = 0x01; + static final byte NOPLACE_FOR_MAILBOX = 0x12; + static final byte BAD_RSSI = 0x14; + + public SMACKTeachInResponse() { + super(4, 0, new byte[] { Response.ResponseType.RET_OK.getValue(), RESPONSE_TIME_HVALUE, RESPONSE_TIME_LVALUE, + TEACH_IN }); + } + + public void setTeachOutResponse() { + data[3] = TEACH_OUT; + } + + public boolean isTeachOut() { + return data[3] == TEACH_OUT; + } + + public void setRepeatedTeachInResponse() { + data[3] = REPEATED_TEACH_IN; + } + + public void setNoPlaceForFurtherMailbox() { + data[3] = NOPLACE_FOR_MAILBOX; + } + + public void setBadRSSI() { + data[3] = BAD_RSSI; + } + + public void setTeachIn() { + data[3] = TEACH_IN; + } + + public boolean isTeachIn() { + return data[3] == TEACH_IN; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java new file mode 100644 index 0000000000000..e63087ee58717 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/messages/SAMessage.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.messages; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public class SAMessage extends BasePacket { + + public enum SAMessageType { + SA_WR_LEARNMODE((byte) 0x01, 7), + SA_RD_LEARNMODE((byte) 0x02, 1), + SA_WR_LEARNCONFIRM((byte) 0x03, 1), + SA_WR_CLIENTLEARNRQ((byte) 0x04, 6), + SA_WR_RESET((byte) 0x05, 1), + SA_RD_LEARNEDCLIENTS((byte) 0x06, 1), + SA_WR_RECLAIMS((byte) 0x07, 1), + SA_WR_POSTMASTER((byte) 0x08, 2), + SA_RD_MAILBOX_STATUS((byte) 0x09, 9); + + private byte value; + private int dataLength; + + SAMessageType(byte value, int dataLength) { + this.value = value; + this.dataLength = dataLength; + } + + public byte getValue() { + return this.value; + } + + public int getDataLength() { + return dataLength; + } + } + + private SAMessageType type; + + public SAMessage(SAMessageType type) { + this(type, new byte[] { type.getValue() }); + } + + public SAMessage(SAMessageType type, byte[] payload) { + super(type.getDataLength(), 0, ESPPacketType.SMART_ACK_COMMAND, payload); + + this.type = type; + } + + public SAMessageType getSAMessageType() { + return type; + } +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java index db15a3fec0c5f..28e8b04209630 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanESP3Transceiver.java @@ -18,8 +18,6 @@ import org.openhab.binding.enocean.internal.EnOceanException; import org.openhab.binding.enocean.internal.messages.BasePacket; -import org.openhab.binding.enocean.internal.messages.ERP1Message; -import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; import org.openhab.binding.enocean.internal.messages.ESP3Packet; import org.openhab.binding.enocean.internal.messages.ESP3PacketFactory; import org.openhab.binding.enocean.internal.messages.Response; @@ -138,21 +136,8 @@ protected void processMessage(byte firstByte) { HexUtils.bytesToHex(packet.getPayload())); break; case EVENT: - logger.debug("Event occured: {}", HexUtils.bytesToHex(packet.getPayload())); - break; - case RADIO_ERP1: { - ERP1Message msg = (ERP1Message) packet; - logger.debug("{} with RORG {} for {} payload {} received", - packet.getPacketType().name(), msg.getRORG().name(), - HexUtils.bytesToHex(msg.getSenderId()), HexUtils.bytesToHex( - Arrays.copyOf(dataBuffer, dataLength + optionalLength))); - - if (msg.getRORG() != RORG.Unknown) { - informListeners(msg); - } else { - logger.debug("Received unknown RORG"); - } - } + case RADIO_ERP1: + informListeners(packet); break; case RADIO_ERP2: break; diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java index df729a1dbc0e2..55be4988b0259 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EnOceanTransceiver.java @@ -27,9 +27,13 @@ import org.openhab.binding.enocean.internal.EnOceanBindingConstants; import org.openhab.binding.enocean.internal.EnOceanException; +import org.openhab.binding.enocean.internal.Helper; import org.openhab.binding.enocean.internal.messages.BasePacket; +import org.openhab.binding.enocean.internal.messages.BasePacket.ESPPacketType; import org.openhab.binding.enocean.internal.messages.ERP1Message; import org.openhab.binding.enocean.internal.messages.ERP1Message.RORG; +import org.openhab.binding.enocean.internal.messages.EventMessage; +import org.openhab.binding.enocean.internal.messages.EventMessage.EventMessageType; import org.openhab.binding.enocean.internal.messages.Response; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; @@ -138,7 +142,8 @@ private synchronized void send() throws IOException { Request currentRequest = null; protected Map > listeners; - protected PacketListener teachInListener; + protected HashSet eventListeners; + protected TeachInListener teachInListener; protected InputStream inputStream; protected OutputStream outputStream; @@ -151,6 +156,7 @@ public EnOceanTransceiver(String path, TransceiverErrorListener errorListener, S requestQueue = new RequestQueue(scheduler); listeners = new HashMap<>(); + eventListeners = new HashSet<>(); teachInListener = null; this.errorListener = errorListener; @@ -192,6 +198,7 @@ public void run() { } }); } + logger.info("EnOceanSerialTransceiver RX thread started"); } public void ShutDown() { @@ -266,36 +273,65 @@ protected int read(byte[] buffer, int length) { } } - protected void informListeners(ERP1Message msg) { + protected void informListeners(BasePacket packet) { try { - byte[] senderId = msg.getSenderId(); + if (packet.getPacketType() == ESPPacketType.RADIO_ERP1) { + ERP1Message msg = (ERP1Message) packet; + byte[] senderId = msg.getSenderId(); + byte[] d = Helper.concatAll(msg.getPayload(), msg.getOptionalPayload()); + + logger.debug("{} with RORG {} for {} payload {} received", packet.getPacketType().name(), + msg.getRORG().name(), HexUtils.bytesToHex(msg.getSenderId()), HexUtils.bytesToHex(d)); + + if (msg.getRORG() != RORG.Unknown) { + if (senderId != null) { + if (filteredDeviceId != null && senderId[0] == filteredDeviceId[0] + && senderId[1] == filteredDeviceId[1] && senderId[2] == filteredDeviceId[2]) { + // filter away own messages which are received through a repeater + return; + } - if (senderId != null) { - if (filteredDeviceId != null && senderId[0] == filteredDeviceId[0] && senderId[1] == filteredDeviceId[1] - && senderId[2] == filteredDeviceId[2]) { - // filter away own messages which are received through a repeater - return; - } + if (teachInListener != null && (msg.getIsTeachIn() || msg.getRORG() == RORG.RPS)) { + logger.info("Received teach in message from {}", HexUtils.bytesToHex(msg.getSenderId())); + teachInListener.packetReceived(msg); + return; + } else if (teachInListener == null && msg.getIsTeachIn()) { + logger.info("Discard message because this is a teach-in telegram from {}!", + HexUtils.bytesToHex(msg.getSenderId())); + return; + } - if (teachInListener != null) { - if (msg.getIsTeachIn() || (msg.getRORG() == RORG.RPS)) { - logger.info("Received teach in message from {}", HexUtils.bytesToHex(msg.getSenderId())); - teachInListener.packetReceived(msg); - return; + long s = Long.parseLong(HexUtils.bytesToHex(senderId), 16); + HashSet pl = listeners.get(s); + if (pl != null) { + pl.forEach(l -> l.packetReceived(msg)); + } } } else { - if (msg.getIsTeachIn()) { - logger.info("Discard message because this is a teach-in telegram from {}!", - HexUtils.bytesToHex(msg.getSenderId())); + logger.debug("Received unknown RORG"); + } + } else if (packet.getPacketType() == ESPPacketType.EVENT) { + EventMessage event = (EventMessage) packet; + + byte[] d = Helper.concatAll(packet.getPayload(), packet.getOptionalPayload()); + logger.debug("{} with type {} payload {} received", ESPPacketType.EVENT.name(), + event.getEventMessageType().name(), HexUtils.bytesToHex(d)); + + if (event.getEventMessageType() == EventMessageType.SA_CONFIRM_LEARN) { + byte[] senderId = event.getPayload(EventMessageType.SA_CONFIRM_LEARN.getDataLength() - 5, 4); + + if (teachInListener != null) { + logger.info("Received smart teach in from {}", HexUtils.bytesToHex(senderId)); + teachInListener.eventReceived(event); + return; + } else { + logger.info("Discard message because this is a smart teach-in telegram from {}!", + HexUtils.bytesToHex(senderId)); return; } } - long s = Long.parseLong(HexUtils.bytesToHex(senderId), 16); - HashSet pl = listeners.get(s); - if (pl != null) { - pl.forEach(l -> l.packetReceived(msg)); - } + eventListeners.forEach(l -> l.eventReceived(event)); } } catch (Exception e) { logger.error("Exception in informListeners", e); @@ -354,7 +390,15 @@ public void removePacketListener(PacketListener listener, long senderIdToListenT } } - public void startDiscovery(PacketListener teachInListener) { + public void addEventMessageListener(EventListener listener) { + eventListeners.add(listener); + } + + public void removeEventMessageListener(EventListener listener) { + eventListeners.remove(listener); + } + + public void startDiscovery(TeachInListener teachInListener) { this.teachInListener = teachInListener; } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java new file mode 100644 index 0000000000000..2a27bfb14f15b --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/EventListener.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.transceiver; + +import org.openhab.binding.enocean.internal.messages.EventMessage; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public interface EventListener { + public void eventReceived(EventMessage event); +} diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java index 04e55fdba857b..da161441aa87a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/PacketListener.java @@ -22,5 +22,5 @@ public interface PacketListener { public void packetReceived(BasePacket packet); - public long getSenderIdToListenTo(); + public long getEnOceanIdToListenTo(); } diff --git a/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java new file mode 100644 index 0000000000000..06a26c41800c0 --- /dev/null +++ b/bundles/org.openhab.binding.enocean/src/main/java/org/openhab/binding/enocean/internal/transceiver/TeachInListener.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2010-2021 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.enocean.internal.transceiver; + +/** + * + * @author Daniel Weber - Initial contribution + */ +public interface TeachInListener extends PacketListener, EventListener { + +} diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml index 7115374ab58f8..855048b00add7 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/CentralCommand.xml @@ -18,7 +18,7 @@ EnOceanId of device this thing belongs to true -+ Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml index 6d24ab2084156..93b135f8a779f 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/ClassicDevice.xml @@ -20,7 +20,7 @@- diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml index 9469d48b8c9af..941eb0d76487a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/channels.xml @@ -488,13 +488,6 @@+ - Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml index 2fa60566e40e8..b42b6b76202a7 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/GenericThing.xml @@ -18,7 +18,7 @@EnOceanId of device this thing belongs to true + - Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml index cd58af2cbf2fa..f7430123b677a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/HeatRecoveryVentilation.xml @@ -18,7 +18,7 @@EnOceanId of device this thing belongs to true + - Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml index 0f095d5ed6e18..08e75f162e9f9 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/MeasurementSwitch.xml @@ -18,7 +18,7 @@EnOceanId of device this thing belongs to true + - Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml index 486ceebf0e2e4..4a51f1614bf98 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Rollershutter.xml @@ -18,7 +18,7 @@EnOceanId of device this thing belongs to true + - Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml index 2be75d8e93033..5f1d44fe7e84a 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/Thermostat.xml @@ -18,7 +18,7 @@EnOceanId of device this thing belongs to true + + Id is used to generate the EnOcean Id (Int between [1-127]). If not specified the next free Id will be determined by bridge diff --git a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml index afa99f504c7f8..c712af8cd2d90 100644 --- a/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.enocean/src/main/resources/OH-INF/thing/bridge.xml @@ -31,6 +31,11 @@true ESP3 + + Declare Gateway as a SMACK Postmaster and handle SMACK messages +true +- true @@ -41,9 +46,15 @@00000000 + + +true + +Should a learned in or teach out response been send on a repeated smack teach in request +false +true - +Defines the next device Id, if empty, the next device id is automatically determined - -Switch - -You can send telegrams to the device by switching this channel on -thermostat -Switch From 17f70415247d54d76ac4bef4615ec96a8db08d58 Mon Sep 17 00:00:00 2001 From: lolodomoDate: Sat, 20 Feb 2021 17:21:52 +0100 Subject: [PATCH 017/118] [voicerss] Add support for voices (#10184) Signed-off-by: Laurent Garnier --- .../voicerss/internal/VoiceRSSTTSService.java | 2 +- .../voicerss/internal/VoiceRSSVoice.java | 7 +- .../cloudapi/CachedVoiceRSSCloudImpl.java | 15 ++- .../internal/cloudapi/VoiceRSSCloudAPI.java | 5 +- .../internal/cloudapi/VoiceRSSCloudImpl.java | 100 +++++++++++++++--- .../voice/voicerss/tool/CreateTTSCache.java | 21 ++-- 6 files changed, 118 insertions(+), 32 deletions(-) diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java index a9d9110e5d949..6d5475468e961 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java @@ -136,7 +136,7 @@ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFor // only a default voice try { File cacheAudioFile = voiceRssImpl.getTextToSpeechAsFile(apiKey, trimmedText, - voice.getLocale().toLanguageTag(), getApiAudioFormat(requestedFormat)); + voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioFormat(requestedFormat)); if (cacheAudioFile == null) { throw new TTSException("Could not read from VoiceRSS service"); } diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java index ef3f82387066c..02d33f8502550 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java @@ -15,6 +15,7 @@ import java.util.Locale; import org.openhab.core.voice.Voice; +import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl; /** * Implementation of the Voice interface for VoiceRSS. Label is only "default" @@ -54,7 +55,11 @@ public VoiceRSSVoice(Locale locale, String label) { */ @Override public String getUID() { - return "voicerss:" + locale.toLanguageTag().replaceAll("[^a-zA-Z0-9_]", ""); + String uid = "voicerss:" + locale.toLanguageTag().replaceAll("[^a-zA-Z0-9_]", ""); + if (!label.equals(VoiceRSSCloudImpl.DEFAULT_VOICE)) { + uid += "_" + label.replaceAll("[^a-zA-Z0-9_]", ""); + } + return uid; } /** diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java index 71a16d1ee053e..b94f07cf4793a 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java @@ -55,9 +55,9 @@ public CachedVoiceRSSCloudImpl(String cacheFolderName) { } } - public File getTextToSpeechAsFile(String apiKey, String text, String locale, String audioFormat) + public File getTextToSpeechAsFile(String apiKey, String text, String locale, String voice, String audioFormat) throws IOException { - String fileNameInCache = getUniqueFilenameForText(text, locale); + String fileNameInCache = getUniqueFilenameForText(text, locale, voice); // check if in cache File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase()); if (audioFileInCache.exists()) { @@ -65,7 +65,7 @@ public File getTextToSpeechAsFile(String apiKey, String text, String locale, Str } // if not in cache, get audio data and put to cache - try (InputStream is = super.getTextToSpeech(apiKey, text, locale, audioFormat); + try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioFormat); FileOutputStream fos = new FileOutputStream(audioFileInCache)) { copyStream(is, fos); // write text to file for transparency too @@ -89,7 +89,7 @@ public File getTextToSpeechAsFile(String apiKey, String text, String locale, Str * * Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3" */ - private String getUniqueFilenameForText(String text, String locale) { + private String getUniqueFilenameForText(String text, String locale, String voice) { try { byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8); MessageDigest md = MessageDigest.getInstance("MD5"); @@ -101,7 +101,12 @@ private String getUniqueFilenameForText(String text, String locale) { while (hashtext.length() < 32) { hashtext = "0" + hashtext; } - return locale + "_" + hashtext; + String filename = locale + "_"; + if (!DEFAULT_VOICE.equals(voice)) { + filename += voice + "_"; + } + filename += hashtext; + return filename; } catch (NoSuchAlgorithmException ex) { // should not happen logger.error("Could not create MD5 hash for '{}'", text, ex); diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java index 8a7d4b186856b..175f8cfa625f1 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java @@ -68,6 +68,8 @@ public interface VoiceRSSCloudAPI { * the text to translate into speech * @param locale * the locale to use + * @param voice + * the voice to use, "default" for the default voice * @param audioFormat * the audio format to use * @return an InputStream to the audio data in specified format @@ -75,5 +77,6 @@ public interface VoiceRSSCloudAPI { * will be raised if the audio data can not be retrieved from * cloud service */ - InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat) throws IOException; + InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioFormat) + throws IOException; } diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java index 4582d8b48e2b3..8d50281b96323 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java @@ -21,10 +21,11 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; -import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.stream.Stream; @@ -34,7 +35,7 @@ /** * This class implements the Cloud service from VoiceRSS. For more information, - * see API documentation at http://www.voicerss.org/api/documentation.aspx. + * see API documentation at http://www.voicerss.org/api . * * Current state of implementation: * @@ -50,6 +51,8 @@ */ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI { + public static final String DEFAULT_VOICE = "default"; + private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class); private static final Set
SUPPORTED_AUDIO_FORMATS = Stream.of("MP3", "OGG", "AAC").collect(toSet()); @@ -63,8 +66,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI { SUPPORTED_LOCALES.add(Locale.forLanguageTag("cs-cz")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("da-dk")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-at")); - SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-ch")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-de")); + SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-ch")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("el-gr")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-au")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ca")); @@ -76,8 +79,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI { SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-mx")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("fi-fi")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ca")); - SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ch")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-fr")); + SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ch")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("he-il")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("hi-in")); SUPPORTED_LOCALES.add(Locale.forLanguageTag("hr-hr")); @@ -107,7 +110,58 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI { SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-tw")); } - private static final Set SUPPORTED_VOICES = Collections.singleton("VoiceRSS"); + private static final Map > SUPPORTED_VOICES = new HashMap<>(); + static { + SUPPORTED_VOICES.put("ar-eg", Set.of("Oda")); + SUPPORTED_VOICES.put("ar-sa", Set.of("Salim")); + SUPPORTED_VOICES.put("bg-bg", Set.of("Dimo")); + SUPPORTED_VOICES.put("ca-es", Set.of("Rut")); + SUPPORTED_VOICES.put("cs-cz", Set.of("Josef")); + SUPPORTED_VOICES.put("da-dk", Set.of("Freja")); + SUPPORTED_VOICES.put("de-at", Set.of("Lukas")); + SUPPORTED_VOICES.put("de-de", Set.of("Hanna", "Lina", "Jonas")); + SUPPORTED_VOICES.put("de-ch", Set.of("Tim")); + SUPPORTED_VOICES.put("el-gr", Set.of("Neo")); + SUPPORTED_VOICES.put("en-au", Set.of("Zoe", "Isla", "Evie", "Jack")); + SUPPORTED_VOICES.put("en-ca", Set.of("Rose", "Clara", "Emma", "Mason")); + SUPPORTED_VOICES.put("en-gb", Set.of("Alice", "Nancy", "Lily", "Harry")); + SUPPORTED_VOICES.put("en-ie", Set.of("Oran")); + SUPPORTED_VOICES.put("en-in", Set.of("Eka", "Jai", "Ajit")); + SUPPORTED_VOICES.put("en-us", Set.of("Linda", "Amy", "Mary", "John", "Mike")); + SUPPORTED_VOICES.put("es-es", Set.of("Camila", "Sofia", "Luna", "Diego")); + SUPPORTED_VOICES.put("es-mx", Set.of("Juana", "Silvia", "Teresa", "Jose")); + SUPPORTED_VOICES.put("fi-fi", Set.of("Aada")); + SUPPORTED_VOICES.put("fr-ca", Set.of("Emile", "Olivia", "Logan", "Felix")); + SUPPORTED_VOICES.put("fr-fr", Set.of("Bette", "Iva", "Zola", "Axel")); + SUPPORTED_VOICES.put("fr-ch", Set.of("Theo")); + SUPPORTED_VOICES.put("he-il", Set.of("Rami")); + SUPPORTED_VOICES.put("hi-in", Set.of("Puja", "Kabir")); + SUPPORTED_VOICES.put("hr-hr", Set.of("Nikola")); + SUPPORTED_VOICES.put("hu-hu", Set.of("Mate")); + SUPPORTED_VOICES.put("id-id", Set.of("Intan")); + SUPPORTED_VOICES.put("it-it", Set.of("Bria", "Mia", "Pietro")); + SUPPORTED_VOICES.put("ja-jp", Set.of("Hina", "Airi", "Fumi", "Akira")); + SUPPORTED_VOICES.put("ko-kr", Set.of("Nari")); + SUPPORTED_VOICES.put("ms-my", Set.of("Aqil")); + SUPPORTED_VOICES.put("nb-no", Set.of("Marte", "Erik")); + SUPPORTED_VOICES.put("nl-be", Set.of("Daan")); + SUPPORTED_VOICES.put("nl-nl", Set.of("Lotte", "Bram")); + SUPPORTED_VOICES.put("pl-pl", Set.of("Julia", "Jan")); + SUPPORTED_VOICES.put("pt-br", Set.of("Marcia", "Ligia", "Yara", "Dinis")); + SUPPORTED_VOICES.put("pt-pt", Set.of("Leonor")); + SUPPORTED_VOICES.put("ro-ro", Set.of("Doru")); + SUPPORTED_VOICES.put("ru-ru", Set.of("Olga", "Marina", "Peter")); + SUPPORTED_VOICES.put("sk-sk", Set.of("Beda")); + SUPPORTED_VOICES.put("sl-si", Set.of("Vid")); + SUPPORTED_VOICES.put("sv-se", Set.of("Molly", "Hugo")); + SUPPORTED_VOICES.put("ta-in", Set.of("Sai")); + SUPPORTED_VOICES.put("th-th", Set.of("Ukrit")); + SUPPORTED_VOICES.put("tr-tr", Set.of("Omer")); + SUPPORTED_VOICES.put("vi-vn", Set.of("Chi")); + SUPPORTED_VOICES.put("zh-cn", Set.of("Luli", "Shu", "Chow", "Wang")); + SUPPORTED_VOICES.put("zh-hk", Set.of("Jia", "Xia", "Chen")); + SUPPORTED_VOICES.put("zh-tw", Set.of("Akemi", "Lin", "Lee")); + } @Override public Set getAvailableAudioFormats() { @@ -121,17 +175,29 @@ public Set getAvailableLocales() { @Override public Set getAvailableVoices() { - return SUPPORTED_VOICES; + // different locales support different voices, so let's list all here in one big set when no locale is provided + Set allvoxes = new HashSet<>(); + allvoxes.add(DEFAULT_VOICE); + for (Set langvoxes : SUPPORTED_VOICES.values()) { + for (String langvox : langvoxes) { + allvoxes.add(langvox); + } + } + return allvoxes; } @Override public Set getAvailableVoices(Locale locale) { - for (Locale voiceLocale : SUPPORTED_LOCALES) { - if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) { - return SUPPORTED_VOICES; + Set allvoxes = new HashSet<>(); + allvoxes.add(DEFAULT_VOICE); + // all maps must be defined with key in lowercase + String langtag = locale.toLanguageTag().toLowerCase(); + if (SUPPORTED_VOICES.containsKey(langtag)) { + for (String langvox : SUPPORTED_VOICES.get(langtag)) { + allvoxes.add(langvox); } } - return new HashSet<>(); + return allvoxes; } /** @@ -142,9 +208,9 @@ public Set getAvailableVoices(Locale locale) { * dependencies. */ @Override - public InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat) + public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioFormat) throws IOException { - String url = createURL(apiKey, text, locale, audioFormat); + String url = createURL(apiKey, text, locale, voice, audioFormat); logger.debug("Call {}", url); URLConnection connection = new URL(url).openConnection(); @@ -188,7 +254,7 @@ public InputStream getTextToSpeech(String apiKey, String text, String locale, St * * It is in package scope to be accessed by tests. */ - private String createURL(String apiKey, String text, String locale, String audioFormat) { + private String createURL(String apiKey, String text, String locale, String voice, String audioFormat) { String encodedMsg; try { encodedMsg = URLEncoder.encode(text, "UTF-8"); @@ -197,7 +263,11 @@ private String createURL(String apiKey, String text, String locale, String audio // fall through and use msg un-encoded encodedMsg = text; } - return "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat - + "&f=44khz_16bit_mono&src=" + encodedMsg; + String url = "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat; + if (!DEFAULT_VOICE.equals(voice)) { + url += "&v=" + voice; + } + url += "&f=44khz_16bit_mono&src=" + encodedMsg; + return url; } } diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java index cb3677490e279..0f7cd5e38e91f 100644 --- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java +++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java @@ -49,18 +49,19 @@ public int doMain(String[] args) throws IOException { String apiKey = args[1]; String cacheDir = args[2]; String locale = args[3]; - if (args[4].startsWith("@")) { - String inputFileName = args[4].substring(1); + String voice = args[4]; + if (args[5].startsWith("@")) { + String inputFileName = args[5].substring(1); File inputFile = new File(inputFileName); if (!inputFile.exists()) { usage(); System.err.println("File " + inputFileName + " not found"); return RC_INPUT_FILE_NOT_FOUND; } - generateCacheForFile(apiKey, cacheDir, locale, inputFileName); + generateCacheForFile(apiKey, cacheDir, locale, voice, inputFileName); } else { - String text = args[4]; - generateCacheForMessage(apiKey, cacheDir, locale, text); + String text = args[5]; + generateCacheForMessage(apiKey, cacheDir, locale, voice, text); } return RC_OK; } @@ -71,6 +72,7 @@ private void usage() { System.out.println(" key the VoiceRSS API Key, e.g. \"123456789\""); System.out.println(" cache-dir is directory where the files will be stored, e.g. \"voicerss-cache\""); System.out.println(" locale the language locale, has to be valid, e.g. \"en-us\", \"de-de\""); + System.out.println(" voice the voice, \"default\" for the default voice"); System.out.println(" text the text to create audio file for, e.g. \"Hello World\""); System.out.println( " inputfile a name of a file, where all lines will be translatet to text, e.g. \"@message.txt\""); @@ -80,19 +82,20 @@ private void usage() { System.out.println(); } - private void generateCacheForFile(String apiKey, String cacheDir, String locale, String inputFileName) + private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, String inputFileName) throws IOException { File inputFile = new File(inputFileName); try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) { String line; while ((line = br.readLine()) != null) { // process the line. - generateCacheForMessage(apiKey, cacheDir, locale, line); + generateCacheForMessage(apiKey, cacheDir, locale, voice, line); } } } - private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String msg) throws IOException { + private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String msg) + throws IOException { if (msg == null) { System.err.println("Ignore msg=null"); return; @@ -103,7 +106,7 @@ private void generateCacheForMessage(String apiKey, String cacheDir, String loca return; } CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir); - File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, "MP3"); + File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, "MP3"); System.out.println( "Created cached audio for locale='" + locale + "', msg='" + trimmedMsg + "' to file=" + cachedFile); } From a30b5dfd00da35a88e30715be73de10aeb644ad9 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sat, 20 Feb 2021 18:29:05 +0100 Subject: [PATCH 018/118] remove @J-N-K from CODEOWNERS (#10108) Signed-off-by: Jan N. Klug --- CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5a94b11062ae5..ec1e73f33f4e1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,12 +52,12 @@ /bundles/org.openhab.binding.daikin/ @caffineehacker /bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.darksky/ @cweitkamp -/bundles/org.openhab.binding.deconz/ @J-N-K +/bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor -/bundles/org.openhab.binding.dmx/ @J-N-K +/bundles/org.openhab.binding.dmx/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.doorbird/ @mhilbush /bundles/org.openhab.binding.draytonwiser/ @andrew-schofield /bundles/org.openhab.binding.dscalarm/ @RSStephens @@ -102,7 +102,7 @@ /bundles/org.openhab.binding.heos/ @Wire82 /bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s /bundles/org.openhab.binding.hpprinter/ @cossey -/bundles/org.openhab.binding.http/ @J-N-K +/bundles/org.openhab.binding.http/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.hue/ @cweitkamp /bundles/org.openhab.binding.hydrawise/ @digitaldan /bundles/org.openhab.binding.hyperion/ @tavalin @@ -144,7 +144,7 @@ /bundles/org.openhab.binding.luftdateninfo/ @weymann /bundles/org.openhab.binding.lutron/ @actong @bobadair /bundles/org.openhab.binding.magentatv/ @markus7017 -/bundles/org.openhab.binding.mail/ @J-N-K +/bundles/org.openhab.binding.mail/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.max/ @marcelrv /bundles/org.openhab.binding.mcp23017/ @aogorek /bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep @@ -194,7 +194,7 @@ /bundles/org.openhab.binding.omnikinverter/ @hansbogert /bundles/org.openhab.binding.omnilink/ @ecdye /bundles/org.openhab.binding.onebusaway/ @sdwilsh -/bundles/org.openhab.binding.onewire/ @J-N-K +/bundles/org.openhab.binding.onewire/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.onewiregpio/ @aogorek /bundles/org.openhab.binding.onkyo/ @pail23 @paulianttila /bundles/org.openhab.binding.opengarage/ @psmedley @@ -247,7 +247,7 @@ /bundles/org.openhab.binding.smartmeter/ @msteigenberger /bundles/org.openhab.binding.smartthings/ @BobRak /bundles/org.openhab.binding.smhi/ @pacive -/bundles/org.openhab.binding.snmp/ @J-N-K +/bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.solaredge/ @alexf2015 /bundles/org.openhab.binding.solarlog/ @johannrichard /bundles/org.openhab.binding.somfymylink/ @loungeflyz @@ -270,7 +270,7 @@ /bundles/org.openhab.binding.tivo/ @mlobstein /bundles/org.openhab.binding.touchwand/ @roieg /bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand -/bundles/org.openhab.binding.tr064/ @J-N-K +/bundles/org.openhab.binding.tr064/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer /bundles/org.openhab.binding.unifi/ @mgbowman /bundles/org.openhab.binding.unifiedremote/ @GiviMAD From fe7b91f4b7b36ba41021d75bb922a167882bc876 Mon Sep 17 00:00:00 2001 From: Stefan Triller Date: Mon, 22 Feb 2021 19:13:51 +0100 Subject: [PATCH 019/118] [novafinedust] Use fire and forget commands to configure device (#10210) Since the device does not follow its own protocol, we do not evaluate its replies to our configuration commands but rather do a fire and forget. Signed-off-by: Stefan Triller --- .../novafinedust/internal/SDS011Handler.java | 98 ++++++-------- .../sds011protocol/SDS011Communicator.java | 125 ++++++------------ 2 files changed, 84 insertions(+), 139 deletions(-) diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java index 87cec1ad18bc2..608663ebd84f8 100644 --- a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java @@ -16,6 +16,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.TooManyListenersException; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -62,6 +63,7 @@ public class SDS011Handler extends BaseThingHandler { private @Nullable ScheduledFuture> dataReadJob; private @Nullable ScheduledFuture> connectionMonitor; + private @Nullable Future> initJob; private @Nullable ScheduledFuture> retryInitJob; private ZonedDateTime lastCommunication = ZonedDateTime.now(); @@ -115,10 +117,10 @@ public void initialize() { if (config.reporting) { timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval); - scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); + initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); } else { timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval); - scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); + initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); } } @@ -130,66 +132,45 @@ private void initializeCommunicator(WorkMode mode, Duration interval) { return; } - boolean initSuccessful = false; - int retryInit = 3; - int retryCount = 0; - // sometimes the device is a little difficult and needs multiple configuration attempts - while (!initSuccessful && retryCount < retryInit) { - logger.trace("Trying to initialize device attempt={}", retryCount); - initSuccessful = doInit(localCommunicator, mode, interval); - retryCount++; - } - - if (initSuccessful) { - lastCommunication = ZonedDateTime.now(); - updateStatus(ThingStatus.ONLINE); + logger.trace("Trying to initialize device"); + doInit(localCommunicator, mode, interval); - if (mode == WorkMode.POLLING) { - dataReadJob = scheduler.scheduleWithFixedDelay(() -> { - try { - localCommunicator.requestSensorData(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Cannot query data from device"); - } - }, 2, config.pollingInterval, TimeUnit.SECONDS); - } else { - // start a job that reads the port until data arrives - int reportingReadStartDelay = 10; - int startReadBeforeDataArrives = 5; - long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay - - startReadBeforeDataArrives; - logger.trace("Scheduling job to receive reported values"); - dataReadJob = scheduler.scheduleWithFixedDelay(() -> { - try { - localCommunicator.readSensorData(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Cannot query data from device, because: " + e.getMessage()); - } - }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS); - } + lastCommunication = ZonedDateTime.now(); - Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive - .plus(CONNECTION_MONITOR_START_DELAY_OFFSET); - connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, - connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), - TimeUnit.SECONDS); + if (mode == WorkMode.POLLING) { + dataReadJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.requestSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device"); + } + }, 2, config.pollingInterval, TimeUnit.SECONDS); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Commands and replies from the device don't seem to match"); - logger.debug( - "Could not configure sensor -> setting Thing to OFFLINE, disposing the handler and reschedule initialize in {} seconds", - RETRY_INIT_DELAY); - doDispose(false); - retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS); + // start a job that reads the port until data arrives + int reportingReadStartDelay = 10; + int startReadBeforeDataArrives = 5; + long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay + - startReadBeforeDataArrives; + logger.trace("Scheduling job to receive reported values"); + dataReadJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.readSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device, because: " + e.getMessage()); + } + }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS); } + + Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET); + connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, + connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS); } - private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) { - boolean initSuccessful = false; + private void doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) { try { - initSuccessful = localCommunicator.initialize(mode, interval); + localCommunicator.initialize(mode, interval); } catch (final IOException ex) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!"); } catch (PortInUseException e) { @@ -201,7 +182,6 @@ private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Dura updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Cannot set serial port parameters"); } - return initSuccessful; } private boolean validateConfiguration() { @@ -243,6 +223,12 @@ private void doDispose(boolean sendDeviceToSleep) { this.connectionMonitor = null; } + Future> localInitJob = this.initJob; + if (localInitJob != null) { + localInitJob.cancel(true); + this.initJob = null; + } + ScheduledFuture> localRetryOpenPortJob = this.retryInitJob; if (localRetryOpenPortJob != null) { localRetryOpenPortJob.cancel(true); diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java index 3e3c11e32980d..8ea0299026d98 100644 --- a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java @@ -29,12 +29,8 @@ import org.openhab.binding.novafinedust.internal.SDS011Handler; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; -import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPortIdentifier; @@ -52,7 +48,7 @@ @NonNullByDefault public class SDS011Communicator { - private static final int MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY = 20; + private static final int MAX_READ_UNTIL_SENSOR_DATA = 6; // at least 6 because we send 5 configuration commands private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class); @@ -82,9 +78,8 @@ public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portI * @throws IOException * @throws UnsupportedCommOperationException */ - public boolean initialize(WorkMode mode, Duration interval) + public void initialize(WorkMode mode, Duration interval) throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException { - boolean initSuccessful = true; logger.trace("Initializing with mode={}, interval={}", mode, interval); @@ -102,30 +97,28 @@ public boolean initialize(WorkMode mode, Duration interval) logger.trace("Input and Outputstream opened for the port"); // wake up the device - initSuccessful &= sendSleep(false); - logger.trace("Wake up call done, initSuccessful={}", initSuccessful); - initSuccessful &= getFirmware(); - logger.trace("Firmware requested, initSuccessful={}", initSuccessful); + sendSleep(false); + logger.trace("Wake up call done"); + getFirmware(); + logger.trace("Firmware requested"); if (mode == WorkMode.POLLING) { - initSuccessful &= setMode(WorkMode.POLLING); - logger.trace("Polling mode set, initSuccessful={}", initSuccessful); - initSuccessful &= setWorkingPeriod((byte) 0); - logger.trace("Working period for polling set, initSuccessful={}", initSuccessful); + setMode(WorkMode.POLLING); + logger.trace("Polling mode set"); + setWorkingPeriod((byte) 0); + logger.trace("Working period for polling set"); } else { // reporting - initSuccessful &= setWorkingPeriod((byte) interval.toMinutes()); - logger.trace("Working period for reporting set, initSuccessful={}", initSuccessful); - initSuccessful &= setMode(WorkMode.REPORTING); - logger.trace("Reporting mode set, initSuccessful={}", initSuccessful); + setWorkingPeriod((byte) interval.toMinutes()); + logger.trace("Working period for reporting set"); + setMode(WorkMode.REPORTING); + logger.trace("Reporting mode set"); } this.serialPort = localSerialPort; - - return initSuccessful; } - private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException { + private void sendCommand(CommandMessage message) throws IOException { byte[] commandData = message.getBytes(); if (logger.isDebugEnabled()) { logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData)); @@ -139,23 +132,12 @@ public boolean initialize(WorkMode mode, Duration interval) } try { - // Give the sensor some time to handle the command + // Give the sensor some time to handle the command before doing something else with it Thread.sleep(500); } catch (InterruptedException e) { - logger.warn("Problem while waiting for reading a reply to our command."); + logger.warn("Interrupted while waiting after sending command={}", message); Thread.currentThread().interrupt(); } - SensorReply reply = readReply(); - // in case there is still another reporting active, we want to discard the sensor data and read the reply to our - // command again, this might happen more often in case the sensor has buffered some data - for (int i = 0; i < MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY; i++) { - if (reply instanceof SensorMeasuredDataReply) { - reply = readReply(); - } else { - break; - } - } - return reply; } private void write(byte[] commandData) throws IOException { @@ -166,21 +148,13 @@ private void write(byte[] commandData) throws IOException { } } - private boolean setWorkingPeriod(byte period) throws IOException { + private void setWorkingPeriod(byte period) throws IOException { CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period }); logger.debug("Sending work period: {}", period); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to setWorkingPeriod command: {}", reply); - if (reply instanceof WorkingPeriodReply) { - WorkingPeriodReply wpReply = (WorkingPeriodReply) reply; - if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) { - return true; - } - } - return false; + sendCommand(m); } - private boolean setMode(WorkMode workMode) throws IOException { + private void setMode(WorkMode workMode) throws IOException { byte haveToRequestData = 0; if (workMode == WorkMode.POLLING) { haveToRequestData = 1; @@ -188,18 +162,10 @@ private boolean setMode(WorkMode workMode) throws IOException { CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData }); logger.debug("Sending mode: {}", workMode); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to setMode command: {}", reply); - if (reply instanceof ModeReply) { - ModeReply mr = (ModeReply) reply; - if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) { - return true; - } - } - return false; + sendCommand(m); } - private boolean sendSleep(boolean doSleep) throws IOException { + private void sendSleep(boolean doSleep) throws IOException { byte payload = (byte) 1; if (doSleep) { payload = (byte) 0; @@ -207,38 +173,21 @@ private boolean sendSleep(boolean doSleep) throws IOException { CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload }); logger.debug("Sending doSleep: {}", doSleep); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to sendSleep command: {}", reply); + sendCommand(m); + // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying + // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration + // commands if (!doSleep) { // sometimes the sensor does not wakeup on the first attempt, thus we try again - for (int i = 0; reply == null && i < 3; i++) { - reply = sendCommand(m); - logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply); - } + sendCommand(m); } - - if (reply instanceof SleepReply) { - SleepReply sr = (SleepReply) reply; - if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) { - return true; - } - } - return false; } - private boolean getFirmware() throws IOException { + private void getFirmware() throws IOException { CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {}); logger.debug("Sending get firmware request"); - SensorReply reply = sendCommand(m); - logger.debug("Got reply to getFirmware command: {}", reply); - - if (reply instanceof SensorFirmwareReply) { - SensorFirmwareReply fwReply = (SensorFirmwareReply) reply; - thingHandler.setFirmware(fwReply.getFirmware()); - return true; - } - return false; + sendCommand(m); } /** @@ -256,7 +205,7 @@ public void requestSensorData() throws IOException { try { Thread.sleep(200); // give the device some time to handle the command } catch (InterruptedException e) { - logger.warn("Interrupted while waiting before reading a reply to our rquest data command."); + logger.warn("Interrupted while waiting before reading a reply to our request data command."); Thread.currentThread().interrupt(); } readSensorData(); @@ -271,7 +220,7 @@ public void requestSensorData() throws IOException { if (localInpuStream != null) { logger.trace("Reading for reply until first byte is found"); while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) { - logger.debug("Trying to find first reply byte now..."); + // logger.trace("Trying to find first reply byte now..."); } readBuffer[0] = (byte) b; int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1); @@ -286,16 +235,26 @@ public void requestSensorData() throws IOException { public void readSensorData() throws IOException { logger.trace("readSensorData() called"); + + boolean foundSensorData = doRead(); + for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) { + foundSensorData = doRead(); + } + } + + private boolean doRead() throws IOException { SensorReply reply = readReply(); - logger.trace("readSensorData(): Read reply={}", reply); + logger.trace("doRead(): Read reply={}", reply); if (reply instanceof SensorMeasuredDataReply) { SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply; logger.trace("We received sensor data"); if (sensorData.isValidData()) { logger.trace("Sensor data is valid => updating channels"); thingHandler.updateChannels(sensorData); + return true; } } + return false; } /** From fd1f7ebe7545a399b1c0c99993ec2d8fa0a41808 Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Tue, 23 Feb 2021 09:48:13 +0100 Subject: [PATCH 020/118] [shelly] Add Shelly Motion, minor improvements (#10054) * Support for Shelly Motion, some minotr improvements, README updated Signed-off-by: Markus Michels * minor changes Signed-off-by: Markus Michels * Bug fixes from hardening Signed-off-by: Markus Michels * review changes applied Signed-off-by: Markus Michels * review change Signed-off-by: Markus Michels * review changes, fix creations of sensors#motion and device#externalPower for H%T; moved images/uiroller*.png to doc/images Signed-off-by: Markus Michels * missing in last fix Signed-off-by: Markus Michels --- bundles/org.openhab.binding.shelly/README.md | 18 ++ .../doc/images/uiroller_fav1.png | Bin 147520 -> 123006 bytes .../doc/images/uiroller_fav2.png | Bin 146006 -> 104050 bytes .../doc/images/uiroller_obs1.png | Bin 147688 -> 123195 bytes .../doc/images/uiroller_obs2.png | Bin 223458 -> 148820 bytes .../doc/images/uiroller_obs3.png | Bin 229027 -> 151522 bytes .../doc/images/uiroller_rlogin.png | Bin 227601 -> 153966 bytes .../doc/images/uiroller_wt.png | Bin 233781 -> 154880 bytes .../internal/ShellyBindingConstants.java | 3 +- .../shelly/internal/ShellyHandlerFactory.java | 22 +- .../shelly/internal/api/ShellyApiJsonDTO.java | 10 + .../internal/api/ShellyDeviceProfile.java | 7 +- .../shelly/internal/api/ShellyHttpApi.java | 23 ++ .../internal/coap/ShellyCoIoTProtocol.java | 2 +- .../internal/coap/ShellyCoIoTVersion2.java | 6 +- .../internal/coap/ShellyCoapHandler.java | 212 +++++++++++------- .../internal/coap/ShellyCoapServer.java | 10 +- .../config/ShellyBindingConfiguration.java | 10 +- .../discovery/ShellyDiscoveryParticipant.java | 2 +- .../discovery/ShellyThingCreator.java | 4 + .../internal/handler/ShellyBaseHandler.java | 90 +++++--- .../internal/handler/ShellyComponents.java | 37 ++- .../internal/handler/ShellyDeviceStats.java | 54 +++++ .../internal/handler/ShellyRelayHandler.java | 4 +- .../provider/ShellyChannelDefinitions.java | 9 +- .../internal/util/ShellyChannelCache.java | 1 + .../shelly/internal/util/ShellyUtils.java | 18 +- .../resources/OH-INF/i18n/shelly.properties | 3 +- .../OH-INF/i18n/shelly_de.properties | 11 +- .../main/resources/OH-INF/thing/device.xml | 10 +- .../main/resources/OH-INF/thing/sensor.xml | 2 +- 31 files changed, 389 insertions(+), 179 deletions(-) create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index 9006b94dd3759..5750f1ff200fb 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -30,6 +30,7 @@ Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB | shellydimmer | Shelly Dimmer | SHDM-1 | | shellydimmer2 | Shelly Dimmer2 | SHDM-2 | | shellyix3 | Shelly ix3 | SHIX3-1 | +| shellyuni | Shelly UNI | SHUNI-1 | | shellyplug | Shelly Plug | SHPLG2-1 | | shellyplugs | Shelly Plug-S | SHPLG-S | | shellyem | Shelly EM with integrated Power Meters | SHEM | @@ -578,6 +579,23 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the | |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush | | |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. | +### Shelly UNI - Low voltage sensor/actor: shellyuni) + +|Group |Channel |Type |read-only|Description | +|----------|-------------|---------|---------|----------------------------------------------------------------------------| +|relay1 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels | +|relay2 | | | |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels | +|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) | +| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) | +| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) | +| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) | +| |voltage |Number |yes |ADCS voltage | +|status |input1 |Switch |yes |State of Input 1 | +| |input2 |Switch |yes |State of Input 2 | +| |button |Trigger |yes |Event trigger, see section Button Events | +| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush | +| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. | + ### Shelly Bulb (thing-type: shellybulb) |Group |Channel |Type |read-only|Description | diff --git a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png index 8a2395c7767a991730128d247edec64ed898d6c5..0d09bdc7e65a3d40e161b9b69506c809f64e0ad3 100644 GIT binary patch literal 123006 zcmZ^}19+s((l8urv(d)f*tTuk*2cE&Og6UdY&OQkwr$(SpXWXAci!`VxaX?7s!Ls6 zUDxz<_e3bjiGPQ~h64cs`7S9TqV$EGzTgE3>g(NOUD)u2Aj}121wlaS0N>sWA-~!` zjU|+1K|nmoKtTM1zjVJ?{zo7nE({ sxKM_ z1OgNV1pJEv{rZ4_VuSqS?TZ581I76_Ef!`00`cE`xi9?BBk=|Q==}>r#DRl=eYH@( zAUHl01o{i3d_ih1*uO2Ea>4&i{}Uhx69W?i7b6oFBg;=lb}kl1E+$40L1ke{$uF6* zv7@P}t at@eRV`&h ed8CHgN7t}pr@Ha*eL|59ul&wYwJY(FDL)YkBF(0v7?2(vxS}Q&wu |7K?Yl`6ZheA)jy%KxA3zs3Jg2hJ8||BrtEk^IZ- zzY_DWczOPbhD*W1-PBrB#KPuluD-^`$IQgQ^IviPACmtW>Ax&holG5t?QFgbo%#M} zvj5xoKZXChp|G8`oujh7p|L3+ ={;vQv{wF|27WV%P@INH~VfpW5afui@ zn+iLce!Wfj{wY^lMkZQD7M1^1t~~Vrch!^-uD+ zmg0E+rVC^d6eudC2wbYo%@2&!sD0e<8Vrr>Fe-wHBog(k$qPZ5k(e}d+qkp~<@r4H zV9(hzxmfeMXerrbs&_SNi@+mmbkSw*mSIZ4GQMDcOw|qmrV@;mbUlu}n&Drm_5fzEkxeNy#vc%ubdP|5~_zp8kZ zOjHN6lDi{-6fPew@ufu# !6I4eGN z)(wun7^n{tWqQcBd~ONu9R6YaYzQggzdz85?#Pd&p%64?{qxnlqd?~YUV;2n7{1rq z&ML~n-uK$rzv|Y8#mP^)k&BZ^wr<-3_;=J NDx3?o4XgeymYn~E76Mf6whTSlL zC+0 K_D2TmT%6Bv|mRW-G5^6TLpxTyRxjN`WEvi z8_D3%kV2;p!C-?=GW9--`Zk6ny^lF#v_u8mjt#zH&h}O2ZF*N4{KV`L94gDBru2k2 zX2y((77WIJfN~EHKy}oO)*uZxx>x|&RY3KjGEBqFZNqM!n=p5dVfPohh;#@wB)e)t z7 TU 2Lk3}L_s%2dqOM|?G?Xwn!sio9 zw4l--MXXGbbr*2szqQgcN_O>-deR(}`xX|X*zZ9^r;B9^L3dZb>?Dd^Fin6N;L`fE z=5KJEmM(S?#7I*WyzJDW(j%150i+6R!}0`JASfRPcd2C=(kFv9g}<3o+)EKe1~* zFyyuq)^w_c^jZM)+1F>=D|y8O$}*Qs6xXs)M4d%oQ54h7I$EOvB!X3Y>cjfO4e1Yj zelzX9d*~{_P^Inf`8P~YmL%l8RXbAmL05z92B-Jxm-9&DpnwaAm)}9EEl0_#x6iw& z!a7kzoQ+1FKe9picdX(TcFYMI`Ux;1MeT4k_F(qVA^N{h+im}Oa(WYKffsdV2OSY) zVe;y@mOxaMua p zjz@6~?}|B(p~M$iG0{cSt`5EBTA!pZd^&E%TbphePIyybB!CN*2I?H%R00ObLUJYr zOBApgvz_!pGrYof2g}}fHlVa54*49V*;4<_y2-p|@+ObHI6Ot$9?6dK3xZ8j6OFP@ z0agJ%@|t*n3cm H_36_G_xw`^@j>)zDY=ohLCN^LN`D3a^_t|z9CD3T8oOoOt{ z4;5-oh<@!&cO!CC?ulM yZF!KI8ko)k1@9?xe3{l7*B*iZzL*mHC4~)t zd3>D$1rL}H$pD(0cu)!y)+=#mZiAVILQ*ai3qs#WS5BpVO-`iif&hLUM>H o_H17hf$I&t>#~Vc@%{WZ^!CoRw=ofw($1G39q3Y`DWiVz z^0aPuihJmIf+w(I%?s{{`oCxtucSj8-b!;C$uP!QO)SVW@Vh)&tmtcT&-FlPcKNZw zdjWR^$w&=YM?E2AUA%84*%e)tW44^m(>ZNFI)eQkSUgpM=f7xqB$o!!4Omy`ddHc@ zo6V;lNX;0}5z;T)$F57;Cf9{u7hEfO^?NzDs$JL>4I9pFxgCw+)+0eH#-J3i>w9&f zEC$}Oz!X5Z`(sV*2adN%I8X%S<0lZhFvjW+kT1B|%&z 8nOHX!8OE+f|^pDAW1 zHe}l5 vSHp*KOPZA99|78afkpJHAm9VxD| zYXfxPZrWho!j~QA!(CLCDIiHJ!%ot&g-C)TB#71VJ{y~<7?dOiuu}P)L;BF5QhDn? z%nBq3Ar(xfOVI01JK^<0^L~+v#xV^u=y=}qI-}9vJMe~}s8OblsiY mB_;apY#ya=jkBCeOgVKdUhY_ z-SQoans G#V>vOEeiCj$FqQGIPjsQnuyCg1*obrh zjDURyL3X@%6vk=hor8tZ$F|U`-E%2|1@evP2^a(^GfsT(-iN;DzFjVfryX6BW4oar z5;_>}p!Q|2C9m|0+Rxn#>o$EdxPoybIq95hL#{waAk>GUyeZ7U`tC)1clzeIx>)T3 zOZ}muZ-H0ED~hXvlB&u*?V|F!)mlc}EVYZ%37@#WG%?PJ8cqTizl-RK{a!r@3;o@< zd-PPNv5LQK?I-Ew)R d@l@~ zxIH=A4tljtkmE8zqG9>4y{WoD)}7WJF2s(kjBl)^4bb>+R5~3;j9F8Y4}7T>X~YTq zs)??pJyeTdnXD edM0f;x_&A%|Uv|aUK2g<&|-;gdpKgcE5St zm6_)tc8<<2i8S%$*9Y)pmyJ|lmEcAYRpSPqgaf}TLnKwmcq@J~nPBA;6&_8gh$2`t zXzqW2znq@OOgL|GcvZI~h-{UQ46Eu4?hiD;*@D_eto~?%n%37~t~Mh|`}KE ?=NNMO6&V{cM#(~ (jNMZwHdo+gNF9Uyt8e^ZpI*- z5dnRl(aXBPHY4^xI!tAT)-9Hken_i8n8xu6n+?lbcyAw;7adf9vX|lcb3?+^Z(0lP zS%bL^z$4o#H}tYiO=XqQ7F+kQt5mdwug5LI__Z@@iCT^xFYe%3@*|zRVeT$?4HR8y zb=w;BT1c0Oh*Znt?_A^XaezT55g9QDWm2+lVR-8zl3<}gB+YlY0xzx1I6?|Q9N3dI zz7oi%df6ZNUchAep*WI 69-0B ;gX|5ikDTog-)keBly4C zqN|Z|GVddRL|jS{pc!k<@|&6Q09o`nX4H1yJ4O?aZ3E-!gr?gu*B#ww@0-UnP+1RO zA4lHrnjW&|yIF52Gm*Y{XILg2`>G8bUWKGZ9<}}NdSd_G(UXjO$K9-{>0+|7Vp?n2 zN~uyIeAjwiJ9cHivga8C`U@#CgrV7o?ww`==Sw$zr<}YVidA9CypG@gT7CD7f#(PY z)X7Ha(+-16r5cEPKBhO|!}ZZvOO^%`T?3d>wz6ij)W&RMD_3`+gE7Z(tKH55M;4?O z; 0KH_RVr?9SL8E!~P4TSPfUIiU>&X5W7C zfuA;(X}hqaR-ON_$+6@{XysapMY3TdvwpMLm9Nx%Q)(}I(suT7Fc3Gucjsf-)l_F= z!}09@sh>q+P^EveOZ`_kGfv1Vlw355$X3^iZWl*ktS$KvL6J^41=ONU*gfzK+UIYA zRvE@Pf=_Rvkfu>6s(3jixV1(|B2`7mL04epS`Tr3vQgRE;O;DS7*T)I+BMpeK1<$^ z6*;q_Prqc0@P)`bWuB_fcv*-(g7EIm>l{bXM(cyDuFiBbi<7cfmF71pQB*y{1)r)G zIQg}Rm-LpXP7pKV7t>rv9yEJtAB&`g%h?iro%se}aKQ&F_x#g4`~bUt!z%$n^LYcZ ze$w#P(y>0BJe1}EXROm$$bt9NUstbz+@_1Nm9M2<@MCa3qa4E^yye{2E^OPmv+#Cn zq9j-SwYBQFaVgcbAf=<>Oms*y_T!BGpuOOb^KjqE$Hkx!msh#`zRm_B7uMr&Usq%& z_2%_pvo0(K(eA8)aWa*;VnWk?LI~Z^?wG%v|6VxadimOL>8aUggl|A=A~W8BGza_k zd1cb>4R=t%x6Q_~4&xE(5#NpJ<2Lc+wt`XTw)%PF1HpT`(*AhUXC>RpD=I4oC6In^ zK=0A@QO$PE=(5($p(Ek!iQ>v`=U~&hd1(2>TP&<8X-)Trsp*@WZ)-yMVzc?~-oP=9 zR1DYLD-x7?n(lJZALi`h;a0*^v6;@mU^sAYkC$Qg93IFw$?9FAqbc3IHeq;!eGKX- zHT3%Qb*c`Q_n&Fzi-mcT<|xw%O2%cFG7FWJX_`%2%aO5_ynoVJbIBj&H~i4f#rrl< z{CMGY4$0bLjt<@2K)-2*-U*CyFn#ljr-O_eoX_`%s0gPNPQ)JY*!QI> UNa&p&@cjOCLLeUT z@v85UWe!tbbUhRuTw{1FVMgg4`PoC 4cjz4$o$D~As&^NH|9 zxtjvKqNw=lxaJ83OwDW27h*O(`A7+lnqn{+uJZm+ORr_cf`^a5+mn?TRqy`V?^b+O z1{<)DEgE)J)}b|wgmz2HpLFD_u7+ mr(6F|3*Z?C0|n;NF6U|_MrBwL4g3?Rov7;0_Uhxs zNQ^0T&0I8RA^V^SDBL@?!0&DwMxv+<1U448H9cy!8GWfgTX*ah9MjEN$43G_+feu< zeud$B&FQMTkv>gSSgT@%zG5u!D}EL-eq%-_qah)!@`=)r_QG_O;Pe?^X60m!vG}yv z0Y+F+@`oFaFo>mQj5TyY-A4--k^J(jO7LqABGRBGHAo%%VN(9)By=UUO=4eGf~tzI z6G`z&Gl>r1k_1IFbk-;R?pL&TBKv5X-Sb1 ` zx2T#}C57$VLE@&b29T(AC4!97JIh>2>B_{ex@b-jp)e4e58uy$expFHtOmG(3-XPn zn_!Y*TRv-XA}3%};mvnH@@`tITD}E?(gyB4cWIso22PH4lh9Ii`7j6=^MkYd(4=PK zbDY4hUCKnXWUX+spju_pEt8ew#qsDZsX+#w@J3~C5C$r~h1bdEfXmqP@tC*$5y@4U zi$!y7PyXn6a{U-R4&=krdbbW}FiHT6NM>!e#9INa0sy^KyrI6c-u@8H!eA>|RnM|@ zg*DQ`1QPoq?hm4GG+xQQybhx;6ZKtLy**w-6rLM5ZFc%#KgE(%PtVbQ{B3q{NKwAT zKKq550e6@zM}Ze6p;SWU3g5_i~)`9_n0jN9rLZ`(ma$6)pEUqKJ#OG(^O8$ zuJ|B-faxJ54z%K0S3);uw>V=hC|W%gru};P&@{~XY19eg{G!r_Nq_JNz@mxuPMGI6 z{eIS5RT-jC7djMtL42pw8_TuN39rVz!MqQ{k(O6bN6%ipcpScsVb49i*k)uWOmFA+ z&L3|`o*P@xL85Q@@FFlX>=c_dRA3T@2?{O|xCM6*7g5{<4L9r?3C#tbQn;q2OcIpA z=*Zq^cdE)!h&=_Qu}~quM~1GPtHLu3Zd!0aoKp4m35%-v`1nzdnuF~Gs-e*$L^4)} zuS`U~w#7_(HnF8Te=STR7xRTHh8$s}L|pVueYyQy)S a1v;kwas3Mkn_vTUnT%dLhH++ zCB6gqKQLK>P<>Wplefu(NU_?>b-(GIw?<9i#>%q@7qV}~)@&ajmnc{vOHPvWCf*_S zx{vt`_+)yrqlB4g_^pb%B3r+X_*yeorYkn=T22PyvCprU&9OiHeQkbaIZ;Vb$o9uy z_Ask{4)lb!jGbd3t* {{-j7-`Xh(7x zlEFl?1Ozn~UhBqOih8?=;zuFbpL|s7DzqVapLR82c;7Ck#2J#+qqikFWcQhkh~hHX zJFQdNLb0d+#?Az@wpm$<5J1ORd+vgZ4<+tmXiLcoV5} s44^ zupR4XhUi4_MepkhG>6QFwda5GzUur5FU1#+D)o|dU~JUX*UAuQR{(81hCIIiz~=;s zPp0B(j0F$ZNuYuHl=k|L9EGZHl?KCcF|+lw1@Y3-C2OCQuWMIrye}O|mT;A%bpN9i zKrG 68CJ z3553U(3Xi5TP;Mp2u$rTPpa|~{J_{BQkAmKn{T>SkBaZd?jcVoqQ)$8;~lIB{BlwJ z@d11b6`QphWcXz|2oouQ+A`nXIijum*;=KaoEAJq3iK~|qxt6d)4!sASxperP+@5> zh0D>AdwY++BQhGsa2tgF(X$+aqGyGO6`+a3%(XjIGY`E5u`lnKoDf)#FIOu6Q~Fqw zmZz#}sR|Vr#l89tu1p%@>K8j4r}syGRb2CKEs>A^o#;i|NdC{K(g&0kGryu=mREfn zpzMlxaFgeQ?n!(V_78R2O3PA9 (8lmch;Zh>CJ z=Wxs4xx_lW#_$d-PtNV#+D-#_BaR*&_!A(M5kxZ(l)|x%65DGJEKxFdLJ|exi09+$ z47TPT9yv`plyoSAE=Jo`^)cu5e)p?0Y<8jY|DaryQMB0u773+0eEn*P5_5!6dfSZ# z>HVXNgt^=FS<8FJzW>+)Xnq9YujRE7A3Ev%g<0b>*jXXdXKhyZM(}jS`0eVk=Wv0| zdR2)p7z^#jLEkV9WYU(v@UIBWBOUj+-lgtl2_=+QL)g)BvtRQq>ml1F!{c-6E2Ha8 ztMW$-KdFBs9Ntl_ai&vN-CSF5*8T>9_eik^)Eo0KIvf<@n@u*m4gV|IxxP!2B;&dy z1qU1~KhAa3cCR2RX62v-s1XU_2ZiXud+0er3->j9$k(C5wK7z(BQ!-2YT|C94$E!w z^+*g{PT}^gHj3!2?x|GuY?_g-+-;si$-wvR7cdT2vRcL{I?Arz3%y5>+8O-1qo`MN z>y|b+_g3zqbBN5I#!cte*Fnu4>*9_2H<{vgE1J>Om|u3 MzYID-2I#++Hz0Px22)A^xHE^G@eB}T!(J@TV4}yojhqh13{qC4<)^g7H?lQlO ze<}Wgy?=yqwZL_M=G};(rw!<5_BACV1k`*`{c%U#Q}<2)l@eo8@p~5cUeILpAp!OA z=6tK^T$sMKp!GJie=egDE~al#`XKpu6(@X`rMN#fhnqnJZkC4WYxxJ|+oU5@PjMX_ zU%Kk7z*YM>_!;&|CE<8Hi8~(1@*W-Ky(HjEBmP~r44`IU=d_29 KBb^uRm8We!6 z_(dB=%8Pp>xL6O3CrmiSpsF(&8nI%xf2Y9rd?NY4RP^WAHhDD~=lHF0y9xF~EpRl{ z0BeVDV)+czRxpt%9u;N+G?Y*|G&5Xi1PMTl;8DmMN8A(ji$0LKMa{$t(i(OyfYhUQ z{RwKDHctnN#!!-><483cRnDU)2A& ?gOTapQ z<2h SXaVA>j{rG37$*TQ=x*6W2i `>v0kTum{g>49&pPdz#ygi=W4D5XbEll zDmn#mlF#`bP-sgFEcnd0Eu-v*4NayglbX^pNT)_ ^tfPylp;bzW(qq8UNVvh^fcRU>H}9% zP7q8P0v(F9v^>WhEQbwtPV7Q#cWzB$cUCEObHWXTtB>X|Ss>CEq3><$ e#b(s-j468FQ6&zv^*LM>v86Nt BkoC5wRYcwv%;NV7 z7EYu&Gd2PA>WaCpQbVQ;K0L>x-6ihM=lLoxwZEySi;NLWXo@$+@9xY=J?T4spz*DL zV43SHdqWd@4fj`izYkrX<)V%&xA`d0z02%MIa`eD-8Uf}h%A~J{Md56)0=WwJu0<` zlZ~^cKcp?jlnxmx#?QYONo>&h1n=n(*d_Cd60ZC4n=It1DQSns)vzy)7G5@cn;s^9 z`#b;{TLR&LRE)eg-lt1tD24nv3cVHhEnxz*vDlB1F0HpeCSZji0a_RK*l12rCkSHi zRhw~LUo>$@rCL?vzCfQ7yH3N|;` ocQ%e zR13Xit-Ng C?r@di+8?u$wPnm9|J@;YQiSv R{`<;dbp=dp<&RZ6!7fnRppTf41 zwxsBou#Z*aT;%7!<+?-zn76rkJM=@z%Kh+Omk?zHT9>=H?pm=Fesf@zQvQ~2@{1!9 zcoISo8ON>BE%#zWjFh~jZvU8J;KVW@ynd#z=l9fu(h2w&%{wFNjax4?Xl?0o{s=uL zdqAuR#0<6xql|D5<1B>g{H_d=*A}h&lW@+M#;bI3{^14_MWhNX!14GGZga@XmcyqD zK%f<0U-Cf?X+IuC!2cXzXyJbij~U{c(tO(fb-R64zDctx3igM%V^1jXD*M(6gBhMt zPdr#uU6|x`^P~*ui4hor{R^$*6@P_)r|6K;^opy67_@&Irtqf{)b=bVemBgMi+&_A zj}indFdjl2s$50JeUE*gflBemG+G!OTn-S=_DuHz(~V `n3@{RirXKN1zxs6F}x$qC9r-J5c zz1h#kX|i2+DCeYeY8(YN6fP>EZlqmmd{t+=YnwU0hMVURyWG>N))=k?9ZfCHW=B-d zl_=4+qrB6j?gqWyGV60YHxC9Su!RQTZ_${018=2W`06` z!Wk(QyS;T)3q>E)$BQtCl1TL$hKiUq#V(5+BELe<^+{TZHhoFQt}kwQOgz6X 7BL{i#oC23K72n I~HlonC8glT>h#AYU-+&CL`0X zHT5YEa_S{zP7qIp>t;Za2Y|?{*d}qkpTVqj2E))4R-SO4 c%!^KbCPw4YY2Q`kNB+@sb zURLUOJk2ErIhr_RO!?Q*HTm}W&3=0^ q=)!++xB|R8n1JavV)#s3!dlo!)JRG z3kjU(P#>}g&4Z_te(+~1EKe-yMZ}N;@?F{|rkT`ZIa+vPi`dkGZBBKAex>Z?k4K;d zpHt;DuSvS4_N`dIUO_)ut7%fvrr&YL=Kayu_OY1S=PQ$W#shZBe$X2w6E?rU`NdH) zHU@`?5kcK+yFX5{9E-?SwO1#1UuQlk$>(~ds-oLxjV|NCZozA8;)ENn;3iD!sURVu zhh}{Q%`ky;!e&wb29waCQy|yw!|d189o_0P7zdfx@Abt&oN(kK&53u!#1HNM*a<}0 za82AHT&|lPw7XQOW@NJ-<8Fq%FPP`N7CxD^*RSaKfSqD!cuQc$LsB50`WUHaxy4-g zPBWqp-99ekWUjceL{);6^SKk9 pH(9@W&t(o$r&LdCC zkmW}A6hF9S=nuqfq9)vw%*)KMOW<7a`tgO!85T~XW2gNlB;9W66G4~s+P8)wNI&6h z@Atv#8>~l%M0R*OKe-~#qP;@mSVw11OC_ qH`<>6az ztfs4#y?axCQt^Wa{d$EVQb^uF@omR$FPg5hjmiTi^SKQU>eEMXLC?v-((8}&z-tqW z@Rz%V_EHhg!u0OJ8;QV2K|h9KYoD3{zA2#h%^q>-gAxs1byTOM)fp|n2Wrvk0#g&= zD>-EpM2;nB6ri~ BI3O11ml(tVub8caqlia;v_s`gH|1$RKsmU@1e za51&$bamzvra#V(IG2u)Kkodbv7PL@o#7cKg7zH0W5 _Xdvkoz zDl-s%C#UvACwOtJ?8BFDZlQyNDq^tjCNkz4@Y(0rUSlE`VfOlWc6xs?j_n&gLcud{ z1F95Biri1+-%$Jt2~>v^dFHVLE(N}h2Y0^yFU1EdrQJu)OGevU^)RLYm3%g=|3bzS z>3_lxCN?Z7PFzHgehLf3&4m~7h<~XOBp@Zv5>ev4!K8lg&TAVDST<0w%oOYh!gp?8 zTFO)tZ(eYSP(nQLe4jUx+R5<@s4?I@Ij-4hJ>r8in@*Df^k5*0#w&Aa6qeAIlD}G5 zTdo;*v7ErlhyAc(`9m;WrdfJb*ef_-B@guNgC=GUPmP2(w54zn7Hpp{1T!FoPz7U6 zOW9%D*NQ9{nnQID$SOvKse7!&3#e(8Z U6!!ru$3v|S`qX* z@QGe=MA0M&0MO)_Pr)nOpVLp!@Jcw0d>P2)Sv71Zl|W2IfhcgjmQ4VvV)WRyomDRS z>#o1&uKxogqToI96;*e#F|`=YO`!!jYBMD5iX&~&+@)bDMo#@NddNjFUZ@`NQK_EO zoCY-h2m7j)gaKkVl5X}O#C+|`yXgjWO%j8MXNJ3@vvQ)CF(Jn>FiVF*NFCe#`A22h z@fH#BNO5t#hQEf$p^Fjt!27;Vq9sGI21E+p>E?zSiX||q^w}K!;*Z$brlWB8$m+Cg z+yZ#XI}0>DM_V}aLUAoA#b@LdJgDMcX#Gv4ZE3!$q7B29QPRlRaC4YQvlHfshrsbT z6XskpF~{h1#tX1yr%xmWtGg5OY{!Q2ZE;Y6*X;Ndy*t9ef@T6RGh=}n?V_oCapII9 z4iK^JRgrO^|J8C-W`QgMIauLknqDw36nRKfw&xB|$?@0%bdlR1J7y2t3<7kZY<}lL zWGt{K rG_5O8E0k1i9 ze!y$lCLJ5c!#5Q_^k2xS!hRK&Yb)&b;dtxQS|s+5w 3kGdeRbsP7P(8DK3#4r f zh?(L`!Yl#6OlI({ici)RdQVWMHcAc5LSP>jtRyT);Lzf|yhwJBpj!ne(3SUm@On1N z_kfvNL-e&OfE)Kk`Ya(Xt~UAg+SU(!LiWCm)ytA0bqVe673I#S&m#lcVlaS>euj>9 zFcm0a80%HBG;ZL4Q=Rl{lriZig2%%{aR~4CW l9hQT$iM}9t*NbG ztHJ88@|b!NpQOFisiHi6Ud|TlmXy=M(@c?a(D~;X&gpAFm;(RMA6XM?v*2iin}y-Z z*AtYM5ErcJ+g>#BgJ2+=bHFO{ld} x4yE8Mu zcT`sCsmAPb)SmqMV1!sVlfFf0seoy}7Cnu+ow{rk|Dbgbas40%8ZX*5@NTedYk;H> zc7JcYe!w&_x#eN^Uh?~lHCt793zZ*TUBPe@=|&-tYA31Vy!$+;jzpWWjpqcoa0JzT z4BGK2Qp9XB=+!6SiJwmN8KX+Wrv&~X*F2*|_@zFfq&HS79l#Z!1*$?;huCf`p$gr5 zIg7-MFmoJ1xm{QAs^RMa# i(p+7hrubew3|K?<= L*B#XV &&xIu>JX&5|c#n4n3puOi=h}( GS5nF3Q-=FQm3$ry9@S0AicY&T`ji8 z7eKS@CPe#BTrk9diKr&zEi8?UBQCsp{6UVk0{Znh^)MIQlUIO>Ynb-M`RUo93dzOVXU}QuJl_3$W^`|o%)Q$35l7rn=oOlvx^0?@x?`dnD zG{~UNDq#UQ{?X;dCm#?u goev2e6>mQ%6AA&bP${3YPZUpOldu; z06@Y|17Rd_#e%;G0G>xF`3k)~BSBh 6 zd0Ug#t8vZhCV`DaR>tJoT)v>gjDELDvcjYN!F8`W2D;!V<#`3$Y;eTgTl^O9;F@=r zAS4L0_vv289PSYV>FZWldw7}%li-u)47#u2vH4`keVTc!9{mu4U|WrTi)6JwW0%Td zQ4vVWbJqmXzE6(3q{%kvg|}zhczA;~AmWvzH1xtd?W*{ssix-ZC>rc=?#Lg#K8P}~ zQAjAM-`g}cwS^QA7<~x-O0wIWc}UG@;6-S=3-@x4khI(-bnw7ooxF4>khiV5Z@w;0 zK`^@ZwIuy@dMCK&nCn`ez(7EtcQhs;4FnbYQ{XYJjO^4D9F$kG?X VGIJcgT_p4zO3m42Yq|T*-p%(mzqX|4M`ISJIe^3_+jf+ z2(DJ%&FW@aa8KV0aQQWP;A_g6U4uS?q3v_?&1>2p^oOMlAE=sR@9XHodO4-GxwhG` zywBbANgG%6uXT?~PY;ZEWi5f7- fuya0xn$+7KlrurtVoS;1S zG%3M6=S2?+%!gC0@_OTHPIK8@w=3;kIjoT&eK3DLI;nx$d`CfRQJ)|ULRfvt((d`~ z+XlQR;(Cq{ygO#;Ie`+*?f2hj>|3lPyjKx-PGwC?`A+!rI`U!X6kt+ElSz$8ID0UQ zKRAa)@{Qeh?zIr)bkCsHxw12|eo#gE{gyEoDU}a@7$qp9tbRuNh^hACb;MR40H=7d zk_lJ{{sZopcc_1dpUQ>cTe$C&vJ`Y92D`|G!L*S!SW_jg$L+ge?*SQmcoo?JumJvf z=xI%{dN8{By`bLokp;q@``P0Q+ybFqYW9d`jjG9LUk>&~x7B@{jW }MV#3v<+An;OPxiU@`w3GYb(movmH@4ov+GCJn^bQot!w6uV zjo$SnZRtnf0_7m(@m#IABmI3{3@YB!RKLAC_#m7AJHlI!zYAEU;CJI%zy=CAzc*NZxG3bMB=%@2Ru2|M_Q*NiePij$&YRsoi;6|`5 z%z0GFNB2&UC?tX5CIK2y0siyPD$B*JJE6Ne$FLmNyEpc-I!16Ob0#(THN@1Tdk YZ2b zc`mpF%YODWLvQhp*|_?fZvFAe0K?Bwi+*tP;=;?$_%}9~gTw>#CZR4^`H+PHVvR>V z1id?925uB`Qih4HjETCgj70uzy)$09yI7wtxe$HC)cOwS2KFw=BF~_R-!OrwY~YtA z#Og8dP!}y7n*qSw@+tA1CCRr&eq_doqYF?JLtOcu*9o@?z`)u%t&7i%t=gkD>1SFr zkr@Y1DT%MHnuj_rl VwaG)?w0fsU)UkHSdbsaQX5#*bap{k;15sMK+ZLo zYHj2cZSda0KY>=mV+$VpX&XA3$B~aKGX!bpOuhhW3OZQZ-rGMP8K`|t^%V5Z#2fiq z#MxOE!2v!5Z`|R=6jT&h`fQ-@!(*#BVjHOr;zpHau#43d>(TBz^6y2PT1>MR76B}G zu;ka(M;lq~ {~|JhW#)4Ha4<4jeH{~$1;KX j_Dj<+jn5Z LZc(SqYeU|}Wmp3@57d&)yOtjXS+A7755 zJ-Vz e%Y{2%tc&HuP3EP6My5y9hZKhX-#N_}|6xoeF8k{)aSx28SKz~QjLQDWyLxpi z*vm?j1+eoSE>@uNtfPQEvbn%O{kb}&Hc0)z=X%$LVvV-S_t!o6ZW`-(+bJ}1008eP z07mC!&VO-@{gMT%w+5ff@@V#MmHu?UQkaHn^LA$sBQ!yvlJ8}FIxM>7sVV|d{tRFa zFV-2}E+N44S?|W*%eXIRgKXo# Dhp#oh=JH0#1;? z1gBeaR?of+ExEk7z*@lzkEczYERILi4cT9^2!zZ&QL_u(U@g*oG&>ANPmLxH054*t zIJj_Q-_Yj-d5%GVcQ(UH_@qU2ruzw=?y_;>fZ5j|UA)F)`kNBZb#w~7#K)8=eucC} zk|UR2{UhsovTbR7o>W}AivY;LN6bgi%X38LDSq9W5a??DCd Hqx1c>n z_ u<*r4vfi?Y*Bs&zx32 zwl>=Lzx%$h4sF@j*5CQCn}b*jKcM|M-#X5K%SkO;^fDZ-a?|3qpEm>GIJ>`kvU=D< zU^z6uG4-Yyf=Nduo+^hgowGH@H0ln}4T$1BxIW>Er^H=p^$<-xQlzPkxJP|ZPEd&f z0NFY$)i6FIUc7H&r*_i`q_1B1{(y_cI#SQ4+`paHbJrMG+`McwYt32bGw2<@ml1pq znsZhH^wI(uN_@I@3`_GC@(JD}Kj@`+SV4K(Zf`b6#p!n+pkASI(xQLD_TbwYyZlKj z_9^eGc~IhI(88#oE)6nhq6Y{RgsVQQVyNaB#F&}CWybn@W-fOWYLnIsj%lvC*?f=E zG5(f =FAH4?W0NmQ`XzZDDeS(H z?~TAgw9jd`w;ix9scuRg{Y7tymPD&bmB*d*eb-lE=*BTGoK1(fTs1vm+7qNUS*Hv= zWibppbNvJBQT&_Yrp&yOb|; +%cQ`c*{oB3$iVdQQXTL^;=mlBwuyQHJCXEt0qV(n z_=NGbBAM0G2FX$^_c5M#MS A2j<33(c(kkozN(YLL5&m8CL%t3Nw1g Hc1y*EC 1 z&U7!Q-Bd;jeWZ~kJQA$;qxuI(Ug1CJV2kWjofRhT9r04puUX>zhU=NEBi{;aic4w& z>`ijp6BuSqmHyyLnwMELA{ox0KdokNqWmM0+z)^wxQkxEzov3vynrLkYuAVE3Gl?e zkA2`?fW9rwlU)x006+jqL_t(a>zq;!yRWR-Sc@S=FN$uhhtBdD7vf%AJ+7s2?i3pzK=Y&^uTe{2h8`k$J_z=MF8pz15=~i$D8#zw1s4`k3Z= z+N9*Z^0zBd{2@Q~TikalA$o47rkyrG$QZX+4@y^DS~0g`rZK<0u+S`=3&I}eG)o74 zzUy&k_dkqToc-^wD^9^s;yeD-{t5U$q16+u`oO5NSXR`fpE0(hgFx(!%#a=f-y>Dy zs!)W~mML+`C>$>oxjJ^M$q(KW=n-gV!n}63Bw7Xoij&im`;uZmap~WeG(|q^TP3vG z# C~Typd~++ZY*OC&}Lxx^VTR*a;K!piGvXMSa&Le&p$NpaNolZ zm~e7>QokhH9iDu&Wg!AMX ;(NBqjxHjR$W5+=N$OYjjd+2Z|U*as5^97ARaR?iZa%?FQrFhjOiV TTXKY*BQIJ!#@lskq2)vJ {CrkKwy?W4c&{zKX~ek-%O(FZuj zfz2;&zKKPnN8*OWg-p0)X4#aoKZvYf?(49h_K|n%e~K)|QCw^LJCmShn)|Zea>zWe zl9-lHS|~>p#-KLfr1T==GS5KICv%v{b;AJ?9W9q58x99$aC+4lbYt%R8_s`W67VC} zi@~YiJ2Ch>z>99cF$9bG;@C`IHbUe8|6sI>fQy*n{|S9BJI^{87E_Wxy|sXA?fH7( zKJcE?EpQI-TW6fpp8EH}g T|#1(dSn0tET;wK{Kb{j)9gB zDX%Lp!CnS8{j|wL(BFUGBL}Y(zKTXA_X#qFM@)XBB`rU0{w<1kTE$%zuMr7f&a{ng zCMCaV*S;MXcEcy5b&DTc-^TrBjnB@+(L`HDzwzD_KbgdGD7F4Lk)r* jhk#QK?cUTpA#!p zl;49o!hhai6V-BlbPxZ>!LL!o&&~Y5h+Vr+fJ=y68-d^0?L?(~4$ze_%L= LIeiHCx z3=4-%d_3-Llgy*#&s_g|g0CZRaA$z`Br=em;DrP)1=fCUJXhN=W#s;U_}z?s&nyh) zy4aWQLzL^QfS{jI|Lz?GIEo7?1<5da#qlwRE%Bxlot@utX(p0dzcX@x@&YiHQLhni zJopPajJDexBA`9R>11=k_e*S%ca-l!b}P@PeiwHbIf1dI&O8oZ>cRJ`Vv|8X%elzG z;Dg@;k^}XWKSPW~p0Q;25`9B~`Hq8*$lXqJPA7HG=PW_IZ7;)I)h*Uz3x>HGQlyak z@w|tC58GQ |bz^d?6FAS9OK-V(c#@+It-!H~f4z`Ot* z@ot_k^c4xbysx!`2eQl9^}&F7i|u@^0^7Ovd0yV2RQX(QWBL{IrS`pS9uMXor2Go^ zeo~@*ywP?99OlrxV_|Up-StQHa<6^uN}nl9AZ3|axTp9X<_K|ZN4F|OS5J Z_LNwc3gzk+usbD~BjKUU~o7&{ZZ-E4^kUu-t-*v3N!GVMRkm(cO6 zGfNpj>~=iaQSjXtT^~bG7*jcamBJm-1~%L(^!T-gB@ac+4AdnJ1-c#&e>L~@6+0+` zx4H;^wi4QVbIu}SmmJhaYHR{(qa1-7y`u-ZQDFbIkSDR2a-3_OIIRKdF>@*SMhqbx z_&)qu4Ag$2DEkuw{|7jn_2TZ(Go2I7S3%Do>@doJHOCb_7oh4=s0CnJ%y{eUQTrd` zUlSPboJYGkykz8I47!_pe++|+J{>KHsegU9)iPi=%u9wP1BnCvOT1h|yK++9Li+m; zeypLqD+bsDIDv>c9=Mg>MMuLcU`}VGBe~uJC&kbGj6{Z!M~d4KsF#C9!Rx=EJB;yG z$Y2;ny$3r|uLIYtTX>&EnCTM!>yO!W2X2LqZNS^@>8t|CAiLTj=)tFEIll%SZl(QV z`f3b?yf79@y8%~0{Z`;YT<0q4|H-6Jd|k%@jxE6P?5ds#)O*ucgXdy9L~~;IGk39z z{__eGJuc0tKFa1Z{M8BMmGy;bbyEN{s{9YW&Li!!Dd^^CKRcTfVuytdoBJ+60M z4nTkBr6iYP!4s(Xo;?#&G$c$Tv>eTD&P(nnuLJKLEUajbwfAzpo;lJw?&}RVbYS&_ zzZ7et>8X=A{O^Cc%WfEy&-HO4MI~@IY~2U!4nyRDb;OBlcRX{pE4iKnTjtQ>Upp1$ z3F~zW_F~F2U 5NeDd@QZq| z%NMA=E^j|22T>qJtvY@?<2Q+HIEOXfJB~)UZ+Xl3nh~XfufpDIMXw&Wr46T%mTKSC zPUq0S3{a29wk94eHrsGLVboRgN@LlVv%UF$B-qaY9-+5mz%mqAhO`?+6<)o-Bmis= z9bxdljmY2QdLjn_CIj1Gw$NvH@b*9yg?Mrp`VCaBba70`W2C*4a!JRbd==kHv&`eT zQpB+B%dAWaH~^Iwl!6}y7DJ`z*olMK3)BI_INIgl7&0)-7tKE|{4|r@AJ_uY*U5F$ zIP3<_X8p_cFJ`OWfX}lMJ8=s6cBVNKmmX$(-q-7~fq%q%-Zw 0eJIb%XfldYHqCKrlKDwPF$8I}c-ZR{54~F?F%k|Gw;cm_{%mn`6ey-G( zjpUy_54n!+H&DKY6CLtwO+8=cGXjDUzMo>~1Wy&$t6`5k&MHzhDN+|N6_G+#V4{iu z{!6n4*f!oGfZo+21JU wGIa2oI!lpg{1v0)SnzSECRPd6-DZKu5) zNDO{hU^A=NaOq)(MxI>!b4>a0Nqp4!b$VC)^xwsUQF_j52h!cc|4Yyd@uP`N@#xU= zjjB_^IFAGSok(%DZ L8v $x}xaGk`kNjh^BJJKCYZY-rEHP)9PO9>w(t-2QN29sy8v4#w4-g=2fjN}t2Q zBm0g`DEfv5`#EC79K<< VyYhfmDbON3n_Lp8;nRUm2ji1W$N5*ZXh*h17eR zMU R?Mu)B-`yA7?cQrccT)?;#tD&8_C3l!2J;uxzsdd9dz;-|w$$^)R1yCfZIwqy zDh5GjVDmUa)|2w{nE_4!E@IUGfahhm(vypx! Hm{5fKz#`7}UJy!vDaR@CR7_yDww8mK?gRnz*NMQ)qXA XoC~O2PNxgp~5KWkS1Yyhig1>b=18iL)tdVdIop*(c3e%G5Y9 z X|4Rk_ rHuULK^r;(_3N0#*RQg&!>D1 z-)*po@?ySbXen?WdUF8yv~j%5WvaPfi+izG+`Dm=N|mpXp}PazTZrz)j+=VHcM~?a zZuIZyAw7s2O^Ft86wXA)M~6mx(C}BjP>)n5_CxHlm=t6#Z;XBmn8R{{Lq|VizA}u} zq=3tSdR)04cmZF#Cj Bw$RfS;D4l!2-=IkI0R-)rQY_>K;p+pu3MN!wl(l;R7Z5? z5DPNFn+v`?@am;b@_3wKl%v5A{W%}s59%Gd*DIxjcR&3}aVDAO-nhDgUy8pH|GF5s zlSAz5z#Gs1N#I?LVy@@^VbmW3)IO#Z;B(S48CWZ>S^?WbO&F-SmjE0`EJQJhYWY%N z)UMDw`lgwj%e9`WI1Ic7UwaKu4{&L; Di&L3U-Gu8b XqxQjQOOFn{X7u>qpEg=qTg9G6EU)tvKXcfqEge#`gtZ^6@@UPt&vju0xOh1L{2o z8NdtR-(|pWk;f;%A{>zTI+%QD81NAN;8@sm&GYm^;|Ov60^c#BfYb1F%sMh8;q3t* z2LPWmqb&wZSUE?Fcsz{ooW{$MzjN&ksOjC#56*MI=bdMqj~MFiKz;!I<86t%0zdN{ z?4iKnK$KMW{@D+=eI&_s>+g@e7 l?>O^-=zI ZylAV}VEd77!oV^626%QfcZVy*Y6(!peb3FS1VPr(UileM{w6_%^|})xd7- zvXTLr*U5FrI9QuVu*l0Pbq+c3E3hH3Qd6+t=Yh|I*iWzp-(>*3!CT|rk9*kU$K4S( zBX}#R?3_#Hc9^>c1V{0dvw=_BOdAGqnB6of%pz2HAn-UWVIT<4^*rSeM8QCL+3p lu6cmkwgdle#8t<&a(CgM0DGl?$6(M;zS(y4)_+X;R{%BVl@~@G6gjQlgrIB9 zLH7eshv6N7J*g>tM=*2IaUtWL4(x$ybqDT+4iuTuq j@fKjN*hacO( z?$Z~f)TiS=pgg++44S5vNXgW|e_^KfH$}$?)VD3LBLbGe=^H@i125-j{GGt&C|nDm zo=@Ea%zzJ4V7-b;2DzRQAx93X6{7Nr9M)SK(9E=&>*er6c%`7pK)#X1VqhqZ|1u7_ z1C4(y{16?Bz_%D!13efH+R(>4AdroT%Ry|R9~2Jh>FrN=a7?cdc#QfxphGXQ+{JYr z6pK#{uV7h0WM9s;9{;`;*xF> #FD)$NMn5-t{OpDP zb)$4VW(OVY9`DduU3(D;we)g=343!zEfz`M&FNWu*ok?H&*P9J@*zd_-;4kiq_?sw zhaQ*%yXiO|t@?iEeMitwP <4 zfu&B4Qw5yqoZ;LK%yybM&44|emJSN*Z00`*WxMv6g96&Q|+9*EvqQgQpC<>zo&O zjq9)3wm1p6)H&!Z1~&B%^E#7w&)S7t_W*AvV7wFO1c3Ybj$e$DkiXJ-lB1$WooWXL zJCk{{^(DYKei(;yrqjVW9r&@6>%c%ejFt 6JqlPN!kxAB|>3_EGw_=2>Ea{ULMZV~VeTD$_xbFv%^#2Llg;YI+B6LfG$ z&XrCtXEJzSa8jLQggn^E_vrZ7*{4;=zzxozz%|sXc7`~20Lz>@ZyRTuQ|$bI?7eq< zRn^w-z1H4434}C4OQ@S(lK=sviGV0o1O&05q97_LSg;&L#eyBhhP?oaqKHTlX-e-+ zIz(U-LZ~5uB&6*2et&Ca8P0jm`#$fz&;93qK4 W6!zfnsdz2W*>7m;a=oNI&M5} zY~`B-ePFbOFN)_{42Q6=u5Y3E*TB|XfQkiobpijYfYG8H@QACeMW2_6aT$56yB+2j zT>^KtotLF{KjrG~ew1|QU7cMI0JB~Bu1sJS^u_?f{id$o)T0*{vUcOU%H%`K U_=(U51x7CRalS} io`5OOBD*aN4A0aw8n8VYk1 z@_}J-ZFWszKE!a1bLF^R0go-NmTtUkpdCT?yGDY$%3C!)Nt!r$GTn#{Rv)< Q_7(2|_CTBiM2 z-i^HPOIzL**oGbfj3e)CthQ}&i-7t{^l45Gv^cDm)mOcldCoCbk*|E{`BBV^lrjoD zYH(gNmiH=O`3@$3;n54r+X+}1yXpr1LCdlbc#@jz2M&Ph2pZI@YPC??z!L*O2g>$e z8V?>}!d<|#4X`;Do|afW6;003uE&UDX4vRStC(O`V(f#UQ2&--#?B$&wZQJsI~}O? zQRVj=RR4i` O+GzZ21veGxP(hs3uBzmWi zqZfFRzQO_EPsry?pq80ZfY;mkK$}AAy$)_GT|?T-D!^DU(K7 )nqFXz(cRSd6Og2vspsuL>Zz;VgA! vf56EmS)h_#|CVS{9=N=Pp(NPf|hQnM_VKfpS855%?DhOod^d?FzptSSApz zhJax4cx!j%exyVEEn$gQHJ}_sNDv31@;3rAFaTIU41zqGG8sfikCv3p5JBre@fJpZ zVzAcRih%WCK^ -GWTz$+106)eOkdsDZ@QufZ4Ii&oNcoX_aAtl~bfrC*G z;gJajCx8-8)o*~=458Pa+d_-Jo0#6;OjsKZJiwaNL-f+vQHJgi*r^`%sbxK&s1XCy zjwFSTlmAiROj6ATYKlntB!PwKH5{yO1|A|mo36Is*8Xh85FRbV_N=GI_In81oo3au zs#WhJlpsOV@`_+9_}d03^^yE&Y^eG=>$jg%(ieo|P`FCKy2z0iIGh}lfLd=o1=KNS z6L>400fb*_I9Hwlpt BD&F?KWY zQq=oFZ!EBn*}$^MdzgNaU@s-7ML=!g(&Ud^I@|*Kn^+9kK>S)pSi*BFYPu2l584b7 zXcc(;0hCXk2w*CVO$E*% W z&?B#L6@U}L;|ZWFt_1jZsJRlT9>Y%HO|&q>f$<2K3=d;y%TRpHm`3&M4W8EnJE4AE zf#0AOGr*?>bWkrvzI-IO+A{YAupv}XeG4%>mw{Rb6kbn2&xw4eovh3b!jEJ8$?({| zb4HNk2*PUFOyFAZ5I^RC=?{E28nL>I@8l~;hG_xcF9zxWw0d6hXCDC0WZG>8upMID z4ycwV6WE4yQlR_!CHatRGe7w*K<+f#s|^t{gttI%;o;{`iu4;&Dm*vCAK~*FZRBgf zAECo9z_Lj|dMjR|JK;6ZcQsJ1UA6$bpn{!%+R?QG_yBGAcwleSYxOMlg-=`Z`3txx zKb>x=54_Q|$DDt%$f55RgU1q}b}~tx-(iaHUEueavG0KLJyZnL>O^ (>jJ%W#?<0`MEm^_@C4;%0n>?X4|EL0X$+a- zaSdV_oLR7X24QtQ)l%GzK^h5^fCho5Dfcu`9_eM+qz>YRCN=g0C5XAedZ?@Ls9?%5 zBkRH7u>p7rHVc34x03N$jbKXP&Xcb!d_yQHyx)gY(}CK%D+4nItnUGyhC0f>kbETY zYAMB6ovT#*0sfbO$thR}*u<=2eV@ZI?lXippm^(ma@Hj}=kq7N?t(&!*VP0Hv%YKf zR6amjB&Z$0qzmviq+uvfzLG?5IX_h%>XM5O7sx^31JvdauqymZ1U4}%Q#U39ag}ly zsG-0rpbW0kYxdg*Tn$wv2Qr+)fIjk7{0byThVKcQk|%-nkbBXW9a8G@Qdir5?N%JK zNIpdu@q=X}zLWu}W*Ov$$fxMJoO|#@Uo{+SfDLJ98vzePKj9}l6hDfZi|*1J83b(^ zRDKg^&Lp3y)GigfT_vf;(4?A^Cqq`-iPB&yFqj1AX@?#J>Q*8VTsL+|kTe@6!H^Fd zTOicNN)YQ=7TZ}~(7|CbbUUJ>ipfID!r=7CV$e=l@h9PQNZke~=UiKX_6`qq?NV4A z&%**gPDK(d1uNyfZn+erE0?t#=LnrA>?QvOz)qMADT-R6NZ@)1umPwsK_;*r3f+!; zPor38t$dyHfHBsr=kKInMLM;h!nZB#Y70Dqd6D9HsIBTNyDvf2>XzC* 3TA2K zn@%l-hrGW@(KaBc>w%8p()mRhG6`fBQ2T_!KxbLxcNi>zY7K*xj})p7Fbgp{0d(${ zREO9@*x9k9n`fjj9jHwQ@M&o=TFx5nda5N>zIGpit|=U8M#%9rpq(qxd?z(m+pMmh z1h^LzxegeEBa>L5=q~=tp|j{9JR $THmcNQ(SeV zB~K~TuN6?sFOr)Hs = zfFJh|c6PGmgRu;p%Bcp_GR{Gu#@bT+PUP1asL^s|pi};zq|*gM9gvIm8e_gjHQy$| z6h!BFpe(p5n#_mEz`ay&4^SU;0P2{k2%u$p6 jn5ZVCLWW*2oeOfJPUW1#8$u)RP&3l*3}I_*u&1g20<8gK^!BE@YD*`&zP zKoPtPB$b8R#(Non$7xR=1CFEm }b}r=<^%f3>Fi9gpSoCz( %$Kt4@@;b`c(AT&Qcp&*1^m%svfEV*&7?%(q3s0{VD0hE25O6# zgmD1W6dv*?FM;V`*0Q>Wv%)*9G4%s1p^qbeA}xdPh^C(r1=L>9lR&u>%>#~v9(RC8 zN3(&6;IJFzEZeo|39#rSx@%HO{L`YC3z&i6`hZ2mtNy)^mL5R)_=^U%LD1U*r_xru z30zFOu?RQ;j!7#y;;^0v3n-Z!eb{h9y>{QlJuv5>HJ;A zpXLUHrx*HqflYu-fz|n!1gt}CRnAa4_iCB1h7(e>$B`Pzg|m(+Du}<%*1ng_j&gbi z9@R|@G&{HtSUaJnqe`EUPCX5!*C0W9qOR*+poTuz1JzSgL(&-SkU`Q_3!>X-I>OSE zd%<4t5#)pVVze_XgZU@9qHi9QaudYUdRa@Dqm1lL<<(4kC&+L__EzHWhO~DBvtfk< z;~PYC7VsUi)c0CoKM!mNYkH!;6I?ybR#v(X&1DieCJhpir)QXeKaJtZ4t=J*t>!82 z0~F&P1FdICcSE=XUT@euZ$4~oQCr3)ITXs=&1iEC(9P8YC+KO6;x6kS`R)+=OeA0* zz_t&8y-?ljfcN8(VKi_)Tly9NZ-!A~OkeJX=tn+D7?kRSEAzivfJ}Df?f`Z~EPcRR zAnP#FrLhwSg@L^;rDH4E4(nj_Gj!CzlW}vBr5NYgL)wt=GEBe{;61SCE}$lF#Hfu> zXA7_qPS08Z8)K5Z G<5@C5oJf%YknCS}ESWTO;We8K@YYMOdSb&%LC7Z{m0pl@jTj56sZdw)}wSn#| z&;(;C>{2dVUkZGcQSe*9mKd;9U_54|60ipKkzo(AM^%sR=T`mpu+E5p8lSO3{SokH z)N3%Xfmu;4JnGxTlya(7SNg6T0ayKYA=vQIJi>}R^$TB%<5LW<@rpS~hT8oG%Yxm& zcWr2(69jUzrYZz z09ebcY~ddhF0fkKzL>-Q#5Y2+8USBMZB_4wp~5&|BHxog+S6DK13eOZgM)e`eF ka$r{k=xShnQ-_$?+zvaR0csMVB2bo2h^mFn0k%RQ+EKwa zY*4_0ou8Zc!o}_w@^ENi2t}y!VeDJ6+tH(a86%F(Meg2g+PWbMcM4ZUx#SmrDmtSg zict0E%1 vq?leE)oCXs86b4i313RNV8md#JlMaGR&dy$bxifg^z;4BVuW zt4qb`ZV+e$dN?gICX$xK^Rs8A+ef?!!=qtXI%0b_;cpOeKMiW_(q^~?G2Ca|<6SM$ zuNpYHkYQMeUB5{B28=vX32)^z$_DUU?{DKrAk8Se5FRG|KF>=L2xe${C??bxJl6$Q z1W>q0U&Lt-3dAXyLJr6ofmVSyK%Q!XSEvdH+%S-ody^abm&W_=4p4rj5f!hm@D6iN{VHv& zMER~n*SF>)=*xxR>Ck%cs>~5#=o)oAa$)2o^4o# ?+tcSpXa-bK%HPU%H16*2*Ho2ry~1C)Z=|Uc4x~#pAC-C-7F D23H}VMdI1z56<9zuC3O_8|%9O%`mqzPe&$_qQm{bEp!>Z*;*N^c3p^`x*LzrZL zHuwI#y@>goN`s;?$WUc}1?1dy%sgiH0gngGXJq(GH~V}0k%!QWq1Bcr{2^v_%K(4E zMnmvImu!dV&UsO7YEK}(F?!M%sIKt}poWk^pt{_#z!VI&3{R8d)P&Y1v$DT(6*iWY zu4as-i1Ol=M_3kUCr}muOG$H`O`O(uc3WN4yV!9!l<>FVG2!vd^50o(mi)#Tx6_DW zw^I;Xv&U)qW%Hj~H*5pp;Sb#88UlhJ@Axw3D+n|ozW4jrPiAO2J+F7(do;igyxHrG zuH ?Rs?TV_!n>*`?2wBH*9A@j>DHFzCN~>eXx7 ztr;!U@#DT3S-Hx$q<(?D`}N;`>fP4t#TKtn&N%-@KlI7od}K++L*+>umA1@Cd6qe? z$bF6Xyi^{Kh~67FM&aVmE~%v~dy+i$r#0M;eGD9X_=pTk*;DO8{i_;2XZY@%eDiC= z@F)=Sh98&ZkGLk{Yy^Uk|Inqy`PmG0!U> Cbmy~A(BbfB7G~xfzVG_X{AtEV<#df%Qgd6Kt1-;I zW1GiNpX07Wl@4?-|K9o4^42!X(kCD !j54 zAumHu>^bf={(c(NZT@si _b53=|6r@@?cu9PS