From dba8f272406dd42b356d8a914821a85a61a189cb Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Wed, 7 Sep 2022 10:06:00 -0700 Subject: [PATCH] feat: Introduce alarms (#135) --- docs/AWSIoTSiteWiseSource.md | 35 ++ docs/imgs/lineChartWithAlarms.png | Bin 0 -> 56816 bytes docs/imgs/statusGridWithAlarms.png | Bin 0 -> 69870 bytes docs/imgs/statusTimelineWithAlarms.png | Bin 0 -> 70530 bytes packages/components/src/components.d.ts | 2 + .../common/combineAnnotations.spec.ts | 22 ++ .../components/common/combineAnnotations.ts | 8 + .../common/getAlarmStreamAnnotations.spec.ts | 3 + .../common/getAlarmStreamAnnotations.ts | 19 ++ .../iot-bar-chart/iot-bar-chart.spec.ts | 2 + .../iot-bar-chart/iot-bar-chart.tsx | 5 +- .../src/components/iot-kpi/iot-kpi.spec.ts | 2 + .../src/components/iot-kpi/iot-kpi.tsx | 5 +- .../iot-line-chart/iot-line-chart.spec.ts | 2 + .../iot-line-chart/iot-line-chart.tsx | 5 +- .../iot-resource-explorer.spec.ts | 3 +- .../iot-scatter-chart.spec.ts | 2 + .../iot-scatter-chart/iot-scatter-chart.tsx | 5 +- .../iot-status-grid/iot-status-grid.spec.ts | 2 + .../iot-status-grid/iot-status-grid.tsx | 5 +- .../iot-status-timeline.spec.ts | 2 + .../iot-status-timeline.tsx | 43 ++- .../components/iot-table/iot-table.spec.tsx | 2 + .../iot-time-series-connector.spec.ts | 208 ++++++++---- .../iot-time-series-connector.tsx | 13 +- .../iot-bar-chart.spec.component.ts | 2 +- .../iot-kpi/iot-kpi.spec.component.ts | 2 +- .../iot-line-chart.spec.component.ts | 2 +- .../iot-scatter-chart.spec.component.ts | 2 +- .../components/src/testing/mocks/eventsSDK.ts | 7 + .../data-module/TimeSeriesDataModule.spec.ts | 165 ++++----- .../subscription-store/subscriptionStore.ts | 2 +- packages/core/src/data-module/types.ts | 3 +- packages/source-iotsitewise/package.json | 7 +- .../source-iotsitewise/src/__mocks__/alarm.ts | 316 ++++++++++++++++++ .../source-iotsitewise/src/__mocks__/index.ts | 2 + .../src/__mocks__/ioteventsSDK.ts | 29 ++ .../src/alarms/iotevents/cache.spec.ts | 3 + .../src/alarms/iotevents/cache.ts | 46 +++ .../src/alarms/iotevents/client.spec.ts | 3 + .../src/alarms/iotevents/client.ts | 17 + .../src/alarms/iotevents/constants.ts | 74 ++++ .../src/alarms/iotevents/index.ts | 5 + .../iotevents/siteWiseAlarmModule.spec.ts | 3 + .../alarms/iotevents/siteWiseAlarmModule.ts | 96 ++++++ .../src/alarms/iotevents/types.ts | 28 ++ .../util/completeAlarmStream.spec.ts | 13 + .../iotevents/util/completeAlarmStream.ts | 58 ++++ .../util/constructAlarmStream.spec.ts | 7 + .../util/constructAlarmStreamData.ts | 11 + .../util/constructAlarmThresholds.spec.ts | 3 + .../util/constructAlarmThresholds.ts | 39 +++ .../util/fetchAlarmsFromQuery.spec.ts | 3 + .../iotevents/util/fetchAlarmsFromQuery.ts | 30 ++ .../iotevents/util/getAlarmModelName.spec.ts | 3 + .../iotevents/util/getAlarmModelName.ts | 5 + .../util/getAlarmSourceProperty.spec.ts | 3 + .../iotevents/util/getAlarmSourceProperty.ts | 12 + .../util/getAlarmStateProperty.spec.ts | 3 + .../iotevents/util/getAlarmStateProperty.ts | 11 + .../iotevents/util/getPropertyId.spec.ts | 3 + .../alarms/iotevents/util/getPropertyId.ts | 31 ++ .../iotevents/util/isCompleteAlarmStream.ts | 24 ++ .../util/isCompleteAlarmStreams.spec.ts | 3 + .../iotevents/util/parseAlarmData.spec.ts | 3 + .../alarms/iotevents/util/parseAlarmData.ts | 20 ++ .../util/completePropertyStream.spec.ts | 3 + .../util/completePropertyStream.ts | 47 +++ .../util/fetchAssetModelsFromQuery.spec.ts | 3 + .../util/fetchAssetModelsFromQuery.ts | 30 ++ .../src/common/predicates.spec.ts | 21 +- .../src/common/predicates.ts | 2 + .../src/completeDataStreams.spec.ts | 142 +++++++- .../src/completeDataStreams.ts | 68 ++-- .../src/component-session.ts | 6 + packages/source-iotsitewise/src/events-sdk.ts | 36 ++ packages/source-iotsitewise/src/index.ts | 1 + packages/source-iotsitewise/src/initialize.ts | 38 ++- packages/source-iotsitewise/src/sessions.ts | 5 + .../source-iotsitewise/src/sitewise-sdk.ts | 29 +- .../src/time-series-data/provider.spec.ts | 5 + .../src/time-series-data/provider.ts | 11 +- .../src/time-series-data/store.spec.ts | 22 ++ .../src/time-series-data/store.ts | 61 ++++ .../subscribeToTimeSeriesData.spec.ts | 265 ++++++++++++++- .../subscribeToTimeSeriesData.ts | 106 +++--- .../src/time-series-data/types.ts | 4 + .../time-series-data/util/toDataType.spec.ts | 3 + .../src/time-series-data/util/toDataType.ts | 13 + 89 files changed, 2098 insertions(+), 317 deletions(-) create mode 100644 docs/imgs/lineChartWithAlarms.png create mode 100644 docs/imgs/statusGridWithAlarms.png create mode 100644 docs/imgs/statusTimelineWithAlarms.png create mode 100644 packages/components/src/components/common/combineAnnotations.spec.ts create mode 100644 packages/components/src/components/common/combineAnnotations.ts create mode 100644 packages/components/src/components/common/getAlarmStreamAnnotations.spec.ts create mode 100644 packages/components/src/components/common/getAlarmStreamAnnotations.ts create mode 100644 packages/components/src/testing/mocks/eventsSDK.ts create mode 100644 packages/source-iotsitewise/src/__mocks__/alarm.ts create mode 100644 packages/source-iotsitewise/src/__mocks__/ioteventsSDK.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/cache.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/cache.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/client.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/client.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/constants.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/index.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/types.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStream.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStreamData.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStream.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStreams.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.spec.ts create mode 100644 packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.ts create mode 100644 packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.spec.ts create mode 100644 packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.ts create mode 100644 packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.spec.ts create mode 100644 packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.ts create mode 100644 packages/source-iotsitewise/src/events-sdk.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/store.spec.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/store.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/util/toDataType.spec.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/util/toDataType.ts diff --git a/docs/AWSIoTSiteWiseSource.md b/docs/AWSIoTSiteWiseSource.md index 28166da90..42f53ec0b 100644 --- a/docs/AWSIoTSiteWiseSource.md +++ b/docs/AWSIoTSiteWiseSource.md @@ -112,6 +112,41 @@ Each asset contains the following fields: Type: String +#### Alarms + +AWS IoT SiteWise has a concept of [alarms](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/industrial-alarms.html). + +The source of alarms in IoT Application Kit is AWS IoT Events. + +AWS IoT Events alarms are able to process and alarm on AWS IoT SiteWise data. + +To query for an alarm you have to know the **AlarmState Property ID**. The **AlarmState Property ID** can be found in the AWS IoT SiteWise console on the **Models** page. Find the model which the alarm was created on. Then under the **Alarm definitions** tab you should see your alarm. Use the **AlarmState Property ID** as the `propertyId` in the asset property query. + +``` +query.timeSeriesData({ + assets: [{ + assetId: 'id', + properties: [{ propertyId: 'alarmStatePropertyId' }] + }] +}) +``` + +What this entails: +- **streamType** for the **alarmStatePropertyId dataStream** will be set to `'ALARM'`. +- if the **inputPropertyId** is requested in the **AssetQuery** an **associatedStream** will be added to the **inputPropertyId dataStream**: + ``` + associatedStreams: [ ..., { id: toId({ assetId, propertyId: alarmStatePropertyId }), type: 'ALARM' } ] + ``` +- **thresholds** will be constructed to represent the alarm: + - A threshold for the **inputPropertyId dataStream**, used on charts with a y axis e.g. on `iot-line-chart`. + - Thresholds for every **AWS IoT Events AlarmState** for components that visualize alarms e.g. `iot-status-timeline`. More on alarm state [here](https://docs.aws.amazon.com/iotevents/latest/apireference/API_iotevents-data_AlarmState.html). + +![status grid with alarms](./imgs/statusGridWithAlarms.png) + +![status timeline with alarms](./imgs/statusTimelineWithAlarms.png) + +![line chart with alarms](./imgs/lineChartWithAlarms.png) + ### TimeSeriesDataSettings parameter (Optional) Specifies how IoT Application Kit requests time series data. Learn more about how to configure TimeSeriesDataSettings, see TimeSeriesDataSettings under [Core](https://github.com/awslabs/iot-app-kit/tree/main/docs/Core.md). diff --git a/docs/imgs/lineChartWithAlarms.png b/docs/imgs/lineChartWithAlarms.png new file mode 100644 index 0000000000000000000000000000000000000000..23c1b03b55d7676a6636d1c5f1ab3c8a864fa185 GIT binary patch literal 56816 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 literal 0 HcmV?d00001 diff --git a/docs/imgs/statusGridWithAlarms.png b/docs/imgs/statusGridWithAlarms.png new file mode 100644 index 0000000000000000000000000000000000000000..eda90abae52737ae77a5d438f23eb6aa9154c118 GIT binary patch literal 69870 zcmZ^}1AL{+vOXMJ6Wg|JJDJ$FZF8cD?TO8q*tV02ZQCpV%sJ=ov-kb(x9a!i?bY2+ zS9NuDbya6YD9TH~!(hVz0Rh2FNs1}~0f7X3mRq49KL7GJy!8M9!SPy(h$u>lh!83| z*_&C~m;wPwMkK34YABDO=V&VtH40KH3EKxrGAIdKd;u$jVHZXwOhN(`>iAft40ukZ_d;i+JFsFvr7T1bV*aW$D;J6_LC4mxHtArBwmI!Tk8=jU^P5u+?N3i(J${~r^wkKh7|s-o`fXnVV>0x3eu=q zcv{$yp8~;%?;8f}O^|UizcyJ5)2N1pZbrz2(h7qU*-dl3Mr@JhH7(gkCLFNDNNgFJ zX+N2;Z0PSFf@w(5b=dviJvHmLrE{6Yu+D~w2qIX;Q@X(WnLxf;*`=)U%!W+~M{YAui8#X8i&3UL5{Ku09 z*%YLspFy+{tvs5!1kJl6i;!FCxE(~DwVqI=6Kzf$B1T~lfda~*#1Pd@e-v|zaRJ)% z7L6d7K`>!Am{MsOA%+Qvp^UvR!&}J_G_VdK@Ub8eZc1?;3aAN zl7nRTWd-`N7$%BLz=R8-Q3QZwN~hFS18xV-wSFbIhEXqKmKBHt1i6MbXt)hvtjB4r zPxc%u?|ln=wP$&{u%mpZlt)eaAp@QF`;Aj+LVm(FO-$|d^0Z)>&9!;sY&(Zwx1p zp7Lz+eDb+yzRy9z%RJb%&I2r@y?4<*B2n;492?d`|^70MZDB?HCS30@*@%?K>&F)iW9=c&hOLQk$Kmh6-#`y2s;c0y30F;jEu|P0?wjFs zVb1t2FA_B!dc^F)rxki3b~?>+gm#C%^1V~^l_{X?Q+8D6ZeAvK^iVQm%QUde;AEqH z4K%eVw%%7CQ0pPDx>(LHCA+d~z|~ufy!eOPb5~qXN6tWaaNTbj^&=OFGTMhGK9ulO;SfbWHAbrpizoqx5_ZRic^}qMe^|KpM z)a8m2A19tkbrw=0rzR;-6WJ1;lVua@Qn<$R$EgmJo0vO6ai!A|V9hM@N*{iCkjH|S( z3|2s?9+Xw8bLBaGEfU!;C{HQRrq*^%C|7Y!KcQckD}PkkoV8%3(E_4NPf^eouPDwg z;w$!5EU4frcbtE+a$caGKbfzt=uj#t)h=(CFDb<+ORXSK>MGu-5Spo-HL!MIJFrl( zz+GrAn5mIk!mYw8N-t>nFPek8+!=cdlEMTkiLi?-lYv^7-$9(gQ}c+6Q?vSI`ZXWJKpeF!nD&h81`R!2!5d#z zNPNVv#FM^ra_)2zSy`B48gtG$v~8>Q`H>hGD3a98Kom5HcMQVq0`1D~vJFzic*`D< z`;iMLvXPg_>M^>e(VDS4^EIR|n_F36n^Ci^o2?je8<^Rm+pF39+B}QrT3DOoo$!hA zO!UkRjTqpJ(nM!Nf25;I$w|RVIi+RL=xnP0CEciHqH4cjTV*~`M_*rE=Vu|(aNU${ zrfljwmtt{hnQ7WK@@oO%r*p$tZAERHS?o&ND(pGOa#IXx^c^-8wkF%n_uz8SvHb4* z!Ti4XZOm^3Gg<{h-|6w!IbUKLvRS5DdY_A7 zC&>Cut7U@6tku2M15eGC+U#7K9POgkGS)hqy_;p$tk=DHX?c}+srfEEth~j&>F?W* z#I9iOGEQCBP79ja+&dqIE(LsXed>K4o?K$<@JTQmjAct|(ixWAGws_5<`@R-EA-pC z$^@3WMn9wiJb?rZ%)6aa=x7purruLeQz;^QkY4evX7XkFaznwwz*ECpK`+95N8G@C zi1R5wgT}PG3l3> z-DyYTp(^}G##v@1R80c#ADmw+3h>3WB^09g*>gDgy?h3q%$W*|%?5P76%EP9tfRcr z(=*z6t_6E!dRY1>2YK}thMa^vMhHbzAg@wHXE1vACX)t}8Y+n}Q*jY_nmc{;Z$(&B zxXC$XsxTivn4FG;CNjt|r6us~_p98Ot(AoB(z4S! z1j4ft6P#5F$wrac4zUl>4}L|Q50LFVSZ>TM%AY2N$haL4c_!Xs-~8Mb2b9l?za%ym zbl2ql*y_6rISz+mgB1%^H~&4M!MyAFRdR-bvHHYpdZf%QO)J#e}UG6WKrj2#<)~f2!@^T*@NAI2oa~@Hu6CA^^z8}Y z1b({=-zuIPj`bx=97#x@#OabWy&#K+@In?p8R^QFM_p^fOGYW^wQrlPyKftG<9U$%)Uo!Ev| z8=n8`MYfdt%H_?a+vZs3FftfG!j`vb$ ziICatlc@@~Ier&fPtgyRZ!IIYCw7MyyL?1^RSe{vM_yEZT+`Y&;RgiCH8y&90G6$( z3SX3$Ry`R##7@l@wU6`aE}ajnlh_OYH~Pc%?$=qw7Xml`yEl(*<(-m4&^dub{+bV? z*P?6dr={>JXYu7z4!_)@FN;23R01WFQw0{sMWGtdG4Q%E?M6FaU~L)|dSq z1VK6*lU0ugOCSWnpD+OuXr=XZJ~}{xx;kO5N0sCP>Z2i0Q^4B6CPy15Be&g%KRBKJ zQXgaK;_6z12v+@pQ*(S^$mor@Khud6Qw=FIIXNKe&oUGcI5090=w}J|lYoG+fx!P& z1_F`>#`&wP1Wfs#J|I9q;g&#P|LLRk`Tp~X`+Ppp|9OKZgaJW*enb9zLh?ZVr#DDI z9_as+fm%P?fP|Dqq@+II%EnHnrgqL2_Aa)CHJ+ap&<>KC&OkuuWPcuDDJ9aY&wS{& zrHY1&hMX+7vAr$5p^3ebDZPiS!=G`0cs;m3i?*gNhJ+rrHg?Y39(=_A>cRb4{!`6B zO!%)ZF4la+8ghz+BKA(EgzWT;^o+#(FocAJyiO)&+)AS2|ABvg<0H0kadF^gU~qSL zr*~(ew|6pUVB+H9Vqj!uU}mQK>_O-3Y3E|-L1*Vo^6x?ZXB<&eXJaQz2Nz3wJHkKX z8XDQVy6_Pb|6%mk=ihRgdRYFKlb!Q_ru8{NhCeL~O!SNl{};@~((L~S_NV3Fuz$_# z-|Tq*492Z!>0xT4A!=#+sp?PB_?cK)IC=lY=l|36UrGNBRdqIX60x`aM7r?*cd`Bh z{(Iy92L6ku=6`vzGW~<)-&+2L{4)n`B~xd68`nQNRJF5o;b-P$_`jfIf7bqQw(xI5`&aF!rSZe? zGW=z={4k6S00|%U$z8tm^jkf0xT8{U-pMHGci@BW)DJY%NoAx2`Cd#T!HljB*>W(VWxEG+K2 z_?(Ve!2$oE?o9~b#~RI9eVNok@zF*Ewuw6f&qa^#_yB8mF$y|J;J+vk0tw2F(55Rm z8f4e4t*E+M-s3}Tl2W9-O{cRkjs2E+GPG7dD^=W#xr!IGBS zJ1yVw0s-Xj)I|c5(s9F&XdN4}K&&KwX+55p}xz@15RLaHDQX_W98y%3NYugH6@d=J5ga4+#ZheHn0PCMB@=x~efo~@#v>9)2 zAjiuMsX9(08lDH~=0(4LsTC{=Uo(Bbeou9f!@MMycXdr}znyGz+787iH!`tEq1B~! z-}H^u;e9~n0)!;)t=-bcY1?(cOD{?P1X}cb(8J$Q_uFeH@=LqyoSlBsYh~zsQ=)G@ zB7WZ6Rq!=6<%N zZDv~ILwJ&u%|oLSPy*!Zr~=NaaBqbigCvL$3#UEhzs z!}~r#KAW3}h_=G(naOf7m0(WM&&C@~4mT|Q(D(Xm)&E;Po7MccuVh3-ff1)Hsg*L) z(tA1n>l9ug-1hsmCu=9 z;M+N8n#1mp`w$lt=U?>Jf%KFP9+%QwUT#%ZUA+t&QGQ~zja=!d!diMLihBdok zieFu=Mvuj5|(+{pjh|B$Ag{-B;lOJD` zoIBCp&@kTya*DR#n&mjLIxkPqjR<}R8P!BcQ(L`l3DB8bp;mUYRG}as9zWIfCjHBH zwRmcAfCLRK`NiUf!y8W%v8CJe_JB@FN2ii?FW6--*k{Jlv(`nPA5;8;d+k^sULYmE zv{Z6)0rCEq@uB<(_{Mi6K_p8K_KgdLoNU1_BX}o+8_)oEra5yWJzg@ByqC)C!dU(WB6%r94 zWkqGZ)?9aYu`Z+4WLIQpy;zxC`o!Au5eT^-ilHa!#aE=+ph7pKATXhVeK5pYBAZP; z6p)%!EJsE~^<}JqWB7nh+edmM3?65%_0&GDxtY^4C4+hhmq)x<;X`jOZy{E#gCh4O z7k9BLmQ@V&_3S(Lux&PlV;pt@4)=L&|r7Gg@xu~pvN z?3Y9d0$-h##&a-00x4fd(cRIq!eFew?jv$NuD&o8c_)GIUESN$t^%{iFr(+ov3V_` zbG*yZnl3&N0)fv}KCbgPCS6}1k;LdLfe&G>KOpq(m`CvCK|nYGS=pp__0l3|a^Ua2 z%svDNmDsfw|JZlE)ir1S087P?S21ZGQ97%0&VHrQ^5pTzIJRQ5*>TA;GBS#9Nry*% zGuImc4u6<^cknSkH+f~!c(LUhd*AI;?DF__sz)$Ul&X8db@7D47lWcSe|bHWtC;ce z+Qo6-;kx>elc>No0UA1+nBFp?tPRVPy0ncH#(;O=dB3pa^@7ORd)VZ(PhIr(l4;-W zXP6UC;MecGpt^L#^Bjy^=@U^=_prBt_&lqo-#@qf{g*s3Sv*=>uOX$X!)pJvDww{fB|YV$BP~i#abQopQ3Elt(8EP- zd*v>M&L<_yXb>owyyDw(bs?_p{zP6)O-+1S8832KkJVI!WRjOm7k%$@*O=7A#NCz` zY?*YQn~{=vM47&vG5WS%3F{r+o8bcd**Q9mTEV`|CY#fAKJ)6AD!qom60q+C9~qDo z(R{VNRO|C)n^f>Ao_u`X_xFu>wJs-?bJY!<4V5Zc z?x{&Y4gRGBv~l>f0@f?z!R-iG+-t!_7E_UEGpvJt@$+2)&1iMr`n%y zlT%zp`9CfF=y@AbCNKnTQHBCLJfs-jg7k2QaO*F+N)_vNhYFF&)PX=4Fjw`uKY{iY zGU<5>WxhFzzqzr#90teGqeT&T0~t%LFtJukq3!E|Ywbfg6(&>Ky~&5lQMtk9T*xK+l>{7++Ra6U2&h=FS@pu zHoRbOxnImALy?)C;(KMam>@!KqCaK!^Y%$FJ(})e_Fjzr4XR%1effEe4iY}wt+1xE zm{;>N^w-nkDX{d_mriA}+oy!%`+>dU1lP4&<85ut+5lTD*8Cjh_jz*IXX2)oPPM|C z2~M0j5FER^tTNpd3Pwnso>q(iS)VERo^0O*P)cFYuO3_K&-Q*iYHpMn>Lc_yFgI=1 zkGrhI;C7xC({~sdmP}Pv;2XejA4wqF$YYd}lbSg^n8-ZsC~SngpHa!3o0}smP~{m} zYhG+RoW+@G`Nugm@a2jsfR+MHkeW1`$9>Vrey#cI3pmVwUS7m+*cNB;6BVr(iWUB(eu(AWR%rqaZipL&tSDq^?(mTBiu=J{lT7cY$m| z*yvnoJc%hI>VWeUpSGUgv+DES+|>T_Va``EMqb`5lTm*$&rtpb-e`#-P_(tSRDH$5 zOPOm;EPNm$g&{omphlbYFy)~-rNGOZnXg=-$q3=R^+j@-YEG!Ah8K?&Wo3&Fdd-lr z-#4Fj1$5E)jYViZI8ENH?aMX`r3gkx(P-lQ%l7sUD>e`HeW;8Fqr>Vk_g-zHzk)U7 z-F>=}?3;&>4`nSqqg~nocs1@^0)nS9JU^MZ{iY-j8DI&hH#%Ini~fQiz0?Gc)ZCMQ z_nemz-Fh&DXi%*wmX=-y+xw~dNoW;|W%`d7>pr=r+xg#5Za^sIjpuJf$@*V=68O3{ zPk(~VCYaKCx}BoEi{zos+O9Ngta0+vgRg>FTaH0RJAHoCcdm__m7yGQii57!m=)Q=J?g~XpIVm{AX+0{pV zv|&8ZU;rdmgvg>?^EUjeC*F12e^wjCjsEX(w!bHsUonsp^bb?(;kcgK`i|0EL@d{F z3*Et@qS)%vX2nZBZ!MVtA86>Gv;pEhjE{;n$1L8LlaLN4F4 zWwEQ(5+yzFajV<9*758n=es$D{o!qilZXT8 zOFvQjV<<9Oetv{a_%o_p|A&Yzg9t5NWo6~YXxx}12QAz8BYj?b1;Uj_~M@0*N3&?{L$gvPVDUZT$NACS7 zcHgw9PpNb&7Vz-lees++dnFjQz6szZ2p2dSr`p=^eR+>gIlkJ0Ssxsio%&WF{vI7> zS?}ZW(P=+5qVKI|-*sCZcI(pki5W-fd?Jlzc?nU*C{(!5Qr9xF@rHUWkr?tx%>%PS(TO!mMQ2XdcF|D z!H=62&qBZn2v4@_flnH&RT(`#s;D=P8GJ}b9;gzVdrKtfPC5tlp4d>GufR%v^t>vqhyU*Ic88xD8M%LUy;zg-uOcPDtn3!b87-GS zj^xW88Ike|7}9`6Q~6)#uIQFI455r=lAw14=<{;l`Bb3A)=JL4sW`jaxT9jf8ZO)0HWw&Zs+aA zUnlz(;nk9$K@DF(v1B92SE9Thf8vl+ z=K=f`laAD-|C_@Il?kHxn=D6P2W?e@uVx)mvkXaCml;CH>Jy7i=zIo}>tJA-^5$6%;3W zcks|;#PL7tC5a8*A6AJ*TKgLRG4SWmJ%n470g%BNC_{fc7`uhGki@&c0wHdXI%~N9 zmHz!n1UWImaKWz(6rFeezPUM}aC@ORVYY~LJP}U+?(YylAcF%6fbik0q)64g;jaJW94z!_Y7lG`-~XYEZldTvlJ+Ja*8bhVKh?_kXKFop2mOx! zR7J>_Pf10ZZ*Uj?xsV7Dfm;Fby!iJA3#KGj{!QTL_rH;JMGU>k>T?;o+`L{#Q=Vte z8R%bk?De|XuXH}9~2SX0{ga~Wd0xMe-r&9b|{;FZFna>Q*07Ox_`_Y`&sIGz-g zO?46Be+)DF`@L_DWOn6#yarzRcRT6==$HiU^ja_o3x#Sx4+c`S@|pwRWKq4-JlWkv zcKtWin*};Pcy{N`A%mpNZ&=*#-$GMgX3M8GO%4ezKJ1Q8+6X9peUIO;0g%08fzt_1 zphW_4H}k2#y1G8RMLcn#Y(EEoEm|yI!_p{wS zG(Rxj$~K-i9<2AXxBytgzPs$ZgyY+$!|t-WYvC?p<>W*_Xf_9Bk)Dg-wC)QmciQNAK@)UV!U!z^m7<63eNf zJ=<7&i*r98@+$wd2z!OOeppnq+1gDh`<={Yw=Un%<=x*mAxej3Rf$_yjL_EezvUYb zmn+^DRMezRzvjywoD(hInj9A7n-4PjoJDjTj`Z)ZbMpA+Im}{>zvWKwTn1*14CP1= za=)avPc&rHEBYj|AD_LYdjK5aI-+>(EQN-K@zu~yXx|kApKRSuR__i^v#q{{y+Tj`z_Rj8bEM&>Osl<9` z*|Q>+;^Xej@xAZqg1~N;%^hWE`g(mboNw*QW+#JlaC7>`uJ_5NSC02(W_j~`O6_Q4 z`}OoNwWv*v=F2Jd z8U%?yz5|{F2?WiFV}6P3{bA8q-&9r=tr+3(8&-#^;2 z7eAf_E#!2(vjnHy1W<<7a9~I#I(y}~yAI!%wvLx1`}7Wci<)X2ytNLV1$S_v&U*^& zciz%3Xj#@f2fm3l)l>+C0WM!7yEeBCFi)|j55{kX{89)ZOkKT`2S62p{H3@+2H^KF zO3%V3*Fa~cr||9kt7A7da~+Q!P{g7<2$tUWjrCn#VKyOgUf~`THa~oGp*^4xNcDUh z@1HLb5dn^m_niWk03~dHd_PMsNaU{kb{v>hb^O;zZ^G@@ANPl$%v0dm6DX+V?jKiz zmw{-4RtYX?V%}ENL_%?_-U99S)q(IjAk&`8Z!N4w_2Tt$!@Mk>C?lCoeB)qP*!tMf z=I#JX;&F^6dGj2(JOYA~^ZmY@)R$eOM;58{XdlD-BHO7k3@pS#L`63;=p=N|Gsh$( zjE_6`80f?GsVP!>is$8q2;&Pkkf-NS+sDHOsrJr#ENq_5hdTsTfseO|Ha@xCL;t30 zEBtq<2d~H7svb_iMu2Ordh4qfS6t(pC5|*sBk5)ZJR7P%f#C`{9Q;KmyV2WOJJ?M_ zIL95_GMOtIdT6{k{0vJa=G(mR#tgU;(CO}w9sKCm@_Sygm6e%0W;qV>nUpKTyz^Nj<>MX|je5n0j~54*ay|CmjY z`D23s2Wq$>pb>BxXd#>NPzxGiZZWZ*ljDhiiatCwz2wf}IKD2AF+3Hr+IKcZp5^ZE z|JwGx+z-8H-h0w^sgU)aUsvK!c=u<72bahDGlHd$b^NZ+oq(4MIs{B@eR4DLyT9GKO3Hp6{Q`QG_f9}ppnl*e1 zBwIoOl#+Z#5#x)n#kClJkw@Q|MGU=Me_zjYhK_s|_hNH@K+*A1YK}iX;PQUo`t7uU z18`t>gm|!_hz&3VT#hruZT4K%2{@j4F6W**f<8muusEI>9xSiH$GqcH(#GZT$>*7) zNL)5wT~+Ja!=etInty+07u*5+_mv5*71Y_%+Or}}QeVx#T$P{!m=E#PBU0Mr!6{_` zJ7bo_KjVci(9Zzl$-UIHB>JDB4f|(+F%1!Rd-RV$<;q>~Grssh_<5G}$20wn!n*UT zfKCqa9{l?Xs@DA1c6)AoYOnob6TGY*eVle5hcDwMf^F}StZTw)7yZ8uevCWi@ed-`)eK&$91xn-Lz(?(ClzugnenTqS>7=FuK8?d$*WCdN~b zYYq~*YN2-#e&0xaq-E{e+{id?~8QIg%O6EQJ=djLG z=Oi?rnW(8pg-3%c`)_r@DbU@)0_1^imt{ru<9>>W__%#s@&-B|>si#7Ti0=zxqdh* zegfce+2ou$Ix}@Iw@x|I0E6`&%D_s-{))1=)&$zOJ!roxPEwGVD1mh_F)CUu9*W#D z%$t(4JLAPJc)!=&@*5qhpJduS#zCp(B#iBNq5FhW3uxXYQQwu-+ffAUu%YvVb3JCl z+BilCZi<5#mC94;<63QriNZNB+`;_3k+L^R`s?+syerB}IY@y3aUf~3@PO?>k}Evn z@nE+STN*~O;7-?~)*AB%3fbb`tKE1N zS{KGu^(=jswYxwsh%kMYicLN`qtmc)8PUn^UvL65cQUvqC3YO#88l!{gz|GHttz#R zfKr3Sg|atAtRKwRsU}Y{yY=#?HoP3a$c_fcS+Vy66NcPM-ZI>_5?bqZL~BGf6Hz5n z?h)NL__efa^{fumB^5E=UeuIzN=1L8`HaF)1FW*1`dX?}fo(b9M=+Dz6||XMmF=Cp zAqjOy85LaPzk4Ffl7#u=X`OfDCBn7GsdK$TSt2mjbU1I+a8O_4R^0WCs3@v_S*t1S zii6spBL+N}u3;2(#exifhu^PZcMnxz2*d~V#9XUFK?T)nKxnLIBK%R$DG5RGGbHYJ z%!jaQDd$*ePKT(~wU|G5FU~x~M;TW07DbNV;#1PuV~ZfB!yslkFNTb+=NHU98_0cg zl91YpoNStlUj{!ty*-rk@7YnW@MkM=u&Ggp*FC>IYSVzf0z-ovS|rWe=~6>uY!ZIj zVgkprNSO|c2=8i-C)I3s|9Yt7=sMWrlZvARnO3jpsgJ2}U@H)~3Vxk%jqP+ON@bEl(0m1^ZodPK96W-8D_7Tb0^=~*& z*Z%I_n${1s`w2jIu2&`XfWa?uup7o^6b}`o;amIj(sofR@*FE;vFEDWx9nN-53#O& zutuvDoe5SKN$c}dcrD=*Q^b27r-PIM-=56%p*=Qa`;d&KTz+I(& zCG`t9%?`8^ttOe@rLPcEx^NHwpiT0G})=jSbkNMG+hyfQfKNG|Go@P%e~h4_>?L+fMJc+8ke6GZ-di)CU^tV2DvxwZneme$UU#=&;{lw!BGb% z<_18Z?0F{@e|7!(W$&M zCE4vWX`Zefd@sFPnsMJTa5Y8V? zzuza$l6+p?MhKW7r|yQ}Wbwx2{EJI0SH}8{gwhA z8_TdqUT@$F15L%-SdOhaheaG&!8}Sn^bAc@fa|`o;$F%Q;bRD>tMGZ2&4LwtXqlSe zn5+quv7Ap$=5!H^j*NWO9ijis8Khoo_(6Xy)IZN;79xzYt5Y?;qaX&u&&|Emr)F~o zg`iYrks&X;M&J2(1tlFGjs)#$Mevj*&R1&DGf`wSM0{}R1sUVW*jhp-C4?$*zB^kd zKtDG?I&1s6%B~_>??XN(QnU)bhsI1_oqhg$K4<%Kuv@>wCne;bNBpJz>AMOl>Tu8u zR&Cf|MB5jN3r|J3A4Mbg9|1ma@j;fkQgS`)vdD=@DGaXK^@XeM`qC;^Ea?IlMpUmh zKSCbxwsfJl)nGVxl|K)l^%=fUzADRZ(=2vXQl!j*+N;8^Q}rF66utZ8_A}`)FN`z0q3ptjR0s zDRN0A&GH6?W0os)n0!>K^NIu;a2LdL?47uIE*Wz-!lE2UG$xoT3}t!GaL4Sma;r?z zlKm}=U=N`gfX4TIxASEVvukyq5KRyPKaJXeowT(-GEz8ru1LeGiSf`{1Q-cbuey}8 zOqVX!tKKfCRdd-u6=zA_ zK(&?zZq9-lSNy9G_;CU*I21sa)|;{t<7HADTx2n7n^)pw*Gn`n^P7`_0anXQpi9mB>fLREJ5 zzJe4`_%yt50Vw#hpN?M?V8G6b42Rs~?YVDcC2l!*=q?pC=5t@|+V>Bg3Kytooesus zmh6zEx}LOR8lG=si(Kj?2j9(QR*q2M^Dp({b7^okJw@F3U07(f*J<&;E^%YlU#P|f zXL(v>3Uf8W=|yWoaQD$1OUV$OWa=8H$BY$6vajGnY7Ndc2BM8YMP*Q{Y}B_1{|NlR zj_5kFuykK`%@FfVO_0Me*?3im;>l-A5)&k z56>^*k73<}hAz*itLksCTd#ztQgnQI9V}Dgb4@M;YEOm>&BGfj#w5WFV7Lhi}><%3qepe7`)1sb9&84+07_`80 z^Sh}|0q{pQdmJO8ljt;Jx~#x1|f6+zl>rCJkfoM~ic}U`mC$^vlOa zidsol1cx+c4UTV+2_m0X%IB{}Aayp;F-L(&7}9Fu)r%Pv=@qM=_lPYRpQp)~2SZty zC)9Jd<`Sa1!=2)Nzc)$_folvqTiB48fG-Cf^!3Csw;v2u&ED0$7LCQYTee#7B$%B3 zt{}bpjjr}}TJ*QRFfo&#!4G0a5GMjwiky{SBP6TH`)F6n&vKq8iBV9y8in^t#W&KH z5h%|hc;&YbhS+c_zpn5+j?l?a#D#D0uozVJCsOB22vqkO{1;%B{T}^E_cvAxcU3v^ zZO`}|%PYzgZ09Is4nR{YW_JhGfG)7+nP{hf=R2XFeYHF7$A7N3YxJ$XP$XAlSp|J1 z-lV$4iX8=iT44HZE_FX>&S$-%hBNcN;%QAr>f7kj`k)3KX-H5#WVm2SME)3xk{hV% zndbrHPN~2RAR(|`tE}}P*Y8hm4O{}3_#u}M*HbCMPIQr7Y4+R=Ya(-=KyB&btZ&I= zgc_+#v#RfpI~cT#Jl~lt$(3@W;YR>b$@9t?t!|gO)2T5^z&(qNb!pq622>r1I9TAO_v+!We4;BU;R&yXvqzVc<$_lQ1Qr87wH=?AU5wqv|+HZ`50z z!)@MoNXqD`leWYik(WAyRSXBu>|!`Q)0Oy~DP&iMy~zMyVs(4r)6v$tV7bwK7?G5R z=Pz_9*%U1lwJN5H0*gUu?UOB%EVl4f)F#|-U9%cm(MH#&c;<{rB4pz-WUtx>BWrk5 z^9y<%p(XIH%2qp+iRF29klQ1zHFSbV5pc)`7-Xj*Wair0DjEkwNNUD3$ro<(r-_IX z1<2(Y&6`4Nqq=FiCCa`j=s=l~uKY{jU*sA#yf8nJRIjLtU z8XRMJ5Axa(z#o%8!#r#3Ud78MT(CBmKKrdqx?Ovr)X}El*72U?pr>BUNDK)s`EG8P>E;i!`W3Y{I7wlId zI%UfyyRHV29+B=xPE@;;HQ|9v#o_rZXvVYgdA2oXf$G!2VYW0+!}8RuF16x32V3aT z4;ccN6q*E4UPFTWZk~nD!%WD5%iD`1`clV>6Ufm$r9P+RNjUe4ED}L1+?v6hue}^m z$^H#KUqjv1VXtOsrL6MS8nQzXt&8sUAZfJkt7Xg5GC|ubYvcI8DpIe?D7wen5_Ho2 zL_}Nd}dYR?L-zwn_r-0j)qzDp|2<`j?1X&f5NC(3!mD@9PzjZ2yO=u%Z1^R@$Bb*i$+2?LaP0_9L1`OoZcuB z(8W_+Hg86xK&zR_IFRNoqzcsw)8zzQ`Ju+?j3}Niyn>JtSymgJM^hAo8S!k?QkfIj zQCF`j9A8b-&csbaPUs&X*vNU`Mvd^hH)!88g$>G=;#&pA)nVu4Nm$;Y zJGQNKA%>97pkau&#& z_h!_0HzM^*mo=g_l38OO{pHD6mQLbBG#ZVOv^znG4=o7c48m0*IKh&weTzg(asmeG zrw^;O3Qir1V{j``3?=31*78$}lH$c=97$Gb8vAgYI*}~b31-nc*E!C9D0BR%C0At@ z4uyPNUM*vUluV*YQrW;vL7m*YucjkJG@@nJ(@dlq4VyBfTb|O0fG*S!&mDz4SOo_C z7X|t;Aib}+yFo;oI!tMe6KmCq$j0i(gy)fN>M@S6qa01pstTIb+vIvsshl_^`CS;6 zU1qw?M%t3DlkA$W4>-ukxugr=Exp{5XN$@(jCH@cjIn7)OBJFu+x*iAf8xX|KD#m(Sd8yPpuuBR6Jy$tYf; zF7v%=pOdg1ku2~Sevy?<9aS(pVs52D7t^-d7Omv8124*NkF!jYOR`_Y0t}HG_2BuN_T(dDu0{dsa95x;5MyQv3n8Fa^T0KBGops>(eu``y3LL7N-m~v$~Y#q8e0*&b3LDXs$;-E53X4o)4>@Fr;fr zKS`|EYp>F3^?3r{HMqq|CXf|G3KzQ6-H|itwtz7yEs+!q`qWS~$O>F`lN)wE@-chI zYlcnpHhsjP;BH{;WRN~%5~jNj6=mv=0cWz0fpD@? z5eZdXtCWe zEO|Sf)z80a&k|L_x(gpsAM0VbZEel#CESfuAx?Fr!gVgb(eJdaIBUP8h9+=m zBz=GN$^NO1VW|1I3$66?1jogRRZ*H2VBO( zZC=c@pv=bPB=yUL1U8yNYU=`00Y9R_Sjom%y!zW`YXhlchKb7{W-JT}RmCUgE`Z>u znLjC!ErgN%m@M=um!eWNzdmuMAy9zQPw!>n5pMu6v{g)pSej`?z>rcJ54*_dpYT4C37IlU!@kAnaN6J_B3^DHVlu)9XHN!L3m z#Eg%W?}b#^$Gduv!xhzK14k8izXAGkOBZR5=jRWsi!`-KAN$mcxUKP5 z^jIi6Ov@b>Q4gf?k~&HQMaTe=+X=5DILN%U$)w*o{yR7{s{Ib=D2%}CKEOi4Z$-(&9Gk~+>yF%pJKQdKT{qU6S5 z%T5O?ze-J1lkEr-SiW>gkQU>SOAQw|EF+KjpVFVf+Y$>wV=1xu{u0gh3L;%|&Iq9( z|9=2oK%&1&RU!J^l+UE}+X1!$JXR$(*47rR zk%*C_tO+}?se|$okNvJuBQBEN-@JzMV??AQJ)KTGaYGwUxcf$l>Cs<8v6{l*l1&>W z_v2SFG1`E7Y9vh_eHe18Mza5Tmo$CiKN1Y(CU??Hl5q2-khdXdo8u+rrduT+eeHvyV$u zD`-N%tpH<2!CMbYaNFLPEN;WNx&s*8C(-S@VC8`IyixK#d|s-zEP|qrv5PAgtO`!B zOA96Iit7|ds7ew#;Z;=tcV}*p4VPV`WrMe*Jn@i(Tof(Yz)`BIv6pk5G&=cuiS5)) zC}T=rd|Tr0y&ae~lU+|;D~&I`Mq+#PlN_vKQTe9}!Wh}&(FY{HM;~n$`7@_V@uXLQ zF=jn*bv5~sh49d%9QUtl3_v-E9fikTJrHrMQnQ+2ig@Zd7r1wD`Cq&( z)rEPOAT5@#wtXe_+Dj#}ImR9I>-<^MrRc5yNob#;()6awFdn6m8%aL#TxmRXw90~_ zFQ1a2WKKHD5n>v?lKrNa15*GY3^`-0gaxU|)59dW&j8sN9WCom`$ED7HJ6O@E>i_2 z3|5~F)8Cbvb>-6H@e3qn(2=ql4LH0-hIAfxt7Jo9&41=~X*qP1v_9nwX$JFeI6w^w zhq~ZZGGj7Y%bF{W!g~24iNE+3>2lNEYK5TyY(8Wd32tUeS@htjF5I`Um%sop8^4b*%&&A=6C!_vc64w+47HzvFX(%v8P@v zo$q{56@a=bEK|d@pFV1|tZ3RyvR}AIEom9!CrQQ$r)r)$qryOz-~Vt~dFK^U@##;J z@Xwp1)Ae^~!&Rke=y2#yRG{G4QoW8(O$H6qx6GFw6Zds@t1yf?`7$Yf_a$+sQp9*2 zh~1b?R8L+a?Jz@2Ibx^|2;94+Wx>+V8VnX2F1S>hop89czu+<{gC&cNG$OgFlx)wE zHR~2j`|~cA>Qm2BMSx36+Ubx*5|$pLn>7c|n4WtZVLU8+%PyLt80r!Vmi0W9KmQV}2xBG=#ga=&K3@9{l*r^{3CD!MxTGGv zWa(8arF!*Di5_!~biDlmWfu$srT=^!n~ zoFs|Iog-T=nt{Vwr4rU-q#X9p^Ag(utD;cMqtI8jh$BvjB(5)!UmK_tm zl(1uAF)hjWVWfe7#07Y9qE?H^WaQSXv5*MaMIJd1){}uk4Wp&&y_Ottp(F3 zN#&F^5_jME(&3V;ByZVb**)!D>2~WqI)MN9-b0wwq)Ug3E{D~8H)L2P6e+e}OCF>Q zJVF*-yIm@lPlr{YzjS)&6^ZKrvjZL4aNThyDwdvFD5dbKDFi-nF|FE3%3aq>;p>=y zpn5__55ORmB<1gYDiLFcXqzP+_vHpdUK<&k)2F|zzH==0O0S21vb2BxJyljXiDNyd z!Fsi&Qy1BI&&g6X8+)d1yF)r(ce^e_8@ZTv)F@e=kSzKCdqP4F?(w^ zbyX*6y=$9{$4O88)y=PzglkSlTaJ|N)87L&rP2ZNfd+u0|3Lvvp~9#s=*11XDj1)d zjyhdBUvmTeu%t$dm$#4ZfQ(vw%^m7V4m@Uqu6o%_X*F@8q@Qq_#xngT zAF_AFZIE?e%#&6V&XuT^t)vVxIx@Kl5+)cfwJ6r!b{`bbR=^f2Cgewp;Ui@kl$^Zh z9)pFdg|r#}ouqol6BJ0}6&`t%toqlDQnBPKSkI^ga;!ucmNCOmI06&*cyU9~3id7q zb6GU1JAzvru2#c#7hWczF)@<&Emqce`Wg0~CQlwG%?BTe)#+-3*T zl&uJDb}ZU!B@7$4OL$T;*ag4P4N?aWl|?1RQugB{2~JLx4sZN~Ivl9ugW@!Dv~0z= zwEpVRs2hwIWP)+g7TqzJv@Dn_jjy;;qA@PyVkI*3s9&7n*4>#yW&h){^Y|_iW!4(6o%VBoutdc+&|Uh zxp$@Q|9vTqN8ccolfH!l!sR98i^fXFY}dX0y3D=eELr#FEAVj%_X0le7_igNI8ROC@n_$y%3oPdw#*xMk^FS>5XptGSL@x0=PN#K zF1|{YzwO^o1IxmB(7}-DaU|X{Nh9@S_{(}T+;B- z)@j-b2s#x2@C4twGyr21fFT?+Kllej0p9-O_cHIA^JVob&uE?rP{ayhxm$4iwX*ENyL6TtkNOMmc@}2B z8_}s@{-@HyyTAQ~WeGr2v{7ScP)v!n?6E%2B zyW|E{{&vjzQGPt}2$_HLmFVzgx`!})Xlqr*>QGssu%PXlH(Tc4Fjn_-S{8%OQnY@Z zEV}y!Sqx>H%g~L0W6YqFF!2y+d+i;n{O!at^t@}%lVuOzBUPYj0RhqEmdCJEv;rLw z0)mZ!SYf%RHgB8M?pgus3zlGLwSc2Qs9mug^}azBEA^iuCz=2NKmbWZK~(dGDFF&b zJjo{ovMV?mmV79bn88zt+>8SNYu^4pEc<3z$`c~xELtE7F<>wH$8}Paxk2|6rCoZn zVq*G49RxfvGZxjbxJ21H=p)$UO670E2k*#?;pwsh zRss%?El)oW4Glb&=IEe=1a%pzHfMu`!hn?28+{DQMe(euYFTHQxI__*Ze59qM)IhW zRr$-FJ5#222$2Q1UM=g;FG?3;LUqMIRr%ZW`G@j-?@(EI$2eL4;Y2B0xlkINH%67E zIw&iP@4riaJi4P~efODa3LZ z@a5zWUvE*F=Rw_uKuIauuwEAb>o)DfrMs{d1bsDb=&`VnqHkPvw<>>y>(X!vWW*e)T>PoHGA!y^v`;rC*E`S` z7Qh0rpJCm`^*^@6Ku@{*)z}4q(s_ z&;)HbqT5N>Uxju~ODHDNbR}CiiDxCkF@H9)g4--=mhFNi z6enx3+!_=InFq@-T~xE?YPF5K;1*T>%5!sM-VK+^Pp9k|I}%Q~K-&-dJ{|kTlK=cu z7NX9Ip>T3?&K0ti&ps)0FFjRqvGPJ?EeRI(3M?5nyXrwz{>o9V#s9il7F<78X&OT= zW?X%TuDa>u9TOULkXdyFS+K~0XNY%B@aTB2U5x62MY1+~If?~3$~={otVovG;*p?G ziQw`r9m~1x$LX@+pWChMNSHc;|duUxj~*vz`4RUK>D zy3!B~xnGPill8m+D;vZh46BZf`(cFw!(_8-A5rBmZ~0Q0dG2VLbJcl5Tw5GO^UF#8CC0b>;|} zb^h_HCuC3kTHYN07wzgCJrpD&50US{RQh6HCi&JI}X zR$@lPsw(gC4rXhoN_gYOFuO@-AQ&lB)*cOO1(cthmvABsGm_X#uF-VZhzh@c zL6%^!Zh7t{LPe$p_Gxf-lyl;lvhLyArAjiDmY3{VKgb50AP&ud1sQ2I?$TY7utzPr zGxoC`vqoamTR}LlmCUow*T%hl<~V7FM#U3pp{+#X`w!N%HoW?b6g>U9K5y;vx+EU< zq(oqFh&tmaiNOr60t(`qvqtE?8`>*fkug|qy9EWuGu~_GNpq{u2rWVE|pnUJ}pi;TTS_# zpoAZhjJ=Ymp;?5l$%fC6pFwP(rhk7ufo3mcNA3L%#r*55uv$}ujo zPTW{p>h{lv6bnLV6h5X$XC^H>=g*ZD=ojEt+eyYbVk`?erFW6m6|md4_ZYh}}^ zXKTD&Kiwp0$DUwWNl{*Yo=0%Pl0sbf)Xv-{Nl?t9(6_SZ&yj+OQy~neN(rpSaj-1L z9DS8!FMiH9S>rNgSl5A)^WW#ObMblY7jmi9C?a4P4^GUGT@O92Rt~n-^a-!)q-o>( zZ_BQ`A5;vsJb8yS8#+Q_+jWxsk7q$CdRWqNV5k~1|1DSDsO=>b`?*{(&D1JO!THBK zG&oFn+>M$^1r7k@ZF+y=Mu=$jc>muJ8r*MJVT-Uq5pD;Z4Fif z*kZLWU_!ywm{`;`|HI95P&9PEI zc^`uEsAIpy1bf*XS4#fNA1H1i&C$;&!{F9G_D?CA@Rhc;=4WG>4hr3-H(!wL_dKd; zHXe4I#B)25*Fwnt??OA|UUSN`5G-ZVRh2_&0TV%F#^*pw8wU?Kks4ypQ)&(Bp^ed3 z5tMwyFs0t~)=QHAJEF7YjZbai&H4Z z`aUogl}gs#V+HeK$-+tHRwrY-7A(+(&riTORfr~pNe5Q`B0KlSc7lgx{d?ak#)-FF zsd@4cYGmJ&CGWA9q~MkJG_3Iz=j!TR36%bg*qckfcTB%iT8uea8n=ap!*;`&qG51PL{LJyRD3=Qv(MF-HTILnm*1$% zQJfWIef^oV!jedIhr=WUOICEb*t3Uf)k@{NX<)uPIvcxH$Vo>^P-UJ9OonlfLR9Oc z7zMK-d>R1v$iAmy=Jh_>CYCbPS)C;)rMncq`-~J#dPbs;yI7hYd$Ke?awKNCXzaH> zC<`VH@a0cyUKr#-XrGbd$@~VwafhZ9fxQ+r{YL0>YHJUX#9VTha z*5~+a)r8N>dO!e|02#(ZIdt5Q6mj}+-N(sgpnj9>IHQ1YTuxC10K%7;h7E6vaTsQN zT9N68|Iq%!#Fe{4>atcV|BOeodt}dJmAP{4c_iy$$M%C|Z;q6DEwEv+I9-TNS67OB z(H1xXxYuKMMBq*GT1zEM5F=|`Z@XxCnZLfgSw z5uQmxso1bgYVhd}?h_4e1ro1yT=(qZ)qqTa$UYJ}x*yUiu*wTu>hiZr?C`Tl31uWh zhQP{6T3Sg%pzzl0Mj1m5UZ_wP@iqy9FJGY zPI5ngTlb{Wx;^lNpLAlB_2nnB1IojWiLdJ6C0bybOxdWL$G6}R%dWc~kZ>rcbW}pq zuwROpnSR1&j+$^9*kCNdMyKmuy$bIN3YSY`PwIu$KueS9{rXqpeiM^6@Ik8~oh!(V zL&(sg;F7RkYPheBK`tUbEmfgMwZY`mvMiNgA7}(V_fflZzJzu@PU8UtC`YjDtf-Xa zkte7hCpQ^yzNXK{7?6DAD9M2})3*T)p_Ek~h-er3M`u_~xXj1_xTd7UT8`$H2kfQO zE{bxpoz)iSOW4p3*scQ^w`DdWTWo|hYb9a5(UXesH3V;61UHJ8a2(*PT~}aDc1emp zp^oyv8<+2Hq~D+eE$fyti{A!x$5>-v1y*>NOeBneJ4xwutPUc+RUcj~m?v=HVYA>A?8EzDD#6!0 zI7tp^jV8kJ1T;OL|4>{nLD9q5N2QHUu{2f1F%hdh)!#0Wl*b>`EdXWMN1l4$c;!D7 z`$Xvyp%`u()J)1IO_wcaeJU9zogv-sen={C!h6RL(HGN z$itYJ{j+1`(|L+0{EGh^_P?T-kB9-5) z71sk6eeUulQiWv_HaZTXOl;?8@1mkDMaT3FZanqmA(+|-9FZ`73hb|ZRs96wA6nEh64vUcL#jsppW zX;v1>E0$ghq5N$Hi5F}N4dP9wLYVc86J4B*gGl{Z4<0O8xtk$Rw&(qIUQ!dq1swFF zsr1*R@!-3=a551~nY02_Ksh&&K&tJIo6GeiTT4~gxF5`u$#g6p{aG@7gue*m!cv+k zFK6)F4@Fmzmm^$u)Xxk-;7}1|o2}Z30i;oL)DZ?soPw)fKUbp89);N|ELCu-z+peH zI`6<2kLBfv5gnv*<&U!Bx>Kd;MYl`SBZlg7b)#!<(PgsDW3Q5B)t5`t)5l8F!-rtW zvXjK!_Agn0J%ClkXsB?78`kRqDIVHlAE9DVvVM&!Q#{p4#{rv0S5IS{?4#wS9t@QD zkGOyz-6x9E+67ZSlv-H8Xsr<}y@6}lIq?1UoAcQ_T#z4D-|=Id zWrW!C;2;(c#F_eJ?RzL3fQhlpcyQ{_6NCDLyaJ__5aXf^xnonseKZcLZC-+Xn^R>+ z?<=Iwr&(%Ui5Yy76n*)!_5()plxK?v%QciCraf_Kx;AG48PIfh&6%ZEUlU%3zQltp z@u%Ia;U!QU^A|7BEjg6u8&)3OQ3cUnUArf97YYE7^cz>bR_48soF1)s>WgZb@*~c z9omP_5l8Uup$KY}N@r!gaj0<_$`}*F}p0158Q5UR6jMvq-}| zl)JD!r55G1j5Rgd<_hsuhu~fumr!59xU=ktm+aji83}89d+ZmroK?u!v~*0S&%t+! zACXOWo{7V5DF25dnxv+-tds5z}m=(acgWJbAK2tIp1R2 z1>_gEPVD?~I+o`n!6Dqxnvpq2m>k#{3uuD&tePRbKgC5wMlKMKZ`W3ZnLZ&Xdlsin8W9E}+;UBROHvi2%`OcKuwXWybr z!d^BiuCdLUOZaeX=KXA@#j{r!vfSh-Vc-x%a$|M@!TsTzm=R>?j6b|tnzV;iHKEI4 zILV5%(Rk^o7*aiL)aOK1{>m|nnTwB+LYEQz(1r}&2yH`qIH@hiII8%06;x#ucqry% zLJMbMZuvnNx#)@f3~t1lAkz=&P^n{itG$DKbh|Es&q`4j@x|e1dZ?DTKgV(sh+AXk z&L#9xY?d#XJ4Na?t&&Oza+<<7J#ew)&&W}H$r5oUmQh%8q$yq}0a&IrMI&F!foIvm_kh(~x}+A;vbmH`#Xc60HNV|bcq)sEaJUFomL4$>R2&Tv$O75Qsfs7O ze0~f^+H{-@GrbBNEYhe{Y`A}gH1IIIQ8=je+xHtx7oDV;wB6!)tCDsP)5r{s|xJYpz6PXNm)Hj8^O zSk$7f8eKbFCk)&}w&^wOagD-Yfzix^cHeFs_D`mzOZ4GmB>P?{*qhhMx~XFY(In%U z$M89y8zsK);kv1!?EPu77XPae)@kCKdaxrI2TO|9ksAGV+g4d}+)2s@-&uwTSOU0& z$u}(%t~DF+whZ>_GyNha$G|CVFrdHWlLi&6Z1;b>#rvQU9GN=sr?JM49R<706z z$LYc|P)Ed!lV4Ps>KK9#6^BZGGbz$$s3Eia#|q0=Sad7Ub!lmqLY#?}C}kh!o|GFMt(g(YdMyu=-Qo@7nFL;C`8soc0i z+j63JlCu2X8)VZHudCc0fQ|edpoux1VvWAYb_`E?SpD0Jzv9e}Mf~i38XFf=Pv)*N z0TGBfTs15^bdiu{Zdl^D?-^HK)Dl;Pb{w>Ejrt^Dzib+o-Rbf%Az%wAN4a^hr(nfW zS%C=@^+nS%!qX79JyKIy;*}R(p!e|Vm3bI+%sxC$q*;1e`nbn7n(1Tt(QVlH8qXLxSm=ZI7H^kpEnOqI;wL~UhMbd z()#xI|4*`>e_OG%Y68Odk)ZUB=(vcX zK!fOaa^&j(9jXG1(xOoD#e6LLtk5M9CQ`Hn-!|eMGyG;lhHJXnbD#k1M7f~lo578m zIzix_&b)&{--B>kkTZm!MhQ~#;=59O%fBVIbzABB=1ST2?H3XQr6=Wxp-Rg%+2lIh z_qHy5MSXfCCOrcuha+Xx@KKUI`7@k=8YZ2tyj~(8q<3%1)J?7N z9Xp|8FTv6J>!skrD>RL+FMW+=aeQ_L0wdLn36-)aw2UTRfwtw1=cI7_hZ2G|P=|?A z9b{(muw!&jOp_~~#!sw$qI}0RZ!Ya$cv5!bs5=i!aMOSKgx7UO+-$^1%UY?FxEN){ zTV`!oY+v-pEQR~jv=Y!d*})7p)UtF~#&}?6X_^*WG3y62Di^-;&-yD~y$nl2JtVp< z_Af9Ts~sjzV%P2x{O;>QWtRJ-VsP+-M43<$s#joyLqnm3m8VYoe6&I~ee!`?jX299 z*6W^lz*@e--kl)t{vAuEcwliw+hDoCw=!bmmHYh4=VCSu<(%6S4tsfuY)0A<_=HK@ zvo2sEBzx+YS|rZa^n>N9@S;RW&rD8CIke3Zw*=G+_J+`Lk6gxt*QFAJXX>!w5(y<% z4>r-Xk8%-9@~nfq;)Y24y;h{p52AK?>@C?g`WxNAodH2Xw|&}V6%=X#!#kG=i1D62 zV+mqYY0QI5>md+;`PkExzj8VDE*&QQ*RGOnlfOn6t;V64c6!=)!+&ms)n_A?8ak_W zH#iL^D$$PVwomy=c0BxogrV(mS+RNxl*8UfV59n}l7u~A5znPaH1_Njty?Emx!KrT z(_Q27S5eZQdmLLoFxafZXL^7|R7P73tHk%v@+aIT)fZo`A3=6we`~=~d>awgfw=bV z@%fN^S&KcJlq1|$!6lP6AH9dntEFH8ww8SIEKXlyTLLY&e;Or_mMsiKFmU9~`a$>G zaf=HNe^JmEtz0T{(rNmUdwxf)pXYQ|;|ZRB72|L z(kn>_B(%^wNN)n7D0W?4tgEhdarJM*vUXj2?_JkkcI^!-3QDgc5K01pKoSV)J+J-0 z@7#CeBgO()h|c^V@4b8P%$@mW=9@ET&YW>*1zVhFb0ME4S8ZP~N0*>_pEn(@p1MJn zX;g0sY#(eJzE!BhdoPtvFCL@9L^aGC*tgqH++X6ex?__^kyO5MmqhJ*lDfNyN^L8H zSAVPi-6G&pqZ_>dAnceoOWWBFXi>P+d)Ol{%Z3A9gG2f<-JzVAl_mLay(%SFTrbrp zo@%u*2s1@r&(`=!bmM`6eC;7e$u`(>C!BJY?qZL?1~Cei?Z!>8IDy>@F*31Jl40At zucQFG%cF;248ZP~x+QgLM@|7S6B|zoF8UYf0zx;+)a~NyWrP1;y3|a!Tp3f(gpFW# zNy93W`cK9Cxm-;+wcin2#3PTX=zu=h)Sq!(I~qW4y2ceCJfqbS8k5+!G_cOW5A>1f z?h8A_1z(7qdM4~1JHuoHcAy_&BL*~-3^7>J?DgO0VfvJ;u0GoI?XT^Zo1aP^fh&`4 zSRwj+hGa}SNP3+LQZ=f*&$&QXA1JJ6(H0E?6Ybs`KUpH4NobVR&LREp0(cKU>^ArvF3@B) zz@ID>9?@-5qzg9B?U)FALU>!q7&l&{+wX3QgZ9}JShbujBXGxm6ok`ijFmJ~@W6y? z2Q3KZxY}{2A+1bkb(fx@hAHMm07NB@CY+T`#;=H+0rP(G}MvOY_7{FqKceDJ5rP+<{rwXvjcY zKA8cE^I`1kjGgK6l7}6~^|HEYl`-od(3n=`I6oN{hb660IAOOnlkZZR16 zX*Co6fFn%!&Y^0YotG8A?%_M7&-80mdRGY^=~Bx`S0lFLVLs29Rg>EUCXCqImn@Ln zd#_VT9HYBl^}^Vo2h_l_LkOq62xmY|*HK}diP2w?pDJk|cD$j((3qJH zi8VlAC4j6W6%IK8fUsA#Xs!r$nI?7b0nH8G1r3Ak_leGE)fk=$K=7j`IRCvjWg85E zBaRq{-DTOb>0WH{$H0^hWBA^u|3hEJKRgYE%cj9-7>=W^y%2VharR@R1#J<_M71K@ zAjnOk;8-=3R|?n6=f+3i*YTojb#q11C08<8ZD$)_e?@lOev^0(phHir%ZCdXNL!cP zz%Kd@{HB^-$u_;8)XaVY%MGV$dvC&hD@X!@5qJG+V?}lt*Gb`$GgiBY;XB)XkoHp` zvfdZ~G~jwZCN)J5UZ@%kTYE!7VL9~up}boW!2BRd)|U?h2kVp1t-t?P>3!+-I+@{w z#tx;Jpan~zdWpi>u*g%OXtTuZe-t!)ov}%#R4TChH+mR6^=FPjezA-aH&=Fi^_g^m zU>=XQD_8>TfX!4LLINYxt=hCvHs5=>bU5!O$((ec{cLw$TA73U;L^RetoiT*vf)2KSKjdFG|)xUhakg9fR&p^<%%}cl>k$+Va^mqz)bd zqS1ECR#?0Ls%E_*)qjV32~6~gu~8zh6w{QPW3_|4=JJ9n|%Zt#InsNS+!a-Vw~%l8mU4uA;@ zgdV2L6PWs(eZD?LZq9q_zu07OwZ!h|Ji=IwCaI9 zAo+lJ5NrBn)#Yi6^|fCr-oRe9@{}VW$lHBQHKiebvo(qKC%KEnqqgsv1EuPf*QDyI z+ttHB;)KIwD+EAp!k~u%3LhL3Y~hL;s=}|okj;1fpE%mLlL(w`Ty_h_jy?lbFk>GJ z4<+9$)1N+QaLM1A2Q&t4HleXn`GBT=%zp#834xkdsy2!qIS;$?)1@PX@jhpS_yyNF z(cX0TrErUusCly{z2FfIXh|rez9ZBIZQggpLx^9QCxtUVmUy%YDs}ju8$GKFw_?K3 zRmb1(xHeyD(in<3F=k!VVOf+LhU^J0$G!FU!Ic&sU*1P(>0+%E1e&`)4IW!Q!~iNJ49 zzY#i#b|vq?PWU%4MqR4zfZ}@(fSCe(`9Yg+2YQ3p@Z4hQl*c%NQ2Hes_H zT8eDIxZ{Sxij(ogX+7=eQklCJd{(U*z=Dt8Q)LzV5dRl1o-bR_S6yAAp(5sVlD+%4 z2{26}4I$veUMg7o(sPo3@@W!r&|q!&%2X}ohYbhh5_a`r&z0Ctq6dz~ z?4e$27k`2Q6oVn`%{(0j!fnMq@Q1SrGrrd(qR*ic-D3b8MB~iF^KkD}Uw@arWyjzF9fa;XhAE@6*nb&2U?>>E7v*@|S;N!%>w~%>7szzQXRP z;m~eiX5`DotgdYm>;cYzR-+p#+og_EtLA680$op)u3Wu#ap+erPwSFq{m0Q?@RZKh%6 zr_ewpYg*Am4^*v3E!qO}>@Qla+Ns3Dro)F=7KqKQPGGC%ypOsr!+-(u=MpbY4RN>a zCei&zs3Y9^B_9I=l$*PuNjwy)VT2Yl9R4FA)Yg9eI@-%*)%R0W|GvyHA%y&`Ea`e4Fxp`A!|7jOkX@uIZ zoggPH)O?gci@6*c8~1*_LeH`7HVecF5C!WBC3^U=x~ZvV;m1nsY;4i{?61da)vjD1 z0qi)0N+Z;_CaK_2yAV7Tl?ZGnqfMj#E&c%Yl7wYMoCV%{Uy1D44ZAjRia^17)Ke9X z-71!(W2{vMx@{?c`c63MJ`tDI=eGL!GBDoeE9-p;uz!94}QXmSB_0YvSsUou`rTXNfbZ1IzOz{*2Sn7yD^_ z&~B-63073rd;m=foJB8RfZhMSB>}dDbec@QsKJD{>g#v#onjocZg4Q2c;a~ygC)MY z?c1>_?{FFE5=u-x?pa61rXYm}O=UWAEHJB#xOGumIUI!|K8ovIrK>Xv~6@)kkh#X;qaM?k#DUfMor zyX86_@6@t6KG*Glhl>#s)x96w=4?Qne+gUjbd&>j@x>d_4lj~;Oe`C)%v?6}9hhuD zWBixPrEcvCj9)Xbnc+CJU*vBUPX5`t0_{8*?Rq+eT{FavIRd}^_>HlPZ3AOsWH0m);9R$MIb4iEVuH<) zZ_Wlun0A`PfrC5=3F>6J0%!X+=FWl=qcdSrPUa~o(F4b!Oz~2)WENIJwpeB{loF8+ z?PDW*0psrd(UxZEb4l>Q8rcpG!(pmaHg_fzH890UPl3xj@FC8-=f=$jO#aJeekiW!$PRQ8Upe;^w6QfX+36uX*dlhy2@;*v1H)`JP6%S3 zN1bw)4Ec9=1G!B%`Qc=r$OIYn77hmhCia-!QnGs{04gB{#rx%YwYzim>4SG*-5yIf zzBvFG;=OwUrpz2@L59JK_2OF$!Ue~2iHFC;@u(Zl9-VFbVjzbl10wmBu2Vy0&%RwW z9*=MF?^p*LmP+z3b_?QiZ}^hKF)EyqyE_a}UtRvM^Hd7A3(C5WIB*{&kox!LNym@? zxCP5zt0v>r5!~l9F3d#TnWJ>3!2CNPBn2vAUxW1cJ_dabzRsKpU9~(sYRrez7yKKo z`~2&~MASVTQa{Lr_XA~^vEwK(%`-dTXiE^~5nm42M|v0G7%~*wl|c{SwMYPf5i1H> z7^uBpZPNF8Cd0-B2g`fm5{CKkOoXriC&oMi$IhQ;EQGEt`PlJ>`v(9I>~HtYEm9S? z3&IsOt6B#z9On^9dW2B_&|rsm`ex;7A>HFaWPAxo!M{3Nm+w6|N(nnX(2zodVu%bI zXDyV}E3TA5*l0s7>Y_t3RDPllsqo-HyLJ!jgO1S#xNc+O7DiFnB!L3Zv9( z6v35=`ygF)gsQ?H@NT-xC-@`^524V%Zmnu@+@qm6;~BNUPrc`d@?@v!0l5cDU>CD0 zR4>5IHyd_ys7Br+=3p|Ui=zXP7e^<2!!c1_)Byty%)qkDP$em~^Q+zrO)4~qXh!ZX zsQ0op_>IjQ9A7%2v2c=r{P~t(MhUYl&-g6-hQ=1V+_FP@E&3i1tE( zmw{vtfx|n0hdLE^4@BEw{_MJ?K+HZkJ6p@(hRY}ayuzLOm1hJTQm6LNKIvOnvD5q9 z`*sC+CgL5K#3ZyAU;YB|VRh6F%#$1angTq}NoJXOG+_vN^|zD>&!h5%^i z5+v(i1zWm8l!b#k>lj?2fHc3Bkpl%!^oF+_C~BU;P+sVS7_1~ z2ptg5qZgft7&yQIiVgf#HeoZqyZFD&!Fn^|PDEVX@NZh9KHO}S6kpVjyUPeTrH0={ zI6xlp*vq=?zF;Iai{v4FJgmo^4TcsEJQHk$+YJb&9B93tFKKEjOw+|CO#3A8lQTXY zLRKzpts#`~CW5|?u6v@<=TN6X2R68c`m^2}3;%cdTO_y@c^=zUkN(u_2Y>`^S0qdb z?T`J7M~u?IpGS&0AS{vN7?h(iA@r_>u$HHn#MKY)kA-`Wg;<^)n~4b#CZ%(M7ZgR> zFSPH0v)CtBqi%{q>CzsSeS8x*BNx|3f-_<;sNhq^6RF{4mkH^{|v0R@@4;Z4Tpz?_Sl$+4R31B06~M5rblV6p1)JQIRU?WF!&5@ z%eQKED6g58kb?dgD99E+WCZsJxMqp#sILfC<%1g_M4>4-Gw=t2M+kc7F9L7qPiwr` zzaQEY`Wknda#IY+Gd!^GD~{&IVnfJ1DNEJXRSWcOWT`{gR|jf1jDBttTqcg_%fC?G_LyQ=b|2vmwh|q zPc-iP5Cw11@<$>4Rccz{9)@;Y249G18+LGU$n8E9@vG53FgAqn<$bn8cW12NRTjX7 z6@=#63N4R~GRwjC%afz1AmCB6zU-yI#=^oy`6ms1jAJxG*xkUg{-y#TUP$tDCZu7o zXtb8pLXC-)OJJ=5A0^_m5izKA2mu(FgIIo}VCRfYM&+S{U|>aJd=Nx~&WK8*5M~Wd zhzku2cU>xEs5m-`wGL56TD7Ix@U4UC0RmtFyo|9Obl5zbzF>h1*+iG zmxDgA)U=_6)wJp06Y&ETMcTkQVdgHP#W=vXUF3+} z^S%XIv$BMji**YD-odgnz0kyj@i~)Z+t4&&x5Ca&a)pC13wPwj2^&co87gihuMiG4 z#`O23LJs>1-lg?J!bJG4kDy&tqn;roTYUz<(awLkk6-`=`G2p?g0P5v6S3f(%&)dB z%&@Kc2JWm64fw9@0cAkm7+2HOe!=zwa@019K8L(n_@W#v3-7U>8_>QX2oNtP>V?=? zeFSv?EP^%Vz_cFsQAfB(LbNc#``CX}FxUekWI28n0Z;+bd~02@{+rqd+9f8A#Fq7G zX`j%RoB9^GknI)yR@(>rANv|PkL`+F7>)CPu`;0j1h1lIo_wK!xUnwCCkV=`5Nybw z0J}g$zZM6v{h$s*b-;GiIB%ZB!j^LswBBAUm(D*5Hb&79Dx%}Q>th@wb$|?U19>~F zu5rGfY6tmFE1%?H#3$#mU6XsX&e6BgUg&1X!8|by=9!$w!4}^GXwMWP8Jflg{bm0l z_oLl`0|FJqikO1K$h~YAXvTCU!kIvHHa4weJB}&Bsn$2F$guA+Kn6 z${DOLq^0>s*_y^A2yVo)Nnm2%0j@r{Zc080oAkJ0U>1&B{OB?9^u`JbumS{{Z;gY= zc7SQv*R+`Yfh@K(c0fR5KD2FN1wp-H=vachV*Jzgq5K937lF7^vl_se-m2MSec0`d z51@ZFVTZizR2JTE3W$dpEBV72n}qbLtn|>nl;_C9?8gjjE5sfB*9rW%x}ciPShy-2WB~#ku#r6SHZVm#!HCL|4fU4Y1!|xu7s}Phmts- zXP!|m=8<8iJmbDTXQzW_6r*Nkw_-AKD1AKFls?k1aprxd&pg?AH_0W_K--UOh5&NK}V4yZcugDk6-@JV^)mP}<+P?6twg)_~WeKILiT}u-rn2+-u(D{K@)EAuxHB1jcL*n5 zZ=OFsqu24N#GrU7j?efR3 zDF8C>HvSZJ?V=Df2>dFfaajq1zLuYp00tXttxsaevW9|Pcf4j}qwP0@z2Zjvffqwl zTC8LIw$mcsc!YTfRBXbsUvJ4ggvq7WZ+QRFL_>99x0&#|)BE^i z=X0m`v+vq%lWF`&p6zz^y?2LRVB@OyL;2w?8yCBcv5*>2c;?A&gFD3!J;D#g1##@> z^cv!Y$JJ|ihU$BLpIs*22n9UP0p1U{JJfi&h9b$NzR;vrTXF2?_-W%9e$D>QeA@M< zm@;pCAIrl!Y$>m}uDCEF1D?^g#5NO_SG|Vcc00APvm*WQUu|zKm7SQdPLU=*HSjyj z5PG-fnZMZ|?EK+DEt?$|>F}PNR`Wcw>`ma)jOJgcwSlFP=mDdw$uBY#Sc&3e*UMfY3B}qDEvEL zhK7y0_Dsf(O*UxQZ))Jo=4Xdy`LxSq$8RyOp>o^L@bi24@EX##@z(F*5q{$P50%}n zM@A3N+fLV8$e%tRcAfAsgvYZkgtZI19k+mIc-zx>p|rx&3(ueZOw&8U`r%IZLO;Ue zhS$kX-^1`_wD5a&n&H=&xA6SwGtKJXj<;8SqCS9&gZ=fU= zh|dr%VOM?x^A`G>9TDZHhSt>)cCQ@)CC4<3FW9wcKk@TE!}F<_hDH0idDZ%AA+O;W zhkp-yMT=j5Xgub7Y~1ZnCSY^xj|nm1?Zs}pyw*H4&u8=Bd_Fv_rub}cVO-V{cJ{MB zcJF)XhuW}x-A;p_&Fe1w9>0f=JNzB~JbyE^bj`Z+oBj<$ZV%oILZQmkUxKp(m!dE9 z0|roWMs^)4-DW(1ov}Gmc;VG6 z53?gBLB-PGxd&jHsCzR4I=i;)km!PiE!`FY?N;v`0A7#wNau&2Qb)=84_}83NwCAk zhAu8&eqZ?g6${M4lG+Rmjvj7$LjHC4=dbw6KTpySqg9zfLB_8AKTqjRN&h$q`~m_X zgg>anefi5JZoiWyl1a6UvZ@wS9C3{KwNVqzJ53mhWPm-v^`!?D2{T?M!d#B&y#eej7>eApJy^yAS ze`EyqPynRn54I+75#mmcm!20~sV+gvvb#$0E4SbThFr;g?FBdnJy$xObe5Dq@E?(Z zP_6#v!rvdc3QgGz0tSI!6#-6gupx*$5R+~mEtv-%_TB4v&tB2M=7sHYKm3l~AZgFP zFC7m$REmxqB@MZA#M5a64CZM>9=5dfT`K0ALBJqj5ctgzXkjsa7xJKM`EY+Qw?MM4 zeMN;o#^&re*ofoe2E)_*2d@YZyXSE|v8Nm%-t4*90fQYyO-7Kr@ILdXLBJqj5csVT z0MBvfvu8{@Dfr*>viPb?WXW|`$lTMXN-liqG1Qmrkiz%hkO**I{o9|(jup%F)Pa=4 z{wDsFcz}~$#J;V^#;0tSJ-9RV)$VwbzS%V=>8PLsBmoGnA{egb^wmqn)?Dm(6bN+OS*pzf|5 zaCKLklZ_JL3~`*x>p2jP+2Jo!{UNHo?A~5jlRtxiLEz7T!0rivBr&)*@WTsvue;yY zGkJ4fcv33nd<4guJckjx&*)DS!2y4cHMy1m$V0Mq>jh`iW;w(H$ow@37zA1c0z9e0 zw{;$l59=mFdPNej{1M#)9?>Dxxo}u{ z95#Ho5^*k`o_?ZwKXsVO-`1`>Nl;EC8zB#i%*>_ z^ABz-`JaA>Go2%)<1r^nFb73-;CMSO8RH+FL|cyWdHoLogDHeTz##AkL15PeK%DyE z#MzW|gxdC?qp$=Lr8D5c*WE7rzWJpLz_JK^V5%{fO#4Ok6Q> zQnxi<0_FMQNWh~!BGUwefI+|@u$LgPYXTsgm(l)iS0Y`^9fEe6$yJOwuS z=+QFz&AHN8xc4^QTQ2t`h@vo5HJWB1okon zaHJYdUm9jCmOfuTCGCd6?iqgm8mnt0CM`|U1`pK?Z~5dy3BcDgkI1$sy_MJi=lwEI z+>_w}!3$ex+iCP(Rz#B>gMdNc&xgRS34p|h2SCFX#?^j+xI4$F2GAKDFV0P~#huYz zYT*m87EV#4M@H$nztNKqm87h$(okL|6<^N~Hyou(1GdB&BK-5UHdA7QK&wOmOSd$C z0rl17 z)!9-R<;x9N|O<`(_3Z?XoJHgC{JmXMb(()ILzYDqS|^IvKBx)hV%ac0uHH`lGnn?c}Diol);fc9*b%emaui_<+^ zooNpfkU&KNj{Eh=>Id(T`YoHJVfCljJTgFnbrnYY_a|*^rq~97){Ou)ctP;k5d0_S zaW%x^z93dXc)q!7LKoTc<}0%0tyiV|^Vh^Pu9F0-OA(Kj(k2)L3<3s$y$69k6aYg7 z(c_Isj&K59SFRTwQAy%0%$04od@0Th$cF9v>C&)$JX|Q|@AQGZDtR|2WtIVi-W2TT zY_f0ueA9zJ{6!sJ(+a4CpssNE4&38F!e@VrVB246z3t`;nRmr7EWYO7#*gLVGr0d- zmMtth=Bq*Amqh?=8$w*mO^6ziew|}g_ z?2p>_?LNwf!^&XCwVzXdATNbKZ$I=SpZ4=D@e8kM`l0wcamJ6NxyQFc<>0$_H!yB< z&ZA&&mzU{=KV!_Cb{A9nV&jmm{k2o*uHW#E@*5R! z=huJ)xNHqgr86=H-u&UO!CuBj-W=HAqE1^G2_4-5zh8pqg4nd;jEcweAbw~yck1vv zzud^QN2gY`!9`!morkF*K|$PE$V zh{VPY*alkeT{wB8a1#`F798S(2bJ^a`o-qN-?MD&_w0|f-S$!-wS+o{(}OHiunC#- zAdik{_-&=oN}iy=%QC9R29)oo0^eivi|%^1C$;2WA25qY`tb@Cg(a>Ch3Ds|lHaZS zx^bfyA?Q|MJHjbi_$vQG?@R!S{)xOJcF=>C#M59@_7w4zr@VKdbi$dA{hjJ zMFjR-0ECbfs4fCYY}bt+K`(c@sn=8OD3*}H613C_es1TmiDy^SXqG-mj^q@m*(Tm? zCE`l!iEM$Ocs8*Moz@eL9m>c*g%}Dhey{j9E)i#PTk)>X7H8*zeG0`O(S{EeD77&z-{kC!{~75P!DB+e6A&8T zo2#Yq42yz0pn(#oD#k;I-xQOvO)vHC>Zvp@k7 z9>3^OvqttC0C&zZ%XqK}1X%P5b z5kODa!vHr3u6Q{6OX(v1(k)?~*-qE)taituzy-1a(NO3L`Z3dF2eUh$c1Apd`}WM5 zLYvM~`7EE~f1O!}W_6sclF(Fe!TZ>`snOH)L1OwF=}}m%D}&(IOFSJxRK^(#OaGD5FK0;Y(*J=d;1C-#aQYN${9wLB9e0qVP8)+LKB>#emWCCtizA6- zi&<)jbTl27kMAU9vYZsF@NWIio!G>Rf!IMSr?Yy-VW2Q>;{h$EZ}oCXyYOZ_?*fmVS4xd*}uHT#~1}3|{;#>$(;6#0&2T^g7lbtV?f%(Pz9Z?D5%LTt&aGN;r&TTheDW2g&C0Mfq z+OuTw=PgD$BB?k23nxfLNqtd))Gd17$}5FZ3YnN7YWm^)rI?_+)Vkt*mQn$jDz1Dt zzACQ3G&C;gMT{5Dcucg{ERxtm{w~Sqjh1b%J|ceDr#qstf&q*u96Dl?)J{{&PenlU z08S0ol}hwMQ+4Ne5U0C1JRb2D<-;j>A;yOyo%l9lQXKQQ!zHHg5UE@Ht<*gGrbJ+8 zc(4JuDwxd^Hopx5zb^uNAOM0LAP{&zUnPBJek}1_y2+C9uzBeor=3b7qOGL^5)sM4 z17U&Js*81|MBA7E>;>6Dxx_$Kx~kaWB$~t+tif_9mkaPa03&J=t|KN9+Pg9(3P)Tj zhyns#ZG|`!Ab^4l^d5vR&NTTvRq$M9C0UV#oN?`?aoHM)J$Z`s{O2XI?uiGb>b=Lc z1`?0GKr$Zqt89GqE@@mhOZ*GVB>mnyr2W1VW#fxaNx$jW%Bs6=#O}W*By#it=+t${ zH)gjWZf895RlP%rwkcoLCD;w!UOF9kh}7q;m+E(5Jl+NZE6PjlS-X30m8`#>gq0R+ zX6?X$kn>E2ls)@^M2?xH%OYHQ=IqgdW74VpA7&&Dq~(YKN3j2*Z?oT0(CfG41u0*fEqN!N zChoEA!HsFs`^(RyY{haZ`oHPo*{>HS^C{BzriUc&wdbY7(Z@?2d>m#Ua+Jg#3(mvR zvHpiZqwk_=M+3?R;f{h719GHSQvB|7;@b+`V^TDpfBU!M{S2IUKz}{j&rcU+ zAwgH$CF+SnwJ)bgGOjui?tS`0CdQGb;I;UUoum->)dx8nFE59)k`T3A+i9P@sI3_!b8+BN_}<`eN}z5OHOIC(z$KBS9ds>Ec@k6+2Fg z<2={X6MtZO;)62E-5J+5S*Up?bi=c$;#-erK{l@TShdA7oUD3=fJ531$8k)H#Gj3O znMi*tKG8YdogjEXU~>=XCe`o%MBNg{p0$|$s{iXe=u~IT`mTdpxZPnVR z;dc;uuX^JN+0dnjG;GcR+2MWv`wY54q-$kKg)YyeJ|Kz+@h*hpSIB}+n$f`QjmJX; zxdg!RfEu{X@P!D>y8iV<*|BPcY~Ysr(Z9N9(`UU6R1((q{hPp zgFq`nfD=yIIXlt6QihF^sMK^x9X(!3H{2;n(@v5u&z>tW86A|2av#4(YQA_80wHI4ouqblu9Rl4lti3c z%s>G#x-Fc#R+LG6pMe6+zIgg#*4VMLZ20es@bi@_kqMZ!=6@~8Q!kK&J_E#wb3T*$ z^oQUZFIzu;PYUikPf5AWv`ZxG#J`IRv)hW?^*A3HhT=X@5O{~$O#e4H8gIX$m<7j5 z(c7;`>D#jflYFU*1lK@1irW9!8`#T$FnfLQ zmLx;yreK->-kY-RfyAk3f_^xx$D%ErNfa^Wy?GNlO5M#DF=k>9tO$LUbX^p1{-Un>VxN{ zK2Ky2j=!oeE0b@J8;YeB5a^^{k~(mZY{ksD0GbhuHL~X7qh;h<-$?3B*U0vN{advq zmOUT@K@$uDtpx#0TzokjCGq5cNKAS=+3@G{7GrE)`#?0DpQ zxWj^E2A=Hp`b$!_Y>|||exLOJ;uDGQ+C%*Hb+QT1ZoB#x>G1GfSPqSrn9Pn?@Q9O~ zNAHpDH&4TgN>_1%TeKhFeKkxh7GV|FFMU7#P?9jYsxRKI+*z|ZPl_J9SR9b>dcF6C zBqKlMg{>dIF9lazB%V=2RB&`BixhtFhV;ASY6v5frS`v{N?hMT;;pJsu8Kb8D5-e< zZAl$7UMe=^O3jB$CH?5rq;f@}c=`{Mj1$g*EaZ?xa9#}fqhjr9$-VOm@xn$rvfo7M zbN_X^3RH!YQli^IfW)UxFsUejFvbvzGM3;B`J!iDmw5cHNB8IQGegE@4~RQH0r+%R z-mclaNpkMK4$G|TB(SkSG9JELG7mTyycjE`E0@d08&4Bo!6u0wI7RwAc$vfjZ4&89y(e|1+xb6_X`DJ>r6xp9_rGz)1lqT5N1Z~;B7~YlI;uU%eD{R)N?VD&pAmdpM6`> zMvjy8!>7VSez64BqJtc9oWynSEuOaBr4LA`I)AgQzU^9xNKBSqH^D~i^K8jD@G!~x z%W@wim+cBf7m-Ua`qss|h4*!cp zq-UsBuJ7eHqMtX)s#~rWUu}(QpMuzbKus4rH2Z()(uES+HeDhQ8YI!2%1^*f|`JS!;LcdhCf3o!(wHgT%KlnlIgs$Eg6B zeI(}Klcetjm+R3`IZxcLGwaAi*q1ayvWvj0v8)qu;674$-;+|hc8v_WJ5bg>bhm7q_qFsq z^(=`Qd$`0OdWQ5m=K?ADW|?H)^>0b&-c!0=agQ{v#?1T97nPjXJ$}DbZ`vTS?XZMm z*EvZE%O)-?*A_ieO~-6hp)SE=w9ed1x_n^z=t#oHvd*8tT-IIy!Cqk^Vo{ul&W z3j!3Z0?;z0>^liG;+GtlaJV8OBz3}ps`(=SbcPlvh`!;Sy+M+uoGk6|Y-8aj>HFN* z&=|IrHTT{k6*=pq*IDPmlme5%p8cd3`mifHT1wF0oUsYYQ+fY;N!C62kR)TW+3DP? zrDlm=dfoq(w86wL_u0qw*$im!(#A}b+7%)_AN^b>r0Wr<=<6>f3mlbv{>50XUJ6Zp zw73WLl*$htfT?ezBu_k8Ay4eyL-~vLdU5^tQNblKtE-g{JkfsA6iFHZC)W9krSsv( zO54%nWXHm}vf<@tC3PPNg{NL54QoXD-SUX?;rb^ZkeV%AlYzysc$BQ2lKjQVd_HGplS6+9q)R&e@p9`-Pf%dTd)elM5F~>{(N0^+? zcpDS^bO^fG3<47*fUT<;?#zC`ZlTPb zePgi7E1j_PKNu?{$u&j1sG}ze4!@Gh9GJ_r0axOh?xcnd zBfjCk2?l|^1A$%F0D>%NbK%QbE=iaDQB23~)c#6hsEp7okIyN?jhc#zx+S4sDiPnY;H2Sc#KOb>Q51+P6L18#j- zB99y^Nf5wlOGIGrf$7d^Y*2=1}6I7Tf!!kv?VH+i8&_qcbF<$C$ z4)Tgg2XXTpRR0gksQgV>t_9O!;z>aZ>@Kdc9c0b4ankA9|4OGrkCM)Z9WC2Htm_W@ z3xumg)kLm)=nmO_?^8-19%wn+j^0;_4j&l&)EgrOE_J?A%3G-h72rzxkpxWY3Y5 zr$3gY6K@uOU9A*DL-@_a$>M?8h^t40ta{>Qi9N8d)PAxW8o3GBlNKlOSt3b2d&#Oh zZ!FzQadL3OHapmZu}J?C6BuaNU;> zN}yRw2Dfd)l4%S$W9>tCN~hybm6*=mB!227-JG%GgI6#qh5Q3erP}bOc9q7;a#?%- zty1#fbIM6+WA>HEq$Fu`_VL0=?($2|k;)g|lS1SxdUi+H_d&?QcTNTaW!u2-gQeh~ z7m2UB8rrtwWeB*f{GXT0h9~YtzSb#kxu~~*a|B1w%aH^yue_OuzOg2E!rT_-^ z!9@LJ*afy1Dc+#hXd6xwrG{k-Ba#)Be#t-cT#14ijBbj<#GHzZ zgdROr$W0wOQrd3aEZO&A1#I@iu!Hu94;;rb2Vtik1mF8%3#j&(xJD9$6{v*?f16+s z*b5NYMF9|9i47TqM#I|l0}od%SC6OOgZzLUvzWC}MNjjUEsx(O?Vo#J68?FLM0Uth z+mCIRT`PUQnk6xqjSYBVzHT0HV=1+6J5)y?DbBEi5aJ+?VG3OmMNHMA%mN;N!&A3jt3$`=uBr3U$^u6Ih?c@|X8Y{}NX#&Q| zvFL!UHCwkxn-SQB3k_X8@=*Ewd*ZwL4r#Oh5t7iYhm@_f+!b&JYb9pY z7?%?~6HunAP1sF5TI0J=j;O>0)pQZm=o1dXk|>RRtT_4?8Z-_f)FevMDA{oIRN2%Q zv(@R3Nskl$CV@9!lP%9Zh&T>$!w@_Q%OgBlr84_lX*+I`#Pk^of{u{RM;;4<=E)#U zx8mP`yvO$)EOD8gWbop7N~Te1ZKZ5up2R}KK*8FJW%!t>2a4mrpJ-j^<`O$=cptX? zp$_XL>7uiw{b9!dI+9*^*>tn zx$6n(`1GT)?fHAGdO%&U=+tO9+n^lKrV?ijNM~nYtjp%FL16Dh0IMC)-uNMuB%O4z z#A8C$P*x%%p`DM-%#?(z?vixrxl(*5CT0KHBIySm0o&pyBmv8~w1aiE?I5n$c`6wUuiVy7IB$y6y3W+#$j1)f$GmuNKk)+GdliufFCjDTGJ@3ia zbn@;*d7=h_D>8<_;{wiDE`>?Xz~SJLprrI4AcL1+%*|BV~q++&~^6#r8 z|C*bnGA|Dr!Ks)8I;8^Yac+i>h>nI}u})S(qZRp=@v3I8e|LdI95r4$5us7d(T-f{ zb^gcDYHgJ@$4`*BW6zY{7hQ#3EY%q!J3nV_F6_kPA?)A&9oncg z5GpoaG|rwU&VC(XyxdDw@pOnv;bh11CDMNEcq}E?!Zu}vD(R}QG@U--0A1QC1L5#F zh9J_`PYS7=>3LD+27dSIl1%`!lTjnsXXBWYJ(ByQLn`qwX%#`VzBA{$-^gb}!A z1(XoNw}0JgX@B{hFl0VKsS7nvT4?Obpa9EwWn<13toIz-2EUBr=?5n2XE_uq`MtM%gLckm=A-% z-irYGo`Cm!8!_qK|6ufwEmFSfTTDJPq-4ngZQH41_mk}q01Ib+3KNbaBm=g5Ts0`Z z`CiF@mdFSDwAELiqi$ndZ90l~?JTLs5@v_vPKCJ&Tq|ML-1zxQ=>ePG#ID_B`Sdek zzA;1kJ`LMHtS8p3_#YZa$xfV zc!(2gu3C`WDDVYI;(E!tPr;AN*ZwVYq+wx$=F3-GjRlqOHh~}_G;QGFkLH3mK_L@* z$W~}E8&=Jc;V*tJ@#p+aH)xPU>#&^dTCr53$EAX!qNSl|t2i*GaGY6(CF0l<4;Eih zw$#qdlP=Fct+wAQPaiFg_Je>;vNXK@r8q!W4tQ;FK`5?W4Cm9xGSlJFq$(X^fT03h`{fvXXXCpSm04`OUSnjo$8xsrydj|p(OMZR`qBF&(N&{5( z9j<#8@f@=HfGM)>*pp;EmJwE9IkRrtHpzg^8oU4I*I$&(aT8(BHbl0*@v>_6N|(&j z%?K&C-5`zM&c?=!UXpO(S(*pB1>h`@0-z%yO*dH7!B>}rRY8^ z8AJ8o_sdV=)9y^^g$)Y{*pkoLaOvmoVsl9wNt^btc;S%Lk(e%t=Ru1It)<#@a1#TT zawEodmf~0LlQ`_;O6t~AN? z+h@2AD#V#KE*|+QJYKIE@WdY!FX}{?hj5 zt0n%tzl*2q{*raub&}e*pKf}gxLdk`BHfs!3b3aCVcS^>YS3v;W2r6(%{IQo~vmCpD+u{~~C5;fo+F`OS$z8->Q-ulh zg|IP%nN8`p$Ui1~Fh6l7c9i-zA$US4X*+6PaK=34>GD}KBo<4_G%wll={xF0fE&HI zj7#B@RObQzahX$R$(Z4i2P2Y_rR0XY^qFq|hBow=Nz(3$^CfjK>|(fT!oZ0hG;M*Z zd`ZEkls4DE-Auc#k}`F&biML^wb8D9`~{@pQInOnFe{41y>2(XsvG~dU3;t478mQX zd)%4FNyGY2;hXgkNrY)j`RtE%153u$uZVX6R%;++CS89ycH4@SgG)Ode3+#C<0MHq z?jXrL;#e$MCu8!8o1`9W%%evK3elV2eO(q0?+2k4Bd4%!~>Sf%@v*dxbD8FoLF|LrVY z9w>*qmAE6vOTkqasJ%%~_|R%N>>JnrX9Vpyi7Yxu(DY4-g*s&yeCM? zr)zZvT8rIWek>8!V`Ia*XP?w1kA(lb9J}GRNFA07XcLqF3U&%l{X#k)idp#Bak@0Z zd)Xh~1EZIoO3qxKpqjq%qoq>3;u{cIlGHDnp)LcemM_p{kdoy~B~S?Nf`-eFWrZ3n z#{|J;%pdc?Weqwtr3R48CbS_ezUtpnG;E}FJMLuhVrLmQqEthJ=>s7<;s6@S5JW)F z`|Yr!Ee09{A8c!A^;vfO$VV4~#b;Ao!Nw-ta zkzwzwfhz@Q4&aJqEo^MIUU9C(_vkGHu*_b6${7;TCRKL8zgO8E56OBMRd(jJ0u7U99a9ZP90!rOfU%S zt&CrHhE^L1048@}$g z|Mzi;+_$UL&zmVZFFz+;k3U7)yaa&^@qE63teigs6XqgN2bM@70D6YHVFL1q^u78v z8Tna0WDdA+fiPLUC0{3}YyW+T^t$FQ>G_Wf)qI7!?;Y}mkf&y#>ta;` z+N)gj``ULvO&RbPNGH#Fs7U*|BUx|hMBf=1sT#zMufHOlj`)it-tm}j$m5C-%cX)3xR%;K-#0mu zIDH5<2kr~kGVt9B-lBOe=>`OOO!Ub6Y2ma@(*G81x#GJlXWYJF;ck1rj+7 zeYWQz5i@|%0(L(M;x3n zgB;X}D+D2R%xonDUwd%Y3J0!SYVoftkho(`)E&uH3qI53Qb!CNG}~w4az+U|Kv#NC ziS4@&T->BdBZ#*0^XH^tei`f@r%BJV|0x^(^C}6Qx|D~kXo~19nvmy0D&NvY4og1X!$B3`0@Jpv~(my6dD>$cZc3;Ur0AMH5=AR)nhNK z-$!SBdkK_$D}iF4B>r`pc%acJhhIhC8u%fE+X>o0YMW8)%pZfmuY>?5S+2CMQupQ; z(*6;+U;!^^AeV7a4+YwMQ43dsO$N*1FiPlnlnsRrbGcu-VRo6#-Xz zZ%LeZ6dsL`nq2V0`lVVFEw6Go%MSiQS?FL|&oNg1#s)BK60v-KB4G+YP4J?d@0i1LE$a;OJoM~o~eXQ zC$v1S&A)Xi&b72k;)un2_UWX;8{h5yVvVLrs&R4LXafh*7=ZkFhy&XeC%hZDI)mU{ zI2|N9HT3q-2Qgtik{nwMxJNPsCjOjU32e3UPrBsNE(b*ly3Av+woJTVZPL8BF*dq7 zLU8Bu3*(z$5cstb_$38E_6aUyR$vl|;7sbP!k#X3qA!pG{oA0~!>L1h=C_xV!8~l7 z!r;$IB)bjlj(zLl06)hnIQyZyANZsu9~Z!N+~y0OAwT*Hmuvr~`H2%pRytxk;t0p} z()spBg>Iph^gvlQWJ9cKB=p36pJp;p0bYv+cSV55%hrSQx*$;shbU4FRy_h(fk?pV zPw;QbBj8vzCn&|$4ZmyT4OkMkIu5#o zY6qb$<5D4JjqF%-7|n*y-J{m53)e^@yiNmS+KQl)@OgcfJJvvinkIw=`d{KQ2<_Cg z|KQz$9m^&2!Y3tpFm{MxgGn8hVDs*~M!Z`vqfUi`WDqd5SyluqUwWjFPTRxqrjUt_ z%RE@;HR2xM2@>X+y?W*(1r}aY?Io6SIYR`Qsb(}pMUGjEj17M`rQBr$3;fensM zVmkJ_6?c}2Sh5}zM;t!|0iA~3T2Ij8S!Zf~mnS=KKmLhEW@E&OgfX^`u3|*`tZFF zG%&g1*@p8p zW4@Q?h7&K1i@Xpkd(1#yg76ffWntd!!1oe&jRPJA2kAx@6-2?sECUO=tAXip%wpX# z|C}VVzMIB1j%DC+<|Tx;=Hq+#@9K-q^9F&RiNJ3r05U&(naU3OXXhO1z0ma*pCi*S zK09|Ps(ue$3r`Il&k9U0bRV-R-+XA$*q1}WGa?=;ek>vTC@7(GGG6oa?T2^zM4HS) zsMG2#J8l?RH|3WPfA4u*Xo}DKe1IW@LDN0!s{XZ~(LC~bjm+d?EPbNrOy{O49B)L zv27AM|<|H#oCbn(cwkEc1JK3@AJM;TLSJiWUf&0~6wcqU8wfpRIb@%Fp<5(*| zH+jdk;a8oCEiCwJ)-yZI+w9i|EC(3<4zI6eH74ijgS`4aeUXfZn=G195WS5nn^6Jb zN>af@%XKA=&@TVeM_P90%fe}@x~eLt`xK2Fgf&yiHYb3XP^f?WM_Erxu6cctUx#B# zFX|bJg?=YYl7u96j3yd|e(Uu)yqmc?bln)FaMaxjEoEg7*-R`~bw9FM(+-puwB5`$VD7Z-O7b?G-L|4JV`9`|sTRnBu%+t~={iqbqYYCt zah?Z-VpXt#09cIey$tlWbHn6~^b~0STwY4c>cggxeib<~9A_0U7WSCWv7{Q(pB%K9 zD=zyNQ#oE6IlCKaKXc4o;S+sKyIGvQ-GROGdb%{TTm$rF&s?Htm9q3#tg*pq0ajP- z7e`agZm&B2K7bouB6$Zj-#<8tw?H1NEu6x{GRciGDfvlPiH=?4{iVyfePWKlXKLqyG88&q^k86XcJ)x=d4c z`LNG*b>r(|xP$-II|u7|_&w0P8)TtkhR0#dGH?Jp1t6_B77s7fA-**EW#Cs{j*ww#Yk1>2zm`|_7VRcDsgX#vEW>bsTP*cm@ zRZAB#1py!8K^dYOlGz`2QBt6=n{{4Pi$XAB*Ii_K z;?x!6(=$2BrT=)U_M1eB_#Y()FF)4&rPqmT%wcfI0tNb}`Z@pO1Va5IZgvw-pP(>< z8I+f^#vA_k-QYix*H;WC$OjeV5dna{_QhBIAB*toA427#-7r3tRim;&h+zM}5&r#r zh6^SEvd;DMv6jzP3P)6Y2DPsrIno1>_s4{O?Vsjl<_nAj#V|fK!+2 zI?>Ap|F_p~6V+?i=dTZTUN_O;fL^H}az zZ(l;QqqCu>!VaovKc%`IyE(?k!JK0MwHbX?ScLu3$DQl(an;sG_Lgxer6TI0`2`-(znS3G7AEXXt{FU-qP5JN##`IWsId zn+1$f?`c$=kW=iF;1t&5GJ?HGWtOrB7W zz;;e#`f&W`Vvu9&Zs&p?>nAzA1fM0hU~mzAjKAnR(9g5rn~mTu3H&&Bq;fYMaDERH z8}9+#GV3F!oe~~zU@jLw)nxg^8*Wxw2udXj!e(c+?{3DN`u+gNUN zUO#$bLZ!XnR5A{Wvx?4pI`Zdv84A*tNQ|yOT*EACkaUL#G^c&sED&b+d zUJ#DuX{wLs5*KyN*LKD5hy#TVILQOepHjMnGo}T3=i0Ps{F%XiP!QLA+FkN)7nD_f zc$=GX%lK*EbF7Ca)!tOcHTB(GR* zeXg%GBg8|BcQL_wJOU3pAAfoWw|{2*nL-VU@j=p@Lk2E05*EoV#=F9JRcTF%Uax5!W z>9!$XMJq=n&H7!tJ9A;&wvlnh)c%e?z9B=W)?x+7FlQ8|qfQ^vlj{LsF}Egy1iax{ z*rNXKnx-i2=<8!e)`?2|n+B%(-y$s6?XPq$z5A`wqEiZ=0r8AdfaaHm9MB-aI zhNwr@YdI}139V4GE)ix;#OSeLwPkeeN-|49dr@4*EJ|8Fvhm}gRDr!B44Q^DcZ$U` ziz3m}8nJ)bEv)+q;}!Q4G}k`qwxhCtutru~hT|#3yZzVX>!vk`H+IHC+f&w0qd!e~ zZP2&uWJlr@p5-+5s&E8k;8Ke4k%8x@KaSdLV708uY7fU93^SL5(F}3Yy0m;7xb(!% zrel|XT_W0_X+x_Z6zYLCq)gTvg0l2Ukg(I>n%b?y2RI1&8!Bnhd9{ND*>1I4Y^=*c z&}&2bl4WX$)nJaOf>QFq4x=?>9B891zhgLhZ)-vfQ1xsoN(!$NEndOccpb^l3w~nt z+Wkclxby@AZYy%9;Ta;riub&^a@!v|T@f+kOzt|>6hw+mN$LqnDWefgO!T9(F%Utd zEvi*~Tf!}!+Z3LqDOOoFPfwL4Ock}Z!1>elfIMHR9VDpGg(RcFO%NH$3<=c@AVx_i ze4$J8N`vKc?Qdw-6FoUL?>m0*A21TR7`n_m8sRg%*p%^o&c((?+4D9bKgetW^>ZtC zQwQZ~wjjkoKgmsAQwuMtw-b{QCbW5-9!vB3KeYh3dI%E1)ZrV!PKNeMK0jKORD@Kv zEih1IKpS-{w`_%GZxu+6=SP+uQLn31kPjwz={G>E2dVE|fedHvpSG$U&-CNRCRYKL z-|<2gCxo_Bbz{65x>~?I zGjjUiUM*?uO5-MY5}s@#a*kxv{y-169LP*{Cvh592$Sl;_#*hhZ+V9`n&k=c@$T+g zqqEcL#uME=p$%YN@H_W&7>yH?E0`sOQ&}0LZ^Iu$VSNG%Jaifp%(-GfpkrXadCdtg z@+sl7y9Oe(&D ztvM`>^ZUq+l-a0dJ?ipswyFyFl7=*8Ek3};Pum(iBM=fo9!%eY|Ll}A%m~qIB%ouU z%>Wmx?-y##*t^iKI0gm4FQ0}K{UJ=`%=Ox`k!z^u{OI!~*`l;UYz|0K##kf6IesUw ze!sqFLBkJXt%GLs{AzHVHrTPCVx20jB1iEc>zy%9=J|(up~*%!gi*zun6w;Ur$uN) zJv+~1#KSL6)ACBh4N(I323|D^1s5i>AhK(HM(j`| zTdsPeBm>zL<|!4cqt4Z=FJU6=bSaV?=V7=+I1iaD`mz! zUcZL49ZpFJTq1IFiX$0{@G&1%z!eVaXm3%h+4C-kb8EVoLH;gF8L;DjuLQ;OxEdO7U#E=G~oa{ z&+1cjI)UV;sj#nPgvIQbaFB0$u9m`wEVSnrdGt2dQL@(^S%4* z6}>Tk;m5MuG05LgXW*m|D@QOi&^!Damz=N5R089suaB@Vv!t0@3Qytle($cRSkX#n z46)SJBfygTEM2YF6{RLBza_8;d5H0XB>s`TkFUfq#QlhXJ$|Lq%^azB5@(!ex!4{P zG~SDQ7vAKF?w77EJ0v?~R3O-{xp{tSEf*djBl={`ndCZwO);YMuG$(BVD}Z$4y$u2 zx1^kqc<2pAO~@#-uQJ-8J?lEdd)U5)pXpXoY$|XPy;<-UU*XE`Now{@@`WSccxT1C zeaJmYJw#!5WoON5!6vc9wWur?|0ur9ze!D=ahjC0peVu+{Da>0p4CrngvBzYrx~#t z4J5n1enl0P83Ybpir&vL^}4MJEhGO7s4M~l&U;zt{Kf6){2;i8e5P)@WPy;ylgiEf z7W*qVv%Pj8 z=UJ%R(&WwR*3wc7QHDr!8@a3bo&QzB08AZ{v13LPyp`xGi<%a)CcMq`)HaGzycU;fxSE|{I zH>%Idr)W|H=GWsqnjD%dIvODzpE`C$``2G2y|c3@ZapIwFD8O{mo4I{KV-UI!E!|h z`_Ksdz;=H)O)@%uY`BKBT&gY0+$1h^n_34^v_LCU$^~)IB&KMB9wKQ$Q)UF!bUKQX z1l*FYXV-^JMDLuA7KvgjT^i!xjPP>)O$OiW&o|i;=DFU4O(TH9e-Q`=A?d^9F-5zp= zeMs^=U_N8IYO}k7;OJ_Jw6!K(E|uT=zRXgbQ`!I#sZTHI+6=vHE9+(t#Ri=)TEWpzA=FR!&=+OsvI! zpohVz8It0afPg%2D5vJEI`s_wMs-IXKPMGH>hPl#@}n3*R8fcEP!<5&e)> ztMW}Fjapaj@J!6WgH$A5W%_i{J;r6rk$Vxvx0L19VuZhB->O7ylM^<7DA(l}jr$R; z_z0d_h_-me=5#}<#-odq2@s5bQAmu?8$it@4LL{<6hgah*nA!e4v}w$3EP~4X*#>1uW4ZjkuS>nB(^3tP z9MFO>8rinR<3W18NR=Hw?WyGq)HKri6Sow4#)lF~Ar5o72;TQ_E}>X`ZJ(t0Qm{=0 z&778eCNj#PFvrzOXj;7m`N`72+b}=lCj?c;kA3l-HLH0eI1h77@vRp@!|zC)p^DeT zNhVsbM5`_CrvL|xyp&k4{R^MGceL!5RAC`Hz?S6}P9>GwjQ@MmI|vozuc6Mu8YFlT zAccC)cJKhTt1Z@6V6bq=CnGH(Vy^aEg}9K@&)uhZ1yLz0sGCEtyp(||b)9G^qvIaC zP7#8@lcU?B?vFh|4Jya{A{KnedVtX2cr)r|AzhQeo4Ts)O25C`PCp;q0^XgJ$(N#z zk_Vm1BGN{MirL-))DV>&Vc2%PD1`7l^3Kon@DP)NVFKt?Pd+2OJHHR@s2riIkAb81 z+|lg7)85^+}Y`{ga^Z?iibk%S#KQagp z7j%XkFF?0A0D{7c>^KCgpdVGDD^)&(K6{7*q6NVPsz2Fiv%PSf=(-MbBg_a;*q$Pr z%kSI9J>W~Jvcu}OvJuuUE{~^{r;u)+HH*?j?wiD{c)B*WE4Q>gEtI zB!H~M9r&10x2Otu;go8drG<4mEX}|xDU5XwXwVeO_!=?fc>1^|#x$@ru&yXD6KYxh zCDiRDIyBF}*_h-hG_w@u!TbonA8$^B>Y#b3C<3mmv ze`dd3NqVAokEC4(dNbhmPtOEkBF;(Go( z%>Bh@ve^vDQ~>yprcvqg7gAEnL+Y6Pg^-1S7=SqWdF>-(Jp&3U_Var`m{jBIgTd0C zxA;YcbM0wham?*eWdtL-JdHhK1}2{+8zlsM42;UCi^hY1b*aEQjHDCK4WS!a_`C;Y-#;X2{WMq> z)Gs2}(l!kQtLcu-RC?HC3&R4%oj00wJOic6g$r*-1Le-ya)mj_AJG@CZAF$k&1>a$ z@u3Xpu98<}`oL1#ylgp@Rkh2np6ZCskqE*!6WRfH0 z{z=fUghuk>pq@VFdmsucqrxZ-fF2ZhEwA<90!ck%_0~F%H>hz>O(qex$60Q77BH%N z5fh1^S@s84Yj7yrwUCf`c5+1gubaIcdaVr79y+Mk+Xg$$X(-RDQRZ17n39U_rX^%S zIgTnTDvSYthX0W&P~W#WLej+wuHug3tl9D#)Z|u}k35SM;E=RH@EwkUXlezfxjA4O_RSqj;RI8|>Z4K8dT5=Yb;iGhp0&~9tY0`8LJ*Vzbns1u2~ z&UYf=moL(G{eMSZvaq|_7zDFV-21ER9(C$FV_y+DS>O)pcQ1{Cf<_@$8Z4_?+9W37 z3Fs{v#GJq)gagO%+N2zV!n^wTP}uL^eN$y)Y{L(N=Hq4#2M98Q0;sY@46^5F5ZR)#0${d}4L<{_VA35SD>fuR5&2?omhdfqp8Kaw zoGz@Byi3Hd7{g$J6&hFo5u5wewtNO3&1nExe_xA!XPGT`YDX6w7SX8uoXOB+HifYs z{AAII^xQ&*!Alg%#^Uv?)(l)cC}bXr?Xf8f@U|if$aLkPFC}vstwcFS-U_X`lbu)A zCSGl61#e-0-3+L?9YlZo9s3NTfWRtYxHq~I(Sj*HU4WdUj9$Z*Y7J^P0~c~f7g6`L zKirWrAuejM)<$^!CluDUrD!(#ABVpzJhwjK&ti2 z;Jr$x>9(6+7l3Va_^5hIuV-w-b8>xE0=zoGw4sU>75ZLK9eifwr|=qjy~$!Ajo74? z=GHrYCw7PfXS-%9*kWGhY9Izq;#)pY^hr!;EI>b&&-Igl59RpzIc})i&87*hH~b(4 z7D1yp?VQ`D3~~FTkp_0o8s;-eZc7)J;TFjN$@xJ zpUyV|r0Hs=!^A_J>Q*EQx}?0j+eaGiVwVcqJTT04^m0xXg#gPQ9>Wwlle&$z5#qT5 zdPZ)e8>G>^X&LM+v?|kcaa;WpMb$|c3_ACRq}-6~J3dS&VWXjwe;$RsBV35p@e-z2 zMA5JftD=7XpHa}lP+HFi&VU<7&HFWl{I`UWqt9*j<)0nt2Bzx8MiakqOEjeB=n4le z^e1{gPN&4--XVh9n`4R>%3!?=Epn058xN)S^&ic}ssXT6o=;?2qhX20j@P3H)Gp?{ zb&OqDuPM)YAKCSWf@BF^UVH}UT!aEYy?u(zf)q-sP5HY`5~iICEmkL(mH4nE^)myM z_NJA!`ZYXJM~6(8?x-kk5YTY*X%iwWXhiv>LUB3qFtwX=b}ALN0Z~PoFP|yYkZZ7I zG)!BC@;J$psipFdVV_~;4Je?ZQ({vzk2Iw}L>M7vb(Egwkg44d2q6$*`#y?`h+pk4 zji;@N`mUR`ctwNBf?3ed+rn_s!wq$h*6C+!x|5_9(8?8MS8vWNY?Pfq-+=iGeW8(MKX^>gw{j|tg*q48SGh_R+?a*=*-Y)xOR_r(3YG+5 zzzU+jpDy0HM|7$Ca~$DU+Z<%xpAwDF@o;Gxxs- zfQ_h^jB|^R-;iMP=aS>)>X%cX4RtjND*k!?zP^7t!vkcm%6Hfsh^3#>`rY-EyD0AM zyb#cn!(KNiqzWWvBuxFL>)wXMnav#}X5#+0nN{JQu0^jsXxfJ>pMv&1t{88ay5Q7a zl1nVEBwtfZBA)7*$$iU9{W|gaGwEn%M&VGsfQ=dP!McZCa;3~sALPf5T6*xGlg1@V z@ZzUcb~srSkTdbsfsxAQ8(h^TDwbX(cSGfqfevyMy&o>PBCTKY+Y)B4&>wd?BEv}) z@)oHQE|UJXZIId47*!lUhr(%}05_=92B(5`zYOc$sxx2p7CIt|fl{dtRoB_%5-8%I`vxbE_Q71zJj5(hLi+ zmd?PLn?9%r1N&K&d0UMU75m%7V=?jNYCafq`ER7vulZp^KRS-kgB(VD1LLgSuasA) zo*+7i;1fr^Pgl#5^}!iVPJ1R`?H%}C(+0|S-*AIwJ+Sdfl1wD zHezBX`m7dyw>sf6eGD@7cx>|eO8d;rysmNv8w7=ye*00S9~A$4-$wN6FB@u1nshxw zB?;twoGFWx0}lgc&vgSDv!LDw<6pQxBscAVaum&lTk~s_0YO%C{*LcOBi90bqVGSs zNhPz&QnFs005k3BW0zUbTrM&4z4kOjLacoJ!o^o#${z<}W|bI0`TVuNk-ImkAC3Yk z9S1`XX{p5uK9pv^4&t0b{M)0oQ3@7S-?UUA6`y< z827iyl0z%R8jFxz>6Ms4R%s++p;>DGYgAIJw5Uk+PT z1<-^N_bxuL`YZ;_G?=BlY6=^!(eZF`R$b7{y-^G0!!Js0AiRf zDcy9%QvoE7tOBVR4y*tvP&!r_FEy((ys)Po)(u4$Y_F~f`?m%n%H2?&Oe1z-MUo&8 zki%bCwm}Z~wL!C4JT2j1xXx_I@epD)enHr-mclqyRA1Ghn_(`DIA^$~2*$WQet*}v zC!tI@_mx3BnUKS?4@bOcSA|$o@L3UR)S=tKuM`j|)D&FMyFx#lr&Ic|M+P z#XS#PSN{%7zC}hYw#izKzlSZlA?pFy6D*Gsfqd{Gem9P&l)6Z@ZvNaGc2}H|y!y>& zu}e}p@2(_QDgr>!dH2JeiV<$wBW%O=g_WjuyT;*=5<7}1Smv09EJs>KNykh4qD{szUGg_wSH4H4Y8l_Tu2Ds+Uszsf#}pc>3v=A4Z}o zxTsV0!#)ud*a9h|4>vq>r;Rn9;3?%DIP`7%!0^)>og}j%X1RcpCB@;)aKM7f1-2@l zkPrbuWtqmYk-0#9l%qMlUbAHdLuUgATHPSZtlp5nGl`Xa2d z-CyYA5&ikKGlpqED8pr3z-W41VpdRL}x@BQ>1gE z`ndNyVQP`xl#k!h5|ZQou!h$~F$d8mqNQ?78P!pTGn+v$%ov4zuID-x#_oYZ%5r0; zGAZ!AqsCFE8EACm7Uue1^pOob@TA~R1Va1L2qp^ zo7A#jE&W0$jfySy=K1}=O+My5;Lsrq*1`-5V1jnxE1BmF#zRQzm{?W39FutpoUHNb z_Cn~g_|J6eJFYhI?Jx-4?0m=?KmLiE#OKn1GZWrL_Y)Zx;cLWn_~ir zsDu?kCYT~hS0O^g$`^HtKUBsOJ;tHf29OuQ{d81LVBKhWT6oJye)MuHmwi=y1fp(n z&*Z_PNPA;VxjYYh3Of1)29%)M78o!^pGZ4z##@pym&d!XZ_&;LfT*o`5K74KHce3N zEp$x1DHnq*mshXj1@5ggB61DJ298*VB}v+jxh;T%3G;I7)x)DeNV5Afj>da=jo1n6D-yIB^m ztB~TM(vsuo>x$RM`&nCs1=*_N0`(GCDk*rs?hVojyj1p9U&fj@ICIWd|B$G`I8gm9 zW>!mK;fU^eLnXry3`UhHMP#?v7^E?q$0p-<7qF@1)HyJ?2>rVoZY|kA*4KRE3cYpT z{^ByV7wIbkZg{3q5g!~k=oqP#GVJDx%!1#1c}{)n&QSOza(2jJ(sxQy7-MltncY2} z^6(vl56QY8VTtt-BYD+&aic5LamQUg60f~)28pn};EbfPyea2>3^=1UM3e`gX3V04 zIb@#utl013q;U!Jr)AZ57B40D4oP-_e9054-7S`Z%;qM|{&3cjp=(v0Q=1cjxkad> zO){i500KX<^|kAj-q5L@k5Y<|Un^@lQPWrVl$Lu>wVCLDL5v|0P%m$KN>;Dm8e@}B z#gnASei~3(2J!?IoiYOjA#@SNe3HY>r9o~VINZcr^?zHLD-z4I{Ma#{fGJsLWZ=m7 z+Z@ZUYA-0kqIpNg*trm9n0ARtFzPZ43gDLo13vZA1*v+eR+WD!uWFt>5AR$BX{%f= z=iD2?xZ6CcJ)@3eSkagQv5wu>BtyG|^f7Pl9P2pV{M zsEW~}E+g~5bNs2?ap9*sqfg^jZi)ILq<>3IY=l)jb_$*4Am5{@2 zmmb`fp&Ub3s%a^|6Pu=voQ|w^cfNRwOT_-AQ>@ek)#pxX`=Gkyq z@;yt3O{T3{JSkd)EU`QN{T2~ayr5k zsJnuPICmBKz93665IqmDfrgxMB(T{!tW$`HLOl4G)m% zs#&Xd@Qk9&Dl&Kj?}vPfu9uW&?mH~(9>XvY=umXw759QZhE_je-Bf$ zGar~vydm!xVFbloW0SbPi47tmilBj0U=RK=qvbS{%L^>nXjTLk8dQx>`Q`e#`UOHE z(YhnCDDGr6;3DKA%!~hkS+G;kmed z@BCZnp zl;nv1Ww4V`-cVN%T0ND?nEc=H{==yKL+vNzBjlkd+j%j0u_e0dU=Gr-T$53ycUof= zCnzhm_W;_#iCgomF*HgsY#fI(ZJFbAjpVcn9IfYI$A$BtHXjhFp7n$zSvhh!0gl)0 z;le2-WDuDYH-~0sKWE+^Ev!eM+K7e!UQMhB#2 z5G>(Mgc6>R5qaYmrlykz7io;)!oN2kb!j|Q)G`Q4d8;$S8d-Q^l90ON5H-b8{O3v| zT%hbN7M}qzT$g?~y>ttNfRT3Aaf!f178&Q>kl#T z>r)n|9q&+8+pukGn6Y!dMXvvp8X06%CMd?Xkq!Tn8=u?H^xTv#0k$o%%|xzl?;6}5 zvTEUhR|R5c=$PkgSrfe{=sTAO6&p%pa{+%rYTEH|u2P6~u$^@mlvpkn6$z~%s{{<0 zxOdW@80Zn<_v*3~5TbxAt9qubUJQxxGZsr2xmJ=IHhc*OB&C>6?p)8{vT?F6&xDf@ zl}lH8uojeIt#hc8%cV7H)}Cw+Hb}=6X|C9mCbZe~RqLP+#vC2YO^zE$QXI7c~KDA#pv3pY394RQ@qZy8$Aq^n8X_n&3$#~X||0B!s zc&))5U&{eVV&Q=|S20k=L%yKXKcODi(vau+&W|zahK@Pbn&Udf`SeKOm(xj>&{W;X zoXj)4L>-xzS_>vBnJFWjQFJ&ur=7;|e;Q*}M>V%cM1^BkeBpnJw9mVCh zp8gkN9|DDpiUh^@D$VJ?)~V(DR7|8sxd3K0_YnV_?~5Vb+#d8(T}K98f(c{O1^+?M z5z;R{wbgcCqo3aqnwIdL4{Q>zhg%(Co(hOT(_R+C(10X~Xo_N9ByB}|;EVQsX-C(c z_zR8zk;w-35GTNoF^e0OI4;0{PGh3$mN<&En;O7FvQ#0o(7$lxx_4go-A}9d^TOjk z`>6}VJK4{7uBg08mo3F8G{WNX)J|#2Jyqs6K&1pSN=~ww2AMSI+5Oy!L&LofGUP81 z54h-=kt&Xq>V_Bo(`Wy+Li>u7ek37jpi2q+pEZLX57tkuu7X%WMmGKYCi5d|>l_#Y&ebCHCQ-I)Lsy*zRQtgo_gU;#%vN4rInD%5|cIbAdp&$p&D5N%u z=T@aGWOCJ!TBGYXhhSu3kh!vf3{OlXy-)K19d?u^#9LJU7ahmXQ`KEVI|`UFAK(q^ znd>QqV@Fbi76Ah}JFFxMn}VAq6dP@#(U@-G(eqI_?Sjt0gY7ckInrI%=dZ+{Rc_ee z`q^U%wtT*mM1UYhjQPU-_IK7a!4H%UFO7Wmu(CLBopGzW!}nj*9(R-}^(Cysn@w*F*3D zBYSWRA5N^%w1nr`Kuj=NkwzS;7fa^k-vCYa(VcO9Q3eu~ve*`c zf&XdECM4XgRH{Huxfc3q|}DB=7mlf~%s zj2bx~_4?!gojx*11o<=UvrG$8mJgidspt;T*|wb00+hZD);UK z?&K@J?1?N8Vqgag3AUZwQg0A}<1SeI&l35YBa}W>FZ5mNk9Xt2i(I(@A(n!KML8Nh zMV(KeNFhco|KB6J^_aHcqE%`jOcz2GB8Jd$Uokgplgv24d_@gHc38w@O3+GYB1@M# z>w(nj_Yq$oT2gH2)fcK{j;kEieFPllus$J7?zHA33|7N?!lcMI!|z6OuJ*mEd;|ll z^xL5!fZFHL>t$tTf>CPw$lgpU$ z&D`TMw+YVgE!WiNp;L^DpnH5U^-ks_6Oh2Z{~0QO3%hH7F>Y?PJ^6(A*V+5C7pEQf ztv4`1A6O@q`V#{Y7G>ByUOGZJt8`!PDHBc3P*lJ?3-}O;WU>L}v!kIwj88!_Yq;GZ zjJl|A!S&n8J@n?|&(wu26&T`EbB{%SlaqYdu`@blAXSNiZW3N zgH2;<^gkRlX%-}PuZTs~aHc7Na8<6JydrG8<$qSx71DyJ&>-d}xg{4MS%9lPru0C7pdC19 zH49Mqi?U;R9A=Q$7~XoquSIXQ;2>neo+p`2YR-w0CN3!q%8-;-EhxU{r(2_*m}mJ6 zZL)l0z%;hEf)G7TrxpveYwBxCrTu;(Y{)but%mw3n{!j{Y`CYT+}rvn)=#5OO1w&T zm&EviM3P@+aDy@xgf0=)N8=cH?68F%(SLxNQ&`;==6~?+IP(vhQ$=Hspw9vtGM<(q z3IT;rZ3#tHM4a=vf-u)hkQ~bHG`+qmOJ&5O{WS2@_ajxbcuxg)_6t|>`iD-WR8@M^ zJK&Z@H=`{V=VPy}flnm-Pt_Vx)j-V>U>h`^okODFG)4NbyD?a6kkcMbC0O5r^6mB7 z#BLEn1I@j}2a|5+GNJ++^j20%e6(NoAz@Z%rgnEk>+0Q%f0{|Iv;u_~% zFmPJSNcpF&L3>{kRs#zE0D(>q`cUP7EC0&)cDc>ceFwo350T ztGVrZ>GhaDBogY|BuE*K{yx6g2UM^BU#@-#9TF*bkZGk+su_a2Cvlcy3l-!o#%rRo zrn)r9KLu^`le{3)6IH|wIlwsc+e4+$3#PX!TQXzX5IA{zq9edoxsKUtvfU~my31!= zuk6H}%7T+%T->+^^gg>sX8>Vm2lc_CG}kjw{OzjO<#(m!Cfu?ali6@G*g#5{T_lS{ z@5r(mMxT5#W(rbnZUcPI8qiL30ElSSnF2~hk|%>jVaZp9n=Z3h$8lp4hH=4lc!|9C zy39?#*Gh+uR=c-rXs`V82p^>qQCpn z8bmxA5k90frT9)g4$#lPih`MrF!s;caR&(gF=a1pnjPz34<*STzJnbicXidpTr&`; z%;w#uZ1_EUIn~`3np<{5-9~Hb3&9LvpbLU5^jKhh zi-`#+?C(g#7+m@VaFQ|a%b4$AcH9(wKw-+$}(N(!KDS z_ju<8m!3PfKL-1L%3xKghb-v-u{9;RnCviVUrYHwP@Yy7=j7~Sg01%DB_u@E;@w!%Rd zZy}DMz+Q}t{xX82ynvDY%gN$D9N@6#c{#dR*?TgNQ1R*sr8d{x+AzT(p3?FZ{t*zS zsj_2Ggi1q0l^rE~7kg1}#slS1ho5WAB)ZogX^$>b7=XqHJ%?Gm)a!Z`9csL8g=#7w z-ehp`Eojv)Tk_pk2NOx+4SOI>`J>N+^$&&{mv_)Ar?uQ@X*^4S`zh}wya|(Li16zp zvC9)r1TM8sWlAUq5cjLt-q$+D{Sh(=aqTSJuWD4^mIZdr@VrW za>=F-?ptss=?Qn4)H{sn{%j4hOOnk|>n;AWPoJkAH_;UYn<9+T2-1xu{b1stOYV*y zPo}9tet#JbIA1SX9gk2-^ca|+Zg&kcqAr}pLoF2<7c+enM=4Z=6;~N@l;v<20Ge4w zn`o8N#Y$$lAHYtlo8jRH`Lm*^_q#(49j*cP*KsVLW|wMo#}RZHR2f? zxx1Kg5$@y25k?B%hB_%1=wV&Mhxr?0;&wJ-7_5E&c3u4uuAYilPxlLL`Sy)HF;H6m zV~{0M2e`jQV!BSF6D=Rrt2oLJ6@?#vRQ}JG{3QvR*UN%6lXX2ET)^^M^}KsfS|H|Q zoSrBRL9E$Z)wO=7o^T^}XsaUuBm&=bp04M3mPzIa| zazgo46(p>MJG-;}!&~gUN$!ZKNo@$6RVao>OV?-MOK5>cd>=@;t9Iv!5H@w4T1rrnX}Etna^Q9zEs|u-5#cFn#VdKf;ok2vmv?a>R8*0^6CxO6id4@d68pt!&@k#A zDh95+vkl+LpG&d56FXFPPWTp;N%8kAs^g(`#0MZ2`<3c1bpO3WFJ5?IM{Uc3sU{*8 zZRRC@c7g$i%|l#)U2f#R3st&X|SB>m0>}%{zpX~q_%$O~SrF1vA z#ffz9ob3$hYMkF;X|Q*1Z6{q{ab21(WZLD<%-$_&^R8^hZBN^9*Bh+{)xEm>WRUlW z;iZ(jUv?t2$S_JH<0hyzavG7BGx{6)?2Oi5BE))q^Ai%QWBH5oS`Voj6HM5x`V3`? zFtK%=(8a_of1JE144QOKSWX9~IFbSrzB59K99A}Mh4V32`iRjk;pgKK9G2fHD!7F~ z#RvNVndP;94sn(y9TjHB5(rUYLZ?J3GFCI*$2di5{*`7G3V3meJ#&*;e~-E$P+yc4JmU{Z{=-GB=@5SP09s z`b5gQ+5i`g%9mGkA_mLx1^Kb4vmcvNKh$Lr7=GN=)V(8C7`J3uFa}0_WtYD4OOJ{| zozlBLZG~DpBQ-7MsNEJ6C)xsji8o7ZJ~yig7VkLt(@X(9Qm3@rPnO9;3lsqcYz^3R zIN~=6b`+-}32;vy78LkcrEGq=y2CE5wi8+VhqLOOvcT`%!x(6VSsP81VDVD(W>&;J zLB_&e!Xu@yyjE>^+yqrT{{Mac{lCuqRB_+Ih$KvEgF_q_nTQ;m#PNMz_#H@($&U&5kpGi{FCbJK|m>||= zu(n_K&)_)I^)Eb;Um7l^Z;Z=zl-^F;F+V^KH0bMn!Uoe;`ej`s8x`0Em_%4YtQ|`` z8OIvBx&n{5T<&&=i;^8xnHD+}-ZaBRW*NVQc;0KgHdi|m4@nD&yW^a2p`tmk?JE2J zzGr@fzbgZ!MQ+cMxpu#JWy~}dYwhq`K^5g2XefW8v$3C7o(%XC(vKIqlh*R%>k(f_ z@P!6l{=!CjZfq~yCHKRbY`;zPYP~xTuU<$o)VD7<0J$9~_s{XcgftzSLdGw~9|GCj zTz_lBqskCNi6F)4-Kzk@E5+xlCsfXOaMj4jQP;hel0PKxke3rp-Ah{TIiAK?jaB?@ zYVg*PSBE$;EsO-ee|;S4^{nVKunIMnhDu8^A!8>VGk(6FwPF|V18>8l>H!TphIhUa zt%_MT57G3yOpoQa)n1*M6iaLmYCqu|9XB)o0lt0g(}pSQsAlZdZTsW1lJhM(sa7XK z8~1S?f^F%M=VtmJ^w`Y{>}bwL?;ObC!Dd^cGM;-iwIzY)Fw^b@M~16Uhll1ER=32} zj5QfP$a@zD#hIf59wQL&l$$p)t>rb>&v?_>A|TgPT$T?x-8 z%oUrTjyw~>Ag?2r)uA6&T|#e;upRN>*E&=NoZd4N2#jVN$^qQ55`~l{#6nnd`Bcs6 z`Zam+F!6w~6D$!nEGe1^lSZ~pwp>AoG)%@fw^0xn2tVl0NPd2HK@pM=tW`Y5uk2dl z45=E-GVNx

z@ihvJGni=&4=WG={QMnr`@(%a{SPE|z_(H}yxM!CZe72j@-1IS4cPUzO2<$!{veR= zvj#UI(tgeb2^B2e$GjfZEfo7rC$8$t#_Gq4Zw)LRzl${pbBCwvUpb_F(9bV3ffj1g zpWFqN`*%<)bI|3d#KZ<6vEVbOgya85?IP|Rcc&z&!GY&89l-N1R}YnTwpn0F0=1Q7 zAzF8dT#|rYD|VxcT2oOx4v9pgJfbl8DW0!}h|!tF-r?h;f{BDdD2c40Q*OA?JA^@S zE^P9AC4UJ?gV!{5*r>L@xq%nAkkk_%HT;w#6jw4sE%dKwy5b2XZr7M6`?KNIbUZdh z%pfb7+ogpJI15fAl=D-X%}Eq*QMy$!O_aC~wO z+&P#EE_IQrv)!iI<2z&@l6n#b%@@dz$oM+MG=+0U#Y9Y#Slt8SiIWZ6c-(du)_yWx z;YRP9u))x$8?%3_LE}8rtHxzsV}@#&OKx_&-tma_(x)NT@r}N&LLwbis)$)Ec<;g0 z4r`;O9wQG3qeatf^@q3JwTjw4+;%31ME!EA*&KsiE0G;mESwTTF<7!%b#Nq0YbBq@ zC5t8rXB8e%ny=T9^nx^3=@*iBIGf<3(8d5=`Fb4=Q+J7dhuE;<#K$5G?WQwz`t}C)eQIw)3r-Vqz;) z&5hJ#0<{%(Di)U++O?9zLM0(&9_?0%Yw4_IUT*DE&kMe|a$$+y02%n^5~|c$qm8C; zIG-)+X!{kT;?iy$g4zA1AnT@cE?GHD#d5n)ZTSD_A#Yl2deIA1{p4Ol{uXfb9w4bY zSq%oIEAD~2E!SMXD2#x-7|w8GI^~%>)VJ6Neglov)Gyz5TDZRoCK)j$pol;FmHGY( z#Pu`_!@?nlU0>%PG!yxrc&)RY0KUSV^jum#s6>uG*lDw_xJO8VnSK=Q?m3xUiy{G) zO~eZ$g-)nB}xKJvv*(>aVmA#{D+L7ir+b0A#S@8~iLlZf&^vF?febR?9-M(07Q>?;5b|F3wO z9|VFKYPuF$hf=GTJ5?p&Zy@UF>D@u^*>&LC8HfH&t9GpTebb?{cmMC0Zsi~KeBgr0}(HKy*H{~bHi9IDKpnyAH59cBO1l5d`AKO{CUo_|9(J%7g*C*Z{y n?-xg92wk@KcT5!OFRh3PN)CHt|D+S3Ru8>5bPscP?1eu8FN*p? literal 0 HcmV?d00001 diff --git a/docs/imgs/statusTimelineWithAlarms.png b/docs/imgs/statusTimelineWithAlarms.png new file mode 100644 index 0000000000000000000000000000000000000000..9c3032a3b99cc70c4e21762a1bb39f0e4c14e2d6 GIT binary patch literal 70530 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%0DVKa%b=cvILQ@646)(t8q&Jbq%w%M8MeWjcQ+5z8 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 literal 0 HcmV?d00001 diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index d58a97782..915f3bfdd 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -149,6 +149,7 @@ export namespace Components { interface IotTestRoutes { } interface IotTimeSeriesConnector { + "annotations": Annotations; "assignDefaultColors": boolean | undefined; "initialViewport": Viewport; "provider": Provider; @@ -433,6 +434,7 @@ declare namespace LocalJSX { interface IotTestRoutes { } interface IotTimeSeriesConnector { + "annotations"?: Annotations; "assignDefaultColors"?: boolean | undefined; "initialViewport"?: Viewport; "provider"?: Provider; diff --git a/packages/components/src/components/common/combineAnnotations.spec.ts b/packages/components/src/components/common/combineAnnotations.spec.ts new file mode 100644 index 000000000..e1fd5aad1 --- /dev/null +++ b/packages/components/src/components/common/combineAnnotations.spec.ts @@ -0,0 +1,22 @@ +import { combineAnnotations } from './combineAnnotations'; +import { TIME_SERIES_DATA_WITH_ALARMS } from '@iot-app-kit/source-iotsitewise'; + +it('correctly combines annotations annotations', () => { + const combinedAnnotations = combineAnnotations( + { + colorDataAcrossThresholds: true, + show: true, + thresholdOptions: true, + }, + { + y: TIME_SERIES_DATA_WITH_ALARMS.annotations.y, + } + ); + + expect(combinedAnnotations).toEqual({ + colorDataAcrossThresholds: true, + show: true, + thresholdOptions: true, + y: TIME_SERIES_DATA_WITH_ALARMS.annotations.y, + }); +}); diff --git a/packages/components/src/components/common/combineAnnotations.ts b/packages/components/src/components/common/combineAnnotations.ts new file mode 100644 index 000000000..ac9fb602d --- /dev/null +++ b/packages/components/src/components/common/combineAnnotations.ts @@ -0,0 +1,8 @@ +import { Annotations } from '@synchro-charts/core'; + +export const combineAnnotations = (prev: Annotations, cur: Annotations): Annotations => { + return { + ...prev, + y: [...(prev?.y || []), ...(cur?.y || [])], + }; +}; diff --git a/packages/components/src/components/common/getAlarmStreamAnnotations.spec.ts b/packages/components/src/components/common/getAlarmStreamAnnotations.spec.ts new file mode 100644 index 000000000..4488148a1 --- /dev/null +++ b/packages/components/src/components/common/getAlarmStreamAnnotations.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + // TODO +}); diff --git a/packages/components/src/components/common/getAlarmStreamAnnotations.ts b/packages/components/src/components/common/getAlarmStreamAnnotations.ts new file mode 100644 index 000000000..897b187df --- /dev/null +++ b/packages/components/src/components/common/getAlarmStreamAnnotations.ts @@ -0,0 +1,19 @@ +import { Annotations, YAnnotation } from '@synchro-charts/core'; +import { DataStream } from '@iot-app-kit/core'; + +export const getAlarmStreamAnnotations = ({ + annotations, + dataStreams, +}: { + annotations: Annotations; + dataStreams: DataStream[]; +}): { y: YAnnotation[] | undefined } => ({ + y: (annotations as Annotations).y?.filter((yAnnotation) => { + return ( + 'dataStreamIds' in yAnnotation && + yAnnotation.dataStreamIds?.some((dataStreamId) => + dataStreams.some((dataStream) => dataStream.streamType === 'ALARM' && dataStreamId === dataStream.id) + ) + ); + }), +}); diff --git a/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts b/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts index ec9783e64..a658d73f1 100644 --- a/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts +++ b/packages/components/src/components/iot-bar-chart/iot-bar-chart.spec.ts @@ -7,6 +7,7 @@ import { IotTimeSeriesConnector } from '../iot-time-series-connector/iot-time-se import { CustomHTMLElement } from '../../testing/types'; import { update } from '../../testing/update'; import { mockSiteWiseSDK } from '../../testing/mocks/siteWiseSDK'; +import { mockEventsSDK } from '../../testing/mocks/eventsSDK'; const viewport: MinimalLiveViewport = { duration: 1000, @@ -15,6 +16,7 @@ const viewport: MinimalLiveViewport = { const barChartSpecPage = async (propOverrides: Partial = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx b/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx index 0ed113f0f..d45a6b557 100644 --- a/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx +++ b/packages/components/src/components/iot-bar-chart/iot-bar-chart.tsx @@ -101,7 +101,8 @@ export class IotBarChart { provider={this.provider} styleSettings={this.styleSettings} assignDefaultColors - renderFunc={({ dataStreams }) => ( + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => ( = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-kpi/iot-kpi.tsx b/packages/components/src/components/iot-kpi/iot-kpi.tsx index 7e16314e3..dde2836d4 100644 --- a/packages/components/src/components/iot-kpi/iot-kpi.tsx +++ b/packages/components/src/components/iot-kpi/iot-kpi.tsx @@ -76,10 +76,11 @@ export class IotKpi { ( + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => ( = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-line-chart/iot-line-chart.tsx b/packages/components/src/components/iot-line-chart/iot-line-chart.tsx index dee65b86d..be476297f 100644 --- a/packages/components/src/components/iot-line-chart/iot-line-chart.tsx +++ b/packages/components/src/components/iot-line-chart/iot-line-chart.tsx @@ -93,7 +93,8 @@ export class IotLineChart { provider={this.provider} styleSettings={this.styleSettings} assignDefaultColors - renderFunc={({ dataStreams }) => { + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => { return ( { const { query } = initialize({ iotSiteWiseClient: iotSiteWiseClient, + iotEventsClient: createMockIoTEventsSDK(), }); const page = await newSpecPage({ components: [IotResourceExplorer], diff --git a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts index df6bbe46e..815bb0323 100644 --- a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts +++ b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.spec.ts @@ -7,6 +7,7 @@ import { IotTimeSeriesConnector } from '../iot-time-series-connector/iot-time-se import { CustomHTMLElement } from '../../testing/types'; import { update } from '../../testing/update'; import { mockSiteWiseSDK } from '../../testing/mocks/siteWiseSDK'; +import { mockEventsSDK } from '../../testing/mocks/eventsSDK'; const viewport: MinimalLiveViewport = { duration: 1000, @@ -15,6 +16,7 @@ const viewport: MinimalLiveViewport = { const scatterChartSpecPage = async (propOverrides: Partial = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx index 1c67e71b0..0d54bb8e5 100644 --- a/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx +++ b/packages/components/src/components/iot-scatter-chart/iot-scatter-chart.tsx @@ -96,11 +96,12 @@ export class IotScatterChart { provider={this.provider} styleSettings={this.styleSettings} assignDefaultColors - renderFunc={({ dataStreams }) => { + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => { return ( = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-status-grid/iot-status-grid.tsx b/packages/components/src/components/iot-status-grid/iot-status-grid.tsx index df8c989b8..19064773a 100644 --- a/packages/components/src/components/iot-status-grid/iot-status-grid.tsx +++ b/packages/components/src/components/iot-status-grid/iot-status-grid.tsx @@ -76,10 +76,11 @@ export class IotStatusGrid { ( + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => ( = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx b/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx index f7e5cf69c..29a9ebd63 100644 --- a/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx +++ b/packages/components/src/components/iot-status-timeline/iot-status-timeline.tsx @@ -21,6 +21,8 @@ import { combineProviders, } from '@iot-app-kit/core'; import { v4 as uuidv4 } from 'uuid'; +import { combineAnnotations } from '../common/combineAnnotations'; +import { getAlarmStreamAnnotations } from '../common/getAlarmStreamAnnotations'; @Component({ tag: 'iot-status-timeline', @@ -94,23 +96,30 @@ export class IotStatusTimeline { provider={this.provider} styleSettings={this.styleSettings} assignDefaultColors - renderFunc={({ dataStreams }) => ( - - )} + annotations={this.annotations} + renderFunc={({ dataStreams, annotations }) => { + const alarmStreamAnnotations = getAlarmStreamAnnotations({ annotations, dataStreams }); + + return ( + + ); + }} /> ); } diff --git a/packages/components/src/components/iot-table/iot-table.spec.tsx b/packages/components/src/components/iot-table/iot-table.spec.tsx index 345fa8cb3..faf7c596f 100644 --- a/packages/components/src/components/iot-table/iot-table.spec.tsx +++ b/packages/components/src/components/iot-table/iot-table.spec.tsx @@ -7,6 +7,7 @@ import { IotTimeSeriesConnector } from '../iot-time-series-connector/iot-time-se import { CustomHTMLElement } from '../../testing/types'; import { update } from '../../testing/update'; import { mockSiteWiseSDK } from '../../testing/mocks/siteWiseSDK'; +import { mockEventsSDK } from '../../testing/mocks/eventsSDK'; const viewport: MinimalLiveViewport = { duration: 1000, @@ -15,6 +16,7 @@ const viewport: MinimalLiveViewport = { const tableSpecPage = async (propOverrides: Partial = {}) => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const page = await newSpecPage({ diff --git a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts index 4297eb624..28891c4b5 100644 --- a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts +++ b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts @@ -4,8 +4,18 @@ import flushPromises from 'flush-promises'; import { initialize, createMockSiteWiseSDK, + createMockIoTEventsSDK, BATCH_ASSET_PROPERTY_VALUE_HISTORY, BATCH_ASSET_PROPERTY_DOUBLE_VALUE, + ALARM_ASSET_ID, + ALARM_STATE_PROPERTY_ID, + TIME_SERIES_DATA_WITH_ALARMS, + ALARM_MODEL, + ALARM_PROPERTY_VALUE_HISTORY, + ALARM_SOURCE_PROPERTY_VALUE, + ALARM_STATE_PROPERTY_VALUE, + ASSET_MODEL_WITH_ALARM, + THRESHOLD_PROPERTY_VALUE, } from '@iot-app-kit/source-iotsitewise'; import { IotTimeSeriesConnector } from './iot-time-series-connector'; import { update } from '../../testing/update'; @@ -16,6 +26,7 @@ import { DescribeAssetResponse, DescribeAssetModelResponse } from '@aws-sdk/clie import { mockSiteWiseSDK } from '../../testing/mocks/siteWiseSDK'; import { DATA_STREAM, DATA_STREAM_2 } from '@iot-app-kit/core'; import { colorPalette } from '../common/colorPalette'; +import { mockEventsSDK } from '../../testing/mocks/eventsSDK'; const createAssetResponse = ({ assetId, @@ -90,6 +101,7 @@ it('renders', async () => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); await connectorSpecPage({ @@ -102,7 +114,11 @@ it('renders', async () => { await flushPromises(); expect(renderFunc).toBeCalledTimes(1); - expect(renderFunc).toBeCalledWith({ dataStreams: [], viewport: { duration: 10 * 1000 * 60 } }); + expect(renderFunc).toBeCalledWith({ + dataStreams: [], + viewport: { duration: 10 * 1000 * 60 }, + annotations: { y: [] }, + }); }); it('provides data streams', async () => { @@ -113,6 +129,7 @@ it('provides data streams', async () => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); await connectorSpecPage({ @@ -129,17 +146,19 @@ it('provides data streams', async () => { await flushPromises(); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - }), - expect.objectContaining({ - id: DATA_STREAM_2.id, - }), - ], - viewport, - }); + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + id: DATA_STREAM.id, + }), + expect.objectContaining({ + id: DATA_STREAM_2.id, + }), + ]), + viewport, + }) + ); }); it('populates the name, unit, and data type from the asset model information from SiteWise', async () => { @@ -157,6 +176,7 @@ it('populates the name, unit, and data type from the asset model information fro batchGetAssetPropertyValueHistory: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY), batchGetAssetPropertyValue: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE), }), + iotEventsClient: mockEventsSDK, }); await connectorSpecPage({ @@ -170,17 +190,19 @@ it('populates the name, unit, and data type from the asset model information fro await flushPromises(); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - name: 'property-name', - unit: 'm/s', - dataType: 'NUMBER', - }), - ], - viewport, - }); + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + id: DATA_STREAM.id, + name: 'property-name', + unit: 'm/s', + dataType: 'NUMBER', + }), + ]), + viewport, + }) + ); }); it('populates the name, unit, and data type from the asset model information from SiteWise when updating the connector', async () => { @@ -198,6 +220,7 @@ it('populates the name, unit, and data type from the asset model information fro batchGetAssetPropertyValueHistory: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY), batchGetAssetPropertyValue: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE), }), + iotEventsClient: mockEventsSDK, }); const { connector, page } = await connectorSpecPage({ @@ -218,17 +241,19 @@ it('populates the name, unit, and data type from the asset model information fro await page.waitForChanges(); await flushPromises(); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - name: 'property-name', - unit: 'm/s', - dataType: 'NUMBER', - }), - ], - viewport, - }); + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + id: DATA_STREAM.id, + name: 'property-name', + unit: 'm/s', + dataType: 'NUMBER', + }), + ]), + viewport, + }) + ); }); it('updates with new queries', async () => { @@ -239,6 +264,7 @@ it('updates with new queries', async () => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); const { connector, page } = await connectorSpecPage({ @@ -263,17 +289,19 @@ it('updates with new queries', async () => { await flushPromises(); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - }), - expect.objectContaining({ - id: DATA_STREAM_2.id, - }), - ], - viewport, - }); + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + id: DATA_STREAM.id, + }), + expect.objectContaining({ + id: DATA_STREAM_2.id, + }), + ]), + viewport, + }) + ); }); it('binds styles to data streams', async () => { @@ -283,6 +311,7 @@ it('binds styles to data streams', async () => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); await connectorSpecPage({ @@ -298,17 +327,19 @@ it('binds styles to data streams', async () => { }, }); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - refId: REF_ID, - color: 'red', - name: 'my-name', - }), - ], - viewport, - }); + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + id: DATA_STREAM.id, + refId: REF_ID, + color: 'red', + name: 'my-name', + }), + ]), + viewport, + }) + ); }); it('when assignDefaultColors is true, provides a default color', async () => { @@ -318,6 +349,7 @@ it('when assignDefaultColors is true, provides a default color', async () => { const { query } = initialize({ iotSiteWiseClient: mockSiteWiseSDK, + iotEventsClient: mockEventsSDK, }); await connectorSpecPage({ @@ -328,12 +360,62 @@ it('when assignDefaultColors is true, provides a default color', async () => { assignDefaultColors: true, }); - expect(renderFunc).lastCalledWith({ - dataStreams: [ - expect.objectContaining({ - color: colorPalette[0], - }), - ], - viewport, + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + dataStreams: expect.arrayContaining([ + expect.objectContaining({ + color: colorPalette[0], + }), + ]), + viewport, + }) + ); +}); + +it('combines annotations passed to component with the ones provided by time series data', async () => { + const getAlarmModel = jest.fn().mockResolvedValue(ALARM_MODEL); + const describeAsset = jest.fn().mockResolvedValue({ + id: ALARM_ASSET_ID, + assetModelId: ASSET_MODEL_WITH_ALARM.assetModelId, }); + const describeAssetModel = jest.fn().mockResolvedValue(ASSET_MODEL_WITH_ALARM); + const getAssetPropertyValue = jest + .fn() + .mockResolvedValueOnce({ + propertyValue: ALARM_SOURCE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: ALARM_STATE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: THRESHOLD_PROPERTY_VALUE, + }); + const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ALARM_PROPERTY_VALUE_HISTORY); + + const renderFunc = jest.fn(); + + const { query } = initialize({ + iotSiteWiseClient: createMockSiteWiseSDK({ + describeAsset, + describeAssetModel, + getAssetPropertyValue, + getAssetPropertyValueHistory, + }), + iotEventsClient: createMockIoTEventsSDK({ + getAlarmModel, + }), + }); + + await connectorSpecPage({ + renderFunc, + provider: query + .timeSeriesData({ assets: [{ assetId: ALARM_ASSET_ID, properties: [{ propertyId: ALARM_STATE_PROPERTY_ID }] }] }) + .build('widget-id', { viewport }), + }); + + expect(renderFunc).lastCalledWith( + expect.objectContaining({ + annotations: TIME_SERIES_DATA_WITH_ALARMS.annotations, + }) + ); }); diff --git a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.tsx b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.tsx index d37ba2c1f..efe2cc888 100644 --- a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.tsx +++ b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.tsx @@ -1,16 +1,19 @@ import { Component, Prop, State, Watch } from '@stencil/core'; import { Provider, StyleSettingsMap, TimeSeriesData, Viewport } from '@iot-app-kit/core'; import { bindStylesToDataStreams } from '../common/bindStylesToDataStreams'; +import { combineAnnotations } from '../common/combineAnnotations'; +import { Annotations } from '@synchro-charts/core'; const DEFAULT_VIEWPORT = { duration: 10 * 1000 * 60 }; // ten minutes const combineTimeSeriesData = (timeSeresDataResults: TimeSeriesData[]): TimeSeriesData => timeSeresDataResults.reduce( - (timeSeriesData, { dataStreams, viewport }) => ({ + (timeSeriesData, { dataStreams, viewport, annotations }) => ({ dataStreams: [...timeSeriesData.dataStreams, ...dataStreams], viewport, + annotations: combineAnnotations(timeSeriesData.annotations, annotations), }), - { dataStreams: [], viewport: { duration: 0 } } + { dataStreams: [], viewport: { duration: 0 }, annotations: {} } ); @Component({ @@ -18,6 +21,8 @@ const combineTimeSeriesData = (timeSeresDataResults: TimeSeriesData[]): TimeSeri shadow: false, }) export class IotTimeSeriesConnector { + @Prop() annotations: Annotations; + @Prop() provider: Provider; @Prop() renderFunc: (data: TimeSeriesData) => void; @@ -31,6 +36,7 @@ export class IotTimeSeriesConnector { @State() data: TimeSeriesData = { dataStreams: [], viewport: DEFAULT_VIEWPORT, + annotations: {}, }; componentWillLoad() { @@ -58,7 +64,7 @@ export class IotTimeSeriesConnector { render() { const { - data: { dataStreams, viewport }, + data: { dataStreams, viewport, annotations }, } = this; return this.renderFunc({ @@ -68,6 +74,7 @@ export class IotTimeSeriesConnector { assignDefaultColors: this.assignDefaultColors || false, }), viewport, + annotations: combineAnnotations(this.annotations, annotations), }); } } diff --git a/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts b/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts index 19801037b..d9942906f 100644 --- a/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts +++ b/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts @@ -67,7 +67,7 @@ describe('bar chart', () => { yGridVisible: true, }, gestures: true, - annotations: { show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, + annotations: { y: [], show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, isEditing: false, trends: [], messageOverrides: {}, diff --git a/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts b/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts index d19118da8..42a3a3155 100644 --- a/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts +++ b/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts @@ -56,7 +56,7 @@ describe('kpi', () => { const props = { widgetId: '123', viewport: { duration: '5m' }, - annotations: { show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, + annotations: { y: [], show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, isEditing: false, messageOverrides: {}, }; diff --git a/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts b/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts index d07e4dbfa..78ee84f0d 100644 --- a/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts +++ b/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts @@ -76,7 +76,7 @@ describe('line chart', () => { yGridVisible: true, }, gestures: true, - annotations: { show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, + annotations: { y: [], show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, isEditing: false, trends: [], messageOverrides: {}, diff --git a/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts b/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts index da140464d..210bee366 100644 --- a/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts +++ b/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts @@ -66,7 +66,7 @@ describe('scatter chart', () => { yGridVisible: true, }, gestures: true, - annotations: { show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, + annotations: { y: [], show: true, thresholdOptions: true, colorDataAcrossThresholds: true }, isEditing: false, trends: [], messageOverrides: {}, diff --git a/packages/components/src/testing/mocks/eventsSDK.ts b/packages/components/src/testing/mocks/eventsSDK.ts new file mode 100644 index 000000000..14218bb3b --- /dev/null +++ b/packages/components/src/testing/mocks/eventsSDK.ts @@ -0,0 +1,7 @@ +import { ALARM_MODEL, createMockIoTEventsSDK } from '@iot-app-kit/source-iotsitewise'; + +const getAlarmModel = jest.fn().mockResolvedValue(ALARM_MODEL); + +export const mockEventsSDK = createMockIoTEventsSDK({ + getAlarmModel, +}); diff --git a/packages/core/src/data-module/TimeSeriesDataModule.spec.ts b/packages/core/src/data-module/TimeSeriesDataModule.spec.ts index 80b91853e..0b7554811 100644 --- a/packages/core/src/data-module/TimeSeriesDataModule.spec.ts +++ b/packages/core/src/data-module/TimeSeriesDataModule.spec.ts @@ -153,18 +153,20 @@ describe('initial request', () => { await flushPromises(); - expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - refId: REF_ID, - }), - ], - viewport: { - start: START, - end: END, - }, - }); + expect(timeSeriesCallback).toBeCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: DATA_STREAM.id, + refId: REF_ID, + }), + ], + viewport: { + start: START, + end: END, + }, + }) + ); }); it('passes back meta, name, and dataType information', async () => { @@ -208,6 +210,7 @@ describe('initial request', () => { await flushPromises(); expect(timeSeriesCallback).toHaveBeenLastCalledWith({ + annotations: {}, dataStreams: [ expect.objectContaining({ id: DATA_STREAM.id, @@ -245,18 +248,20 @@ describe('initial request', () => { await flushPromises(); - expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - isLoading: true, - } as DataStreamStore), - ], - viewport: { - start: START, - end: END, - }, - }); + expect(timeSeriesCallback).toBeCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: DATA_STREAM.id, + isLoading: true, + } as DataStreamStore), + ], + viewport: { + start: START, + end: END, + }, + }) + ); expect(dataSource.initiateRequest).toBeCalledWith( expect.objectContaining({ @@ -307,18 +312,20 @@ it('subscribes to a single data stream', async () => { await flushPromises(); - expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: DATA_STREAM.id, - resolution: DATA_STREAM.resolution, - }), - ], - viewport: { - start: START, - end: END, - }, - }); + expect(timeSeriesCallback).toBeCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: DATA_STREAM.id, + resolution: DATA_STREAM.resolution, + }), + ], + viewport: { + start: START, + end: END, + }, + }) + ); }); it('requests data from a custom data source', async () => { @@ -350,13 +357,15 @@ it('requests data from a custom data source', async () => { await flushPromises(); - expect(onSuccess).toBeCalledWith({ - dataStreams: [expect.objectContaining({ id: DATA_STREAM.id })], - viewport: { - start: START, - end: END, - }, - }); + expect(onSuccess).toBeCalledWith( + expect.objectContaining({ + dataStreams: [expect.objectContaining({ id: DATA_STREAM.id })], + viewport: { + start: START, + end: END, + }, + }) + ); }); it('subscribes to multiple data streams', async () => { @@ -464,26 +473,28 @@ it('subscribes to multiple queries on the same data source', async () => { }) ); - expect(onSuccess).toBeCalledWith({ - dataStreams: [ - expect.objectContaining({ - id: toDataStreamId({ - assetId: queries[0].assets[0].assetId, - propertyId: queries[0].assets[0].properties[0].propertyId, + expect(onSuccess).toBeCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: toDataStreamId({ + assetId: queries[0].assets[0].assetId, + propertyId: queries[0].assets[0].properties[0].propertyId, + }), }), - }), - expect.objectContaining({ - id: toDataStreamId({ - assetId: queries[1].assets[0].assetId, - propertyId: queries[1].assets[0].properties[0].propertyId, + expect.objectContaining({ + id: toDataStreamId({ + assetId: queries[1].assets[0].assetId, + propertyId: queries[1].assets[0].properties[0].propertyId, + }), }), - }), - ], - viewport: { - start: START, - end: END, - }, - }); + ], + viewport: { + start: START, + end: END, + }, + }) + ); }); it('only requests latest value', async () => { @@ -582,13 +593,15 @@ describe('error handling', () => { await flushPromises(); expect(timeSeriesCallback).toBeCalledTimes(1); - expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [expect.objectContaining({ error: ERR })], - viewport: { - start: START, - end: END, - }, - }); + expect(timeSeriesCallback).toBeCalledWith( + expect.objectContaining({ + dataStreams: [expect.objectContaining({ error: ERR })], + viewport: { + start: START, + end: END, + }, + }) + ); }); it('does not re-request a data stream with an error associated with it', async () => { @@ -642,13 +655,15 @@ describe('error handling', () => { await flushPromises(); expect(timeSeriesCallback).toBeCalledTimes(1); - expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [expect.objectContaining({ error: undefined })], - viewport: { - start: START, - end: END, - }, - }); + expect(timeSeriesCallback).toBeCalledWith( + expect.objectContaining({ + dataStreams: [expect.objectContaining({ error: undefined })], + viewport: { + start: START, + end: END, + }, + }) + ); }); }); diff --git a/packages/core/src/data-module/subscription-store/subscriptionStore.ts b/packages/core/src/data-module/subscription-store/subscriptionStore.ts index ff71ba6ed..b1ce21631 100644 --- a/packages/core/src/data-module/subscription-store/subscriptionStore.ts +++ b/packages/core/src/data-module/subscription-store/subscriptionStore.ts @@ -70,7 +70,7 @@ export default class SubscriptionStore { // Subscribe to changes from the data cache const unsubscribe = this.dataCache.subscribe(requestInfos, (dataStreams) => - subscription.emit({ dataStreams, viewport: subscription.request.viewport }) + subscription.emit({ dataStreams, viewport: subscription.request.viewport, annotations: {} }) ); this.unsubscribeMap[subscriptionId] = () => { diff --git a/packages/core/src/data-module/types.ts b/packages/core/src/data-module/types.ts index a4f53de51..c258b49d6 100644 --- a/packages/core/src/data-module/types.ts +++ b/packages/core/src/data-module/types.ts @@ -2,12 +2,13 @@ import { DataStreamId, MinimalViewPortConfig, Primitive } from '@synchro-charts/ import { TimeSeriesDataRequest } from './data-cache/requestTypes'; export { CacheSettings } from './data-cache/types'; import { CacheSettings } from './data-cache/types'; -import { DataPoint, StreamAssociation } from '@synchro-charts/core'; +import { DataPoint, StreamAssociation, Annotations } from '@synchro-charts/core'; import { ErrorDetails } from '../common/types'; export type TimeSeriesData = { dataStreams: DataStream[]; viewport: MinimalViewPortConfig; + annotations: Annotations; }; // Reference which can be used to associate styles to the associated results from a query diff --git a/packages/source-iotsitewise/package.json b/packages/source-iotsitewise/package.json index 9b86f0e6d..97d6f4673 100644 --- a/packages/source-iotsitewise/package.json +++ b/packages/source-iotsitewise/package.json @@ -38,17 +38,22 @@ "pack": "npm pack" }, "dependencies": { + "@aws-sdk/client-iot-events": "^3.118.1", "@aws-sdk/client-iotsitewise": "^3.87.0", "@iot-app-kit/core": "^2.0.0", "@rollup/plugin-typescript": "^8.3.0", "@synchro-charts/core": "^6.0.0", - "dataloader": "^2.1.0", "flush-promises": "^1.0.2", + "dataloader": "^2.1.0", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", "rxjs": "^7.4.0", "typescript": "4.4.4" }, "devDependencies": { "@types/jest": "^27.4.0", + "@types/lodash.merge": "^4.6.7", + "@types/lodash.mergewith": "^4.6.7", "jest": "^27.5.1", "jest-extended": "^2.0.0", "ts-jest": "^27.1.3" diff --git a/packages/source-iotsitewise/src/__mocks__/alarm.ts b/packages/source-iotsitewise/src/__mocks__/alarm.ts new file mode 100644 index 000000000..545df143e --- /dev/null +++ b/packages/source-iotsitewise/src/__mocks__/alarm.ts @@ -0,0 +1,316 @@ +import { DescribeAlarmModelResponse } from '@aws-sdk/client-iot-events'; +import { + DescribeAssetModelResponse, + AssetPropertyValue, + BatchGetAssetPropertyValueHistoryResponse +} from '@aws-sdk/client-iotsitewise'; +import { ALARM_STATUS } from '../alarms/iotevents/constants'; +import { COMPARISON_OPERATOR } from '@synchro-charts/core'; +import { ComparisonOperator } from '@aws-sdk/client-iot-events'; +import { TimeSeriesData } from '@iot-app-kit/core'; +import { Alarm } from '../alarms/iotevents'; + +export const ALARM_ASSET_MODEL_ID = 'asset-model-with-alarms'; + +export const INPUT_PROPERTY_ID = 'input-property-id'; + +export const THRESHOLD_PROPERTY_ID = 'threshold-property-id'; + +export const ALARM_STATE_PROPERTY_ID = 'alarm-state-property-id'; + +export const ALARM_SOURCE_PROPERTY_ID = 'alarm-source-property-id'; + +export const ALARM_MODEL_NAME = `TestAlarmModel_assetModel_${ALARM_ASSET_MODEL_ID}`; + +export const ALARM_MODEL_ARN = `arn:aws:iotevents:us-east-1:account-id:alarmModel/${ALARM_MODEL_NAME}`; + +export const ALARM_STATE_JSON_BLOB = { + stateName: ALARM_STATUS.ACTIVE, + ruleEvaluation: { + simpleRule: { + inputProperty: 31.524855556613428, + operator: 'GREATER', + threshold: 30.0 + } + } +} + +export const ALARM_MODEL: DescribeAlarmModelResponse = { + creationTime: new Date(), + lastUpdateTime: new Date(), + alarmCapabilities: { + acknowledgeFlow: { + enabled: false + }, + initializationConfiguration: { + disabledOnInitialization: false + } + }, + roleArn: 'arn:aws:iam::account-id:role/IoTSiteWiseDemoAssets-IoTSiteWiseDemoIotEventsActi-7JVOD1P2ET54', + severity: 1, + status: 'ACTIVE', + alarmModelArn: ALARM_MODEL_ARN, + alarmModelDescription: '', + alarmModelName: ALARM_MODEL_NAME, + alarmModelVersion: '1', + alarmRule: { + simpleRule: { + comparisonOperator: 'GREATER', + inputProperty: `$sitewise.assetModel.${'`'}${ALARM_ASSET_MODEL_ID}${'`'}.${'`'}${INPUT_PROPERTY_ID}${'`'}.propertyValue.value`, + threshold: `$sitewise.assetModel.${'`'}${ALARM_ASSET_MODEL_ID}${'`'}.${'`'}${THRESHOLD_PROPERTY_ID}${'`'}.propertyValue.value` + } + } +}; + +export const ASSET_MODEL_COMPOSITE_MODELS_WITH_ALARM = [ + { + name: 'test', + properties: [ + { + dataType: 'STRING', + id: 'alarm-type-id', + name: 'AWS/ALARM_TYPE', + type: { + attribute: { + defaultValue: 'IOT_EVENTS' + } + } + }, + { + dataType: 'STRUCT', + dataTypeSpec: 'AWS/ALARM_STATE', + id: ALARM_STATE_PROPERTY_ID, + name: 'AWS/ALARM_STATE', + type: { + measurement: {} + } + }, + { + dataType: 'STRING', + id: ALARM_SOURCE_PROPERTY_ID, + name: 'AWS/ALARM_SOURCE', + type: { + attribute: { + defaultValue: ALARM_MODEL_ARN + } + } + } + ], + type: 'AWS/ALARM' + } +]; + +export const ASSET_MODEL_WITH_ALARM: DescribeAssetModelResponse = { + assetModelArn: `arn:aws:iotsitewise:us-east-1:account-id:asset-model/${ALARM_ASSET_MODEL_ID}`, + assetModelCompositeModels: ASSET_MODEL_COMPOSITE_MODELS_WITH_ALARM, + assetModelCreationDate: new Date(), + assetModelDescription: 'testAssetModel', + assetModelHierarchies: [], + assetModelId: ALARM_ASSET_MODEL_ID, + assetModelLastUpdateDate: new Date(), + assetModelName: 'testAssetModel', + assetModelProperties: [ + { + dataType: 'INTEGER', + id: INPUT_PROPERTY_ID, + name: 'inputProperty', + type: { + measurement: {} + }, + unit: 'Celsius' + }, + { + dataType: 'INTEGER', + id: THRESHOLD_PROPERTY_ID, + name: 'thresholdProperty', + type: { + measurement: {} + }, + unit: 'Celsius' + }, + ], + assetModelStatus: { + state: 'ACTIVE' + } +}; + +export const ALARM_STATE_PROPERTY_VALUE: AssetPropertyValue = { + value: { + stringValue: JSON.stringify(ALARM_STATE_JSON_BLOB), + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 0, + }, +}; + +export const ALARM_STATE_PROPERTY_VALUE2: AssetPropertyValue = { + value: { + stringValue: JSON.stringify({ + ...ALARM_STATE_JSON_BLOB, + stateName: ALARM_STATUS.NORMAL, + }), + }, + timestamp: { + timeInSeconds: 2000, + offsetInNanos: 0, + }, +}; + +export const ALARM_SOURCE_PROPERTY_VALUE: AssetPropertyValue = { + value: { + stringValue: ALARM_MODEL_ARN + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 0, + }, +}; + +export const THRESHOLD_PROPERTY_VALUE: AssetPropertyValue = { + value: { + stringValue: ComparisonOperator.GREATER + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 0, + }, +}; + +export const ALARM_ASSET_ID = 'alarm-asset-id'; + +export const ALARM: Alarm = { + assetId: ALARM_ASSET_ID, + inputPropertyId: INPUT_PROPERTY_ID, + alarmStatePropertyId: ALARM_STATE_PROPERTY_ID, + thresholdPropertyId: THRESHOLD_PROPERTY_ID, + threshold: 30, + comparisonOperator: COMPARISON_OPERATOR.GREATER_THAN, + severity: 3, + rule: 'RPM > 30', + state: 'ACTIVE', +} + +export const TIME_SERIES_DATA_WITH_ALARMS = { + annotations: { + y: [ + { + color: '#d13212', + comparisonOperator: 'GT', + dataStreamIds: [ + 'alarm-asset-id---input-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'active', + severity: 1, + showValue: true, + value: NaN + }, + { + color: '#d13212', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'active', + severity: 1, + value: 'Active' + }, + { + color: '#f89256', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'latched', + severity: 2, + value: 'Latched' + }, + { + color: '#3184c2', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'acknowledged', + severity: 3, + value: 'Acknowledged' + }, + { + color: '#1d8102', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'normal', + severity: 4, + value: 'Normal' + }, + { + color: '#879596', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'snoozed', + severity: 5, + value: 'SnoozeDisabled' + }, + { + color: '#687078', + comparisonOperator: 'EQ', + dataStreamIds: [ + 'alarm-asset-id---alarm-state-property-id' + ], + description: 'inputProperty > GREATER', + icon: 'disabled', + severity: 6, + value: 'Disabled' + } + ] + }, + dataStreams: [{ + id: 'alarm-asset-id---alarm-state-property-id', + streamType: 'ALARM', + name: 'AWS/ALARM_STATE', + resolution: 0, + refId: undefined, + isRefreshing: false, + isLoading: false, + error: undefined, + dataType: 'NUMBER', + aggregates: {}, + data: [ + { + x: 1000000, + y: 'Active', + }, + { + x: 2000000, + y: 'Normal', + }, + ], + }], + viewport: { + duration: '5m' + } +} as TimeSeriesData; + +export const ALARM_PROPERTY_VALUE_HISTORY: BatchGetAssetPropertyValueHistoryResponse = { + successEntries: [ + { + entryId: '0-0', + assetPropertyValueHistory: [ + ALARM_STATE_PROPERTY_VALUE, + ALARM_STATE_PROPERTY_VALUE2, + ] + } + ], + errorEntries: [], + skippedEntries: [], +}; diff --git a/packages/source-iotsitewise/src/__mocks__/index.ts b/packages/source-iotsitewise/src/__mocks__/index.ts index 897731e00..0612392b8 100644 --- a/packages/source-iotsitewise/src/__mocks__/index.ts +++ b/packages/source-iotsitewise/src/__mocks__/index.ts @@ -2,3 +2,5 @@ export * from './asset'; export * from './assetModel'; export * from './assetPropertyValue'; export * from './iotsitewiseSDK'; +export * from './ioteventsSDK'; +export * from './alarm'; diff --git a/packages/source-iotsitewise/src/__mocks__/ioteventsSDK.ts b/packages/source-iotsitewise/src/__mocks__/ioteventsSDK.ts new file mode 100644 index 000000000..a502ac43d --- /dev/null +++ b/packages/source-iotsitewise/src/__mocks__/ioteventsSDK.ts @@ -0,0 +1,29 @@ +import { + DescribeAlarmModelCommandInput, + DescribeAlarmModelResponse, + IoTEventsClient +} from '@aws-sdk/client-iot-events'; + +const nonOverriddenMock = () => Promise.reject(new Error('Mock method not override.')); + +export const createMockIoTEventsSDK = ({ + getAlarmModel = nonOverriddenMock, +}: { + getAlarmModel?: (input: DescribeAlarmModelCommandInput) => Promise; +} = {}) => + ({ + send: (command: { input: any }) => { + // Mocks out the process of a sending a command within the JS AWS-SDK v3, learn more at + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html#high-level-concepts + const commandName = command.constructor.name; + + switch (commandName) { + case 'DescribeAlarmModelCommand': + return getAlarmModel(command.input); + default: + throw new Error( + `missing mock implementation for command name ${commandName}. Add a new command within the mock SiteWise SDK.` + ); + } + }, + } as unknown as IoTEventsClient); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/cache.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/cache.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/cache.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/cache.ts b/packages/source-iotsitewise/src/alarms/iotevents/cache.ts new file mode 100644 index 000000000..467ac9d45 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/cache.ts @@ -0,0 +1,46 @@ +import { AlarmModels, AlarmModel } from './types'; +import { EventsClient } from './client'; +import { IoTEventsToSynchroChartsComparisonOperator } from './constants'; +import { COMPARISON_OPERATOR } from '@synchro-charts/core'; + +export class Cache { + private readonly client: EventsClient; + private readonly alarmModels: AlarmModels = {}; + + constructor(client: EventsClient) { + this.client = client; + } + + async getAlarmModel(name: string): Promise { + if (this.alarmModels[name]) { + return this.alarmModels[name]; + } + + const { severity, alarmRule } = await this.client.getAlarmModel(name); + + if (!alarmRule?.simpleRule) { + throw new Error('Could not get alarm rule'); + } + + const { inputProperty, comparisonOperator, threshold } = alarmRule.simpleRule; + + if (!inputProperty || !comparisonOperator || !threshold || !severity) { + throw new Error('Could not parse alarm'); + } + + const inputPropertyId = inputProperty; + const scComparisonOperator = IoTEventsToSynchroChartsComparisonOperator[comparisonOperator] as COMPARISON_OPERATOR; + const thresholdPropertyId = threshold; + + const alarm = { + inputPropertyId, + comparisonOperator: scComparisonOperator, + thresholdPropertyId, + severity, + }; + + this.alarmModels[name] = alarm; + + return alarm; + } +} diff --git a/packages/source-iotsitewise/src/alarms/iotevents/client.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/client.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/client.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/client.ts b/packages/source-iotsitewise/src/alarms/iotevents/client.ts new file mode 100644 index 000000000..331883095 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/client.ts @@ -0,0 +1,17 @@ +import { IoTEventsClient, DescribeAlarmModelCommand, DescribeAlarmModelResponse } from '@aws-sdk/client-iot-events'; + +export class EventsClient { + private readonly eventsSdk: IoTEventsClient; + + constructor(eventsSdk: IoTEventsClient) { + this.eventsSdk = eventsSdk; + } + + async getAlarmModel(name: string): Promise { + return this.eventsSdk.send( + new DescribeAlarmModelCommand({ + alarmModelName: name, + }) + ); + } +} diff --git a/packages/source-iotsitewise/src/alarms/iotevents/constants.ts b/packages/source-iotsitewise/src/alarms/iotevents/constants.ts new file mode 100644 index 000000000..6438edf84 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/constants.ts @@ -0,0 +1,74 @@ +import { COMPARISON_OPERATOR, StatusIcon } from '@synchro-charts/core'; +import { ComparisonOperator } from '@aws-sdk/client-iot-events'; + +export const SOURCE = 'iotevents'; + +export const ALARM_STATUS = { + ACTIVE: 'Active', + NORMAL: 'Normal', + LATCHED: 'Latched', + DISABLED: 'Disabled', + ACKNOWLEDGED: 'Acknowledged', + SNOOZE_DISABLED: 'SnoozeDisabled', +} as const; + +export const AWSUI_RED_600 = '#d13212'; + +export const ALARM_STATUS_MAP: { [status: string]: { color: string; icon: StatusIcon; severity: number } } = { + [ALARM_STATUS.ACTIVE]: { + color: AWSUI_RED_600, + icon: StatusIcon.ACTIVE, + severity: 1, + }, + [ALARM_STATUS.LATCHED]: { + color: '#f89256', + icon: StatusIcon.LATCHED, + severity: 2, + }, + [ALARM_STATUS.ACKNOWLEDGED]: { + color: '#3184c2', + icon: StatusIcon.ACKNOWLEDGED, + severity: 3, + }, + [ALARM_STATUS.NORMAL]: { + color: '#1d8102', + icon: StatusIcon.NORMAL, + severity: 4, + }, + [ALARM_STATUS.SNOOZE_DISABLED]: { + color: '#879596', + icon: StatusIcon.SNOOZED, + severity: 5, + }, + [ALARM_STATUS.DISABLED]: { + color: '#687078', + icon: StatusIcon.DISABLED, + severity: 6, + }, +} as const; + +export const SynchroChartsToIoTEventsComparisonOperator: { [key: string]: ComparisonOperator } = { + [COMPARISON_OPERATOR.GREATER_THAN]: ComparisonOperator.GREATER, + [COMPARISON_OPERATOR.GREATER_THAN_EQUAL]: ComparisonOperator.GREATER_OR_EQUAL, + [COMPARISON_OPERATOR.LESS_THAN]: ComparisonOperator.LESS, + [COMPARISON_OPERATOR.LESS_THAN_EQUAL]: ComparisonOperator.LESS_OR_EQUAL, + [COMPARISON_OPERATOR.EQUAL]: ComparisonOperator.EQUAL, + NEQ: ComparisonOperator.NOT_EQUAL, +}; + +export const IoTEventsToSynchroChartsComparisonOperator: { [key: string]: COMPARISON_OPERATOR | 'NEQ' } = { + [ComparisonOperator.GREATER]: COMPARISON_OPERATOR.GREATER_THAN, + [ComparisonOperator.GREATER_OR_EQUAL]: COMPARISON_OPERATOR.GREATER_THAN_EQUAL, + [ComparisonOperator.LESS]: COMPARISON_OPERATOR.LESS_THAN, + [ComparisonOperator.LESS_OR_EQUAL]: COMPARISON_OPERATOR.LESS_THAN_EQUAL, + [ComparisonOperator.EQUAL]: COMPARISON_OPERATOR.EQUAL, + [ComparisonOperator.NOT_EQUAL]: 'NEQ', +}; + +export const COMPARISON_SYMBOL = { + [COMPARISON_OPERATOR.EQUAL]: '=', + [COMPARISON_OPERATOR.LESS_THAN]: '<', + [COMPARISON_OPERATOR.LESS_THAN_EQUAL]: '<=', + [COMPARISON_OPERATOR.GREATER_THAN]: '>', + [COMPARISON_OPERATOR.GREATER_THAN_EQUAL]: '>=', +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/index.ts b/packages/source-iotsitewise/src/alarms/iotevents/index.ts new file mode 100644 index 000000000..5c3f0b2d5 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './siteWiseAlarmModule'; +export { parseAlarmData } from './util/parseAlarmData'; +export { constructAlarmThresholds } from './util/constructAlarmThresholds'; +export { SOURCE } from './constants'; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.ts b/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.ts new file mode 100644 index 000000000..997de4612 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/siteWiseAlarmModule.ts @@ -0,0 +1,96 @@ +import { IoTEventsClient } from '@aws-sdk/client-iot-events'; +import { EventsClient } from './client'; +import { Cache } from './cache'; +import { Alarm, AlarmModel } from './types'; +import { getAlarmModelName } from './util/getAlarmModelName'; +import { parseAlarmData } from './util/parseAlarmData'; +import { getPropertyId } from './util/getPropertyId'; +import { COMPARISON_SYMBOL } from './constants'; +import { toValue } from '../../time-series-data/util/toDataPoint'; +import { SiteWiseAssetModule, SiteWiseAssetSession } from '../../asset-modules'; +import { getAlarmSourceProperty } from './util/getAlarmSourceProperty'; + +export class SiteWiseAlarmModule { + private readonly client: EventsClient; + private readonly assetModuleSession: SiteWiseAssetSession; + private readonly cache: Cache; + + constructor(client: IoTEventsClient, siteWiseAssetModule: SiteWiseAssetModule) { + this.client = new EventsClient(client); + this.assetModuleSession = siteWiseAssetModule.startSession(); + this.cache = new Cache(this.client); + } + + async getAlarmModel(name: string): Promise { + return this.cache.getAlarmModel(name); + } + + async getAlarm({ + assetId, + alarmStatePropertyId, + }: { + assetId: string; + alarmStatePropertyId: string; + }): Promise { + try { + const alarmAsset = await this.assetModuleSession.fetchAssetSummary({ assetId }); + + const assetModelId = alarmAsset?.assetModelId; + + const assetModel = await this.assetModuleSession.fetchAssetModel({ assetModelId: assetModelId as string }); + + const alarmSourceId = getAlarmSourceProperty(assetModel, alarmStatePropertyId)?.id; + + if (!alarmSourceId) { + return undefined; + } + + const alarmSourcePropertyValue = await this.assetModuleSession.fetchAssetPropertyValue({ + assetId, + propertyId: alarmSourceId as string, + }); + + const alarmModelName = getAlarmModelName(toValue(alarmSourcePropertyValue.value) as string); + + const { + comparisonOperator, + thresholdPropertyId: thresholdPropertyIdExpression, + inputPropertyId: inputPropertyIdExpression, + severity, + } = await this.getAlarmModel(alarmModelName); + + const alarmStatePropertyValue = await this.assetModuleSession.fetchAssetPropertyValue({ + assetId: assetId, + propertyId: alarmStatePropertyId, + }); + const state = parseAlarmData(toValue(alarmStatePropertyValue.value) as string); + + const thresholdPropertyId = getPropertyId(thresholdPropertyIdExpression) as string; + const thresholdPropertyValue = await this.assetModuleSession.fetchAssetPropertyValue({ + assetId, + propertyId: thresholdPropertyId as string, + }); + + const inputPropertyId = getPropertyId(inputPropertyIdExpression) as string; + const inputPropertyName = assetModel.assetModelProperties?.find(({ id }) => id === inputPropertyId)?.name; + + const threshold = toValue(thresholdPropertyValue.value); + + const rule = `${inputPropertyName} ${COMPARISON_SYMBOL[comparisonOperator]} ${threshold}`; + + return { + assetId, + alarmStatePropertyId, + inputPropertyId, + thresholdPropertyId, + comparisonOperator, + threshold, + severity, + rule, + state, + }; + } catch { + return undefined; + } + } +} diff --git a/packages/source-iotsitewise/src/alarms/iotevents/types.ts b/packages/source-iotsitewise/src/alarms/iotevents/types.ts new file mode 100644 index 000000000..fba89fc65 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/types.ts @@ -0,0 +1,28 @@ +import { ALARM_STATUS } from './constants'; +import { COMPARISON_OPERATOR, Primitive } from '@synchro-charts/core'; + +export type UpperCaseStateName = keyof typeof ALARM_STATUS; +export type PascalCaseStateName = typeof ALARM_STATUS[UpperCaseStateName]; + +export type AlarmModel = { + inputPropertyId: string; + comparisonOperator: COMPARISON_OPERATOR; + thresholdPropertyId: string; + severity: number; +}; + +export type Alarm = AlarmModel & { + assetId: string; + alarmStatePropertyId: string; + threshold: Primitive; + rule: string; + state: string; +}; + +type AlarmStreamId = string; + +export type Alarms = Record; + +type AlarmModelName = string; + +export type AlarmModels = Record; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.spec.ts new file mode 100644 index 000000000..9b94e934a --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.spec.ts @@ -0,0 +1,13 @@ +//import { completeAlarmStream } from './completeAlarmStream'; + +it('returns alarm stream if property found in asset model composite model', () => { + // TODO +}); + +it('returns alarm stream if no asset model but inferred to be iot events alarm state property value', () => { + // TODO +}); + +it('returns undefined if stream is not alarm stream', () => { + // TODO +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.ts new file mode 100644 index 000000000..5b7c5ec9f --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/completeAlarmStream.ts @@ -0,0 +1,58 @@ +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import { DataStream } from '@iot-app-kit/core'; +import { getAlarmStateProperty } from './getAlarmStateProperty'; +import { constructAlarmStreamData } from './constructAlarmStreamData'; +import { toDataType } from '../../../time-series-data/util/toDataType'; +import { ALARM_STATUS } from '../constants'; + +/** + * infer if stream is an AWS IoT Events alarm stream ingested into AWS SiteWise Asset Alarm State Property + */ +const isIoTEventsAlarmStateProperty = (propertyValue?: string | number): boolean => { + if (typeof propertyValue === 'string') { + try { + const { stateName } = JSON.parse(propertyValue); + + return Object.keys(ALARM_STATUS).includes(stateName); + } catch { + return false; + } + } + return false; +}; + +export const completeAlarmStream = ({ + assetModel, + propertyId, + dataStream, +}: { + propertyId: string; + assetModel?: DescribeAssetModelResponse; + dataStream: DataStream; +}): DataStream | undefined => { + if (!assetModel) { + if (isIoTEventsAlarmStateProperty(dataStream.data[dataStream.data?.length - 1]?.y)) { + return { + ...dataStream, + streamType: 'ALARM', + data: constructAlarmStreamData({ data: dataStream.data }), + } as DataStream; + } + + return dataStream; + } + + const alarmStateProperty = getAlarmStateProperty(assetModel, propertyId); + + if (!alarmStateProperty) { + return; + } + + return { + ...dataStream, + name: alarmStateProperty.name, + streamType: 'ALARM', + data: constructAlarmStreamData({ data: dataStream.data }), + dataType: toDataType(alarmStateProperty.dataType), + } as DataStream; +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStream.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStream.spec.ts new file mode 100644 index 000000000..c1358fda7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStream.spec.ts @@ -0,0 +1,7 @@ +it('should parse alarm state property value', () => { + // TODO +}); + +it('should return stream if not alarm state property value', () => { + // TODO +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStreamData.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStreamData.ts new file mode 100644 index 000000000..aaabe07f9 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmStreamData.ts @@ -0,0 +1,11 @@ +import { DataPoint } from '@synchro-charts/core'; +import { parseAlarmData } from './parseAlarmData'; + +export const constructAlarmStreamData = ({ data }: { data: DataPoint[] }): DataPoint[] => { + return data.map(({ x, y }: DataPoint): DataPoint => { + if (typeof y === 'string') { + return { x, y: parseAlarmData(y) }; + } + return { x, y }; + }); +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.spec.ts new file mode 100644 index 000000000..cdba9dbd7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.spec.ts @@ -0,0 +1,3 @@ +it('constructs the alarm and input property stream thresholds', () => { + // TODO +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.ts new file mode 100644 index 000000000..6e8583f8d --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/constructAlarmThresholds.ts @@ -0,0 +1,39 @@ +import { COMPARISON_OPERATOR, Threshold } from '@synchro-charts/core'; +import { ALARM_STATUS_MAP, AWSUI_RED_600 } from '../constants'; +import { PascalCaseStateName, Alarm } from '../types'; +import { toId } from '../../../time-series-data/util/dataStreamId'; +import { isNumber } from '../../../common/predicates'; + +export const constructAlarmThresholds = (alarm: Alarm): Threshold[] => { + const propertyStreamId = toId({ assetId: alarm.assetId, propertyId: alarm.inputPropertyId }); + const alarmStreamId = toId({ assetId: alarm.assetId, propertyId: alarm.alarmStatePropertyId }); + + const alarmStatus = ALARM_STATUS_MAP[alarm.state]; + + const inputPropertyThreshold: Threshold = { + comparisonOperator: alarm.comparisonOperator, + severity: alarm.severity, + value: isNumber(alarm.threshold) ? alarm.threshold : parseFloat(alarm.threshold), + dataStreamIds: [propertyStreamId], + color: AWSUI_RED_600, + showValue: true, + icon: alarmStatus.icon, + description: alarm.rule, + }; + + const alarmStatePropertyThresholds = Object.keys(ALARM_STATUS_MAP).map((alarmStatus) => { + const status = ALARM_STATUS_MAP[alarmStatus as PascalCaseStateName]; + + return { + value: alarmStatus, + color: status.color, + severity: status.severity, + icon: status.icon, + comparisonOperator: COMPARISON_OPERATOR.EQUAL, + dataStreamIds: [alarmStreamId], + description: alarm.rule, + }; + }); + + return [inputPropertyThreshold, ...alarmStatePropertyThresholds]; +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.spec.ts new file mode 100644 index 000000000..e629c420b --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.spec.ts @@ -0,0 +1,3 @@ +it('correctly parses query and yields alarms and annotations', () => { + // TODO +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.ts new file mode 100644 index 000000000..9ef04fca6 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/fetchAlarmsFromQuery.ts @@ -0,0 +1,30 @@ +import { toId } from '../../../time-series-data/util/dataStreamId'; +import { constructAlarmThresholds } from './constructAlarmThresholds'; +import { SiteWiseDataStreamQuery } from '@iot-app-kit/core'; +import { Alarms } from '../types'; +import { Annotations } from '@synchro-charts/core'; +import { SiteWiseAlarmModule } from '../siteWiseAlarmModule'; + +export async function* fetchAlarmsFromQuery({ + queries, + alarmModule, +}: { + queries: SiteWiseDataStreamQuery[]; + alarmModule: SiteWiseAlarmModule; +}): AsyncGenerator<{ alarms: Alarms; annotations: Annotations }> { + for (const { assets } of queries) { + for (const { assetId, properties } of assets) { + for (const { propertyId } of properties) { + const alarm = await alarmModule.getAlarm({ assetId, alarmStatePropertyId: propertyId }); + + if (alarm) { + const alarmStreamId = toId({ assetId, propertyId }); + + const thresholds = constructAlarmThresholds(alarm); + + yield { alarms: { [alarmStreamId]: alarm }, annotations: { y: thresholds } }; + } + } + } + } +} diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.ts new file mode 100644 index 000000000..ef841ebd5 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmModelName.ts @@ -0,0 +1,5 @@ +export const getAlarmModelName = (alarmSourceArn: string): string => { + const splitAlarmArn = alarmSourceArn.split('/'); + + return splitAlarmArn[splitAlarmArn.length - 1]; +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.ts new file mode 100644 index 000000000..60f8ecafb --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmSourceProperty.ts @@ -0,0 +1,12 @@ +import { AssetModelProperty, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; + +export const getAlarmSourceProperty = ( + assetModel: DescribeAssetModelResponse, + alarmStatePropertyId: string +): AssetModelProperty | undefined => + assetModel.assetModelCompositeModels + ?.filter(({ type }) => type === 'AWS/ALARM') + .map(({ properties }) => properties) + .filter((properties) => properties?.some((property) => property?.id === alarmStatePropertyId)) + .flat() + .find((property) => property?.name === 'AWS/ALARM_SOURCE'); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.ts new file mode 100644 index 000000000..5691fffec --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getAlarmStateProperty.ts @@ -0,0 +1,11 @@ +import { DescribeAssetModelResponse, AssetModelProperty } from '@aws-sdk/client-iotsitewise'; + +export const getAlarmStateProperty = ( + assetModel: DescribeAssetModelResponse, + alarmStatePropertyId: string +): AssetModelProperty | undefined => + assetModel.assetModelCompositeModels + ?.filter(({ type }) => type === 'AWS/ALARM') + .map(({ properties }) => properties) + .flat() + .find((property) => property?.id === alarmStatePropertyId && property?.name === 'AWS/ALARM_STATE'); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.ts new file mode 100644 index 000000000..db0433aa9 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/getPropertyId.ts @@ -0,0 +1,31 @@ +export const removeBackticks = (value: string) => { + return value.replace(/^`|`$/g, ''); +}; + +export const SITE_WISE_BACKED_PROPERTY_PREFIX = '$sitewise'; + +export const isBackedBySiteWiseAssetProperty = (maybeAssetProperty: string): boolean => + maybeAssetProperty.startsWith(SITE_WISE_BACKED_PROPERTY_PREFIX); + +/** + * Extracts the propertyId from an iot events expressions + * + * $sitewise.assetModel.`assetModelId`.`propertyId`.propertyValue.value + * + * "In AWS IoT Events, you use expressions to specify values in alarm models" + * + * More on that here https://docs.aws.amazon.com/iot-sitewise/latest/userguide/define-iot-events-alarm-cli.html + */ +export const getPropertyId = (modelPropertyId: string | undefined): string | undefined => { + if (modelPropertyId == null) { + return undefined; + } + + if (!isBackedBySiteWiseAssetProperty(modelPropertyId)) { + return undefined; + } + + const splitInputProperty = modelPropertyId.split('.'); + + return removeBackticks(splitInputProperty[3]); +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStream.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStream.ts new file mode 100644 index 000000000..1508f8291 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStream.ts @@ -0,0 +1,24 @@ +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import { getAlarmStateProperty } from './getAlarmStateProperty'; +import { Alarms } from '../types'; +import { isDefined } from '../../../common/predicates'; + +export const isCompleteAlarmStream = ({ + propertyId, + dataStreamId, + assetModel, + alarms, +}: { + propertyId: string; + dataStreamId: string; + assetModel: DescribeAssetModelResponse; + alarms: Alarms; +}): boolean => { + const alarmStateProperty = getAlarmStateProperty(assetModel, propertyId); + + if (alarmStateProperty) { + return isDefined(alarms[dataStreamId]); + } + + return true; +}; diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStreams.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStreams.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/isCompleteAlarmStreams.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.spec.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.ts b/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.ts new file mode 100644 index 000000000..f5bc9a5fb --- /dev/null +++ b/packages/source-iotsitewise/src/alarms/iotevents/util/parseAlarmData.ts @@ -0,0 +1,20 @@ +import { PascalCaseStateName, UpperCaseStateName } from '../types'; +import { ALARM_STATUS } from '../constants'; + +export const parseAlarmData = (value: string) => { + try { + const { stateName } = JSON.parse(value); + + let normalizedStateName: PascalCaseStateName; + + if (ALARM_STATUS[stateName as UpperCaseStateName] != null) { + normalizedStateName = ALARM_STATUS[stateName as UpperCaseStateName]; + } else { + normalizedStateName = stateName; + } + + return normalizedStateName; + } catch (err) { + throw new Error(`Could not parse alarm data: ${err}`); + } +}; diff --git a/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.spec.ts b/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.ts b/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.ts new file mode 100644 index 000000000..5923cbed5 --- /dev/null +++ b/packages/source-iotsitewise/src/asset-modules/util/completePropertyStream.ts @@ -0,0 +1,47 @@ +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import { DataStream } from '@iot-app-kit/core'; +import { toDataType } from '../../time-series-data/util/toDataType'; +import { StreamType } from '@synchro-charts/core'; +import { Alarms } from '../../alarms/iotevents'; + +export const completePropertyStream = ({ + assetModel, + assetId, + propertyId, + dataStream, + alarms, +}: { + assetModel?: DescribeAssetModelResponse; + assetId: string; + propertyId: string; + dataStream: DataStream; + alarms: Alarms; +}): DataStream | undefined => { + if (!assetModel) { + return; + } + + const { assetModelProperties } = assetModel; + + const property = assetModelProperties?.find(({ id }) => id === propertyId); + + if (!property) { + return; + } + + return { + ...dataStream, + name: property.name, + unit: property.unit, + dataType: toDataType(property.dataType), + associatedStreams: Object.keys(alarms) + .filter((id) => { + const alarm = alarms[id]; + return alarm.assetId === assetId && alarm.inputPropertyId === propertyId; + }) + .map((id) => ({ + id, + type: StreamType.ALARM, + })), + }; +}; diff --git a/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.spec.ts b/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.ts b/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.ts new file mode 100644 index 000000000..1e10ed686 --- /dev/null +++ b/packages/source-iotsitewise/src/asset-modules/util/fetchAssetModelsFromQuery.ts @@ -0,0 +1,30 @@ +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import { SiteWiseAssetDataStreamQuery } from '../../time-series-data/types'; +import { ErrorDetails } from '@iot-app-kit/core'; +import { SiteWiseAssetSession } from '../index'; + +export async function* fetchAssetModelsFromQuery({ + queries, + assetModuleSession, +}: { + queries: SiteWiseAssetDataStreamQuery[]; + assetModuleSession: SiteWiseAssetSession; +}): AsyncGenerator< + { assetModels: Record } | { errors: Record } +> { + for (const { assets } of queries) { + for (const asset of assets) { + try { + const { assetModelId } = await assetModuleSession.fetchAssetSummary({ assetId: asset.assetId }); + + const assetModelResponse = assetModelId && (await assetModuleSession.fetchAssetModel({ assetModelId })); + + if (assetModelResponse) { + yield { assetModels: { [asset.assetId]: assetModelResponse } }; + } + } catch (err) { + yield { errors: { [asset.assetId]: err as ErrorDetails } }; + } + } + } +} diff --git a/packages/source-iotsitewise/src/common/predicates.spec.ts b/packages/source-iotsitewise/src/common/predicates.spec.ts index 564fef042..4920eceb8 100644 --- a/packages/source-iotsitewise/src/common/predicates.spec.ts +++ b/packages/source-iotsitewise/src/common/predicates.spec.ts @@ -1,4 +1,4 @@ -import { isDefined } from './predicates'; +import { isDefined, isNumber } from './predicates'; describe('isDefined', () => { it('returns false when passed null', () => { @@ -21,3 +21,22 @@ describe('isDefined', () => { expect(isDefined({})).toBe(true); }); }); + +describe('isNumber', () => { + describe.each` + value | expected + ${''} | ${false} + ${new Date()} | ${false} + ${123} | ${true} + ${true} | ${false} + ${'TEST'} | ${false} + ${'123'} | ${false} + ${123.3} | ${true} + ${NaN} | ${true} + ${12e3} | ${true} + `('checks if value is a number', ({ value, expected }) => { + test(`${value}) is ${expected ? '' : 'not '}a number`, () => { + expect(isNumber(value)).toBe(expected); + }); + }); +}); diff --git a/packages/source-iotsitewise/src/common/predicates.ts b/packages/source-iotsitewise/src/common/predicates.ts index 320288bb7..433404bd2 100644 --- a/packages/source-iotsitewise/src/common/predicates.ts +++ b/packages/source-iotsitewise/src/common/predicates.ts @@ -27,3 +27,5 @@ */ export const isDefined = (value: T | null | undefined): value is T => value != null; + +export const isNumber = (val: T | number): val is number => typeof val === 'number'; diff --git a/packages/source-iotsitewise/src/completeDataStreams.spec.ts b/packages/source-iotsitewise/src/completeDataStreams.spec.ts index 204421bd0..aef017dff 100644 --- a/packages/source-iotsitewise/src/completeDataStreams.spec.ts +++ b/packages/source-iotsitewise/src/completeDataStreams.spec.ts @@ -1,41 +1,147 @@ import { completeDataStreams } from './completeDataStreams'; -import { DATA_STREAM, DATA_STREAM_2 } from '../../core/src/mockWidgetProperties'; +import { DATA_STREAM, STRING_INFO_1, DATA_STREAM_2 } from '../../core/src/mockWidgetProperties'; import { toId } from './time-series-data/util/dataStreamId'; import { DataStream } from '@iot-app-kit/core/src/data-module/types'; import { ASSET_MODEL } from './__mocks__/assetModel'; +import { + ASSET_MODEL_WITH_ALARM, + ALARM_STATE_PROPERTY_ID, + ALARM_STATE_JSON_BLOB, + INPUT_PROPERTY_ID, + ALARM_ASSET_ID, + ALARM, +} from './__mocks__/alarm'; import { AssetModelProperty } from '@aws-sdk/client-iotsitewise'; it('returns empty array when provided no data streams or asset models', () => { - expect(completeDataStreams({ dataStreams: [], assetModels: {} })).toBeEmpty(); + expect(completeDataStreams({ dataStreams: [], assetModels: {}, alarms: {} })).toBeEmpty(); }); it('returns the provided data stream when no asset models are given', () => { - expect(completeDataStreams({ dataStreams: [DATA_STREAM, DATA_STREAM_2], assetModels: {} })).toEqual([ + expect(completeDataStreams({ dataStreams: [DATA_STREAM, DATA_STREAM_2], assetModels: {}, alarms: {} })).toEqual([ DATA_STREAM, DATA_STREAM_2, ]); }); -it('returns the provided data stream when no asset model has no matching properties', () => { +it('returns empty array when provided alarm stream but no alarms', () => { const assetId = 'asset-id'; - const propertyId = 'property-id'; + const propertyId = ALARM_STATE_PROPERTY_ID; - const dataStream: DataStream = { + const alarmStreamId = toId({ assetId, propertyId }); + + const alarmStream: DataStream = { ...DATA_STREAM, - name: undefined, - unit: undefined, - dataType: undefined, - id: toId({ assetId, propertyId }), + id: alarmStreamId, + }; + + const assetModels = { + [assetId]: { + ...ASSET_MODEL, + assetModelCompositeModels: ASSET_MODEL_WITH_ALARM.assetModelCompositeModels, + }, + }; + + const alarms = {}; + + expect(completeDataStreams({ dataStreams: [alarmStream], assetModels, alarms })).toBeEmpty(); +}); + +it('parses alarm stream and sets streamType to ALARM when corresponding alarm provided', () => { + const assetId = 'asset-id'; + const propertyId = ALARM_STATE_PROPERTY_ID; + + const alarmStreamId = toId({ assetId, propertyId }); + + const alarmStream: DataStream = { + ...STRING_INFO_1, + id: alarmStreamId, + data: [ + { + x: 1000, + y: JSON.stringify(ALARM_STATE_JSON_BLOB), + }, + ], + }; + + const assetModels = { + [assetId]: { + ...ASSET_MODEL, + assetModelCompositeModels: ASSET_MODEL_WITH_ALARM.assetModelCompositeModels, + }, + }; + + const alarms = { [alarmStreamId]: ALARM }; + + expect(completeDataStreams({ dataStreams: [alarmStream], assetModels, alarms })).toEqual([ + expect.objectContaining({ + streamType: 'ALARM', + data: [ + expect.objectContaining({ + y: 'Active', + }), + ], + }), + ]); +}); + +it('associates alarms stream with input property stream', () => { + const assetId = ALARM_ASSET_ID; + const propertyId = ALARM_STATE_PROPERTY_ID; + + const alarmStreamId = toId({ assetId, propertyId }); + + const inputPropertyStreamId = toId({ assetId, propertyId: INPUT_PROPERTY_ID }); + + const alarmStream: DataStream = { + ...STRING_INFO_1, + id: alarmStreamId, + data: [ + { + x: 1000, + y: JSON.stringify(ALARM_STATE_JSON_BLOB), + }, + ], + }; + + const inputPropertyStream: DataStream = { + ...DATA_STREAM, + id: inputPropertyStreamId, }; const assetModels = { [assetId]: { ...ASSET_MODEL, - assetModelProperties: [], + assetModelCompositeModels: ASSET_MODEL_WITH_ALARM.assetModelCompositeModels, + assetModelProperties: [ + { + id: INPUT_PROPERTY_ID, + name: 'input property', + dataType: 'INTEGER', + type: { + measurement: {}, + }, + unit: 'Celsius', + }, + ], }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([dataStream]); + const alarms = { [alarmStreamId]: ALARM }; + + expect(completeDataStreams({ dataStreams: [alarmStream, inputPropertyStream], assetModels, alarms })).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: inputPropertyStreamId, + associatedStreams: [ + expect.objectContaining({ + id: alarmStreamId, + type: 'ALARM', + }), + ], + }), + ]) + ); }); it('returns data stream with property name and unit from asset model property', () => { @@ -65,7 +171,7 @@ it('returns data stream with property name and unit from asset model property', }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ name: property.name, unit: property.unit, @@ -101,7 +207,7 @@ describe('parses data type correctly', () => { }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ dataType: 'NUMBER', }), @@ -124,7 +230,7 @@ describe('parses data type correctly', () => { }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ dataType: 'BOOLEAN', }), @@ -147,7 +253,7 @@ describe('parses data type correctly', () => { }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ dataType: 'STRING', }), @@ -170,7 +276,7 @@ describe('parses data type correctly', () => { }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ dataType: 'NUMBER', }), @@ -193,7 +299,7 @@ describe('parses data type correctly', () => { }, }; - expect(completeDataStreams({ dataStreams: [dataStream], assetModels })).toEqual([ + expect(completeDataStreams({ dataStreams: [dataStream], assetModels, alarms: {} })).toEqual([ expect.objectContaining({ dataType: 'NUMBER', }), diff --git a/packages/source-iotsitewise/src/completeDataStreams.ts b/packages/source-iotsitewise/src/completeDataStreams.ts index 8ee6ce2c7..ffa2d8105 100644 --- a/packages/source-iotsitewise/src/completeDataStreams.ts +++ b/packages/source-iotsitewise/src/completeDataStreams.ts @@ -1,46 +1,50 @@ -import { DescribeAssetModelResponse, PropertyDataType } from '@aws-sdk/client-iotsitewise'; -import { DataStream, DataType } from '@iot-app-kit/core'; +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import { DataStream } from '@iot-app-kit/core'; import { toSiteWiseAssetProperty } from './time-series-data/util/dataStreamId'; - -const toDataType = (propertyDataType: PropertyDataType | string | undefined): DataType => { - if (propertyDataType === 'STRING') { - return 'STRING'; - } - if (propertyDataType === 'BOOLEAN') { - return 'BOOLEAN'; - } - - return 'NUMBER'; -}; +import { Alarms } from './alarms/iotevents'; +import { isCompleteAlarmStream } from './alarms/iotevents/util/isCompleteAlarmStream'; +import { completePropertyStream } from './asset-modules/util/completePropertyStream'; +import { completeAlarmStream } from './alarms/iotevents/util/completeAlarmStream'; /** - * Get completed data streams by merging together the data streams with the asset models. + * Get completed data streams by merging together the data streams with the asset models and alarms. */ export const completeDataStreams = ({ dataStreams, assetModels, + alarms, }: { dataStreams: DataStream[]; assetModels: Record; -}): DataStream[] => - dataStreams.map((dataStream) => { - const { assetId, propertyId } = toSiteWiseAssetProperty(dataStream.id); - const assetModel = assetModels[assetId]; + alarms: Alarms; +}): DataStream[] => { + return dataStreams + .filter((dataStream) => { + const dataStreamId = dataStream.id; + const { assetId, propertyId } = toSiteWiseAssetProperty(dataStreamId); + const assetModel = assetModels[assetId]; - if (assetModel == null || assetModel.assetModelProperties == null) { - return dataStream; - } + if (!assetModel) { + return true; + } - const property = assetModel.assetModelProperties.find(({ id }) => id === propertyId); + return isCompleteAlarmStream({ propertyId, dataStreamId, assetModel, alarms }); + }) + .map((dataStream) => { + const { assetId, propertyId } = toSiteWiseAssetProperty(dataStream.id); + const assetModel = assetModels[assetId]; - if (property == null) { - return dataStream; - } + const propertyStream = completePropertyStream({ assetModel, dataStream, assetId, propertyId, alarms }); + const alarmPropertyStream = completeAlarmStream({ assetModel, propertyId, dataStream }); - return { - ...dataStream, - name: property.name, - unit: property.unit, - dataType: toDataType(property.dataType), - }; - }); + if (propertyStream) { + return propertyStream; + } + + if (alarmPropertyStream) { + return alarmPropertyStream; + } + + return dataStream; + }); +}; diff --git a/packages/source-iotsitewise/src/component-session.ts b/packages/source-iotsitewise/src/component-session.ts index 75d400465..fe10885c8 100644 --- a/packages/source-iotsitewise/src/component-session.ts +++ b/packages/source-iotsitewise/src/component-session.ts @@ -1,5 +1,6 @@ import { DataModuleSession, TimeSeriesDataModule, Session } from '@iot-app-kit/core'; import { SiteWiseAssetModule } from './asset-modules'; +import { SiteWiseAlarmModule } from './alarms/iotevents'; import { SiteWiseAssetDataStreamQuery } from './time-series-data/types'; /** @@ -13,20 +14,25 @@ export class SiteWiseComponentSession implements Session { public siteWiseAssetModule: SiteWiseAssetModule; + public siteWiseAlarmModule: SiteWiseAlarmModule; + private sessions: DataModuleSession[] = []; constructor({ componentId, siteWiseTimeSeriesModule, siteWiseAssetModule, + siteWiseAlarmModule, }: { componentId: string; siteWiseTimeSeriesModule: TimeSeriesDataModule; siteWiseAssetModule: SiteWiseAssetModule; + siteWiseAlarmModule: SiteWiseAlarmModule; }) { this.componentId = componentId; this.siteWiseTimeSeriesModule = siteWiseTimeSeriesModule; this.siteWiseAssetModule = siteWiseAssetModule; + this.siteWiseAlarmModule = siteWiseAlarmModule; } attachDataModuleSession(session: DataModuleSession): void { diff --git a/packages/source-iotsitewise/src/events-sdk.ts b/packages/source-iotsitewise/src/events-sdk.ts new file mode 100644 index 000000000..2ce9dc343 --- /dev/null +++ b/packages/source-iotsitewise/src/events-sdk.ts @@ -0,0 +1,36 @@ +import { IoTEventsClient } from '@aws-sdk/client-iot-events'; +import { Credentials, Provider } from '@aws-sdk/types'; +import { SiteWiseDataSourceInitInputs } from './initialize'; + +const DEFAULT_REGION = 'us-west-2'; + +const DEFAULT_PARTITION = 'com'; + +export const eventsSdk = ({ + credentials, + awsRegion, + awsPartition, +}: { + credentials: Credentials | Provider; + awsRegion?: string; + awsPartition?: string; +}) => + new IoTEventsClient({ + region: awsRegion || DEFAULT_REGION, + endpoint: `https://iotevents.${awsRegion || DEFAULT_REGION}.amazonaws.${awsPartition || DEFAULT_PARTITION}/`, + credentials, + }); + +export const getIotEventsClient = (input: SiteWiseDataSourceInitInputs): IoTEventsClient => { + const { iotEventsClient, awsCredentials, awsRegion } = input; + + if (iotEventsClient) { + return iotEventsClient; + } + + if (awsCredentials) { + return eventsSdk({ credentials: awsCredentials, awsRegion }); + } + + throw Error('IoTEventsClient not found or credentials missing'); +}; diff --git a/packages/source-iotsitewise/src/index.ts b/packages/source-iotsitewise/src/index.ts index 1e337eab8..bedf5fcf8 100644 --- a/packages/source-iotsitewise/src/index.ts +++ b/packages/source-iotsitewise/src/index.ts @@ -4,3 +4,4 @@ export { BranchReference } from './asset-modules/sitewise-asset-tree/types'; export { SiteWiseAssetTreeNode } from './asset-modules/sitewise-asset-tree/types'; export { HierarchyGroup } from './asset-modules'; export { toId } from './time-series-data/util/dataStreamId'; +export * from './alarms/iotevents'; diff --git a/packages/source-iotsitewise/src/initialize.ts b/packages/source-iotsitewise/src/initialize.ts index ad9ca1088..f0193214d 100644 --- a/packages/source-iotsitewise/src/initialize.ts +++ b/packages/source-iotsitewise/src/initialize.ts @@ -11,22 +11,22 @@ import { SiteWiseAssetTreeSession, } from './asset-modules'; import { SiteWiseComponentSession } from './component-session'; -import { sitewiseSdk } from './sitewise-sdk'; +import { getSiteWiseClient } from './sitewise-sdk'; +import { getIotEventsClient } from './events-sdk'; import { createSiteWiseAssetDataSource } from './asset-modules/asset-data-source'; import { createDataSource } from './time-series-data'; import { Credentials, Provider as AWSCredentialsProvider } from '@aws-sdk/types'; import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; +import { IoTEventsClient } from '@aws-sdk/client-iot-events'; import { assetSession } from './sessions'; +import { SiteWiseAlarmModule } from './alarms/iotevents'; -type SiteWiseDataSourceInitInputs = ( - | { - iotSiteWiseClient: IoTSiteWiseClient; - } - | { - awsCredentials: Credentials | AWSCredentialsProvider; - awsRegion: string; - } -) & { +export type SiteWiseDataSourceInitInputs = { + registerDataSources?: boolean; + iotSiteWiseClient?: IoTSiteWiseClient; + iotEventsClient?: IoTEventsClient; + awsCredentials?: Credentials | AWSCredentialsProvider; + awsRegion?: string; settings?: SiteWiseDataSourceSettings; }; @@ -45,19 +45,25 @@ export type SiteWiseQuery = { * @param awsRegion - Region for AWS based data sources to point towards, i.e. us-east-1 */ export const initialize = (input: SiteWiseDataSourceInitInputs) => { - const siteWiseSdk = - 'iotSiteWiseClient' in input ? input.iotSiteWiseClient : sitewiseSdk(input.awsCredentials, input.awsRegion); + const siteWiseClient = getSiteWiseClient(input); + const iotEventsClient = getIotEventsClient(input); - const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(siteWiseSdk); + const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(siteWiseClient); const siteWiseAssetModule = new SiteWiseAssetModule(assetDataSource); - const siteWiseTimeSeriesModule = new TimeSeriesDataModule(createDataSource(siteWiseSdk, input.settings)); + const siteWiseTimeSeriesModule = new TimeSeriesDataModule(createDataSource(siteWiseClient, input.settings)); + const siteWiseAlarmModule = new SiteWiseAlarmModule(iotEventsClient, siteWiseAssetModule); return { query: { timeSeriesData: (assetQuery: SiteWiseAssetQuery): TimeQuery => ({ build: (sessionId: string, params: TimeSeriesDataRequest) => new SiteWiseTimeSeriesDataProvider( - new SiteWiseComponentSession({ componentId: sessionId, siteWiseTimeSeriesModule, siteWiseAssetModule }), + new SiteWiseComponentSession({ + componentId: sessionId, + siteWiseTimeSeriesModule, + siteWiseAssetModule, + siteWiseAlarmModule, + }), { queries: [assetQuery], request: params, @@ -74,6 +80,7 @@ export const initialize = (input: SiteWiseDataSourceInitInputs) => { componentId: sessionId, siteWiseTimeSeriesModule, siteWiseAssetModule, + siteWiseAlarmModule, }); return new SiteWiseAssetTreeSession(assetSession(session), args); }, @@ -87,6 +94,7 @@ export const initialize = (input: SiteWiseDataSourceInitInputs) => { componentId: sessionId, siteWiseTimeSeriesModule, siteWiseAssetModule, + siteWiseAlarmModule, }); return new SiteWiseAssetTreeSession(assetSession(session), args); }, diff --git a/packages/source-iotsitewise/src/sessions.ts b/packages/source-iotsitewise/src/sessions.ts index 42ec28a54..fed7d4f29 100644 --- a/packages/source-iotsitewise/src/sessions.ts +++ b/packages/source-iotsitewise/src/sessions.ts @@ -2,6 +2,7 @@ import { TimeSeriesDataModule } from '@iot-app-kit/core'; import { SiteWiseComponentSession } from './component-session'; import { SiteWiseAssetSession } from './asset-modules'; import { SiteWiseAssetDataStreamQuery } from './time-series-data/types'; +import { SiteWiseAlarmModule } from './alarms/iotevents'; export const timeSeriesDataSession = ( session: SiteWiseComponentSession @@ -14,3 +15,7 @@ export const assetSession = (session: SiteWiseComponentSession): SiteWiseAssetSe session.attachDataModuleSession(assetSession); return assetSession; }; + +export const alarmsSession = (session: SiteWiseComponentSession): SiteWiseAlarmModule => { + return session.siteWiseAlarmModule; +}; diff --git a/packages/source-iotsitewise/src/sitewise-sdk.ts b/packages/source-iotsitewise/src/sitewise-sdk.ts index 227a7456d..2fa61ca8c 100644 --- a/packages/source-iotsitewise/src/sitewise-sdk.ts +++ b/packages/source-iotsitewise/src/sitewise-sdk.ts @@ -1,11 +1,36 @@ import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; import { Credentials, Provider } from '@aws-sdk/types'; +import { SiteWiseDataSourceInitInputs } from './initialize'; const DEFAULT_REGION = 'us-west-2'; -export const sitewiseSdk = (credentials: Credentials | Provider, awsRegion?: string) => +const DEFAULT_PARTITION = 'com'; + +export const sitewiseSdk = ({ + credentials, + awsRegion, + awsPartition, +}: { + credentials: Credentials | Provider; + awsRegion?: string; + awsPartition?: string; +}) => new IoTSiteWiseClient({ region: awsRegion || DEFAULT_REGION, - endpoint: `https://iotsitewise.${awsRegion || DEFAULT_REGION}.amazonaws.com/`, + endpoint: `https://iotsitewise.${awsRegion || DEFAULT_REGION}.amazonaws.${awsPartition || DEFAULT_PARTITION}/`, credentials, }); + +export const getSiteWiseClient = (input: SiteWiseDataSourceInitInputs): IoTSiteWiseClient => { + const { iotSiteWiseClient, awsCredentials, awsRegion } = input; + + if (iotSiteWiseClient) { + return iotSiteWiseClient; + } + + if (awsCredentials) { + return sitewiseSdk({ credentials: awsCredentials, awsRegion }); + } + + throw Error('IoTSiteWiseClient not found or credentials missing'); +}; diff --git a/packages/source-iotsitewise/src/time-series-data/provider.spec.ts b/packages/source-iotsitewise/src/time-series-data/provider.spec.ts index 20d36e22d..0c1afd889 100644 --- a/packages/source-iotsitewise/src/time-series-data/provider.spec.ts +++ b/packages/source-iotsitewise/src/time-series-data/provider.spec.ts @@ -12,7 +12,9 @@ import { DESCRIBE_ASSET_RESPONSE } from '../__mocks__/asset'; import { SiteWiseComponentSession } from '../component-session'; import { SiteWiseDataStreamQuery } from './types'; import { createMockSiteWiseSDK } from '../__mocks__/iotsitewiseSDK'; +import { createMockIoTEventsSDK } from '../__mocks__/ioteventsSDK'; import { SiteWiseAssetModule } from '../asset-modules'; +import { SiteWiseAlarmModule } from '../alarms/iotevents'; const createMockSource = (dataStreams: DataStream[]): DataSource => ({ initiateRequest: jest.fn(({ onSuccess }: { onSuccess: OnSuccessCallback }) => @@ -32,10 +34,13 @@ const assetModule = new SiteWiseAssetModule( ) ); +const siteWiseAlarmModule = new SiteWiseAlarmModule(createMockIoTEventsSDK(), assetModule); + const componentSession = new SiteWiseComponentSession({ componentId: 'componentId', siteWiseAssetModule: assetModule, siteWiseTimeSeriesModule: timeSeriesModule, + siteWiseAlarmModule, }); beforeAll(() => { diff --git a/packages/source-iotsitewise/src/time-series-data/provider.ts b/packages/source-iotsitewise/src/time-series-data/provider.ts index 7af608815..e54ce1c83 100644 --- a/packages/source-iotsitewise/src/time-series-data/provider.ts +++ b/packages/source-iotsitewise/src/time-series-data/provider.ts @@ -9,7 +9,7 @@ import { subscribeToTimeSeriesData } from './subscribeToTimeSeriesData'; import { SiteWiseDataStreamQuery } from './types'; import { MinimalViewPortConfig } from '@synchro-charts/core'; import { SiteWiseComponentSession } from '../component-session'; -import { timeSeriesDataSession, assetSession } from '../sessions'; +import { timeSeriesDataSession, assetSession, alarmsSession } from '../sessions'; /** * Provider for SiteWise time series data @@ -29,10 +29,11 @@ export class SiteWiseTimeSeriesDataProvider implements Provider) { const { session } = this; - const { update, unsubscribe } = subscribeToTimeSeriesData(timeSeriesDataSession(session), assetSession(session))( - this.input, - (timeSeriesData: TimeSeriesData) => observer.next([timeSeriesData]) - ); + const { update, unsubscribe } = subscribeToTimeSeriesData( + timeSeriesDataSession(session), + assetSession(session), + alarmsSession(session) + )(this.input, (timeSeriesData: TimeSeriesData) => observer.next([timeSeriesData])); this.update = update; diff --git a/packages/source-iotsitewise/src/time-series-data/store.spec.ts b/packages/source-iotsitewise/src/time-series-data/store.spec.ts new file mode 100644 index 000000000..5e2396d32 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/store.spec.ts @@ -0,0 +1,22 @@ +// import { CreateTimeSeriesDataStore } from './store'; + +describe('TimeSeriesDataStore', () => { + it('can append time series data', () => { + // TODO: set state and then get it, callback also gets called + // we actually append the annotations to existing state + + // const initialState = { + // dataStreams: [], + // annotations: {}, + // assetModels: {}, + // alarms: {}, + // errors: {}, + // }; + // + // const callback = () => {}; + // + // const store = new CreateTimeSeriesDataStore({ initialState, callback }); + + expect(true).toBeTruthy(); + }); +}); diff --git a/packages/source-iotsitewise/src/time-series-data/store.ts b/packages/source-iotsitewise/src/time-series-data/store.ts new file mode 100644 index 000000000..13754c3e9 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/store.ts @@ -0,0 +1,61 @@ +import { DataStream, ErrorDetails, TimeSeriesData } from '@iot-app-kit/core'; +import { Annotations, MinimalViewPortConfig } from '@synchro-charts/core'; +import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; +import mergeWith from 'lodash.mergewith'; +import merge from 'lodash.merge'; +import { Alarms } from '../alarms/iotevents'; +import { completeDataStreams } from '../completeDataStreams'; + +export type TimeSeriesDataStore = { + dataStreams: DataStream[]; + viewport: MinimalViewPortConfig; + annotations: Annotations; + assetModels: Record; + alarms: Alarms; + errors: Record; +}; + +export class CreateTimeSeriesDataStore { + private readonly state: TimeSeriesDataStore; + private readonly callback: (data: TimeSeriesData) => void; + + constructor({ + initialState, + callback, + }: { + initialState: Partial; + callback: (data: TimeSeriesData) => void; + }) { + this.callback = callback; + this.state = initialState as TimeSeriesDataStore; + } + + update() { + const { annotations, viewport, alarms, assetModels, dataStreams } = this.state; + + this.callback({ + dataStreams: completeDataStreams({ + dataStreams, + assetModels, + alarms, + }), + viewport, + annotations, + }); + } + + appendTimeSeriesData(updatedState: Partial): void { + const { annotations, ...rest } = updatedState; + merge(this.state, rest); + mergeWith(this.state, { annotations }, (objValue, srcValue) => { + if (Array.isArray(objValue)) { + return objValue.concat(srcValue); + } + }); + this.update(); + } + + getState() { + return this.state; + } +} diff --git a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts index d5bb65e53..47aa52246 100644 --- a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts +++ b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts @@ -3,24 +3,48 @@ import { createSiteWiseAssetDataSource } from '../asset-modules/asset-data-sourc import { createMockSiteWiseSDK } from '../__mocks__/iotsitewiseSDK'; import { TimeSeriesDataModule } from '@iot-app-kit/core'; import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; +import { IoTEventsClient } from '@aws-sdk/client-iot-events'; import flushPromises from 'flush-promises'; import { createDataSource } from './data-source'; import { createAssetModelResponse, createAssetResponse } from '../__mocks__/asset'; import { toId } from './util/dataStreamId'; import { BATCH_ASSET_PROPERTY_VALUE_HISTORY } from '../__mocks__/assetPropertyValue'; import { SiteWiseAssetDataSource, SiteWiseAssetModule } from '../asset-modules'; +import { createMockIoTEventsSDK } from '../__mocks__/ioteventsSDK'; +import { SiteWiseAlarmModule } from '../alarms/iotevents'; +import { + ALARM_ASSET_ID, + ALARM_MODEL, + ALARM_STATE_PROPERTY_ID, + ASSET_MODEL_WITH_ALARM, + ALARM_SOURCE_PROPERTY_VALUE, + ALARM_STATE_PROPERTY_VALUE, + THRESHOLD_PROPERTY_VALUE, + TIME_SERIES_DATA_WITH_ALARMS, + ALARM_PROPERTY_VALUE_HISTORY, + ALARM_ASSET_MODEL_ID, + ALARM_SOURCE_PROPERTY_ID, + THRESHOLD_PROPERTY_ID, +} from '../__mocks__/alarm'; -const initializeSubscribeToTimeSeriesData = (client: IoTSiteWiseClient) => { - const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(client); +const initializeSubscribeToTimeSeriesData = ({ + ioTSiteWiseClient, + ioTEventsClient, +}: { + ioTSiteWiseClient: IoTSiteWiseClient; + ioTEventsClient?: IoTEventsClient; +}) => { + const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(ioTSiteWiseClient); const siteWiseAssetModule = new SiteWiseAssetModule(assetDataSource); const siteWiseAssetModuleSession = siteWiseAssetModule.startSession(); - const dataModule = new TimeSeriesDataModule(createDataSource(client)); + const dataModule = new TimeSeriesDataModule(createDataSource(ioTSiteWiseClient)); + const siteWiseAlarmModule = new SiteWiseAlarmModule(ioTEventsClient || createMockIoTEventsSDK(), siteWiseAssetModule); - return subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession); + return subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession, siteWiseAlarmModule); }; it('does not emit any data streams when empty query is subscribed to', async () => { - const subscribe = initializeSubscribeToTimeSeriesData(createMockSiteWiseSDK()); + const subscribe = initializeSubscribeToTimeSeriesData({ ioTSiteWiseClient: createMockSiteWiseSDK() }); const cb = jest.fn(); const { unsubscribe } = subscribe({ queries: [], request: { viewport: { duration: '5m' } } }, cb); @@ -35,6 +59,7 @@ it('unsubscribes', () => { const siteWiseAssetModule = new SiteWiseAssetModule(assetDataSource); const siteWiseAssetModuleSession = siteWiseAssetModule.startSession(); const dataModule = new TimeSeriesDataModule(createDataSource(createMockSiteWiseSDK())); + const siteWiseAlarmModule = new SiteWiseAlarmModule(createMockIoTEventsSDK(), siteWiseAssetModule); const unsubscribeSpy = jest.fn(); jest.spyOn(dataModule, 'subscribeToDataStreams').mockImplementation(() => ({ @@ -42,7 +67,7 @@ it('unsubscribes', () => { update: async () => {}, })); - const subscribe = subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession); + const subscribe = subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession, siteWiseAlarmModule); const { unsubscribe } = subscribe({ queries: [], request: { viewport: { duration: '5m' } } }, () => {}); unsubscribe(); @@ -71,13 +96,13 @@ it('provides time series data from iotsitewise', async () => { ) ); - const subscribe = initializeSubscribeToTimeSeriesData( - createMockSiteWiseSDK({ + const subscribe = initializeSubscribeToTimeSeriesData({ + ioTSiteWiseClient: createMockSiteWiseSDK({ describeAsset, describeAssetModel, batchGetAssetPropertyValueHistory, - }) - ); + }), + }); const cb = jest.fn(); const { unsubscribe } = subscribe( @@ -163,13 +188,13 @@ it('provides timeseries data from iotsitewise when subscription is updated', asy ) ); - const subscribe = initializeSubscribeToTimeSeriesData( - createMockSiteWiseSDK({ + const subscribe = initializeSubscribeToTimeSeriesData({ + ioTSiteWiseClient: createMockSiteWiseSDK({ describeAsset, describeAssetModel, batchGetAssetPropertyValueHistory, - }) - ); + }), + }); const cb = jest.fn(); const { update, unsubscribe } = subscribe( @@ -238,3 +263,215 @@ it('provides timeseries data from iotsitewise when subscription is updated', asy unsubscribe(); }); + +it('provides alarm data from iot-events', async () => { + const getAlarmModel = jest.fn().mockResolvedValue(ALARM_MODEL); + const describeAsset = jest.fn().mockResolvedValue({ + id: ALARM_ASSET_ID, + assetModelId: ASSET_MODEL_WITH_ALARM.assetModelId, + }); + const describeAssetModel = jest.fn().mockResolvedValue(ASSET_MODEL_WITH_ALARM); + const getAssetPropertyValue = jest + .fn() + .mockResolvedValueOnce({ + propertyValue: ALARM_SOURCE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: ALARM_STATE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: THRESHOLD_PROPERTY_VALUE, + }); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(ALARM_PROPERTY_VALUE_HISTORY); + + const subscribe = initializeSubscribeToTimeSeriesData({ + ioTEventsClient: createMockIoTEventsSDK({ + getAlarmModel, + }), + ioTSiteWiseClient: createMockSiteWiseSDK({ + describeAsset, + describeAssetModel, + getAssetPropertyValue, + batchGetAssetPropertyValueHistory, + }), + }); + + const cb = jest.fn(); + const { unsubscribe } = subscribe( + { + queries: [ + { + assets: [ + { + assetId: ALARM_ASSET_ID, + properties: [{ propertyId: ALARM_STATE_PROPERTY_ID }], + }, + ], + }, + ], + request: { viewport: { duration: '5m' }, settings: { fetchFromStartToEnd: true } }, + }, + cb + ); + + await flushPromises(); + + // fetches the asset summary + expect(describeAsset).toBeCalledTimes(1); + expect(describeAsset).toBeCalledWith(expect.objectContaining({ assetId: ALARM_ASSET_ID })); + + // fetches the asset model + expect(describeAssetModel).toBeCalledTimes(1); + expect(describeAssetModel).toBeCalledWith(expect.objectContaining({ assetModelId: ALARM_ASSET_MODEL_ID })); + + // fetches alarm source, state and threshold property value + expect(getAssetPropertyValue).toBeCalledTimes(3); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_SOURCE_PROPERTY_ID, + }) + ); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_STATE_PROPERTY_ID, + }) + ); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: THRESHOLD_PROPERTY_ID, + }) + ); + + // fetches alarm state historical value + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_STATE_PROPERTY_ID, + }), + ]), + }) + ); + + // provides the time series data + expect(cb).toHaveBeenLastCalledWith(TIME_SERIES_DATA_WITH_ALARMS); + + unsubscribe(); +}); + +it('provides alarm data from iot-events when subscription is updated', async () => { + const getAlarmModel = jest.fn().mockResolvedValue(ALARM_MODEL); + const describeAsset = jest.fn().mockResolvedValue({ + id: ALARM_ASSET_ID, + assetModelId: ASSET_MODEL_WITH_ALARM.assetModelId, + }); + const describeAssetModel = jest.fn().mockResolvedValue(ASSET_MODEL_WITH_ALARM); + const getAssetPropertyValue = jest + .fn() + .mockResolvedValueOnce({ + propertyValue: ALARM_SOURCE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: ALARM_STATE_PROPERTY_VALUE, + }) + .mockResolvedValueOnce({ + propertyValue: THRESHOLD_PROPERTY_VALUE, + }); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(ALARM_PROPERTY_VALUE_HISTORY); + + const subscribe = initializeSubscribeToTimeSeriesData({ + ioTEventsClient: createMockIoTEventsSDK({ + getAlarmModel, + }), + ioTSiteWiseClient: createMockSiteWiseSDK({ + describeAsset, + describeAssetModel, + getAssetPropertyValue, + batchGetAssetPropertyValueHistory, + }), + }); + + const cb = jest.fn(); + const { update, unsubscribe } = subscribe( + { + queries: [], + request: { viewport: { duration: '5m' }, settings: { fetchFromStartToEnd: true } }, + }, + cb + ); + + await flushPromises(); + + update({ + queries: [ + { + assets: [ + { + assetId: ALARM_ASSET_ID, + properties: [{ propertyId: ALARM_STATE_PROPERTY_ID }], + }, + ], + }, + ], + }); + + await flushPromises(); + + // fetches the asset summary + expect(describeAsset).toBeCalledTimes(1); + expect(describeAsset).toBeCalledWith(expect.objectContaining({ assetId: ALARM_ASSET_ID })); + + // fetches the asset model + expect(describeAssetModel).toBeCalledTimes(1); + expect(describeAssetModel).toBeCalledWith(expect.objectContaining({ assetModelId: ALARM_ASSET_MODEL_ID })); + + // fetches alarm source, state and threshold property value + expect(getAssetPropertyValue).toBeCalledTimes(3); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_SOURCE_PROPERTY_ID, + }) + ); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_STATE_PROPERTY_ID, + }) + ); + expect(getAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: THRESHOLD_PROPERTY_ID, + }) + ); + + // fetches alarm state historical value + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ALARM_ASSET_ID, + propertyId: ALARM_STATE_PROPERTY_ID, + }), + ]), + }) + ); + + // provides the time series data + expect(cb).toHaveBeenLastCalledWith({ + ...TIME_SERIES_DATA_WITH_ALARMS, + annotations: { + ...TIME_SERIES_DATA_WITH_ALARMS.annotations, + y: [...(TIME_SERIES_DATA_WITH_ALARMS.annotations.y || []), ...(TIME_SERIES_DATA_WITH_ALARMS.annotations.y || [])], + }, + }); + + unsubscribe(); +}); diff --git a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.ts b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.ts index 90150ea4a..f567f96ca 100644 --- a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.ts +++ b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.ts @@ -1,68 +1,61 @@ -import { DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; -import { completeDataStreams } from '../completeDataStreams'; +import { TimeSeriesDataModule, DataModuleSubscription, TimeSeriesData, SubscriptionUpdate } from '@iot-app-kit/core'; import { SiteWiseDataStreamQuery } from './types'; -import { MinimalViewPortConfig } from '@synchro-charts/core'; -import { - ErrorDetails, - TimeSeriesDataModule, - DataModuleSubscription, - DataStream, - TimeSeriesData, - SubscriptionUpdate, -} from '@iot-app-kit/core'; import { SiteWiseAssetSession } from '../asset-modules'; +import { SiteWiseAlarmModule } from '../alarms/iotevents'; +import { fetchAssetModelsFromQuery } from '../asset-modules/util/fetchAssetModelsFromQuery'; +import { fetchAlarmsFromQuery } from '../alarms/iotevents/util/fetchAlarmsFromQuery'; +import { CreateTimeSeriesDataStore } from './store'; + +const initialState = { + dataStreams: [], + annotations: {}, + assetModels: {}, + alarms: {}, + errors: {}, +}; export const subscribeToTimeSeriesData = - (dataModule: TimeSeriesDataModule, assetModuleSession: SiteWiseAssetSession) => + ( + dataModule: TimeSeriesDataModule, + assetModuleSession: SiteWiseAssetSession, + alarmModule: SiteWiseAlarmModule + ) => ({ queries, request }: DataModuleSubscription, callback: (data: TimeSeriesData) => void) => { - let dataStreams: DataStream[] = []; + const store = new CreateTimeSeriesDataStore({ initialState, callback }); - let viewport: MinimalViewPortConfig; + const { update, unsubscribe } = dataModule.subscribeToDataStreams({ queries, request }, (data) => { + store.appendTimeSeriesData({ + dataStreams: data.dataStreams, + viewport: data.viewport, + }); + }); - const assetModels: Record = {}; + const updateAssetModels = (queries: SiteWiseDataStreamQuery[]) => { + (async () => { + for await (const response of fetchAssetModelsFromQuery({ queries, assetModuleSession })) { + const assetModels = 'assetModels' in response && response.assetModels; + const errors = 'errors' in response && response.errors; - const errors: Record = {}; + if (assetModels) { + store.appendTimeSeriesData({ assetModels }); + } - const emit = () => { - callback({ - dataStreams: completeDataStreams({ dataStreams, assetModels }), - viewport, - }); + if (errors) { + store.appendTimeSeriesData({ errors }); + } + } + })(); }; + updateAssetModels(queries); - const { update, unsubscribe } = dataModule.subscribeToDataStreams({ queries, request }, (data) => { - dataStreams = data.dataStreams; - viewport = data.viewport; - emit(); - }); - - const fetchResources = ({ queries }: { queries?: SiteWiseDataStreamQuery[] }) => { - if (queries) { - queries.forEach((query) => { - query.assets.forEach((asset) => { - assetModuleSession - .fetchAssetSummary({ assetId: asset.assetId }) - .then((assetSummary) => { - if (assetSummary && assetSummary.assetModelId != null) { - return assetModuleSession.fetchAssetModel({ assetModelId: assetSummary.assetModelId }); - } - }) - .then((assetModelResponse) => { - if (assetModelResponse) { - assetModels[asset.assetId] = assetModelResponse; - emit(); - } - }) - .catch((err: ErrorDetails) => { - // TODO: Currently these are not used anywhere. Do something with these errors. - errors[asset.assetId] = err; - // emit(); - }); - }); - }); - } + const updateAlarms = (queries: SiteWiseDataStreamQuery[]) => { + (async () => { + for await (const { alarms, annotations } of fetchAlarmsFromQuery({ queries, alarmModule })) { + store.appendTimeSeriesData({ alarms, annotations }); + } + })(); }; - fetchResources({ queries }); + updateAlarms(queries); return { unsubscribe: () => { @@ -70,7 +63,12 @@ export const subscribeToTimeSeriesData = }, update: (subscriptionUpdate: SubscriptionUpdate) => { update(subscriptionUpdate); - fetchResources(subscriptionUpdate); + const { queries } = subscriptionUpdate; + + if (queries) { + updateAssetModels(queries); + updateAlarms(queries); + } }, }; }; diff --git a/packages/source-iotsitewise/src/time-series-data/types.ts b/packages/source-iotsitewise/src/time-series-data/types.ts index f74de6c20..7cd9d512a 100644 --- a/packages/source-iotsitewise/src/time-series-data/types.ts +++ b/packages/source-iotsitewise/src/time-series-data/types.ts @@ -1,9 +1,12 @@ import { CacheSettings, DataStreamQuery, RefId } from '@iot-app-kit/core'; +import { SOURCE as IoTEventsSource } from '../alarms/iotevents'; /** * Learn more about AWS IoT SiteWise assets at https://docs.aws.amazon.com/iot-sitewise/latest/userguide/industrial-asset-models.html */ +export type AlarmSource = typeof IoTEventsSource; + export type AssetPropertyId = string; export type AssetId = string; @@ -13,6 +16,7 @@ export type PropertyQuery = { refId?: RefId; resolution?: string; cacheSettings?: CacheSettings; + alarms?: boolean; }; export type AssetQuery = { diff --git a/packages/source-iotsitewise/src/time-series-data/util/toDataType.spec.ts b/packages/source-iotsitewise/src/time-series-data/util/toDataType.spec.ts new file mode 100644 index 000000000..396b6b7b7 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/util/toDataType.spec.ts @@ -0,0 +1,3 @@ +it('todo', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/source-iotsitewise/src/time-series-data/util/toDataType.ts b/packages/source-iotsitewise/src/time-series-data/util/toDataType.ts new file mode 100644 index 000000000..c5feaacad --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/util/toDataType.ts @@ -0,0 +1,13 @@ +import { PropertyDataType } from '@aws-sdk/client-iotsitewise'; +import { DataType } from '@iot-app-kit/core'; + +export const toDataType = (propertyDataType: PropertyDataType | string | undefined): DataType => { + if (propertyDataType === 'STRING') { + return 'STRING'; + } + if (propertyDataType === 'BOOLEAN') { + return 'BOOLEAN'; + } + + return 'NUMBER'; +};