From 86286962947cf106b9a21f7da4b641f66281de47 Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sun, 23 Sep 2018 14:32:21 +0800 Subject: [PATCH] Initial commit --- .gitignore | 2 + .npmignore | 10 + .travis.yml | 23 ++ Gruntfile.js | 130 +++++++ README.md | 92 +++++ admin/geofency.png | Bin 0 -> 72728 bytes admin/index.html | 90 +++++ admin/words.js | 8 + appveyor.yml | 25 ++ geofency.js | 305 ++++++++++++++++ io-package.json | 92 +++++ lib/utils.js | 83 +++++ package.json | 39 +++ tasks/jscs.js | 17 + tasks/jscsRules.js | 36 ++ tasks/jshint.js | 17 + test/lib/setup.js | 728 +++++++++++++++++++++++++++++++++++++++ test/testAdapter.js | 140 ++++++++ test/testPackageFiles.js | 91 +++++ 19 files changed, 1928 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 Gruntfile.js create mode 100644 README.md create mode 100644 admin/geofency.png create mode 100644 admin/index.html create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 geofency.js create mode 100644 io-package.json create mode 100644 lib/utils.js create mode 100644 package.json create mode 100644 tasks/jscs.js create mode 100644 tasks/jscsRules.js create mode 100644 tasks/jshint.js create mode 100644 test/lib/setup.js create mode 100644 test/testAdapter.js create mode 100644 test/testPackageFiles.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0f2b01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/.idea diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a2a50dd --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +Gruntfile.js +tasks +node_modules +.idea +.gitignore +.git +.DS_Store +test +.travis.yml +appveyor.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a5145d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..12d25d0 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,130 @@ +// To use this file in WebStorm, right click on the file name in the Project Panel (normally left) and select "Open Grunt Console" + +/** @namespace __dirname */ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +"use strict"; + +module.exports = function (grunt) { + + var srcDir = __dirname + '/'; + var dstDir = srcDir + '.build/'; + var pkg = grunt.file.readJSON('package.json'); + var iopackage = grunt.file.readJSON('io-package.json'); + var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; + + // Project configuration. + grunt.initConfig({ + pkg: pkg, + replace: { + core: { + options: { + patterns: [ + { + match: /var version = *'[\.0-9]*';/g, + replacement: "var version = '" + version + "';" + }, + { + match: /"version"\: *"[\.0-9]*",/g, + replacement: '"version": "' + version + '",' + } + ] + }, + files: [ + { + expand: true, + flatten: true, + src: [ + srcDir + 'controller.js', + srcDir + 'package.json', + srcDir + 'io-package.json' + ], + dest: srcDir + } + ] + } + }, + // Javascript code styler + jscs: require(__dirname + '/tasks/jscs.js'), + // Lint + jshint: require(__dirname + '/tasks/jshint.js'), + http: { + get_hjscs: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jscs.js' + }, + dest: 'tasks/jscs.js' + }, + get_jshint: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jshint.js' + }, + dest: 'tasks/jshint.js' + }, + get_gruntfile: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.build/master/adapters/Gruntfile.js' + }, + dest: 'Gruntfile.js' + }, + get_utilsfile: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.build/master/adapters/utils.js' + }, + dest: 'lib/utils.js' + }, + get_jscsRules: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jscsRules.js' + }, + dest: 'tasks/jscsRules.js' + } + } + }); + + grunt.registerTask('updateReadme', function () { + var readme = grunt.file.read('README.md'); + var pos = readme.indexOf('## Changelog\n'); + if (pos != -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) == -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ("0" + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ("0" + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ""; + if (iopackage.common.whatsNew) { + for (var i = 0; i < iopackage.common.whatsNew.length; i++) { + if (typeof iopackage.common.whatsNew[i] == 'string') { + news += '* ' + iopackage.common.whatsNew[i] + '\n'; + } else { + news += '* ' + iopackage.common.whatsNew[i].en + '\n'; + } + } + } + + grunt.file.write('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + }); + + grunt.loadNpmTasks('grunt-replace'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-jscs'); + grunt.loadNpmTasks('grunt-http'); + + grunt.registerTask('default', [ + 'http', + 'replace', + 'updateReadme', + 'jshint', + 'jscs' + ]); + grunt.registerTask('p', [ + 'replace', + 'updateReadme' + ]); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d6c4ea --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +![Logo](admin/geofency.png) +# yunkong2.geofency +==================== + +[![NPM version](http://img.shields.io/npm/v/yunkong2.geofency.svg)](https://www.npmjs.com/package/yunkong2.geofency) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.geofency.svg)](https://www.npmjs.com/package/yunkong2.geofency) + +[![NPM](https://nodei.co/npm/yunkong2.geofency.png?downloads=true)](https://nodei.co/npm/yunkong2.geofency/) + + +This Adapter is able to receive [geofency](http://www.geofency.com/) events when entering or leaving a defined area with your mobile device. +All values of the geofency-webhook of the request are stored under the name of the location in yunkong2. + +## configuration on mobile device: +* for any location -> properties -> webhook settings: + * URL for entry & exit: <your yunkong2 Domain>:<configured port>/<any locationname> + * Post Format: JSON-encoded: enabled + * authentication: set user / password from yunkong2.geofency config + +## in yunkong2 Forum (German) +http://forum.yunkong2.net/viewtopic.php?f=20&t=2076 + +## security note: +It is not recommended to expose this adapter to the public internet. +Some kind of WAF/proxy/entry Server should be put before yunkong2. (e.g. nginx is nice and easy to configure). + +## Changelog +### 0.3.2 (2018-03-07) +* (Apollon77) Fix Authentication + +### 0.3.0 (2017-10-04) +* (Apollon77) BREAKING!!! Make sure 'entry' is really a boolean as defined in object + +### 0.2.0 (2017-06-09) +* (Apollon77) Add missing authentication check +* (Apollon77) Add option to send in data as Message when received over other ways +* (Apollon77) Add option not to start a webserver for cases where data are received using messages + +### 0.1.5 (2016-09-19) +* (soef) support of certificates + +### 0.1.4 (2016-03-29) +* (dschaedl) replaced geofency Icon (on request of bluefox) + +### 0.1.3 (2016-03-29) +* (soef) fixed atHome and atHomeCount state creation + +### 0.1.2 (2016-02-13) +* (soef) Dots in location name will be replaced by an underscore + +### 0.1.1 (2016-02-01) +* (Pmant) Fix config page + +### 0.1.0 (2016-01-26) +* (soef) Fix error with "at home" settings + +### 0.0.4 (2016-01-24) +* (soef) Added some new states + +### 0.0.3 (2016-01-21) +* (soef) Some modifications +* (bluefox) change type + +### 0.0.2 +* (dschaedl) moved to yunkong2/yunkong2.geofency + +### 0.0.1 +* (dschaedl) initial release + +## License + +The MIT License (MIT) + +Copyright (c) 2015 dschaedl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/admin/geofency.png b/admin/geofency.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf838ec74def1dcf12f79724b1b9a1ccded87ab GIT binary patch literal 72728 zcmV*GKxw~;P)aWDSuIdjgvd$qgLu69?efV7GTApw%HNP0GpNASm5K$zPkpPjgtpG+~i6reJ$w*p-m3H^ud(QOlkLl|ER&~v}S3Lh_KX=behwAF; z?r&FB_w z>mGkYm(v&AKDQQ+jSx$X+=LK1YV;0$V{}X~4S*rA$}}Zj1~xN@GI$y%4Cp)mdcmrZ zI#8z(-t)jsymyp``B7Jlk;5Cxlldi?bezY4P$%(gfMAxF?f4{1DU0RLX>y&-H3aS$ zNY8DM%OpM;t~=%L3}EECb6G=R#dVp}FKoKZuLl@3X>eTU$P`RV`W~lCVYytUYuZ-w z&vA~>r7=76oax9tgP`y-Fu*$lCX!ZlWTC@gB|J*!9x8|Akvp%`zzsD2 z^4FPksxuq;R}jwqr19L~BcRs0N1NO#zaTsRBdO28tI)=Bz1aP)BJ? z)wif^R6CfwtjVzk*EspA*<66k&hA#*8?}9&{h__?hR%U{}SOZE0yZea6i@G=A?5ZzjAl<5?IL*g%6r4kIln$EVp)v+Ewf_E>7UD!g% z_uThLs}M6*$(-`LngR76kyT^cf!l+4uw`*wUGp5InXxR*L|f%S>1U#)SxwjW6~!~j zqH2%yTWQZJy=o608!Q`cFP4J^c22`8tW{%~XAYB&?+O-rkPg}7l^IECEB_3Nfb)O~ z#gQyLDG-EdWbULUT~#ML`%b?T^0JmR-8>8*S$m@vSwoD>bJV^&7>9eftVvjdr<#6Uh`j7*HYU~>xGf!68pvkwfWwE@7S5LFD zZUL@mj$@mX2TL|rDBZk5So5-j*9uS=Py>PU8k+eE+SzZ@*%eM1tUU87t-=70owXHD zR-=H{D7_BQQCXZ%T_$a3I;c&TwLz|<#4|8U9Z~#5N>hv4FXweGmZN1S`?w^X0kq2b zZl1Y5m6m5F80OLSB|3vj$x7nwuLV#eD?4GRNt(;>LydL`0UT zwYz!Xr6xs7-%k3bY-O2nU1;nnnx3p|xX6cO#bYk>5MxB>x=#6lU1}zoPMG84v30U?Hs$g}A)O%W8Gj3_M_rMf)0>p{$B*O;S#u%d~)qTF!j&78C1p zuqJu-ZOKecpqukXZB8ol#nh+LNTpoX91Jg6V-CjGxduQgt%J&~Oif!Kq35LI@~pY7 z{v5F4dbz7sPJ7t0RDB&;t^1O`Y(b9xtTw28S~TWVy_|B?_;BKftQ*S{mou+wu4YPg zsx;>ig7RUpcE!F4Yk$kV7cPbJ-j_<%hwh|oB3ez)mM8YU;(MUO^5C|OvDT7s7kX3D z`r{P9n@*xSk(3b})0%&Yw@f#Xxh4Ei8U|XGo_Mvg6FLfn%!E+2j{$=Wx;y|0z?6w> zCqmY4y!Nm>^B^<%>;}~04m z3&^U`b|AW)$7(7hC}nP%i)U`Ts$hkqI_Hp}PzIR|=Z2~}8O%(@11P1jjcZq6JXvN9 z$keCm*gSX*?;sjg=OYbL`4gB2Gb1a2u-b*A$$31pkEQCkMt1HbX9u9y9#kh+OR!~2 zy`>*G@fi?=f373nGiWfVbK6JlGqub2xs6PF2tdu%Bw3c8MoaAlDtoAZai3QUG?I=c zpw7E@#+#jhlzYVRn`L1XoGv)Pp#)|M%W1h&*n1FYQK24fYZeUp2dE4$?_Ap)MJucJ zTrmwAn5qMb&;$T17>n9%H9vD-fv}?usTCSaKZUFPY?8-lWVY_|tb8_(jB|d6R^`v( zx^bB_(OE4-nTfUxk2bYO=cK7*CA{$D;PgXl^0RiU%x3#c{JLX;WWoJS$p*U3+;K!# zH9if^!FHj3Ihb}p!&T**{#JqaJQ@S_UyC2NAH^6OTZq-9It)~V3pOaoCXF?{)SeRx z9~72XF{zLlG}e^OvpRQF!@mMQ5>Q8Bwxi~Hkbv#1mOCqq88|Ql=8GF_0ElvVG&3p_ zkMnY!fm(x=v?v1>fbvW6v&XX?;5>s?b)nhW&fH3SkUlg}CYzmhuq!i}+O#o!rZNG! zoFU+%_F!#a)jvJ6DQUq^wHcR5a$v1nX~WzP8JrzpQCbw;DeM{0G`RYReQFDE)g)i^OXuqG9(WavEjw+Hp(esjWj!JJ>pAT7feXQHzQefIJg! zD9oyh>oNb7V42=PqpEx~sChpbn|*4)Gk*pkEXeZq18WNMAoI3kSUG#7 z9unqtydgPpS)hT)W$?@`b-Qiv-jlxj-S3`u*IoNCo6Y8s9==AS5jJhwh$D_T;^2Lb zIp!lM1F5s+C!Q29UR#i?*4zfeai%RR^8;GTcFi|W_CxZ9X?t5gTTjD7RZq_UgI=~@ znMupr-k^9h;8L}vt@|3h*vYt#C!u)Y;uWR_XS!Ta8k#W%n4bq^Yw-loOf;t+8DJO~ zY}rmn=1$9Cn)hm%x)0WmCjm>F6EhFY+nNpPhgZHG1Ac5;SP(;uv3=9|7rf|;U;5J7 zS6+G5kKA(Wt(Z=yP8HX_+P1~vhaZlUPC9Y_k3IIWuX^lb9{s_ik2>mdIGx}2ouL6i z_f@~M;9%-rSpsIDDlGk%>~J!d_!`JF*c#=I#(d( z*P1?_P+;;`wB`wB4h=@Qw;?RD4nY5If+NS*H9S|U+?A`qiIV%y<| zN02eI+6`{Xk@Ko@7}$~{PNSDas8LS{I5U=XvCr0 zgbZ9L(6#0j3tEYp*@t!g{X-sd_MU%y`MJM%{H|S}0`!X4oVpC%yQZE41g<}bCm*b{ z?;6cxEzQcEtgZHK)3Z#Rc+Wani~pf<>S}_CH+P%KdduQTz-e38tmO9KLTq7bc9yVM zRyFu{5acXErnELaOqgBfcGY_6_>@P&n}K}!nbiNBpE@b?U^gr>ZLUj4e*mf-*bS9o zFT3%Qw@s)zavhAO<(Wpj13!nGa+r_I|9jr|{uf?x<&|hg4Mw98Ms17Hc#O6kqa80` zJYK+fGD&|6Yy3^h?|6*yc#L*DMmuWJj7Dfi4ZiZ#uO9O+AN{B@;$==)#hYk2o`%7KABwY# zI~2ra{v*2!WQ+=c)NI|Ca_vhptP%vl+T>hS_w6uIq5tS!eD)_a%S#3->+cl=sWP zXSvMdo8r9fAfD6Om8rcr3J70~Ce^FEOC}A&ypIJCF5+rtb}oS1W3#~Oq^>f z24{JjW(uyq^?4{iiY*s4P+JYKGFLsYA{nb$nbLE)(q@(4>kD7{(kpNN{`Yr|+A-R; z#c0&_8dlq)X#}{^9;}|YZ4!0yOdGonUDqX`nqoGaqG_g>u1@it?|gUZd*8cp>At6& zB0MQylaZCJPnL_BU{xNHPiNBS36k5Nz|re>w*P?gxxgjQrf&$3p2NM$$8F3L(2#6I z!!g>KcW^-^4<1_{;27CuSxdO-v$`Wcs`5$jWrgc?Sff30J@d2Iv7kEL$x5<2)OnT5t8duXa$pMcw0Sk$@jsa^n!*q2O)9EUv z%_JnzZroLLf2B|J@wm$ zb7vR?D5tWX=Hjl1aHTjscidE=p~_}{jBq^AQ0YTdd)ot@=b^%+kqr^!r}L2}H`YiJURo}pp2 z{c7NHy=+}=TUc6N*=v;HfscB#VL+o{xmQ{iYfb}vu;t;pSmuG3t)GAw0JJ;^48u)u z&=*GIfFA7jt?)b$3EY?tm(v&_SO9F8o>a^TfYne1HWu*U1C-}au$_}T0S_eGD3uS2 z(_7h*oUMi=GmX_oMJBdhAeyY@vaG#kDcSVr7Ook>x~QMC$a$Cym= z4#={M6)r?E2~Fov2xkzQ8Rd>|2ck{S6_?8$v?(2L@Er7^3C&2R0`YHWCk9Sy$}07l zwIItk-CK<`cX-3cJQJ;|qtmIBW{A>B^S#K92c&5a<(u*9$aF&pXvbqjG--8j5o0n5 zLvp&W52^T)7i~LqL?;F$GqKCtCxkz)htZ;xmJ7gwfdY4{ZSZW)X*iC`Rc&Ep%rq?s zbT*vZ1G>+$Ru7<>>@ppr(Rtc^Mc34uL29tinY5jeO9iM_=5Q1S!WITA&+n|-K;qg| zn1oeMM`pqfwxDh5%8_Sh6LZdc zyOXxT;kHj#zFf+uK+BPv>KB~eQ(BzzD!(lCSsq)84>KMsMZu0CigR5}ts|&RAQi$Z zYTU82Rss!9*3@E6jLpSL4NUjmDxy;7&8!OMVcstb-vF6+97~l(@;gAdt};yHtJ&Dj z!lpeuZMV|AIC5Zyf|tg2w5Dm$wqu}aFdk2e!@V6Lgr=gi?kic3F~kIV$-@;RX6+QS z@iebKqBfK4K;<}FcRPW(TB^B}jJ(Rn^!4@`xw^_5oTcz|b>322|HnrQeU5UutD zS}?U>VR`3p24|9z$ zu2l+eJX)O2((3t1n?*ciY9kv{M?D}4@5EPU z_*AbHaUHd$7^?6g8A^JVC(LCuehW@&0wx~q=1!|Admv}qerVvk<`>ncqHQ!QYY|j{ znE1D=s2-@Pdt2vJcBW~0^JL;pjIyJeGqNgfmt}eB(gO%~e%g+tv?5_rgh6R+!!}$F zbu=5^Bw<_n!W+k_jz@WgZdS*QQ0;<4mLmzcvEft?8<%&Y%giNOfj8507-o z3^6S0fg|PT^i2Y)ra==5SQYRAl8uMF5-7o3l%&glP;^3QN;%1yZ)a9X%Ni5656Pu6 zjVMm7O0hh+T?mvcFxc_HS9Jg(mF;QXiZ1bxH5$`id5oBRN*hUZY<)PrEl>G=g_c(j z+a5Fl42}gFFBO2v&kAac~oB~D%Y&t5}^A}o+!D=;?tAX1d zW-_AsvpHw|OW-o1cH3CrsgjN4L+!H2pyTx^0{Y4n9V(OMVu00Zhb1j(Q?SI10NHVA zRZEqJ;=S{tfEv)M@)C`qM#zn4X^bHcpp<7W z1LQB{c!zey-|ctY@$lPj+q-q&zPqq*-#+ZyclXv$UH+N#Z@&3v+;YpUxb61aap2%G z#*+ms9&!j4H!Ssgu*SU)O8~6Lq~e3(k|LsxOaH~RZy}%nK#UQy=@irHDpn32#PY!d zICx+`y6F^$ZQg{fTeskd9XoLH$tQi|foGiYdxspdgw2}|#f}}@Z`^kH;a6AyGjifK ztCrtAS=zJSCcbzAFtYi+OZl>!n@=*177tf2I_%4RiCu^;rOc7xTFknbl zna&&;aGm|$tFzfwmYFnkToiY%imue$2DzWL2>ZNKrRn{e&5*FE|A8?Hb0`?uVJ zTW`G;ci(*v0)S>TLfeipSy;eiaRU}NY`})49$BMu)74e1EHC$fwf`O**uNjk2M=I%Wf`;8DWZp&Ic2x^9Fn!ldRLWl_`8lnb1eD3;^*P%vUcZv~feF-_ zYI4mR)Gi;k-?!<%4kM|YPh|Zyl}u#>R{=lu%Ql6bwVlcqPJfB;VZAYa_Rc( zZ@}(7d(OW7_S+ZlzWW}muCBuBM7=|Ogm%sCtGXQ`JQ1}{{(Off%c&ySH-l{U@fSjv^k(`uVO&Qf`1Ax1t z(sW%n*}He|1Ma-@F5G$Nor~vRc+sU-TzMs~zu^X~EHAf_31GEo+ZN-= z0u~k*v9P!Si%W-KVWHnJ+G|*&ggzpRIB>sKT&_e9SltZM=@hHW%UE7Mh~)$OaZog@ z)fG&qtC&q^h_OrBT$>bv8v+J6 zjS7R3KxN1C)eYraW7ZlfBdeGcj}-6vaBQ6z%%Gi@)gGMzt;W@iB)yd7m;iIlOq4J!EJq_X%V|kUs#j;s+o!a^BS# zlgR|*g~bF~OPDMyU_7QhSkkl#ixMCSF0sPS>5$UK9w@Vh)ph7*Q>-j6V|8U22M->= z@__?bSw4tW(Xi5@fPh?#0$MSHSP9d`AA0DaIQit0@l%ig$v?QyF-ISK@Pi-xiS66A zUBi8W+X?jqG1m$Z4FuZk_uK>SwMJi6JUA2XTm=DU{LZlkszoxY*PuJZtHT_Pv*Op` z&vX7OT#_S_D{aIw;|(xRgQ7}$FHL49oU1c=HL_~JaCa(`@T>Q@Y+4lXbXPr41E{AB zdSziCU*+ImM}rDq%~e-l{lZUQ{+Sc5xZ;Y_uKVV_vsjt0D++6p^z0^Pe4N8M4-|0ntqDTw;B(Eq zUY}o2S`GsYV6irUVRh2lq@J_P9<1iAM0hr`)y#&>zj?sJl|-tfs~I_TEA!9Mq$+bx z%5eDg%HstDmnS<*9XN1s+ue8XTl(sMeC^S%e$8wD@|v$-v-7Tf``VS2l>`Jug9->` zhj<8S-;{)v1NRVuI?JjLFMuVl;q)goETEkv-_9rnwpi1lcu@$+UtI9_ntkt^pl_b?GJzWBWLgV-kx4IC;$c4 zm=MYdpmG{$)ATP9r_Gp6({BXLAjKO(M@>HCN)G93+p$_1w7c9*O|qFc?}}=LP_Ro* z9_>M>*QQpnn$F3ZO{ZA{>ou${pQjbO>wDms001BWNklK`WF>#z2{ zvJ4`vR1b{Nb#3pFF|?KzXDy9^__=i&O2Z#gmCMzHDBKE2X;^YeoHqlR3ahuzp&Q@w zwH|@av(>8(GB3`iaWuyvt!z-kW1wZZeEl2W`00;-;?n>1k&k}#dEflzw{XWDcNArW z%s2)o-X+dpHKL=8Wfiah8Kn&knntv)CcoU=pDvI|3vIBLO-|6T1$xZbqk?)&Lh1pj7~dh<4Os*RJDm z)>&tL`+q+2M?dtKM?d=Ihi=~dZKq$%^}`NKj&u7gYOAFx!vRfL+8(dz*WDgObeNWU zN7(W%e0gPhWz4%gaChpa0Lv2a2D@agd^S>1Z-6=jJ)`pwPz;qHwR6*@3Eg4bRX>{s zKHzy;nQANtu$0r4q?|VBA9$ZFhz#60o zZ1OvP`MGFAa*T0FCt6kktW2A%;lS|Je!KA3NmahpwH8Q%sss=u;(B0Og-k=Q!61t zuyH~I#gy|79n1NQ_GATIsj*&ODf5OKcAxX*fBUy5zU}RAf8H0q@I|anSIdjT8JNWX z6bkq>qXJ0uVTjQv0TzSRh=7Mciv3w3^!~2?9>wen{q_J!0~C`Ku)g&5KIz@0I)=qIY6b*_54nyy>L@(!agW85e(s4+JmG{BuGze4 z(=}F`b}r99URLW@~yq>YC|F%5b9GU6(0k2aUGp=I-^RP=z5#% zxIWztyLUhG^Pm61!!Lf@+yDBitFLbM@84elDnOZeqUnrAMOzxREdf=VPCDqdDDgvS zbC3L4sT?*hdmnlNCgHnRo07jtUOF!Rr@qn{^jXbYH1t*tGk`^Mj%NUaTJ3W|O~8wM zT1DfUWe}SYXw7ER0%Dx5>ktLNviVr}*|lpI9`l$-f90`{e)QYVKKqB?ykT+iW^ce^ zz>jLf(=~77sCP>0x>nO*v>T6)0ZU@Yzve0zDC4o3m=jNc!4W%VNdwFEV_q2p)VqWR z)fi1HyP5^SX&lCL#S9(QfCiM<1Z3J)gRuhjoPi$i)}Lm<)4Mz{_CT$QTV7t?eEx+O zUHst>fB4j|{>Rr&xbv>N5p=pbDWlh5jdfCraAML_` z)XTTr^Hr@1uInNve8!`-5v~WtVkYM39Kd_7ke%kd)uCu+J?M1FS>MgN9;7-0TGL(& zn@x3GF-fEHeIZ7)?HETMxfAC+;t`+ym0$j)&z||92fkpAFoyhBaKhIcKSX!P+8MmO z$$G6a?&Y=CF8-l12Fq5fgeLKjuO|Zv4fK4?QB{XDH(M!d8}nts2J2~{Ia=ae&1yiy zT9=S#v)STpx9#0^=_fDy)7QWLjX(a4|NPIxR;R0wbIU^r1X3e}(WnP2`ZA4tVyg$J zR&6mDjXZx-F(XAg%0LAG2AB$_tOJQn#VK@WVun@6M3bf6aV2LHW{TQkgR zMsw)TqcPXi9ilN$TU;+6^Js*w>s-?ffL+P6ujZ(O@HxNLxKjGc=H_0%v(i!pZ(P;6O`JkA z?Q~!BiV;!jWF7X^?BAEceYyC;kUTbz03 zgTDEb|NDPm{tHif(w}eMylIaEL~efqs+GjKtIq@Xo^Q;Z=;r{;pd%e-Qg+6ba)p>W z4OwQ@wh%`tP+`1E=z@ZAp0RJKTCJF44tju>)1*AAVU0#Nw7<%21k>1d#U%@jeOfWr z%c`a=2VXUs&1Q>t-L-G)-@W9eAOFOqmu|c9rkl3OD^CTe1fZJoCFN1SNwWv6Ub~WA z=NZ&^j}`l+lCzxUZSVPPD$^dGleB-n*SJFYf-bX)8#XfxQMz~X&8bpQL(iey zSJODNV~jX(;K1e^cJF@Dl~-N;o44F@D?$Js_OOS({FIYVe#gROGM!8&cf*X0HOjEQ z=Xt-%VejOt_~m+78jjZFx|~ctrq%xn+DpO2oV$sQy3&xESFPkZY1ZQ)tbAJo0?hfz z#2cDfDiAZ^oMkX4jaq29?Y7&Fy5v3YeZzULf5T(HbN%(j^rK^cN29VCb2MuE50bVm z@~7a%S2bd1I<54D^6HHOD*B&>XFp&KSV5(dc*!4~FbDARGpD!lCI7rU_Sa8OJ~mYg z&J!uka2YDM_`Z_a*hSumnoPwh`?vZNBc{_;OjlQx_Eok|l+CG#5gV44@QY7*^4osy z2~W82{`WuaW24b%>S;|N4IMrB;I`XtJLNyGz4nB^{kxZZ@GD>W3icm3pd_$m%N9KT zCx7Car#|J$zjw+hr(CgNaq;`sgc=yvos==4>44Hf=ok&j^Eayr1bPg4T4653Dm)?| z%db3JlWPRuOS4@m9Z+OR18o~bPJM$XYUXqS6^ANbWB~XR_FpE39Z^2bJ-_9 z^@s1iH+WGt?F6TqyPw~A97l; zYGu_d?To72T`nCimyO1lLgfFU@`f%8a+;fHDA)BWZC*4l@_EVj2st*I9r6jV$~%s$ zi(T{%Y^aJT6j@%Cv-(`Ve>((uJ{%`%(JD&ZlXa2lrVW9S9 zS>S+4JI|8x+&r$8VkdZ@wxS%s1f^5sRnk_cH8{vO&*5HaT#V$Vf>s+EG9CGHwYBJM zsfgtBGB)tNx-1woov!PePks8+PkZr8&wb@rzxvg7I-4~i1ZDa)NtQ3wJ#4xxem%o+ElFh)0<~E9&!kN{dv!Q|1+Qdv}bPIxbarh+7SR(31O2? zt(>nXpDn}C@ZK6xni(*xAPu=;Mt|`af6?UT?Wu+kLWnVj&~wLtk)ry!mg13UbRp!| zTP2M!rqsQZH>V9nm5^{j-F^b?%kBWgP}@WnteV7U^_=*~adb73fYLNlpX+b9;gKKw z;D>(d_h0bGul)9RzB7t3hO#9=PVAhdzXgmJ7R+z@g7SEhoa<}`jRJ{DTGu?41|@nH%#&gl@cl*qXK*t1cs8bv_ZucN{zMA zhpHY%hZAigflE45dLNj)k1Ze;OBT0%p;M5F+JcIw4(P zJ`x?yFKJ`Ck+K4B9G$!Rg5nRVP*w`%baX#-RgQXN!>*-9+HghN&JZ@g0W&rSa~?@^ z&DXzv&Wm3Bk~dv``Dac#aPS~xb7ppwC(UU*o@5_YJD%htTJl~hdd{^2eBfdRSu8e~P&%?<^+g{B(-x zY?hD3$r{({DyFL|>O3!g&#?&sM;~)Ep7e7+ciB^)^5kFpi@*Boi$DC4kDj%%vQm7X zO@kn-#N;CAdFk|PMTzc~Er;Sc&wS?lf9p4Yx#s_MZLw zw_bG7TQ7ax>tBD&_ins#Ti10;E6OW;?HFSLsj)cLCv$+RDKk5;PLSzS{`X$5%MbpxUOP5y|m7hz!uUNJ`h*N&LvYgKKnqn#^r_$Dm7$X)J7qN5a5jWp> z<4rqfvsqqzDgICDHw0K}F+Z`lM0IPB=OKyv_oQ^8)YGz)#L{o9-q#G>H01VSpJ~RtHM6=oe zXMhzU1nl0k=hT0G#VgN$%f)Xydv$d+AIl;jB+m44JIOw(v6y?whb4Qj?2dF#2Bq>y zZWUwJkt`&COy1_e@9shJXQOMtK~Lsd(l%6iy)vH4PV}sIT{Mu)PppQ#8^@mYTt1s` zF*(o+KiOQheTY2uA}4m%=VGKt<+dWZN}WCbXUZr_ddTp8I$h~~XwxZX-3*&IZp3q*^{h*t z{`6n{_05|$$qU-NeFE~#W*KO5XV8jfq0AdE)HkiK3ouUm(haI3pSbHN-&f9K6JdQ? z4$SkABVMfp7}{%TS?e(|txDTh$_GSuwZp2eF~;W7%Pu?j%@*w}xp=k}1Kn(&7&uo&>u}h_p$`>>sfSiv|-TKGY@`b62k=a*iPACAHHL_@q zV2&|1m%QhFZ}_Xf`I}$)ukU{!F~;1PMtMs@0ju#ucdYW>UvkDKXKvQ2@_$iWD5)O? zly!Mg3t0Q1y+@=bBvQSWWi6)cWVU;;V$LVNBB7RL6M?|5)Ch=Vr?mKX*w<&u&kk=4 zr)4wGmcg242)WD}#fxg0r6R($=o4An6nUd8NMznvU6s0Ip^+9BRGpd-(B(rE8_Z;1 zT?!kmjxcRTXr`;_B#FK)`VWEhKG5+(@B0)NgB%%D-X7?;TTI(2n$^_;W~!dR?RVUP zSHJePKfQFw(uL1?)-#`I-5&7Ay#g(3&3I#zfJu!v>*ppw^88&e7^7AjIsMRwRPQyK zCZa>TS?xwnt1=?iFC2TqVBuAuW=z_S>4>m~%*{LQxMRmZzw%YDe8U^x^o0Ef4wSQ~ z(hA^cJjS>kCy<&bpOpYw-?>88sxqhq)0+mJ%GOr{R6TKS9(zSY#>=~oGF;WFzmEhC z^`ZeMjm50=d{TpDNDzaFi_WeD97YDhN}L8RslTMJ%aJ}SN8GS~tKT8Nl1#VNy!~O8 z4iO9?HC}^nD%!(256-qala6)OT=$C?y;8UD@%-G-8Z)JB0GFx55C-0QVTCXAV>L ztye*|MT1^e?M&F<+v<+ZoROQ+nJUUcv(Cj#Cxp;k^YyPk=Z$YZ{}Jza=R2RUd~jK9 z=xoO=+IE6=(u0&z*%MePr+T2y6_P%aJA6QWR+OrxNEK?ZAn0fbXJ3#v;Pd!qt%G<` z15#@?%HbpVEU$rS(+rulL`o;PWU5!$wYi=}19I(G^-O?gf2;aQ89|r9Gy;iT)jJfK zWeaLzWj_{-NmwSqURw{N1`W9rVvGn)m;IsrSO|)}>}x~7 zfrAI}hBv?Y$471Z%4a|G>CawTTH4EFfv&6=@NX`Yd|l-KtS3;?m${ zvrPtZKvY?(F&&7K@Kf7v1t3&9%Tk?B@9i>LNE+-GpVa~aNaM;NN?KH)r2$pEsi=`d zxrwO0=o2UphT;OrBq;gl?=rI1h};0ZSsl=XE>B?j#Ex{Fb-xon4caCor~8;**P_?7 zc(BKa7()iu-t5;ns;s6MamO8Z;I-$S_p?VGb>!sdpZJ8I0`ho~v1K(b>w^Z2wnV_9 zwn!L^@RSAiRb^V1HsuHNrB(^eZ3}$dOP=XM8eGt?+wxm&vayn1 z$_@E{ZSvIb%#}DB>zY#QptHU*yltlrod}Fq@5VHY5ZettIJ#v>?T5$tl=_TdlSZ_}ntdn(57{iXcHeO>F-{sC|(b7L=dEI?@!CV?Kn?z4Kk~e!(C8$)CRP+H0?! zgwT|23X|UPIayf1!ongJ78WsCT*PEyq5qV7_F2iRH3h6v#d|r>U(Z*W2PLe;_Z=~( ziZKs5$(w6N&t+~+vwz={bJ|>Pw!j71ktw%{;>R-gcbo=`Z{ZtRh7cx(C>H(5VOuVRatt@nWQhd@MIo)Z4 zv1sl6AHTXaqP=?kB0-?CSmDhd!A%Q7<4I3*vao>3VtUIWZ(ttbdpF*=@xr%We4dtF zz8_kA^Tr!ck6k;q0kc)XF*nsOxpkP18uKm4Y2FK>tg5k{33A36b%V=`pY7edH_vLk;ZquVJQ-s=UdWwEKK7u_N=H#7 zqWE%PEDbi(vNe++N6ZKom@!iny}&dy-JDl2DkO~{n_HTexWFpjv<(OLWkt!C%OkyL zwt{mW@o;?YYu8{DI?R&kqIuKIOOUe8BJzvYqPgXvYLuJ6E2abW5DO}o$@PvYSz@HG zU#MtEoAryi!D)|Z3QXw}k)rVfu?dK=DHBNuxj)JF7w#k3Bvl$Ka=VU34S*2|fV#VY zc8r+KX1MCAs{pNtNZHm$ESIZA$(Uw;aEe#sn0?`nxmjTgviu8XkZY)GxM0VY6a;M~ zo}K3)goQ?80VB_@ZAd_zj-7t|avXYqR2{?M89sZwG}L#MQW z^p%jzKl#cLeetM~cLawv7Z>I-uo2CXoVzw2qn%88YanZI{i0Db8e#YDJ?DJu+uwc` z^dg(GfQ(fG`eiVYd=|{C--*KjxGzQ9G^$L=2LP?(kKznAt}}q)`LzJJTkGx0gw3Ot zG+AIKT^5|sCXn;gN&sk9S63&mJ@54|czZLk#&{Iaw(Tp}!YE3#@?zqT~2R`nZt&%?x@yU9&Sj@e{IxS~QU|uoV!} zAP~QP4AChADMv=Dq+Lz%Bj-E}3kwT4Y|A#xx*2Z2Yd4~p%%rKw?)%J@(ng{JgkIxd zO`j!{Avtp;d##G{qWKP4114Tret~ zs!{!_*6Q+QX|yI#F1xDcnST~gY?u_tYGbv600KcLod`swGrZ)DaM3?vrin4)jcZEouyBkKbUON zMj70eMy6}R$rKe8gwy3;&YK5R0jZL-3~>ef;4+Gy)^#1W9l8fU#_SY{ZVC*=!h(2Rf)MtLGBpFiuJ|KrI5CgX{co-xOi z>&3H<=(776+5cQ^QL}iR6{Ttu(`!D_bm;dR?%4#}XZ=B7*$z%bO$znbRrj>0Aqx1oY&@U~Q!2|jY z<~N0l4W|Br`YnEVCoLo$(~Xp`Aa}f3@4v> zJa+EfnPq?EF~?!Uc$7~!$R=A%gHP2%v`{W1lz3H-Ncp0P%UEjuq8}%6R%OT1MkLF~ zZ7lVWpIBJQ69&xy(e@ATm&r1(0Mcnrh_QfKu7b!4={Lw}xAH-gv~RHO-*eruefz)d z+I8GTJw3J8O6|Kc;fxEceOdQRXYyJ4%CwZSoCP_Uw3%*eoY$$(6wP#pp8$96t_run$TXEx!Hv@<`WaB0rbL{ch`?aqjj>eGrv3FQUlqyH1AZ0fe z$x+LS!i`KMpW-q~9bzRbD+)>P$<``;T((UMh|Jf~Muv%Ce)GN4mqO6@nN%o__%^n= zotgkV=z$No^^m2dZyQCS`uIyvrrOF>xrPRu@1F8d`L?7C0WC*4v0I(;(K6Od^K8xR zBxjWkMYFORmeQQaVO|wIeWjikDUSUuVjlDkZb?G}Gl587W<-vq z={Qo_TqJ){St*LK0I<|Sg0o55uQJ74b_$E-rQaP}55;Mx-OtRB5htE}Dw;U$Kk@(> zyovYF^Ob#BvcM2x&b9Cu001BWNklVpAgA6k1 zs+{V4kpQGXTfeVLr8i@m6^4eNO&OI1Cz!&@5R8u;X-s=&983mI<2lYfCM>Af&7zzx zuTU_<7H$m2;hZ4+=l@$cLx^ zyb4t=e)gQL=0T=&z@Q`rXQzx1Fg;khczTxANy(FH2ibX4{^ueEI0AuW0qFi)e;tlJ z=1Ao{Zav;J=2IfyhxHj6TCXE z+eszYwa-b4RZ6RMFHldu3IvS1eYtMT63r@Z7 zDX)ND=&}b`HsbeH?AEJzgYF|%X3S0jxhmIDO2a0k>n0Ml&a*@9FJRO;hiSXlsx#@V z4~(@U`#O!SysdyMu+`Po#q%$?@XucQvUC6Zz`^B4oa@O$=ewZvM z={?qlkZG;x;wY;#l*am)hI*d#ciuP@hz7W0N_k9&%${JE--BLdVysL2lR7kA|MSw(C?peDD`d)z7&loH2V54a32(`a3s*9&iAjF?WRlefO@ZGZZr7r*o`_uhW{qHJ3jFZ8Q` zlf^|$7K<4;9@9I4X+tOjg*cntv6G&2&oVPoiqSVhlYU*BfyyrBP#)G5$o$B|KpfmT zpT^Bn2DP^-BaekhyTwyNak$4g!=W2D;2{tB;apCsZ$xMs9DSeTu(Y_CKA+WB*4Bgh z=u?Cu1H+r@TPy>vmzab`Ryt$9I%&($Y7N(!*@bz%VCFI>x@@-<=#^`1eh2L#CObeshx5ej#J+rOKt&=F)5du zPit`eNlLXNXIict-vjj*W>N3WsI)ADz*RW&Wz@vV@&B<;eB$?CcJ9mn=+3+D%rBG5 zksWyx-FQMf*ZY^3Yv%+HAObBju~BOMOOMe>#$Y={g3I>!S}P1OR%Lq=vX@`gJ?GUE zRo8XEY=*NQbOttW-mLSZ4AM=T55Dc+^@j$kk$K0+oC(hD^ zRaMfnjl-O0o6iTdxzBn;(``)O+v_kwhyQWTL;XsnYs1n;9I^9AG$G{g^!3`~TICPL z#acdm&v9$zi+&wd7(3mf$u|@~FU?wXy|iobUZ^O$K_XSZ^Kdj&)Lu{_(fgBA$W@f`V_muU|9 z(zpW6Y&KiG{Ij2X(F^|MPha@WZ+>$zpWHZ^B!AT+78W;<&uW}L&BYr+VVn~&gOYkn zzAx0UEzbcpxCxzPewYDPR?ZAy24fe5%%B539@r*Zlk!zO^InW>)+W9psbf@cWzu&u zoO04}IPt{ed;d<*^d(&gz>!BEi$gaaq5vy0Z7E-YE8UAIc!$}3ng_9x{&;_L_i&W>$|f1X$M zd3A8e;?X{^>YOVOEs(aP%3ykMHk>lhahxr~hKV|+K*_3NsLemvilSZt^!9gIElusS zaA-<60CjRhPIK$6x9$4JfBL6CyzbiT%2`&EaXxN#k{s(}rCI4?VdP&KwdI*XrFSKi zxS$4s1|ADSr3@X$1Gl(Q>RC;a1@82SAg?V^($tLeB+*SJ1-TE|{}J*aie+FJKT<%% zgC1}i4n1^JHQDPcVC&)Aarl-kDX(_E7w!Z7#ln+FBx+fE<*XSJG?T_?&I7*6CSV$L z`$qV$lmwY5GJt3-N}^X-)l_Pu>2!+MU+~s*Kl{ZmKMSOE z7~hoK+k-kej-T_pzmAWKc#Ix4SnwlRY3vAPlQ~SfT#+u>wUyZ?Y9ITLB#$r?wR67V zUz1kS2x?X*5)TsatKOfLA%?z?=-gyoC}H(*Q#P zoh_RW!TnA>2@4Agj)s*alLkmT8RMAac436Bch*xXy`i)4sDWAd<#aYr`IAi=KKwAv z1dhT>!DZ7*mYS~_%YLyQZ(>#k126qa-9n^3XlhUGQ6@EEx^CMFGLzZ9@9uqg-5W1> z@xNYi)pMl29>5yscJ@QDoWyNpwI9@5odACUEj#0hmcj@EG*(I!?#M<^yQwqp#Sj%S zJ@bY!Yw~4!q&_wbFk+8Vnj8m>uvw$?(#06Ng9i_8{ilC^#rxj&_IEtKi!pz-W|Frr zEM)&x{#?>XZwM_qv`IEFvhx1`hzDW7VrxSEY%t}x6HdW~@u)WovuQZYSFGwi%&+|mZl2+=pvsD=eWKze z7`fz?$hopFVX-Phmlso+stm=;P+x#bA7exp%V)>Fd&Bh`Uwz&i&i&8pu6wfMr;_oO z$DNEZ3tB}J%+B@7?IX&~04P@D*qD8sTwwz9s?0E>%BdF{83DOq0Vim{5rz=-PS}i` zhItrTmT@o;E-x=1_3lgFbKZGxc+=yTS60)f;>Sv>8c!w|%R7PT7-OsNL~3M!`#kw; zB+Nd`KNY5F4)onB3@SX<<4|b2yf7b@XTz(io1rUZgLq*1$YAJZ7&pLa_d5~Ww;!It zia|@HS@?`t+^~eB?{h3dH%06^fZi|6<;KDrB8NfOQNlrS7^sKMXXU_UL=zWFy==RI zpP|zM(6qBWsQsYJE@ecg3c)Ltl;XauU>r5dq42M_V3@H zmvim-Ba2EY4xo$hf%j|i`FQFKcJVy{u>LK!!kCZ7=1b4QLgNAyj2cG3)0ptif|X|@ zGf;83eI{B;AY2{*eCIpgJ?*?Vyzysmz3tZCfj;g}U1-x;K;udJpy^oqtd!1=NN$s& zarK=c=(I8BG{v9^C7q;6!P1Fy8In9xh!hs9yyZnZ36D8#%DxXhKh`W2jEM_OqKx+9XC@r2zYY zaz;*2!Tl&MCT=e`u0P2n6#t#biw_Vibx88$SEF)!kOW2K%uwVs0kLeVVo6k>+B@I1 zsU9Oeq#}57=wieyc9_mO%%)Q;FR$R@cf8|i7rf=IFNa=KtCRzF)rfm0g~P!I*Jo|D zHvvauk+E{3+&+>|emP%5KB^|B4PIpWW$lXo!01P2pHx(BI%DRYE7PRPb6BPM?ccwD z>kD7>cdxzfx^L#~3uE?M(N}HcOP`LJAz zh(g9gk!4imOCHthsyVdy6$3)XY#=Im7us4wD=Jr~zDj-1J@??X=e^;VKJ<}~zBX>Yj#jLIYo?Ay2Rv=_bjrT_Yg%Pur=jsDQ6b1$?dOthhhZ`vD=ep~qFXQ$nSqYlPSw9ki>lp=)n=3><(d*dR;1ksQYimcr!kPZ#wwikKS>&%%?W-wyvPsu<2wjKMPdgb~w;l$2aep=#tpIJy*6rAG=wZFn zy^HDFe6gyhuA>tVQxA$$*h%eE$;#%h?pY>MkyPK*Z;Bj~+;jU8&^W}SjW=a@@i{CS z8UmW6s|NHh@|VwtHE6;J-K@ihKKzkKUUbo0-#(qqCP22NDqpq%DXYk+o$hNbs8Jea z{u!4Me-Xa=jzo;HX&6PtZe^y(WAA<1%WgJfv#y~^dSGSkS$m(aJMOsSguj3JKYZYd zE3P=EX`1A>nqV?6hw$;Iy_=>{P3Kz1N=`w~%5&3lSeTP$ATFBN82te?$bg%`_}0qF z-l>RzI>efvoS}54zGHTni~|10BOZ)}h5m?L6>(`%u=XnkKED zr@!$yeZ{=R!Gp_qA|Hz=D}wpN zh4hyvKbXJZPv$X%`Dv5(5|iJ-Ph;*mZ7^v9Dfahiw$1U8^OYUyk_SWe4ahs) zV+@+73-uA}a=3yYI+f3P195Vswi_K3PZs($L1 z+CRK%xMJR1kcq;Ttn>6K% zJZ<@UdH>RXZ;2e(e*nMp`+xAk-Fx=@NS{Y(Kk8o0H`2VUgI;ZC0IEX^T)IMw)7Z<# zvy)psJD;6i%_Y z87Z)mEBltRsZ?Liq*|qjJ+my%l0FY`ln)+I5<5p%j(j^JQNToJirMj6+cWYU%Dwu4 z1J|#l%SSnxbiz*8bqMHi-;<8R5l3t6`Y6_V7bR+jbN#(bt&xFs4!4>EHHmzTF)aM4At{qRRV`eQ&SPIg%l%*Prx z7|A{>$XJimzA=x{L0?DR=6Qg*I0ok)I*sSd4JHlai;G1={H^!T;j^wwbn=x4`GCQZ zJGSEVGfve?rDS3a3TA!7?=#0*mr>KW%qQ+WnbB`_X>~rTYf{Q41%6vmKl{+6zGTv2R$XP`#czB2Q}^xLm$xvC+i^bUsvYshP;#wDKs!TAc=g^W-J4fTQwInoZfI~? zdZq3PAY=3ra#=de`h6S&pdvSPDLSgG2;b@3hsi2l6Fc1Rz9(SE_H8%l zMedfkVXxe?ji~cEm<7ag0!h=zm!{K4Sm@-E{y=~cuDa@Kyzl+*f3A#Q@>{7Rl%T1dQxP8<47rgSi zZ+^4?zFPKO$v4)_F81UE4k4iIyYc-Vl~zCrxL>P9K7KVKnrQBOIWCkm;&!sW7{Q8& zyaAo#*mY8kBZI~6>}+`Tuu*{(flFWKJ|M;pZHRc-Lw^WOV+PA0P$1(z(5r#G{Xy-^GKeX}l`Tx^?RI{3A42H9d;RsNU-*`{ zoF0L+B1j)O6{lhTc3^Iu)bB#>2iX|O6LcCBp@f0&MKCaZI+V%1T8&cl&9Drm+##z$ znOHU-^I_@)E;~e>CphI8Fji^HSV}belwI;+9ldiK9`Jzsfk$H=BtidG@{7D$Snfjz zIQ)p6{a1C=YGKri4bjrma?gte1MQ*YB!9U*qM|3GKiWKouA+%(;%Sw3UcZpEoX(c# z)K{+*d+GGzK?tN}<%yq;3r_&{{abEn&wKsr&$(;gzMYQ$s`?&jYFn$oWIP}rRJOJZ z05z6|w@Sz~LOLS3+Oue`E2#Dt-l!R^MIZhGVk{`gNn&~+X9ZwdAq zmY9XI!V#JVXjnC&<}*&ANOn-YY89az-ZCw7KGqbh)(|M4r7d(K>M|+^n#mjJIWRBC zd192hqTEC0Q+%Z~S@Yq0DmNmgmz_H_=$phUMh($HF{7IX} zl;76L*3aon{>mfwmz0yh7fk?~v}!mSmGP9fI1p>O|r_&n9Vpl%~Kp4S!5H2-^Gjk zOo@nH4lmW}yIxWE&AkO*AzSxh& zbo0>3VlH4IKBaa%!S)?TU}4W=KvNf|d8Vauxm%%J(b5D!Eyr-zjf*e9vcHox}{-WRcp1y3}kE<3V z`Epd-;?6to#JeuJI?N6( zi;nE{N`;8Z=$`+c(3$T6vSEX>=PHDNuYK*B$G`3E?|jDo{rfQ*30ReH2aZN9MoruM ztzaDOy{<34Cu>|au{vT}{)v#CJ@lCYt{&Gcf5e+#A!t0tMBy zqIs^%$sktGmnb@W(&!x?V_mqgmqC(OB{L4PHexz42M-dD9meAZ4>;r0>N8%d2B|5O zX~_R&b5Q?^2DWkYq1e7-2SU`&cdnyiO97Vg*yQp{!$oSud|V4_xgfst zg4@}0phK#j63cxlujG;bo%1`57CFD^N5v6eUV?7YYdfQ4x%5Xe^&r;8h_}D}9b0eM zz2~t>CKe+=N{58=2@7_N3swlIRr|S&#g%>H>Nx4aAyDo{>B-K?$RVb=MUWAgVkZ zU;)Mh14-lVsATqN*x5mE4sQdX%`d}ZhTmlSDy$47u0)hUplCz=-x1pm!$~I|htX)n z%vP7_ZxBGrlYZ>YD)FrpbFt5O%&wEr1~COoc(7%NC_^bTkY~NLV{l|2%Kt%Y*99zs zYG%rjyHx>@qT!dG$3Rhji#6b&P0+WI%Hs)*6A9!?GWEjz(EKfFk`sO;`?K1zTG)x(?IUCpgMScJBKlLYuL2? z>S2?8S-qe5qaXj+XDm>g&a@MrQ@Lt z^7Nb2(cp`ShE8ep5~tH-nyAZ&!lRlJW2Vi&h*XdCtY^+hh=E+MtOhVoLekn2(S(tP z)pZ>J-T`c8gRMu3m}Vi`yByL_zih@C3L z$fw1-Q#~~XA>;M2D07*naRG&9CYvJ%b7-JpZUu=7ZvOjAyqU}Rv>>&b|ed<#`{gto& z$0K8LstM5zFSoRg>8j2dIa zSHAMqhkx-)Upg&DkO|mk&8lfeXh@xs4x3-u@GjFxnxCkXcq1?8HtL$Xk2vd5EPXAO)X1xMls^1Ydf-XI3m!J-==MG^MCyp-uM0w{K5YH z`*)yhLr|wHALhoQd{z9yE?B}@K z_U?V~JKufD3vasV=JMJ=TQ;+mqmFqKTj}n);iQAd+-u}HWK)eh1?sdWpZxK2xwmDq zFGR}l=m-ZGEJN{=XJu0+A!YH&U@kqi=Z7|pDxX{IFlizl`N)T>bFWelQDcnvVDWFF zq=09wzs1E3IO^zQFbW}UZ;**aA2A$_Ota>a8Ax&nd`~SOt^89NSv{eC!vMhYOd6MM zUkw%%9!j|-+9E1YPPGy~bbR4Ks#o|4IlUzDcq{3p0o5+}5gp&uwm@j`z7KrhLA&?t zIZaG88UJQ%vr=GBaDYi;U!+zay^%{?OJtQb;&cbdAubLsT2kZ#`J@2#^_)}yyfD=2 zVs^~m^8H)dFMRQfiy?%pRmn<4J~=c9kfKa43_Ou$593SV4is#7D9?zrnKIL1;YAaN z+*$I5!KhCT4RKx>F_$4j$11WRt1vzDnm_4qPy?Z^0|B+;=Lrh*WFJ(ABab*7r<`(P zm44{58de!&1!OFM!VI7g8f-m$J2q|Xo$vA|pu`&>85px{2*d-)op}?Duos0N(Wq_R;o;}DQebqHxgsl0R8q8*K} z@9w+ts@J~mEiikh*u0B+>w{8eC{M-+QFtOP<_S$s59ssV!Az9x0wEt+N@2=@=7rlh zE|AK}AQnRCE_v_!{`i(#ZtV{l6IFE7<};}K(?c6-M?Q@)J<}{NlTJRBF%-LB&odMc z`b8dIW=@j#!)SgFGXL_4l2P-3n3wT&8RZ=^fKJbfMt=wt7U_@j^c|OItNQjt#Guk~ zR#z!n*qIMH9ZO4#_`kWBerhT$A6w55v3bi@9Cp}Yh*2ExiB`}$0$}n>dsw6@Xx)@;U6Xp!fVMJg?7G=h+V(CedC4&Jm8E|F_}!@ zUhKxv{mfL!GyP=1`EirU0(Km6B*vpAA8pf_Ix6!|s%z?+(8VyN<;u@l1}cr|W2yaP z&chW?Z7hDS%vVvna?`|;r&c_3AND<%6Yf%FeU`5So5*t7(fPe$=~V_U^rX z>3#42z~g4K*&de zeSE41Yyj}`fBNSS-hIzK#o3p@YBU;QBuBi8RyXLF2Fg6l`f%T2qX0<`PhPLwhf7@H zQ~PNw-D1SCzd-=Iqzfv48JL=vpz`NBQTg(^N#3xiMhfSn&hGHd7K>Oj;>4Cgh&aQ^ zCmxGqk2|I^QSBA&bJQkg191f6Lej9r!WsH*uD^0`lik-3mYf0ikasB zN8GyyYnCK;VZS=x_ucpH?%VIjOwaqNeM$QQ2|-9G1TtVSLGTb*7|TKi0`qW$35sxNWLL)b$ zrZGO1R|SVyIT5(M{}S3a!UKkJsbAR9#iZrs*9_ZzkN^0|PyXi*Kk}$)S;5S6icCXm ziq3QwsBF~G+=(!m&cS43b1ec@J2E)qV|wLe8w+21?zunw=}-U8Nj;~hoXEi66HN35 zYGf*LLL*EgPV0b`6BL-dVbu+`jG=b_;r7BJ$ye#zg5cc42MXO2#P~I|Cte)!6Z$Wfug7aS)7RHbBhxal7-*3 z&idccs++OFaH$P=%y4?5y@r#{E!v(;CV271mvG-_KlewjT)A=s!g8gSY*4up6d7K8 z&gPz)e=Yf;eshS3Ux!7%cuHUuqFT69erqxj{LX!!{hqHq|NIU9b!z^0nx4ckI>R@F zlS@+q^B^iCe2AM0K;=yk|Ac2G#>hUhF!F|9#^D`R!^dD(YidENNV{SATi+t}E% zio5T=4YO(WC38Z+<;%6|e({5k2PeY46TSdWoV*3oB=(UC!)pS)BE{EHn`K-vZ`NlF zmT+0M{80KvLVl+}Q~yYKc6ou)hzz%pU*pqQo)|a6L(EGl<NETGs-j2aYgrUu4OHT6z+e!C}S&!9mim5<*ZGBBqWf^1x_1OR;Fg%`S0r=RKE zLbdE<*CUU4>e7i=2u|q-x+j^y&k|s?;-PS)@kUMvr@_$~1h6+nBv4Bkeyf?IYTDhJCwv(e|V$6kA){_{CrPm5*EQ_EH9C4z>hQ4tCrWh3Gr4 z)_JuSez`DDTMGHyE6A9hQo_}%SNHwqZ~fMPfA0Kw_a;KO-{?74Lnk-r5y0Cx-%L>L zOv-(2Gx@xc7QVL9n|Vg;6n+6P53WlKy1otGpze&=1R}#*xV{%GO*?$(!{0FnsV7%W zF4eMtpb(od^=|?}aj>fnqT#0NCOCBD7#5~8I@k|hLPnzIwO7nXgM73<8ez9?k6326 zrRKXA(VoIyWYUP(0^@YG&Z?6jqNb+t z$O5oBPnn!%?o6zp{+W|Ll?{s3$$^01Pjv3wxs$)~|NOt-nVgM0!DP~*>yjx}fY?f2 zovj4|J}Ow58#X)d@RN~zs#Cj8%s6UYpUQ=63+_&NQ0m8dlGZiS-^d$j8Z2LqhsUFM zZBb@Fd==;J+ZtPO8bRD;RECV1f4lhsz@EJu*uQ@RKv_guIo|_s?8pO}FXwHY5?bB^ z*T3cqpdEh6%+y`x9mPTMyW#iI`t!t6<4D<$mAW)I-zVL$0zQw|w=Q4CfA|mo;d}WF z9%wId(5@_w1zhHjjWHI^es$^MiEk^v*$>t64;tCR9AdbuCwM z=_{*iICSt3Iya%9Xe?C5`RQ5%oN;OWrEWXRCv8|#!EN(_#+UyX$uo!Ekk{c(V>tP- zrbpRG0#z)m0M(xJ3$VPekjLw9f95j>Uq5r^eF%$3RWQ+g703Q5(xB8S570WTd<6?I z-Wyo0xytwK-vZ@#%k4`$`Mrjk2LSH7?>3ZyAw3wu;V!l2%k3& zUIbhoF3fRGTO|k=9L$I#(2F7f>-Y#aIGJ7jQl^W_O2B9RHIy}idO-)lJ$K)R{kvFu zWm9wrCYyr>`^}hzB7*$~4`a{TDrCFQ$KDvasLaS2+qnqI3g*Wi@sngS7 z=2b$5lo=CZL9+tFi@zMIl>FY zEp(HqshMdq0Zh9BSrVc*#{4wwlwf})Q^&>SD?`(~%0wkv+d!zeG;m0RPAR`wu2hWh zd5Scz;`jOdPt8S_m+7ItY~>XIy#T%RSY27fyWe#;*49?rbd7|Vi=FRW&p4A-a8w91 z!gRKXV<%2xg5gbJ&pcYBh}B=<+mQRi#aUi)@glK2*Vgw(?dKZL3u(kvWn-U%aE!pE zyraB0H+~EjvQjoI;_k@tKABF?L-3oQ{N&$!@~Nl)1iaG2fsdDQ3CSP&=eGLfU_Px_ z*1bLq4tO>N-3KS&wJ{@59cEF=zW(}|4?OzVWAD1QwUyrvmIb2fY*)9HIjtx(nZB(# z=WCt60-FYNR-sSF}LjvqOQn{Php3y+3kZkqwZO%tq=$^>lutQDGRf}q*wjpVBWpmaMMi_5T&D~6{}o%%h` zeEBQy(G|4aq`WR2_6!aiom)=vfgwknMx%|ds=d}Fr={3o$#=3NT?GjsjinS$c>-X| z5~8Y+JPs++$jYUN!>X${>^mu@hbJh|?P!hCeSpy=;O3i7;ONoAEkNxM1~a?uJdCJ$ z5O0~5LVY9NPo*$d*4D9qV}Cvc)C)i*sBRCj{t!b_euk51DMgF5;Oav7oNr78m`xDu zSISq%K5jO1jhAf3W*l(&)iqIQECM_=C()?gpo;VKmbwp$L^ca;+zzJUTW`IEPk!oC zfA7kbEBm7$AS zXO6t`>Z?w*?7{};jAUTka9&2?3!!*Qv^ZbRjvhuyD~d*ZE)qy`x*e&EnwE0hGWE6H zCr1~TkH`6HT7rjP;Bo#`=z>oZ%Fp=RZUD)B(*u3qV{K^xANaugFrCeaP3CD>K1A(A zgLReSV-r=)H|j%Y(Sili5OKpzw_@7C)?^J-mb&xt*Rjq?>m=pp_#rPut@|UnE8zRB z#3e+&^ZnLJ<2)q%T6sdf5|TUi+n+oUaYfoB#7mP-K4~{4pzC;DR(V&(ys*aGMw3hNyIpL-FzpK54tdhAiuvF+YBp*vh`=VSg2wTn=3;J*?|;Wk@M{diGSwb($W$R9z2AEvVe4~ zE#cDI^{?8JOjRvwZD%TVp$w36Fr(F5P^z!_xi;cd?MN{M4u|GqXc!!@qg>LKmCc6>$Txhcy`$s zhXfp99&I`8$40v;j&UvyFk&5J-e9XLGxYhB@Jq7^KKLQsiW4>0u@EOUUyaJT*TE5g z6c1xu|IEMXY!L?z9mZ@rsRT@$wz~dC6acsI(zQ22IyXn`5BNNZ;8)pXlE9 zZPyD6&v`Yxtx5>xt-q@i{-VUwc+|FMC8Pqf4gj9`(vze{cD8!9ODXVAL(sUDym1zw zI5*96TFlG1TEbCkwE^OpC?%TN(ufwuGdeTnSe{51*Nd2_g4+@<4efwtK9L^NF5%tp zz6Z-oOLdu@0?7r3wVGQ78ofU%m~z3SV{Hp176QS_+B!D&?L!Apo`_t`e5v}QsU^oS z)~0jv12EyzlnvBpPTtL%aJ0{S+#>2{t!3aq9GG zV8r~(an%CQu{h3T%tlhUYwd5`T~r1&<0VX_V*tP{z%UL=V4_QM`sf`Crr+ zxcgD(3?cn;;7`C@;7crw-SoAK~G)1)i%=nk|y9%uSv zJ`G-IO}gQC?b;Ted;a;MxED6SxLav%UNsd-yRx;Nyy>}R{yFuG(@3brMPsIEeeQFg zf1ikSw&+Y3i>3s>Qy!>Wh&jI51E^beEZ()0PuBrd;boZdx&uB5d7AXMU@~2SCKk9ZtKVrXA4+H0Le++<&(Ay2sc{yiH0OM2tV{!B z3R(FoBeWW>A|RF*XSn0`Td=;qzS~MQ^Rb*%VdXR~6*TW)m`rClc;qd)&Gg=FOm`n|e@VK_~ckB{NbZZtT}c%qe36Cvamo?TKYWrHeMSS}xH21%fs zgKd*6!DHHlVML%owhD2MK*Cw;iD)geY_R}_*-c-y*Y(cuV;$*U_u%y+*uQTbcinwQ zXu?sh4k5W_usNf_^Hv|JW9$1m&X*UP@Q)lnF?*{hwe+Ins#=U zN`TBC5Y)1mXo(XP5T$t$wpuY2W)2@Hoy(DSq;O;1-LoLJi=V$mr%X+2yY8BV7+Z_;fcd|vB}L8_H_0aAvO2rj};guXB$8NNMB*%CtV z878FUZ{Zktke)@yL7P^gXDwg&HNq}eaX7erGV6%|(&OfnCvf7%6OpZ2{Lv27ca>sH zRzIIh`Yl%XQc8H?g%@z?(xtMj z-_nz71cw*YI67#l9pg(AN~SzMGO0kZax|xsKyW~W=V0(0T$ou?#O7SXLGQh+Zjn$? zTC{T@MJT!WR`9f$ekLW(xLPEy4)3wh2|oDY_hT}dG+^!~h$E3S zLzP>6e34$pdpu4m-d5P02Ywt#z#)aPvanA@aOLC8zxvf@_g}j7rha|bos}1tDoELq zqfso>Y}I>oR{_Ngw6#@1QyP2S2>9bsnV|l{g^Tw-^X0F6-{!Szm3F0q%Nrr740KX) z`H+1SHN*O#X}U)NoRW^897lWcoH~smQ__ZxkTXM?IqS_+J=md;TGp>HEXm-8{Pi1I z73vmWk43=R#cN>R$}W1T3W0rl)^XR}t*toTTp16kvKXUvuMi{~l)WIE;z{?h*W!JN^qu1i~`s{Ph zeWgRUXt1{qQ`qarw^ibo?4w4EFIS60`tZ|^=USG6r959BgIlr31q3ldtG`ojy9 z48SHXFH{RZiM7260p?FD$52R&2LZ$J5}-(lv>pkfeFTS80@QF*8(O=tqN3wC?*X=T zm0gERZ@q=jf8mQC(|%HUVAQ3uIFtm@0Jq2#eRcfPcfOc~x96BP61^0TaGHu$d^)4}>pjg+=hI058=m07!%<_AKmpZKsx<0w&QaV6pxb?~XGKS6sSu3Ek*QqW}OP z07*naRA|!nk&m_CUt4!8-1B z6QCT5IEH&3UFCl7{zF(>Uc%XnZ$VNw@I&QP^YE%e_eRLj>2oYC@)%>XDbprlKRq!& zH>%}fQ(5IPY1Ct0&?b+xmvJ;w1~o3`X)_7a;DAr5!*kC+2NpyqLdYHJ$sv_5>qMai zgd?jo!qz&lMB}R#V3RQmIorUP;otxxLEOIDwXHpmJofmHZC>5XDr5V27}riC?XPbs zxPRwB$$2AFiuW&}&f^OZqnf~rj9oJu3qnnD3P%vmOove104vf*8kE=FbDCIrD?3OH zpzqNo;Lh7`!M=TaM=H%>o*H;E@fK#Q z%Su*P%){rN|9XK#FYVy{S{*4!e6}9g8Q=1I>9Pj57?grLdWH$eWz4>0pVrpa*7WI9 zr$3|>F|)98wEKtBWn`nPrd}0jLdkhO8>$o1YednhuEE>Vl@-S2EA+V|*#T?QSf(T3 zsK3~wS;}ye{9I0xOd%VsZOiG=2XtU#4{pEnHY_YGpdH!+cU~}RvQiVH4RDQk=*hA6k}BipmjYy9W z(m{5-j|~r@fAI@CaQR9kPew&$4sfkrC4t+YJ5XuxIOxe{=hmF1Z|OR+bk5)vlao zk~}?`b~b@b7-$;TG3RyWlxu78b*33gCs^%guW8dY(v)fw&7nwzm-V?-uR?In7t;eB z?lhgK127J#W|hZsO6GV^T?Ya1{MWy}58)dTapi%grFwyAwP-!crYQrCj`fyln1lQ1 zA={|ieUF!4c_klS-(fheos~?B3_j5EgD3b{?YCIud2x~G!8eJ7fZt;=v?PxH8 z!i!bZ=~>Yk9&~c7?Z|N)B&4bW0jD!-G)>}F->UVPbscWI?H26WvyR<`>kwXAqR}S9 z3yS%tw~mETGc#bBOlLT7;4t2J>+9&H2awvkZB?>6p$e44>QGm^@m%MvrAy_Kh(fIa z%avfotT8THIGT;zq&K9E;b?`b?ev$5LV|=>UU>zmR}`4rNzK9NTxY*|h)0g=0Q6H% zV2Ma-8#~}T7EUZJm5^(j*H&IXa|V;?6n56O3zg)dYlySa&5~&(j?zq4uq>#QViuxx z{!%~F^`IZeY}BIQFmrCW_p)TDo1;Z>l2lguBa8~Kjlt4>gOV`_tG*YktS;hR?|GLu zb-$S~G6P2;GsvsC-om+|ZL-(l(9vUf@x^c8@|Fy5j62og+wInFzs9(wNwq3RaR`_q zoG}NZ-wBp4ivWccpy$bz z(-Xna|E)KIMSXMP&Ty2qK}w6qA*Ss>1{%Ex)LIoBjs}TrS{O_i0B9ysQ>-7LoxJfl zZn}j( zThr;=$rgT_5349D`hxA;`SUlw^6IPKOF--;9m$D~9qx_wR_4o!mVqrC&ChZ3VO1b5 z3S*=xC7d~PhJ=;A!Qv*Uhiue0ESBF;1e~pTD^b}*_3$+)YJv5C3U7#7T3YCT)9RYG zi>k7P6(mIl@-tw~D!F{3{3ZxJ6_Nc^4cWWChCA=Nqd{LyFJsXP8zd+)~HZ+h+3oKiumCf6~>D|<2o!}Y>GMM%33QTnz!9(TGYIpeW`XT0KESC89iROOJR19g&ICwZxr!6cjVc-4(nB=2QhVK@R z)5`r5g;%pbM(SweMnVZ)_o5WD1an>PnL98&yUR^JSZM+>`uHm_~rjW;hT1lFh! z1em7t=4N*YC>oy}WVJ0W0_AlR&M`)1StIM7c=D;Ac;nKgTlwulWi_VaYhq!;9aIJ^ zwH@HgkZUYn%Zgwfi49q9UP&Vft<#e#KDSRi83Zp97M1R@RC_QS_0wK z)sQSdX@iDD!7&W>E&;dPd?OAYZGFb86&eD4EZ;n4w}G15L$#^v{NZhsSzX(UJ!|Ww zEVSKIIW{Hb=NzwXC)q(%jx!SKhw|T>z#5m3-`X%oIU3*xdtb)e5b>l9e(BgWw^90>!HdfNTKu|=mDo(|r<>nph9t~=&z z#rY;e>?;!lF{3o%zJ(zHhmRh|nRDl`*~`#&DYRA2H}HytE)~aD#rLZGsBf)N;+2nb zdHr9`;wl4^g`J@6Z&f@4EkeV^^N$ms33;=3iEr|q63U}R#P+Y7Y(`kMpU0Y01YA9& z)Q!ebN(tNBTex!NiU}H(DWFqe*gwT4Uq;6tYIbI%`LG5;Qp*JFW4Z#>ykt&xmoHz2 z^u0;l(#WDML&vUCJ8E)Hw-+&#Pa93!pO1A6MWSSktDY>Q{E$o3J8y2soJU zm=_E$jV~6&T83%nI-0V@I6@w0p~QAX5mqT{;IF-Q=KC(a`R2Wew(ucqN$i3mWr$5i z_LuWAt1hL4X|x}M7RABYno;8t5xn)*TiEVn**tBfBEDjn$Bh12eG5$mH9ysOd>NaA z>{{4G^TlVX3#F8}w-?6h-EN~u3(+Tl^pcNs0dBqhR_xiko411+!t^?>Z!*eKLK)Yo z!}h{v*CF=h^$M-o!XozW+rY)kFQM-xYrVBwqAuIwo3VCi@v)C%-jH3FiF~#jydK9+ z{It{98^_Utm&bw-y?NGp{mhvUUAlDX*0t5uCxg{yrX6cetZe~c&5EtLE+xP9s(HS< z`lwXm@!P?S*v_-Ld|6e&RHobnR2;KIsO;)6bB%`-u3B8}f|Aw}II&}gi7QgmX3qI9 z#Y^VaAdYddy2g-=d?o*|oMCx3#rr?}f!2hx%dmrnMagOkYF8_Hf`Kzd#JU~xJ@c>w{}ht-5AJYjKrChLbft=Tf8>4KVH=9Sq=z_j?ZeIF;+ zaX9l}#&UKw^?JBhP3!9BCbqV=8l~Z^wy}7!=Bf`?@H#BEDXjoG<;fm*Ol>}|t5>f= zq%T6MvZ?5CC(EPduyh%A2!d?u`+?Zx=| zQ{1-7I*QC38HU*LV@GlH*s-14=WT`5xbs4bBXzNCp}6k!@-{d>>7+$QJHgr&ej~hKHa^A9b()7q z8W~gmH@qnv1Xik#UI&UJzWS2)AY-4`-)FS?AMNm0dQ}VcKrjEmRGQM&z`(hr?>Xu zfi|Y}Cos>FfKxIFMoS|?$kVt9_Y^Db`+1?%si8rplAb7TOp_CMK8z&HuM*Lf3WJyKw+Z zFTaepu5Q`eLg|3AB5tfrBhP7Ei{H4-{JlHV!_Fic@^D{s?=lYSS8%Uz+j|^!T}s|G zaz9p!R!YTp;yQbvKX2GgNZ8uiM&ECnObNbj_t`EA)fb=n?V!f-vIYh2_W6Xh5S2No zR}ty@zNbnilT~FVXmqgYsXVV4o6t2Bs~VQ)&lZf5?&JJcY031oo!qUk6Aq*(@218a8+ADf8HUF(g0ITMu*U9pz~Sg^#j_|K;&_F@ zp!8Ntp?2lNBBh951P;f`kU2=msxES8zOyQ)009hYAY$kH_R$8g=aAPL+^NXI#o!x@ z<644i$Tg_EadxRkTl*989?JNf&w^K)@iBja)<|~)tn?3v3c=uXUYN~r^DQ@FZGCNo zwt3+?1j{b7nlK$H=+oxoiR|diC)99e3yavZcLQCgE78ek2%u`~RP&Si(#)qO1z)Vk z$V>_=h}N5+`kt$-gFbX>)wjP zRE!c^1t%9EwH>TmbiH<$ojxRn6=B6q_i zYlw=Uth~_edkJ0qk2GlA#~8-w z8Dgr5H{91_y)M_P6Njd}n2SU2+pToHI9hp&#R21YBw)K}Sk+rT_D8X!UGqk{f*{CP zIPtFNp0q1Ngy1ZrRqmsdH9L0)KPw!@r}uGY^21e}P-EEC zKh3LlT#J#ax=ExqgOE6cywFuV!>I&<#n}|M-+3pdvsnY%I~hE!y!MLk2l|FLbY_y7 zX%_AwFcTx>#Yp~Qm)Y`=`PptBZ*ncTm$=gRn-h9OW9J z#g?p9<2rB^SW7|dpiEnhyTBO5HG+eeR9H6JD_9siFAHH>MuZwffL^e^yo5XNxqFPF zU4*FDyiF)HI%#h5*LbgkB;*xJ7s{{IcK0pV3D~oD151kw&rO zMmwk`67S(*h$p8?k6@9?c&%k-7(aS4N-COU1X%NS&?6HyZ36s2YP5w*PEoO8ZOeV+ z#>tX=)qEhE_pA=;v9_vKp<$As2c-27Z^ zD8n?^rZ2>$MqJ3PZw7nmG3kIi@3{->d-v>$)}6ynVxMT4(>z7f*?p*F%G*rSu&}(0 zz5Djz&6i)!?Ulv9pE-fFLr;(P-R{FEy)-;33_Nsp0?tBZ411#<*HbH=go@&p$GL4j z1SXRSx~?N%y=}d!%5Ml5y;_*18(3Cu^5{Am0yzz!d_V3?#K1aqC>&-9dz@Pr&mNl?uHibV; zl(*zW-xMDXAJ<3#0H)n!KptA9sG`}k)BIT5{7_Fe>}g=LGN{B-%8DyTwtc|jRA-4; zd1QJx1&`jP;M`SDlv1N!D$kk4(-4;dX4u&lN&t==K7?b(k5{d8ePOp#d8%e}0M6lF z;>38t+P%u;+taxRa#?Lp*J80z9KJ$KN(n2g>sVP@!qqq49J;G3uwmG^*J3*qM9LSj zKk8RUG5Ef&S4(+_Lj!XezaTW?)+fK>D2Q=RdLUNV?ls&Mida5 zX^2Udlgfm=8lrft;d$7MGs17>WwBYqX|Zsb1DVCWC@c9~oXV@d$Fu|Pxa&@=ti7GJ zS8p?JG{OC4hPglB1W4SUwtasJ|)`RJp`K)>Tt=9w0a zLOj{pz9OElLZ?Sw5m!d@NChuU3r0zc&tkdcN(>Jv$w;|?Bg*v9FSaydes`K;qdR3J zY`LK~fi6U4h|NMdy*|Yx3MsI|-<7T_}y$brbB}IDmy$U&Yn!9>Yna$v#w33&b`w0F`6h(wPUr z{FwW64wbM{q^SqqFL{-R39bZuK+XFq$HQ1ct9_AjM5kvpF!UcZe?y9UWv2siiMSa7?p8Yd3{--@Dux33cKoYDuCeF zv7gJ&#M2@;v%}PQE8nibg6(dD`Zf!lQ;JY+m-(*OE&Aq zR21K+Pgzo9mbWPS^n5iCT~udxu2u4AN}g2V)Q!|l;zTf;PH^LmH(>9^ zzN&t|8^P5hl^;N@6u4RVnuUqycplW87cPEm?CYe*vyBKaU0A@{`d&=fI*`IQntg1W zyY->TcQmktlM-V9i4FS7btvo9@ z%?0R*4pkucla}M94ff?|KAbNCY#oWrT2`O(hby$q5B}Ld{liBN9X@T$M@%yu-I^TJ z4)Mvw>no38_jfWlnooOFC|@Z%lot^aMk&xjeg?qNC^?@{-U3%_M|#1bfD+|H7UHoy z$CvgI!WVICJ`oufiT08|W7T70&pPh7_nx=86=ze_3EUkUahirfY)lz{LF!4UKec*|4yut2^mvMZfAsDuEB+E;$| z(jdzGp$Sku%g9w>vGo=qYMctA_yGs670q+{X9md#lG~>!K3gJf51OATDp#e8_m)LX zjq95%hp+1(a0N$>9Kx{^Cq`@W_QMV{ClNVj@z_A1uO!a}yHol*kau}y6)P(%xP0vm zK#DI*`(x;zvS*I(k?W#W)V$`)bNWnN3>@1a5BfxD1446jNByhP=p7De#Ea3z&82V( zM|(WcQ|v7sRQa1CCIo#km+yh#WDhbWeoU1YrUUTH6x>e{1Ql)+u?e+CL+BGLlAIb1 zpOy;TYMhpNB`88B!t`RahGjyM2BhY#1Z=A59dpWZnR>8Eg+nOCDeC#-3bRRv+wQy* zvv0*$adryM>??wbO+w8f+|XeLc3Nd4J?3LrEseBUmf&`iDfaH$he_fV*U3W%NxVmW z--xpsvzC^hB@^uUMfr=GrUkDx9+c$Rv;b*7Tt+;9#c?>svW!Eml9=BFYv_d6&%FL8 z-+1HF2dquJCC9sK^k4;mj%ZJGQYpKhfV|SZwM-$0-fK=K-1zhcAZ${MyklkEUA;H1 zYP}pMdZmQXw{75ghJhf@lBrRqJrA|>@^fT^GPl(sillisG`_qE5)_B~`sxbqdH1^- zIDU6STZ?BV@8{0KZQdBqH_iSx#?Sc9TP})QLPwdWX zJqgrC9ItIiTsh}_1C(VIKBqwZNr?_ma{mXu7+04Bu?A5Pd3s6>VfW%_i^HVeW3L;X zr(MD=x7~)d^>=D3&UJ=N7>|usCXu>U|Ea^$(hByhuj8%DXNtcl`Nh3JoQ#H0UUF;x zvVpxUGUn-yLtfIUd8Nej_$b;_a&Wbr;?qLLeYveArzHoM>lbks#UW9DS)bYosD4b; z^?*sN*=&ZDrIn!_Y6S?WkEpU!8p4}Jr$A}^6tw1nO+8*+YBXh^78Aj~ua{qbN5;4;!)Tmie8hp(&hR*;=^d-@CI~L*$uC@WiMD* zoZ|Mo@51C;c5eF)g2rhLO=Y1>w|P{}gV^j>9c&49Qm9)$z_Pt>L#zv@O zWCGt5e3dzQA`^}0Z%+9Gts%X1XbWpeiH^<4Fm0-te#R-IaX#dH`gUgca25_8Jb#2Hcqm`oCGz3o

((TJ-Q(V)+%=q=?)y&|L^|N5B-HdZ%u10 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 KMNUMnLSTaE`*ec< literal 0 HcmV?d00001 diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..76460c6 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + +

+ + + + + + +

Geofency Adapter Settings

+ + + + + + + + +
atHome
activate_server
port
ssl
user
password
+ + +

port

+

geofency is listening for events on this port

+

user / password

+

set username and password for authentication of your geofenca device. Use the same values in your mobile app webhook settings

+

geofency mobile app

+

+ download geofency for your device
+ * see Geofency website
+ * for any new location -> properties -> webhook settings:
+ -> URL for entry & exit: <your yunkong2 Domain>:<port from above>/<any location name >
+ -> Post Format: JSON-encoded: enabled
+ -> authentication: set user / password from above +

+
+ + diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..9587de5 --- /dev/null +++ b/admin/words.js @@ -0,0 +1,8 @@ +systemDictionary = { + "port": {"de": "Port", "en": "Port", "ru": "Порт"}, + "activate_server": {"de": "Server aktivieren", "en": "Activate Server", "ru": "Activate Server"}, + "user": {"de": "Benutzer", "en": "User", "ru": "Пользователь"}, + "password": {"de": "Passwort", "en": "Password", "ru": "Пароль" }, + "atHome": {"de": "Ortsname für Zuhause", "en": "Name of home", "ru": "Имя места для дома"}, + "ssl": {"de": "SSL (https://) verwenden", "en": "Use SLL (https://)", "ru": "Использовать SSL(https://)"} +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..d278435 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - npm install + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/geofency.js b/geofency.js new file mode 100644 index 0000000..de696ba --- /dev/null +++ b/geofency.js @@ -0,0 +1,305 @@ +/** + * + * geofency adapter + * This Adapter is based on the geofency adapter of ccu.io + * + */ + +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +"use strict"; + +var utils = require(__dirname + '/lib/utils'); // Get common adapter utils + +var webServer = null; +var activate_server = false; + +var adapter = utils.Adapter({ + name: 'geofency', + + unload: function (callback) { + try { + adapter.log.info("terminating http" + (webServer.settings.secure ? "s" : "") + " server on port " + webServer.settings.port); + callback(); + } catch (e) { + callback(); + } + }, + ready: function () { + adapter.log.info("Adapter got 'Ready' Signal - initiating Main function..."); + main(); + }, + message: function (msg) { + processMessage(msg); + } +}); + +function main() { + checkCreateNewObjects(); + if (adapter.config.activate_server !== undefined) activate_server = adapter.config.activate_server; + else activate_server = true; + if (activate_server) { + if (adapter.config.ssl) { + // subscribe on changes of permissions + adapter.subscribeForeignObjects('system.group.*'); + adapter.subscribeForeignObjects('system.user.*'); + + if (!adapter.config.certPublic) { + adapter.config.certPublic = 'defaultPublic'; + } + if (!adapter.config.certPrivate) { + adapter.config.certPrivate = 'defaultPrivate'; + } + + // Load certificates + adapter.getForeignObject('system.certificates', function (err, obj) { + if (err || !obj || !obj.native.certificates || !adapter.config.certPublic || !adapter.config.certPrivate || !obj.native.certificates[adapter.config.certPublic] || !obj.native.certificates[adapter.config.certPrivate] + ) { + adapter.log.error('Cannot enable secure web server, because no certificates found: ' + adapter.config.certPublic + ', ' + adapter.config.certPrivate); + } else { + adapter.config.certificates = { + key: obj.native.certificates[adapter.config.certPrivate], + cert: obj.native.certificates[adapter.config.certPublic] + }; + + } + webServer = initWebServer(adapter.config); + }); + } else { + webServer = initWebServer(adapter.config); + } + } +} + +function initWebServer(settings) { + + var server = { + server: null, + settings: settings + }; + + if (settings.port) { + if (settings.ssl) { + if (!adapter.config.certificates) { + return null; + } + } + + if (settings.ssl) { + server.server = require('https').createServer(adapter.config.certificates, requestProcessor); + } else { + server.server = require('http').createServer(requestProcessor); + } + + server.server.__server = server; + } else { + adapter.log.error('port missing'); + process.exit(1); + } + + if (server.server) { + adapter.getPort(settings.port, function (port) { + if (port != settings.port && !adapter.config.findNextPort) { + adapter.log.error('port ' + settings.port + ' already in use'); + process.exit(1); + } + server.server.listen(port); + adapter.log.info('http' + (settings.ssl ? 's' : '') + ' server listening on port ' + port); + }); + } + + if (server.server) { + return server; + } else { + return null; + } +} + +function requestProcessor(req, res) { + var check_user = adapter.config.user; + var check_pass = adapter.config.pass; + + // If they pass in a basic auth credential it'll be in a header called "Authorization" (note NodeJS lowercases the names of headers in its request object) + var auth = req.headers.authorization; // auth is in base64(username:password) so we need to decode the base64 + adapter.log.debug("Authorization Header is: ", auth); + + var username = ''; + var password = ''; + var request_valid = true; + if (auth && check_user.length > 0 && check_pass.length > 0) { + var tmp = auth.split(' '); // Split on a space, the original auth looks like "Basic Y2hhcmxlczoxMjM0NQ==" and we need the 2nd part + var buf = new Buffer(tmp[1], 'base64'); // create a buffer and tell it the data coming in is base64 + var plain_auth = buf.toString(); // read it back out as a string + + adapter.log.debug("Decoded Authorization ", plain_auth); + // At this point plain_auth = "username:password" + var creds = plain_auth.split(':'); // split on a ':' + username = creds[0]; + password = creds[1]; + if ((username != check_user) || (password != check_pass)) { + adapter.log.warn("User credentials invalid"); + request_valid = false; + } + } + /*else { + adapter.log.warn("Authorization Header missing but user/pass defined"); + request_valid = false; + }*/ + if (!request_valid) { + res.statusCode = 403; + res.end(); + return; + } + if (req.method === 'POST') { + var body = ''; + + adapter.log.debug("request path:" + req.path); + + req.on('data', function (chunk) { + body += chunk; + }); + + req.on('end', function () { + var user = req.url.slice(1); + var jbody = JSON.parse(body); + + handleWebhookRequest(user, jbody); + + res.writeHead(200); + res.write("OK"); + res.end(); + }); + } + else { + res.writeHead(500); + res.write("Request error"); + res.end(); + } +} + +function handleWebhookRequest(user, jbody) { + adapter.log.info("adapter geofency received webhook from device " + user + " with values: name: " + jbody.name + ", entry: " + jbody.entry); + var id = user + '.' + jbody.name.replace(/\s|\./g, '_'); + + // create Objects if not yet available + adapter.getObject(id, function (err, obj) { + if (err || !obj) return createObjects(id, jbody); + setStates(id, jbody); + setAtHome(user, jbody); + }); +} + +var lastStateNames = ["lastLeave", "lastEnter"], + stateAtHomeCount = "atHomeCount", + stateAtHome = "atHome"; + + +function setStates(id, jbody) { + adapter.setState(id + '.entry', {val: ((jbody.entry == "1") ? true : false), ack: true}); + + var ts = adapter.formatDate(new Date(jbody.date), "YYYY-MM-DD hh:mm:ss"); + adapter.setState(id + '.date', {val: ts, ack: true}); + adapter.setState(id + '.' + lastStateNames[(jbody.entry == "1") ? 1 : 0], {val: ts, ack: true}); +} + + +function createObjects(id, b) { + // create all Objects + var children = []; + + var obj = { + type: 'state', + //parent: id, + common: {name: 'entry', read: true, write: true, type: 'boolean'}, + native: {id: id} + }; + adapter.setObjectNotExists(id + ".entry", obj); + children.push(obj); + obj = { + type: 'state', + //parent: id, + common: {name: 'date', read: true, write: true, type: 'string'}, + native: {id: id} + }; + adapter.setObjectNotExists(id + ".date", obj); + children.push(obj); + + for (var i = 0; i < 2; i++) { + obj.common.name = lastStateNames[i]; + adapter.setObjectNotExists(id + "." + lastStateNames[i], obj); + children.push(obj); + } + + adapter.setObjectNotExists(id, { + type: 'device', + //children: children, + common: {id: id, name: b.name}, + native: {name: b.name, lat: b.lat, long: b.long, radius: b.radius, device: b.device, beaconUUID: b.beaconUUID, major: b.major, minor: b.minor} + }, function (err, obj) { + if (!err && obj) setStates(id, b); + }); +} + + +function setAtHome(userName, body) { + if (body.name.toLowerCase() !== adapter.config.atHome.toLowerCase()) return; + var atHomeCount, atHome; + adapter.getState(stateAtHomeCount, function (err, obj) { + if (err) return; + atHomeCount = obj ? obj.val : 0; + adapter.getState(stateAtHome, function (err, obj) { + if (err) return; + atHome = obj ? (obj.val ? JSON.parse(obj.val) : []) : []; + var idx = atHome.indexOf(userName); + if (body.entry === '1') { + if (idx < 0) { + atHome.push(userName); + adapter.setState(stateAtHome, JSON.stringify(atHome), true); + } + } else { + if (idx >= 0) { + atHome.splice(idx, 1); + adapter.setState(stateAtHome, JSON.stringify(atHome), true); + } + } + if (atHomeCount !== atHome.length) adapter.setState(stateAtHomeCount, atHome.length, true); + }); + }); +} + + +function createAndSetObject(id, obj) { + adapter.setObjectNotExists(id, obj, function (err) { + adapter.setState(id, 0, true); + }); + +} + +function checkCreateNewObjects() { + + function doIt() { + var fs = require('fs'), + io = fs.readFileSync(__dirname + "/io-package.json"), + objs = JSON.parse(io); + + for (var i = 0; i < objs.instanceObjects.length; i++) { + createAndSetObject(objs.instanceObjects[i]._id, objs.instanceObjects[i]); + } + } + + var timer = setTimeout(doIt, 2000); + adapter.getState(stateAtHome, function (err, obj) { + clearTimeout(timer); + if (!obj) { + doIt(); + } + }); +} + +function processMessage(message) { + if (!message || !message.message.user || !message.message.data) return; + + adapter.log.info('Message received = ' + JSON.stringify(message)); + + handleWebhookRequest(message.message.user, message.message.data); +} diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..449ac82 --- /dev/null +++ b/io-package.json @@ -0,0 +1,92 @@ +{ + "common": { + "name": "geofency", + "version": "0.3.2", + "news": { + "0.3.2": { + "en": "Fix Authentication", + "de": "Fehler bei Zugangsprüfung behoben", + "ru": "Fix Authentication" + }, + "0.3.0": { + "en": "Make sure 'entry' is really a boolean as defined in object", + "de": "Sicherstellen das 'entry' wirklich ein Boolean ist wie im Objekt definiert.", + "ru": "Make sure 'entry' is really a boolean as defined in object" + }, + "0.2.0": { + "en": "Add Message handling to process webhook data received from other sources then own webserver", + "de": "Behandling von Messages hinzugefügt um Webhook-Daten aus anderen Quellen als dem eigenen Webserver zu verarbeiten", + "ru": "Add Message handling to process webhook data received from other sources then own webserver" + }, + "0.1.6": { + "en": "Catch parse errors", + "de": "Bearbeite Parsefehler", + "ru": "Обработка ошибок парсинга" + } + }, + "title": "Geofency Adapter", + "desc": "listening for geofency events. Based on the location based mobile App (Geofency)", + "platform": "Javascript/Node.js", + "mode": "daemon", + "icon": "geofency.png", + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.geofency/raw/master/admin/geofency.png", + "readme": "https://git.spacen.net/yunkong2/yunkong2.geofency", + "license": "MIT", + "npmLibs": [], + "type": "geoposition", + "keywords": [ + "yunkong2", + "server", + "geofency", + "mobile app" + ], + "loglevel": "info", + "enabled": true, + "localLink": "http://%ip%:%port%", + "messagebox": true, + "subscribe": "messagebox", + "authors": [ + "Daniel Schaedler ", + "Apollon77 " + ] + }, + "native": { + "activate_server": true, + "port": 7999, + "ssl": false, + "user": "geo", + "pass": "geo", + "atHome": "Home" + }, + "objects": [], + "instanceObjects": [ + { + "_id": "atHome", + "type": "state", + "common": { + "name": "atHome", + "type": "string", + "role": "state", + "read": true, + "write": false, + "def": 0, + "desc": "Present persons" + }, + "native": {} + }, + { + "_id": "atHomeCount", + "type": "state", + "common": { + "name": "atHomeCount", + "type": "number", + "role": "state", + "read": true, + "write": false, + "def": [], + "desc": "Number of present persons" + }, + "native": {} + } + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..6af37ff --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'yunkong2.js-controller', + 'yunkong2.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (controllerPath == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5dc04c --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "yunkong2.geofency", + "version": "0.3.2", + "description": "geofency adapter for yunkong2", + "author": "Daniel Schaedler ", + "contributors": [ + "Apollon77 " + ], + "homepage": "", + "license": "MIT", + "keywords": [ + "yunkong2", + "geofency", + "home automation" + ], + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.geofency" + }, + "dependencies": { + "request": "^2.67.0" + }, + "devDependencies": { + "grunt": "^1.0.1", + "grunt-replace": "^1.0.1", + "grunt-contrib-jshint": "^1.1.0", + "grunt-jscs": "^3.0.1", + "grunt-http": "^2.2.0", + "mocha": "^4.1.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.geofency/issues" + }, + "main": "geofency.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + } +} diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..12357a4 --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..89ed888 --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..c54e5c3 --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..e2a1680 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + var curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://git.spacen.net/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..ae9c289 --- /dev/null +++ b/test/testAdapter.js @@ -0,0 +1,140 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log('Try check #' + counter); + if (counter > 30) { + if (cb) cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + if (cb) cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + if (cb) cb(); + } else + if (state && (value === undefined || state.val === value)) { + if (cb) cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test ' + adapterShortName + ' adapter', function() { + before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + //config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + +/* + ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY +*/ + it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }); +/**/ + +/* + PUT YOUR OWN TESTS HERE USING + it('Testname', function ( done) { + ... + }); + + You can also use "sendTo" method to send messages to the started adapter +*/ + + after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..d0759c0 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +});