%Tuv*#$7ByVj}
z%PDp3mNYH0^ZfEJ7wuWFvOqWK3i+qi=dC704t^-^d31mm3FXQ5=^WxW=FS=pLl+Td
z@pmPb&JyzD4-IS%sE7^@>UmG@E!?ud1z7*m!M;6kfdokNDvMYl`hT_H{+nq{=A;+-
zrtaF0c88tnT0yVE11Y$mfofEYO5y}5BR4^@&s7UZCZ^xb9ctufB@3{e6L*Rw8U+NY
z63mI?>IQeO$7IjkmRfu+-7m951etPNGANSiOXcEeAQx<3(Yih#Oif_x%W}RK4OnwKErx6cA_X9)`YDw(`1U0vca<@bJ!Z$inln+
zq&TGX_^YBCmPHD`@|N;jDEcXUM4UD*>H4;kCPBgCvY#TS!6OQf&=
zjof)d=hXLm1p*_RqEt}l4Hjo7o)h~TQ
zm`&DNNzzlb=xvnyhZX$rw)txCOT|j+%e|xkfxIK_Y1e!!N-!tHu3$u8Y<`A1?rLqS
zyh74(e(I<1)y9NcbnV|+SC5|Kjoh@^My22jXpu8{L_+JrqFnp_zkE&|_)3y!Rjdg4>aQ=~C|DCNr|9`1(vL5Z2P5Sj9<6KdXQlf
zeCfx>&0K+mT5>>m^*KtNXQsu~*7oS_hsyd;Pb(obp&>i=rxxv0RP$)NNn!By+B2U7_Dt
z%6{rrOxG0sS%tEf)G>W@zq0TA+m-22;YFDOho=t@AbwiT7Nx=ajwk)&Ikm;_
zjP3~t6!Mw$#*R1Y+xYXCH+Tk*1yXRHOP*KJ?ZQ*>9YCk1o_b!0P^Mg?L38j#Pjs
zVa8WNK|VA%Cz(OaJ075}JLE*U}`1SNfUTaTQ0D
z%Uuz*5{_SCjZE?~g{N64_Vd|~-{BAXVl9J~V&qr4Por#--V`Nnr^D^<1UYPUw>yg`u2lodY
z%cuHCb*nk{!VItDT`dy$;el1QxVqAzvvK~|*0cgKqiDQQX8{^%kbh&2u!ZNw5zup8
zPuANPP%jEw#Z2kp=AmEYm2vHV|FrzI28N8#IvV$~?sagmgi?Wo
zOi@t^Ca468VuFVy7t$och2#@w%in2M34~;3e0)IBz{=}&ioEWr6g3~d;4mQL)5_)O
z#(!$U5v;Js#iZXPq)Gf#iypq^xHr=3w0`{_wxcxyIvbefjrE^`2vIJ63E
zXTz!Gpx`By)>klieeia1MePz~Q_x+?IS-lTVj1goT$$V#KDPxW8J;NnMOyph`iA4I
zsC{{GNnNtRe~bM2JkDCbP3Ber$_EZ9bos#?uPFNceQ37I?=^-=vX$ID{p!YN(jdYS
zwMQol+V4_(ak21gwLukvb
zNU2L-mjbT&Nn%-BDGjUN@3nM-T(bhKd=Au4#@2l3a?dyNb%!qNWVs_}l1PaPn}uQW
z8#KGzC&TLX3Y9cn|0rDM!Tw^X^gN}LN$C=l8@WV#cqWDSiWIXLjMIf^EhvqvROR+euOT(|1n38kc^FAV(*Z#QOH2W{}5*z+Y^ysRHAm7YQHloWf6QFi}a!GLIo~DDYZh6PlGy|-P%=~LM0cUb+(ArxxV;ei7(oCPFVg=gy(aBQz}|-uTH~?}u>B9q^cZ2!o@>N`WuXr4*T$cPN%U{TtT#R%Oh
zr2g`Hn9U=T@d88L)4uPJS%m;19|X4|)Oto*Q`_`e>9bboXE0B(sNTXd_=TXunE#d$
zk8^AML6{CJ^aWe;&_)qyPF+{
zLWT1%G#&3~{3@(t{-AL1<&FDw!E17j0qck{IlMJZLXH!Pu)H0pX#a3XA~7H5C+^pukPE)LTu4`
zb`LM!ujra4Z|20sL7w%mCccelbareGGVd0=##J>>nQTuqUPKF|pLz>vSbZ{-qRuu~
z%2E+l`rLAoT*4W}aCj5pe_N^kv1a@o;Aa6p3;0>U&jNlH@UwuQ1^g`FX8}J8_*uZu
z0)7_ovw)uk{4C&S0Y3})S-{T%eirbvfS(2YEZ}DWKMVL-z|R7H7Vxuxp9TCZ;Aa6p
z3;0>U&jNlH@UwuQ1^g`FX8}J8_*vke1^!v!p9TI|;GYHlS>T@q{#oFk1^!v!p9TI|
z;GYHlS>T@q{#oFk1^!v!p9TI|;GYHlS>T@q{#oFk1^!v!p9TI|;GYHlS>T@q{#oFk
j1^!v!p9TI|;GYHlS>T@q{#oFk1^(IppZ~1i!3RE2
zL41LV;tNm&A59fdQ9%R*wY{~i*0$Puebno-wzg_*Yg>Q!`>}-ewMR1LShLL!Ki9tsK~!J^hgH2qfK;
z&lUypPx;cJ0A{owVdWqEM-SUFv$fI4KgmtLR4MaeQXIjb{5>Ln$AB{Q$S7kaql{WG>W%`9OJ+zbQ
z=MniFhm@hWGHqz0QP`NtMu7fgIS7T0Ko|*8^K9N`v&8140VK{*^eni6vG_B+h`yPOW!WbMC;3D
z97j7s&S`8H;TZWNua7Nng`?uB)L)D0T!hhQo*9NcqsphmPdgMNID!2|7$zU!I7{H0>B)r-$Ax+frKlx&C{aB|
z7GPvodKmeG+WI5fT0T~TAI)o&yzek7RAdCk$a^S7ndL21Bu+hw`BNo;8=-B-AwJwj
z;8=M#BJXw+ZoN>39Qj|2mUk>~J#0gBy53=Qs7oig=C1I7bLhL=JD?#(!bFBls8@iwXE9COCqRk@1)yZ(Dhre8U)-
z5cZG7czMgpTiQQeV`O|N&jfr6-^wFUo^KbBco#7eU>v15QQmYoF$|W8ScH=ToW$;l
zIN1?=#!bXjl;adf@WM^RsrU{iIRag8P=QnB4J&V`QM~IXh31-w3i%&YSVGoZ6`^?L
zjM2$?n5^QRT0nALi05!hXR^K?k=N0y3_*r?g-w!6=0yr?5*@%kRlas-Yv(2+mI*_ds3LoQpRaa^>#
zwl*3|CZoQvr=xYPsir6^E&FQP*h)nkX0|ma($Pt$lN_u4Yz>NY3UCg|Mj3Lo{EH%n
zB>$qwpq1@78hxd!<5LPvzQ3N{49BVO!@d-I{-ZVqIE$R-;9N)W0lt`3;`=zy5q!;B
zj4S28QEds?z^x7g_cAQO+&s)x8Dj;Mq|3lPFANVYKa3qm@Zqr>H^@({{6xdU7s5E%
z8Q@ARmmj0n63Fh9R$8<+WVa00$O}300!)B&DKrDg5xmeVa5v_G#c3XbI61Ar?bLO?
zBlzfEfmO6p(h)rOl~^UuTX|l&mnUl+&FL%R3apgpBJy0f!8t#aWffM-UMqV!e`dwm
zGaX~!&nIiy6OldH`QER70VkKXrT}#mya~;Y;00fUAJS9f5!PjEFJZt6IQ1Eic81RhOV6E(q$nGrT19lFMv0-$*T#t>i%gU~Og12-EzB3{_
zF|Z7M8IiHpOhY>3o8`V4a77ImOA8#q=R!N$MquID%KQ
z9j{^`7C8cE_Cz~g!G*G2wmDpgU>v=!Js06(NAPO5V=FfgiygsN)h*a6Pg!}2V#g;Z
zY462haJFNMJc&y!fp4ZYuzp<{D!K`q<%t}50>n(2w+c8`hit#BvlZPYnFou*NH{RCT
znrP01p_YB}()7ah+&TP+Dc){*%kCU*2%YvhzLX#0MoY+6lp8~*eSv>qMIN)46=h`s
zR(4fUdP%$5W>lUnX1dcR*PC6#W-G^8|Z
zxscqZCS6^C)hv`zOlQ4|F&m>SAK_Bb($SM-#bmwV8^svQMut7jtgBJht^*gCIBP*G
z{}LRbb2=9!U3|ixkp6)qv;3lHVd+bghw@3GIbfv@SiSFX1g#!4(m>payBtAF7@zay
zUMu&aPb4v;CDS;grPARppK>PO94hx%xkpou?*iOGVs~SWBY0wi%;8vzb&lYP4KjtY
z&dNFxt4XC6)Wwo%hjk&bLQ^Dbt*ljIRrM7C){xjexYrRhd(2RCtgt}K-Ilk)QT3@*
zqxvp;aL~g;cyL(n1E1rE%U!t760(7RUns^fa}+k@VS{>UV*xgH75IVNtub^julwCabNiR^50k
zba*%vq|A(#+j8VKO|kaSWMj<)JOb8?M|?(@v1Xjy8j)MOJTE$I3K@(u-;`BWR&_84
zi`SN3erh0
zOs6uLR66SgEfL`wy#+xHsZ23v;ZZ#12p!K=cpO_Dq2sv{PvA*M@F6nAOvO`qTs`QU
zZ3WndrQD1sFp|+)&zL`fR}HhLjeSw>FA_elT6|Wd8QBT$Uq3zLS^p3%fe%N7_^ify+|Nl-73}P&!O4;a<+4P;()|g0FS5-~wn%gOpC?kJ;&uB|G
z#7D(iTDl(hb-hYs|1sgSjmF#d??rBTOG~mL9@A=_rhL)T2~`WznMBis{a;MjCrl~p
z`2d>l491*R&N$;M#yZ-;M!r(`
zw`N)|Ue5^k56}38O@)?avQ(}%vP=gpRk&KcdX26zTxj~`^ZcG8S79f18Q%CgYA45B
zY2``{jl_aXiq_QB1UG8g4-$FUh2HtH1iR%5xg5`8k6dQ?*!>KL-5uZc__=OxfW2hi
zQ!Wiyamnrq73p$i{wy1d<&uaj&K~|Ea>>^${rT9#?=U;$V!22zltr>oF2E7;1C+=D
zX_E|&mo&d6OqNzTUs@!Exza36l9Wc7FAY*J^P~>XNdhaRR^n14F`0`EQq2OgUCy&|
zu~tNW8HmV5*>nweQ2N2o@1Cq&*ikhjvZ%`{Yh|Gyk&IaiM|%8JW+`I0s#sZ|E%LC^
zMk99jLCaE^aBF6%Qun*Rh_voIAzL}0a~f0eq?H!s!UfLK2pzMVGl|ZvSHAz+WTjch
z@g!%Zi7h?tSxIV(XCf<&Y-xRt$o#B54%lE@X$VJ`N2I>n%4B68N9h^B(gAGooRG&I
zKKe)KjE-ZCw{&{HZL^VD4Hljmt<>zdqp%WFp4>xNx{obx04$x@{VH$eJihI2^(m0c
zOx;_Dn3@4*cJbRK6)#s7zf)52N@el8CGVBIZ;CLu_=68iKN>kd7m>={9~Xb{$I@gj
z@@8YIAMpu4lAT=qK@9mEQ!y6>m4h&{8U=HB=#HU3hYo10KMYv=UditFN=VMGRZ6pw
zQ$MG2w!E*6N}YdbX>kk#8%tvtR5H18cMOFkF?{1_k{qm0hh?9JRB~LAjyrs03`6IT
zQDMos=Tu|x_;cAl%(sV(FCWdBQ3^Ar8qouUF#}Mrj+(?U%E%u)UND?-J0~x2Y-Nn
z#0P(bKfzZOM8y03hL__vTQq$@d~r7O&73o{-<&yf&N*8N7cNeg^W%l_lcuKuBr_tg
zU?X910?m2CqX>*3DL>_hMiO?3ve9&4mnK+sN=Xc((?%yZcOj(-_HwLFLBY`k>sl;#
z$af{*(dD?aK~$d&N*WHVajtcsOTH=jhNKF(W+O$iZfH%2$rLh3Bclm18Hr_6$tKC>
zegAs2+;BCw#AFWA@>R)KLsnj#ws{vDJ?Pbhn5+kTup4_cAtvJ*vT0;t-ND{o?8N|P
z;iAgEga{s*$YT-<{9G(({%!d*noYhq@&)}T;Y5pW;8mJlEsSDeW=pWiZDSwyYeMYM
zEVA-hKFLQ*J_D9~GVl@HwV%BY2Q(pZj`J)A%1M)`G3$YbB49jce%D8adeW6moRdAsp6(*tg|XtP2rRUO<7G8NNh?Yc=k>(k6S;JdYwuK5~#4uZAFc<#Ksy`_s-$F*?{%7aC!$
zTBroU_WS;&H5~1GTC&Pl8|_+jPX>Wkt5%|#zZBBGd~u@O3?uj2MEkHdE-1**Iq2N!D#yH`Y8gL9<~FA2o24W<;5WMwOh6)FFo{YoC}kd?ji1fvgkV`yb;_4wJ6JNAgUHhr%&wtlyG
awHuwKjk_ZpQx`L+gwJl)Jn%$7U3XbsMMb>t^{V}SHGiP)mQcE)vN9mPK+7Y
zBR|wJt7DEnE`ZcVB5)8uio*sh+fE6`^_LnzDrZhWJxh`jWgFO!H2Fn-wn#%D&tuz<
z`e5{Q#AjU$cF*pg863G5rB%a8JdMLL|0
z9*2>C^B<|Ed_cU!kskp^eo&N+;lw=Ukne-?J(}g9ennBFA|5U*@md446@W{nP0-Ym
zcuKX=3@2I=PpL|3F5k(w7R`O79FcE~e4|SH#}bZwr%Jz7r6(R!`gKsgMw1+*snX)o
zh}RgvMgUEz@+4$f5>I7QoQxJY*^&gxh`A{;&=RLu63@!!$dIp$e5DG@!lf3c_~T6p
z>`NnG9<|<5Ow#5rg7O9292n2wkz5u5)n@Q%h1QnDQ{57$;&%vH5>ItYgz$TuYDqlR
ztr3#XjeM>udc-R%*}d_WXf2-^`7EhAbWHW9LHQJobI{ON9gbHNCn{8VYd&ponkAuY
zwcSQOk&oqwBcG@XKUQ)?IZxx$7VRua>M(kw4IalIa5~PAk1QL3u8xki=iix@#Dm_3
z^Bs_BNj!+{aJn2eayaR=OncXX0G>0B6bjhOdmQ!dN^qCK8R6RYs#C
zGpPiKjUG>TpXCQvCL3qVA$iZ@Y~KnU`6JG;Bu*!?ccip;jl4^=g^X~Ea}YS2v%=qc~u
z0$gbL@(n4Z9qsv3BY-8)wk3-fa-DIWyd9Lc>o{iha3NCUpXe$F4PU9IxGYg*(bc!5
z3%bic(9Muk+tSUqF*4ULA^
zRc;@oT%r35$XUNN$l-U}xRYmNpg^9pDDV$=6&}X$0ETn;
zN{p~1UaqXde2l~>OX7j6!gUB^v?XymacgJeI(gQ}vjpKE)zN-rRbjR~gF-`69a)7w
zyqUNLV_ZfA;YHG!W@=42qmHH4-9g!nddxZ%ydMJ?!Dp=OvKWh$A>6VB1U^o7f=6qd
zpEx(+erB=p^0Yi<+3*tQCfte=L@kMD$0lrHn3h=*&&ivyMRpk3k#usI4`35E%acKQ
z5+~%qVM4cv`V(7li);_d_FvsM7UTWHxD|KHUlEfh3}3&M7h}G}t++$B1!Y^cs(?J9
zG4r%W&r@LSu(Nu`qR3ye2utPhpgexeJ+e#JJf~|uQg5~6Q;rFi#JhgBVVgWAkIEyC
zJf=*KDtSaXC-8~O!M~Xt2Un%@42)qGi!jH>XkCa`Lx@Ns
z91U^LBACK=xi^xaMjg-MNPliem~+Lvu*R{U;A(B@)s^6ADIevCvtPlvkR`L1G`8bO
zc`zss*0O(-e1JQ!6VqHwQ-IUcF&*_VnyVw0*P|z57k0}7L3yCIdtV;XeRx;{CU`8n
z+%NaZy+-c;&FpfYuDnPAsv4onnO$F9|NsAW4>$-%ixVo@(XeqE^sOw3
zBu3`v_pDvpMLmg;zXts(6UBvD;qvm@`Tp0a%BacX_f4p8Qs^9`=zl^$cPCuhRRu5>v&&|Yh)#u+w%F>lbNWM
zFEP8O#O$O*tx}zAC|XB;QS!5%oO*m_$UPP_{4++>a<|+icRF&nGTo)*PTkde;FaEJ
z)R=kS$e%G2vv4&N;%r>YM0$s8bvVf4dnW43?Q)ykD!0fMxmh;LCL_0N*hD5)#OP@~
zh9FyYQ4*Bf>L^2ka%-&@V^D7SRqO|4OD(}1l$(&9p?r}`^uKjsH`v+yfQ)&^v)irkWp7PT)>Nmv#+b)ay-IG9)pDb(
zl9jSTmdi4^0a>yXon?tEmPIV5u17CfC<|mh@4hiIPv*)TxsC~Dnp|t-Ce7?DCWErN
z4o4ZeQF&N=1Z7pNE;F+7==~6s6~A(xk>xteFNUBjtHq6mfK`4pYzW)G6^$&>7I7I_
zOxM*_JR2J57m$W#)?J$#S;*PC>OMynsFUaGWil_hrAFpbc1f%-YGjV0VTowyxT9Li
zDk8@!pvGPWY~)(b)^#JK%Q_r*y9xF-!*lK5>5#iOul>6na`)x2eW*k30eyeaA@_~E
z_J`U3$Z3V%?T>t%{YmEdhH&$q`mFttFdAn^8zMbVn@zGg-ZZZnnp4X7Kr5J?Z1ft6
zl%m0?KSV!Ui}LypMITRGnBS|d&{Y?nM3K3pkdb#1&K!x1f-qWyaWdt$EGcRUY&_Iq
zH+7qhs#c{H+QzYu
zn3ZW{#&Ha@m7!O3K$=`bhUU5N7`eIzLw)9s`rV6GW@@H%nMQb5ElS5Ces*5Oi?7m4
zZy{CzAr^dbUVxRn!=i||%r|6RoQ}o((723wQ8QtPwS1V@^pR?1YFB9mS4B(=aQ
znIsdX(#0y@WfhW;xRHva%M$9ccnyP%OiH?JVztXEk7JmcblH?@mrbt0;1U-B-ROeH
zy#-dw1Syx8i`Ck@Ny?;DqAr*&bPr2pyc8QLNdg+LfQoB~Gg6iWR9X!vdK|+9Z*>v(
zuv`HxrG!`w2A6IkpfxVmsM57kB;#bPi?!NYClMJVg)Y|lfJRGLMj08M1Qb?4qiTpV
z5=jCYQw^x_IEJDmAXdz1e!~_cV{0(DSgwH9yI8MEH^@jCAy>NCpuLSUTnc2Ei;X^@
zp)y1U8yT7eG(-XM%+!eT;YmOR)qsW_$1pMpXoLbmN*)`_srjNMPtB%J%zfyF<vd1Y@S+xRdmSP>t>|tMjl6rGLJe*~nvl!z`@2-1HZ}pho
z^a#F7E6t~FD)iaFCvX!IW0-V8YP?7KJB*~o3EzK$C0v#zCCj|)PX1!WWs1yQ2QKks
zKO?~wl%*Z6ONg$TBXSo< Standby -> New Source].
+ # If the standby condition is processed, the New Source state will be filtered by the condition below
+ #
+ # 2. Play states that received <1.5 seconds after standby state set:
+ # Some messages come in on the ML and HIP protocols relating to previous state etc.
+ # These can be ignored to avoid false states for the indigo devices
+ try:
+ for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
+ if node.name == message['Device']:
+
+ # Get time since last state update for this device
+ time_delta1 = datetime.now() - node.lastChanged
+ time_delta1 = time_delta1.total_seconds()
+ # Get time since last GOTO_SOURCE command
+ time_delta2 = datetime.now() - self.goto_flag
+ time_delta2 = time_delta2.total_seconds()
+
+ # If standby command received <2.0 seconds after GOTO_SOURCE command , ignore
+ if 'state' in message['State_Update'] and message['State_Update']['state'] == "Standby" \
+ and time_delta2 < 2.0: # Condition 1
+ if self.debug:
+ indigo.server.log(message['Device'] + " ignoring Standby: " + str(round(time_delta2, 2)) +
+ " seconds elapsed since GOTO_STATE command - ignoring message!",
+ level=logging.DEBUG)
+ return False
+
+ # If message received <1.5 seconds after standby state, ignore
+ elif node.states['playState'] == "Standby" and time_delta1 < 1.5: # Condition 2
+ if self.debug:
+ indigo.server.log(message['Device'] + " in Standby: " + str(round(time_delta1, 2)) +
+ " seconds elapsed since last state update - ignoring message!",
+ level=logging.DEBUG)
+ return False
+ else:
+ return True
+ except KeyError:
+ return False
+
+ # #### State tracking
+ def src_tracking(self, message):
+ # Track active renderers via gateway device
+ try:
+ # If new source is an audio source then update the gateway accordingly
+ if message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'):
+ try:
+ # Keep track of which devices are playing audio sources
+ if message['Device'] not in self.gateway.states['AudioRenderers'] and \
+ message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']:
+
+ # Log current audio source (MasterLink allows a single audio source for distribution)
+ source = message['State_Update']['source']
+ sourceName = dict(CONST.available_sources).get(source)
+
+ self.gateway.updateStateOnServer('currentAudioSource', value=source)
+ self.gateway.updateStateOnServer('currentAudioSourceName', value=sourceName)
+
+ try:
+ self.gateway.updateStateOnServer('nowPlaying', value=message['State_Update']['nowPlaying'])
+ except KeyError:
+ self.gateway.updateStateOnServer('nowPlaying', value='Unknown')
+
+ self.add_to_renderers_list(message['Device'], 'Audio')
+
+ # Remove device from Video Renderers list if it is on there
+ if message['Device'] in self.gateway.states['VideoRenderers']:
+ self.remove_from_renderers_list(message['Device'], 'Video')
+
+ except KeyError:
+ pass
+
+ # If source is N.Music then control accordingly
+ if message['State_Update']['source'] == str(self.itunes_source) and self.itunes_control:
+ self.iTunes_transport_control(message)
+
+ # If new source is an video source then update the gateway accordingly
+ elif message['State_Update']['source'] in CONST.source_type_dict.get('Video Sources'):
+ try:
+ # Keep track of which devices are playing video sources
+ if message['Device'] not in self.gateway.states['VideoRenderers'] and \
+ message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']:
+ self.add_to_renderers_list(message['Device'], 'Video')
+
+ # Remove device from Audio Renderers list if it is on there
+ if message['Device'] in self.gateway.states['AudioRenderers']:
+ self.remove_from_renderers_list(message['Device'], 'Audio')
+
+ except KeyError:
+ pass
+ except KeyError:
+ pass
+
+ def dev_update(self, message):
+ # Update device states
+ try:
+ for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
+ if node.name == message['Device']:
+ # Handle Standby state
+ if message['State_Update']['state'] == "Standby":
+ # Update states to standby values
+ node.updateStatesOnServer(CONST.standby_state)
+ node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
+
+ # Remove the device from active renderers list
+ self.remove_from_renderers_list(message['Device'], 'All')
+
+ # Post state update in Apple Notification Centre
+ if self.notifymode:
+ self.iTunes.notify(node.name + " now in Standby", "Device State Update")
+ return
+
+ # If device not in standby then update its state information
+ # get current states as list # Index
+ last_state = [
+ node.states['playState'], # 0
+ node.states['source'], # 1
+ node.states['nowPlaying'], # 2
+ node.states['channelTrack'], # 3
+ node.states['volume'], # 4
+ node.states['mute'], # 5
+ node.states['onOffState'] # 6
+ ]
+
+ # Initialise new state list - the device is not in standby so it must be on
+ new_state = last_state[:]
+ new_state[5] = False
+ new_state[6] = True
+
+ # Update device states with values from message
+ if 'state' in message['State_Update']:
+ if message['State_Update']['state'] not in ['None', 'Standby', '', None]:
+ if last_state[0] == 'Standby' and message['State_Update']['state'] == 'Unknown':
+ new_state[0] = 'Play'
+ elif last_state[0] != 'Standby' and message['State_Update']['state'] == 'Unknown':
+ pass
+ else:
+ new_state[0] = message['State_Update']['state']
+
+ if 'sourceName' in message['State_Update'] and message['State_Update']['sourceName'] != 'Unknown':
+ # Sanitise source name to avoid indigo key errors (remove whitespace)
+ source_name = message['State_Update']['sourceName'].strip().replace(" ", "_")
+ new_state[1] = source_name
+
+ if 'nowPlaying' in message['State_Update']:
+ # Update now playing information unless the state value is empty or unknown
+ if message['State_Update']['nowPlaying'] not in ['', 'Unknown']:
+ new_state[2] = message['State_Update']['nowPlaying']
+ # If the state value is empty/unknown and the source has not changed then no update required
+ elif new_state[1] != last_state[1]:
+ # If the state has changed and the value is unknown, then set as "Unknown"
+ new_state[2] = 'Unknown'
+
+ if 'nowPlayingDetails' in message['State_Update'] and \
+ 'channel_track' in message['State_Update']['nowPlayingDetails']:
+ new_state[3] = message['State_Update']['nowPlayingDetails']['channel_track']
+ elif new_state[1] != last_state[1]:
+ # If the state has changed and the value is unknown, then set as "Unknown"
+ new_state[2] = 0
+
+ if 'volume' in message['State_Update']:
+ new_state[4] = message['State_Update']['volume']
+
+ if new_state != last_state:
+ # Update states on server
+ key_value_list = [
+ {'key': 'playState', 'value': new_state[0]},
+ {'key': 'source', 'value': new_state[1]},
+ {'key': 'nowPlaying', 'value': new_state[2]},
+ {'key': 'channelTrack', 'value': new_state[3]},
+ {'key': 'volume', 'value': new_state[4]},
+ {'key': 'mute', 'value': new_state[5]},
+ {'key': 'onOffState', 'value': new_state[6]},
+ ]
+ node.updateStatesOnServer(key_value_list)
+
+ # Post notifications Notifications
+ if self.notifymode:
+ self.notifications(node.name, last_state, new_state)
+
+ # Update state image on server
+ if new_state[0] == "Stopped":
+ node.updateStateImageOnServer(indigo.kStateImageSel.AvPaused)
+ elif new_state[0] not in ['None', 'Unknown', 'Standby', '', None]:
+ node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
+
+ # If audio source active, update any other active audio renderers accordingly
+ try:
+ if new_state[0] not in ['None', 'Unknown', 'Standby', '', None] and \
+ message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'):
+ self.all_audio_nodes_update(new_state, node.name, message['State_Update']['source'])
+ except KeyError:
+ pass
+
+ break
+ except KeyError:
+ pass
+
+ def all_audio_nodes_update(self, new_state, dev, source):
+ # Loop over all active audio renderers to update them with the latest audio state
+ for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
+ if node.name in self.gateway.states['AudioRenderers'] and node.name != dev:
+ # Get current state of this node
+ last_state = [
+ node.states['playState'],
+ node.states['source'],
+ node.states['nowPlaying'],
+ node.states['channelTrack'],
+ node.states['volume'],
+ node.states['mute'],
+ ]
+
+ if last_state[:4] != new_state[:4]:
+ # Update the play state for active Audio renderers if new values are different from current ones
+ key_value_list = [
+ {'key': 'onOffState', 'value': True},
+ {'key': 'playState', 'value': new_state[0]},
+ {'key': 'source', 'value': new_state[1]},
+ {'key': 'nowPlaying', 'value': new_state[2]},
+ {'key': 'channelTrack', 'value': new_state[3]},
+ {'key': 'mute', 'value': False},
+ ]
+ node.updateStatesOnServer(key_value_list)
+ node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
+
+ # Update the gateway
+ if self.gateway.states['currentAudioSourceName'] != new_state[1]:
+ # If the source has changed, update both source and nowPlaying
+ sourceName = new_state[1]
+
+ key_value_list = [
+ {'key': 'currentAudioSource', 'value': source},
+ {'key': 'currentAudioSourceName', 'value': sourceName},
+ {'key': 'nowPlaying', 'value': new_state[2]},
+ ]
+ self.gateway.updateStatesOnServer(key_value_list)
+
+ elif self.gateway.states['nowPlaying'] != new_state[2] and new_state[2] not in ['', 'Unknown']:
+ # If the source has not changed, and nowPlaying is not Unknown, update nowPlaying
+ self.gateway.updateStateOnServer('nowPlaying', value=new_state[2])
+
+ # #### Active renderer list maintenance
+ def add_to_renderers_list(self, dev, av):
+ if av == "Audio":
+ renderer_list = 'AudioRenderers'
+ renderer_count = 'nAudioRenderers'
+ else:
+ renderer_list = 'VideoRenderers'
+ renderer_count = 'nVideoRenderers'
+
+ # Retrieve the renderers and convert from string to list
+ renderers = self.gateway.states[renderer_list].split(', ')
+
+ # Sanitise the list for stray blanks
+ if '' in renderers:
+ renderers.remove('')
+
+ # Add device to list if not already on there
+ if dev not in renderers:
+ renderers.append(dev)
+ self.gateway.updateStateOnServer(renderer_list, value=', '.join(renderers))
+ self.gateway.updateStateOnServer(renderer_count, value=len(renderers))
+
+ def remove_from_renderers_list(self, dev, av):
+ # Remove devices from renderers lists when the enter standby mode
+ if av in ['Audio', 'All']:
+ if dev in self.gateway.states['AudioRenderers']:
+ renderers = self.gateway.states['AudioRenderers'].split(', ')
+ renderers.remove(dev)
+
+ self.gateway.updateStateOnServer('AudioRenderers', value=', '.join(renderers))
+ self.gateway.updateStateOnServer('nAudioRenderers', value=len(renderers))
+
+ if av in ['Video', 'All']:
+ if dev in self.gateway.states['VideoRenderers']:
+ renderers = self.gateway.states['VideoRenderers'].split(', ')
+ renderers.remove(dev)
+
+ self.gateway.updateStateOnServer('VideoRenderers', value=', '.join(renderers))
+ self.gateway.updateStateOnServer('nVideoRenderers', value=len(renderers))
+
+ # If no audio sources are playing then update the gateway states
+ if self.gateway.states['AudioRenderers'] == '':
+ key_value_list = [
+ {'key': 'AudioRenderers', 'value': ''},
+ {'key': 'nAudioRenderers', 'value': 0},
+ {'key': 'currentAudioSource', 'value': 'Unknown'},
+ {'key': 'currentAudioSourceName', 'value': 'Unknown'},
+ {'key': 'nowPlaying', 'value': 'Unknown'},
+ ]
+ self.gateway.updateStatesOnServer(key_value_list)
+
+ # If no AV renderers are playing N.Music, stop iTunes playback
+ if self.itunes_control:
+ self.iTunes.stop()
+
+ # #### Helper functions
+ @staticmethod
+ def find_source_name(source, sources):
+ # Get the sourceName for source
+ for source_name in sources:
+ if sources[source_name]['source'] == str(source):
+ return str(sources[source_name]).split()[0]
+
+ # if source list exhausted and no valid name found return Unknown
+ return 'Unknown'
+
+ @staticmethod
+ # Get the source ID for sourceName
+ def get_source(sourceName, sources):
+ for source_name in sources:
+ if source_name == sourceName:
+ return str(sources[source_name]['source'])
+
+ # if source list exhausted and no valid name found return Unknown
+ return 'Unknown'
+
+ # ########################################################################################
+ # Apple Music Control and feedback
+ def iTunes_transport_control(self, message):
+ # Transport controls for iTunes
+ try: # If N.MUSIC command, trigger appropriate self.iTunes control
+ if message['State_Update']['state'] not in ["", "Standby"]:
+ self.iTunes.play()
+ except KeyError:
+ pass
+
+ try: # If N.MUSIC selected and Beo4 command received then run appropriate transport commands
+ if message['State_Update']['command'] == "Go/Play":
+ self.iTunes.play()
+
+ elif message['State_Update']['command'] == "Stop":
+ self.iTunes.pause()
+
+ elif message['State_Update']['command'] == "Exit":
+ self.iTunes.stop()
+
+ elif message['State_Update']['command'] == "Step Up":
+ self.iTunes.next_track()
+
+ elif message['State_Update']['command'] == "Step Down":
+ self.iTunes.previous_track()
+
+ elif message['State_Update']['command'] == "Wind":
+ self.iTunes.wind(15)
+
+ elif message['State_Update']['command'] == "Rewind":
+ self.iTunes.rewind(-15)
+
+ elif message['State_Update']['command'] == "Shift-1/Random":
+ self.iTunes.shuffle()
+
+ # If 'Info' pressed - update track info
+ elif message['State_Update']['command'] == "Info":
+ track_info = self.iTunes.get_current_track_info()
+ if track_info[0] not in [None, 'None']:
+ indigo.server.log(
+ "\n\t----------------------------------------------------------------------------"
+ "\n\tiTUNES CURRENT TRACK INFO:"
+ "\n\t============================================================================"
+ "\n\tNow playing: '" + track_info[0] + "'"
+ "\n\t by " + track_info[2] +
+ "\n\t from the album '" + track_info[1] + "'"
+ "\n\t----------------------------------------------------------------------------"
+ "\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n",
+ level=logging.DEBUG
+ )
+
+ self.iTunes.notify(
+ "Now playing: '" + track_info[0] +
+ "' by " + track_info[2] +
+ "from the album '" + track_info[1] + "'",
+ "Apple Music Track Info:"
+ )
+
+ # If 'Guide' pressed - print instructions to indigo log
+ elif message['State_Update']['command'] == "Guide":
+ indigo.server.log(
+ "\n\t----------------------------------------------------------------------------"
+ "\n\tBeo4/BeoRemote One Control of Apple Music"
+ "\n\tKey mapping guide: [Key : Action]"
+ "\n\t============================================================================"
+ "\n\n\t** BASIC TRANSPORT CONTROLS **"
+ "\n\tGO/PLAY : Play"
+ "\n\tSTOP/Pause : Pause"
+ "\n\tEXIT : Stop"
+ "\n\tStep Up/P+ : Next Track"
+ "\n\tStep Down/P- : Previous Track"
+ "\n\tWind : Scan Forwards 15 Seconds"
+ "\n\tRewind : Scan Backwards 15 Seconds"
+ "\n\n\t** FUNCTIONS **"
+ "\n\tShift-1/Random : Toggle Shuffle"
+ "\n\tINFO : Display Track Info for Current Track"
+ "\n\tGUIDE : This Guide"
+ "\n\n\t** ADVANCED CONTROLS **"
+ "\n\tGreen : Shuffle Playlist 'Recently Played'"
+ "\n\tYellow : Play Digital Radio Stations from Playlist Radio"
+ "\n\tRed : More of the Same"
+ "\n\tBlue : Play the Album that the Current Track Resides On\n\n",
+ level=logging.DEBUG
+ )
+
+ # If colour key pressed, execute the appropriate applescript
+ elif message['State_Update']['command'] == "Green":
+ # Play a specific playlist - defaults to Recently Played
+ script = ASBridge.__file__[:-12] + '/Scripts/green.scpt'
+ self.iTunes.run_script(script, self.debug)
+
+ elif message['State_Update']['command'] == "Yellow":
+ # Play a specific playlist - defaults to URL Radio stations
+ script = ASBridge.__file__[:-12] + '/Scripts/yellow.scpt'
+ self.iTunes.run_script(script, self.debug)
+
+ elif message['State_Update']['command'] == "Blue":
+ # Play the current album
+ script = ASBridge.__file__[:-12] + '/Scripts/blue.scpt'
+ self.iTunes.run_script(script, self.debug)
+
+ elif message['State_Update']['command'] in ["0xf2", "Red", "MOTS"]:
+ # More of the same (start a playlist with just current track and let autoplay find similar tunes)
+ script = ASBridge.__file__[:-12] + '/Scripts/red.scpt'
+ self.iTunes.run_script(script, self.debug)
+ except KeyError:
+ pass
+
+ def _get_itunes_track_info(self, message):
+ track_info = self.iTunes.get_current_track_info()
+ if track_info[0] not in [None, 'None']:
+ # Construct track info string
+ track_info_ = "'" + track_info[0] + "' by " + track_info[2] + " from the album '" + track_info[1] + "'"
+
+ # Add now playing info to the message block
+ if 'Type' in message and message['Type'] == "AV RENDERER" and 'source' in message['State_Update'] \
+ and message['State_Update']['source'] == str(self.itunes_source) and \
+ 'nowPlaying' in message['State_Update']:
+ message['State_Update']['nowPlaying'] = track_info_
+ message['State_Update']['nowPlayingDetails']['channel_track'] = int(track_info[3])
+
+ # Print track info to log if trackmode is set to true (via config UI)
+ src = dict(CONST.available_sources).get(self.itunes_source)
+ if self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \
+ track_info_ != self.gateway.states['nowPlaying'] and self.trackmode:
+ indigo.server.log("\n\t----------------------------------------------------------------------------"
+ "\n\tiTUNES CURRENT TRACK INFO:"
+ "\n\t============================================================================"
+ "\n\tNow playing: '" + track_info[0] + "'"
+ "\n\t by " + track_info[2] +
+ "\n\t from the album '" + track_info[1] + "'"
+ "\n\t----------------------------------------------------------------------------"
+ "\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n")
+
+ if self.notifymode:
+ # Post track information to Apple Notification Centre
+ self.iTunes.notify(track_info_ + " from source " + src,
+ "Now Playing:")
+
+ # Update nowPlaying on the gateway device
+ if track_info_ != self.gateway.states['nowPlaying'] and \
+ self.gateway.states['currentAudioSource'] == str(self.itunes_source):
+ self.gateway.updateStateOnServer('nowPlaying', value=track_info_)
+
+ # Update info on active Audio Renderers
+ for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
+ if node.name in self.gateway.states['AudioRenderers']:
+ key_value_list = [
+ {'key': 'onOffState', 'value': True},
+ {'key': 'playState', 'value': 'Play'},
+ {'key': 'source', 'value': src},
+ {'key': 'nowPlaying', 'value': track_info_},
+ {'key': 'channelTrack', 'value': int(track_info[3])},
+ ]
+ node.updateStatesOnServer(key_value_list)
+ node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
+
+ # ########################################################################################
+ # Message Reporting
+ @staticmethod
+ def message_log(name, header, payload, message):
+ # Set reporting level for message logging
+ try: # CLOCK messages are filtered except in debug mode
+ if message['payload_type'] == 'CLOCK':
+ debug_level = logging.DEBUG
+ else: # Everything else is for INFO
+ debug_level = logging.INFO
+ except KeyError:
+ debug_level = logging.INFO
+
+ # Pretty formatting - convert to JSON format then remove braces
+ message = json.dumps(message, indent=4)
+ for r in (('"', ''), (',', ''), ('{', ''), ('}', '')):
+ message = str(message).replace(*r)
+
+ # Print message data
+ if len(payload) + 9 < 73:
+ indigo.server.log("\n\t----------------------------------------------------------------------------" +
+ "\n\t" + name + ": <--DATA-RECEIVED!-<< " +
+ datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
+ "\n\t============================================================================" +
+ "\n\tHeader: " + header +
+ "\n\tPayload: " + payload +
+ "\n\t----------------------------------------------------------------------------" +
+ message, level=debug_level)
+ elif 73 < len(payload) + 9 < 137:
+ indigo.server.log("\n\t----------------------------------------------------------------------------" +
+ "\n\t" + name + ": <--DATA-RECEIVED!-<< " +
+ datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
+ "\n\t============================================================================" +
+ "\n\tHeader: " + header +
+ "\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] +
+ "\n\t----------------------------------------------------------------------------" +
+ message, level=debug_level)
+ else:
+ indigo.server.log("\n\t----------------------------------------------------------------------------" +
+ "\n\t" + name + ": <--DATA-RECEIVED!-<< " +
+ datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
+ "\n\t============================================================================" +
+ "\n\tHeader: " + header +
+ "\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] + "\n\t\t" + payload[137:] +
+ "\n\t----------------------------------------------------------------------------" +
+ message, level=debug_level)
+
+ def notifications(self, name, last_state, new_state):
+ # Post state information to the Apple Notification Centre
+ # Information index:
+ # node.states['playState'], # 0
+ # node.states['source'], # 1
+ # node.states['nowPlaying'], # 2
+ # node.states['channelTrack'], # 3
+ # node.states['volume'], # 4
+ # node.states['mute'], # 5
+ # node.states['onOffState'] # 6
+
+ # Don't post notification if nothing has changed
+ if last_state == new_state:
+ return
+
+ # Source status information
+ if last_state[0] != new_state[0] and new_state[0] == "Standby": # Power off
+ self.iTunes.notify(
+ name + " now in Standby",
+ "Device State Update"
+ )
+ return
+ elif last_state[1] != new_state[1] and new_state[0] != "Standby": # Source Update
+ self.iTunes.notify(
+ name + " now playing from source " + new_state[1],
+ "Device State Update"
+ )
+ return
+ elif last_state[0] != new_state[0] and new_state[0] == "Play": # Power on
+ self.iTunes.notify(
+ name + " Active",
+ "Device State Update"
+ )
+ return
+
+ # Channel/Track information
+ if new_state[2] not in [None, 'None', '', 0, '0', 'Unknown']: # Now Playing Update
+ self.iTunes.notify(
+ new_state[2] + " from source " + new_state[1],
+ name + " Now Playing:"
+ )
+ elif last_state[3] != new_state[3] and new_state[3] not in [0, 255, '0', '255']: # Channel/Track Update
+ self.iTunes.notify(
+ name + " now playing channel/track " + new_state[3] + " from source " + new_state[1],
+ "Device Channel/Track Information"
+ )
+
+ # ########################################################################################
+ # Indigo Server Methods
+ def startup(self):
+ indigo.server.log(u"Startup called")
+
+ # Download the config file from the gateway and initialise the devices
+ config = MLCONFIG.MLConfig(self.host, self.user, self.pwd, self.debug)
+ self.gateway = indigo.devices['Bang and Olufsen Gateway']
+
+ # Create MLGW Protocol and ML_CLI Protocol clients (basic command listening)
+ indigo.server.log('Creating MLGW Protocol Client...', level=logging.WARNING)
+ self.mlgw = MLGW.MLGWClient(self.host, self.port[0], self.user, self.pwd, 'MLGW protocol', self.debug, self.cb)
+ asyncore.loop(count=10, timeout=0.2)
+
+ indigo.server.log('Creating ML Command Line Protocol Client...', level=logging.WARNING)
+ self.mlcli = MLCLI.MLCLIClient(self.host, self.port[2], self.user, self.pwd,
+ 'ML command line interface', self.debug, self.cb)
+ # Log onto the MLCLI client and ascertain the gateway model
+ asyncore.loop(count=10, timeout=0.2)
+
+ # Now MLGW and MasterLink Command Line Client are set up, retrieve MasterLink IDs of products
+ config.get_masterlink_id(self.mlgw, self.mlcli)
+
+ # If the gateway is a BLGW use the BLHIP protocol, else use the legacy MLHIP protocol
+ if self.mlcli.isBLGW:
+ indigo.server.log('Creating BLGW Home Integration Protocol Client...', level=logging.WARNING)
+ self.blgw = BLHIP.BLHIPClient(self.host, self.port[1], self.user, self.pwd,
+ 'BLGW Home Integration Protocol', self.debug, self.cb)
+ self.mltn = None
+ else:
+ indigo.server.log('Creating MLGW Home Integration Protocol Client...', level=logging.WARNING)
+ self.mltn = MLtn.MLtnClient(self.host, self.port[2], self.user, self.pwd, 'ML telnet client',
+ self.debug, self.cb)
+ self.blgw = None
+
+ # Connection polling
+ def check_connection(self, client):
+ last = round(time.time() - client.last_received_at, 2)
+ # Reconnect if socket has disconnected, or if no response received to last ping
+ if not client.is_connected or last > 60:
+ indigo.server.log("\t" + client.name + ": Reconnecting!", level=logging.WARNING)
+ client.handle_close()
+ self.sleep(0.5)
+ client.client_connect()
+
+ # Indigo main program loop
+ def runConcurrentThread(self):
+ try:
+ while True:
+ # Ping all connections every 10 minutes to prompt messages on the network
+ asyncore.loop(count=self.pollinterval, timeout=1)
+ if self.mlgw.is_connected:
+ self.mlgw.ping()
+ if self.mlcli.is_connected:
+ self.mlcli.ping()
+ if self.mlcli.isBLGW:
+ if self.blgw.is_connected:
+ self.blgw.ping()
+ else:
+ if self.mltn.is_connected:
+ self.mltn.ping()
+
+ # Check the connections approximately every 10 minutes to keep sockets open
+ asyncore.loop(count=5, timeout=1)
+ self.check_connection(self.mlgw)
+ self.check_connection(self.mlcli)
+ if self.mlcli.isBLGW:
+ self.check_connection(self.blgw)
+ else:
+ self.check_connection(self.mltn)
+
+ self.sleep(0.5)
+
+ except self.StopThread:
+ raise asyncore.ExitNow('Server is quitting!')
+
+ # Tidy up on shutdown
+ def shutdown(self):
+ indigo.server.log("Shutdown plugin")
+ del self.mlgw
+ del self.mlcli
+ if self.mlcli.isBLGW:
+ del self.blgw
+ else:
+ del self.mltn
+ del self.iTunes
+ raise asyncore.ExitNow('Server is quitting!')