00_SONOS.pm 396 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370
  1. ########################################################################################
  2. #
  3. # SONOS.pm (c) by Reiner Leins, March 2016
  4. # rleins at lmsoft dot de
  5. #
  6. # $Id: 00_SONOS.pm 11082 2016-03-19 12:16:40Z rleins $
  7. #
  8. # FHEM module to commmunicate with a Sonos-System via UPnP
  9. #
  10. ########################################################################################
  11. # !ATTENTION!
  12. # This Module needs additional Perl-Libraries.
  13. # Install:
  14. # * LWP::Simple
  15. # * LWP::UserAgent
  16. # * HTTP::Request
  17. # * SOAP::Lite
  18. #
  19. # e.g. as Debian-Packages (via "sudo apt-get install <packagename>")
  20. # * LWP::Simple-Packagename (incl. LWP::UserAgent and HTTP::Request): libwww-perl
  21. # * SOAP::Lite-Packagename: libsoap-lite-perl
  22. #
  23. # e.g. as Windows ActivePerl (via Perl-Packagemanager)
  24. # * Install Package LWP (incl. LWP::UserAgent and HTTP::Request)
  25. # * Install Package SOAP::Lite
  26. # * SOAP::Lite-Special for Versions after 5.18:
  27. # * Add another Packagesource from suggestions or manual: Bribes de Perl (http://www.bribes.org/perl/ppm)
  28. # * Install Package: SOAP::Lite
  29. #
  30. # Windows ActivePerl 5.20 does currently not work due to missing SOAP::Lite
  31. #
  32. ########################################################################################
  33. # Configuration:
  34. # define <name> SONOS [<host:port> [interval [waittime [delaytime]]]]
  35. #
  36. # where <name> may be replaced by any fhem-devicename string
  37. # <host:port> is the connection identifier to the internal server. Normally "localhost" with a locally free port e.g. "localhost:4711".
  38. # interval is the interval in s, for checking the existence of a ZonePlayer
  39. # waittime is the time to wait for the subprocess. defaults to 8.
  40. # delaytime is the time for delaying the network- and subprocess-part of this module. If the port is longer than neccessary blocked on the subprocess-side, it may be useful.
  41. #
  42. ##############################################
  43. # Example:
  44. # Simplest way to define:
  45. # define Sonos SONOS
  46. #
  47. # Example with control over the used port and the isalive-checker-interval:
  48. # define Sonos SONOS localhost:4711 45
  49. #
  50. ########################################################################################
  51. # Changelog (last 4 entries only, see Wiki for complete changelog)
  52. # Offenes:
  53. # Cover von Amazon funktionieren nicht
  54. #
  55. # SVN-History:
  56. # 19.03.2016
  57. # Bei der Alarmbearbeitung kann man nun mehrere Alarm-IDs mit Komma getrennt angeben, und das Schlüsselwort "All" verwenden, um alle Alarme dieses Players anzusprechen.
  58. # Man kann bei der Alarmbearbeitung nun auch zwei neue, direkte und kürzere, Befehle für Standardaufgaben verwenden: "Enable", "Disable".
  59. # 06.02.2016
  60. # Zusätzlich zu "Mute" (mit Parameter) am Sonos-Device gibt es jetzt auch "MuteOn" und "MuteOff" (jeweils ohne Parameter) zur Verwendung als WebCmd.
  61. # Es gibt ein neues Reading "IsMaster" am Sonosplayer-Device, welches angibt, ob der Player gerade ein Masterplayer ist
  62. # Es gibt ein neues Reading "MasterPlayer" am Sonosplayer-Device, welches angibt, wie der aktuelle MasterPlayer zu diesem Player heißt. Ist der Player selber der Master (also IsMaster = 1), so steht dort der eigene Name drin.
  63. # Es gibt ein neues Reading "SlavePlayer" am Sonosplayer-Device, welches angibt, welche Slaveplayer zu diesem Player zugeordnet sind. Enthält nur Playerdevicenamen, wenn dieser Player ein Masterplayer ist, und dann auch nicht sich selber.
  64. # 31.01.2016
  65. # Im Modul ControlPoint.pm gab es eine fehlerhafte Bearbeitung des Arrays @LWP::Protocol::http::EXTRA_SOCK_OPTS, welche in manchen Fällen zu der Fehlermeldung "Odd number of elements in hash assignment" geführt hat.
  66. # Der Anbieter SoundCloud wird als Quelle erkannt und angezeigt
  67. # Es gibt drei neue Readings "MasterPlayer", "MasterPlayerPlaying" und "MasterPlayerNotPlaying" am Sonos-Device (zzgl. der jeweiligen Angabe der Anzahl)
  68. # Die Ausgabe von "get Sonos Groups" liefert nun stets eine normalisierte Liste (also sortiert).
  69. # 31.12.2015
  70. # Das Reading ZoneGroupID wurde immer länger (mit ":__"), wenn Gruppierungen anderer Player verändert wurden.
  71. # Bei den Settern von "SleepTimer" und "SnoozeAlarm" kann man jetzt auch eine Zahl als Dauer in Sekunden angeben. Dazu wurde auch die Doku entsprechend angepasst.
  72. # In der ControlPoint.pm wurde eine Fehlermeldung korrigiert
  73. #
  74. ########################################################################################
  75. #
  76. # This programm is free software; you can redistribute it and/or modify
  77. # it under the terms of the GNU General Public License as published by
  78. # the Free Software Foundation; either version 2 of the License, or
  79. # (at your option) any later version.
  80. #
  81. # The GNU General Public License can be found at
  82. # http://www.gnu.org/copyleft/gpl.html.
  83. # A copy is found in the textfile GPL.txt and important notices to the license
  84. # from the author is found in LICENSE.txt distributed with these scripts.
  85. #
  86. # This script is distributed in the hope that it will be useful,
  87. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  88. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  89. # GNU General Public License for more details.
  90. #
  91. ########################################################################################
  92. # Use-Declarations
  93. ########################################################################################
  94. package main;
  95. use strict;
  96. use warnings;
  97. use Cwd qw(realpath);
  98. use LWP::Simple;
  99. use LWP::UserAgent;
  100. use URI::Escape;
  101. use HTML::Entities;
  102. use Net::Ping;
  103. use Socket;
  104. use IO::Select;
  105. use IO::Socket::INET;
  106. use File::Path;
  107. use File::stat;
  108. use Time::HiRes qw(usleep gettimeofday);
  109. use Scalar::Util qw(reftype looks_like_number);
  110. use PerlIO::encoding;
  111. use Encode;
  112. use feature 'unicode_strings';
  113. use Digest::MD5 qw(md5_hex);
  114. use File::Temp;
  115. use File::Copy;
  116. use Data::Dumper;
  117. $Data::Dumper::Terse = 1;
  118. use threads;
  119. use Thread::Queue;
  120. use threads::shared;
  121. use feature 'state';
  122. # SmartMatch-Fehlermeldung unterdrücken...
  123. no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  124. ########################################################
  125. # IP-Adressen, die vom UPnP-Modul ignoriert werden sollen.
  126. # Diese können über ein Attribut gesetzt werden.
  127. ########################################################
  128. my %ignoredIPs = ();
  129. my %usedonlyIPs = ();
  130. ########################################################
  131. # Standards aus FHEM einbinden
  132. ########################################################
  133. use vars qw{%attr %defs %intAt %data};
  134. ########################################################
  135. # Prozeduren für den Betrieb des Standalone-Parts
  136. ########################################################
  137. sub Log($$);
  138. sub Log3($$$);
  139. sub SONOS_Log($$$);
  140. sub SONOS_StartClientProcessIfNeccessary($);
  141. sub SONOS_Client_Notifier($);
  142. sub SONOS_Client_ConsumeMessage($$);
  143. sub SONOS_RecursiveStructure($$$$);
  144. sub SONOS_RCLayout();
  145. sub SONOS_URI_Escape($);
  146. ########################################################
  147. # Verrenkungen um in allen Situationen das benötigte
  148. # Modul sauber geladen zu bekommen..
  149. ########################################################
  150. my $gPath = '';
  151. BEGIN {
  152. $gPath = substr($0, 0, rindex($0, '/'));
  153. }
  154. if (lc(substr($0, -7)) eq 'fhem.pl') {
  155. $gPath = $attr{global}{modpath}.'/FHEM';
  156. }
  157. use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './', '/usr/local/FHEM/share/fhem/FHEM/lib');
  158. print 'Current: "'.$0.'", gPath: "'.$gPath."\"\n";
  159. if (lc(substr($0, -7)) eq 'fhem.pl') {
  160. require 'DevIo.pm';
  161. } else {
  162. use UPnP::ControlPoint;
  163. ########################################################
  164. # Change all carp-calls in the UPnP-Module to croak-calls
  165. # This will ensure you can "catch" carp with an enclosing
  166. # "eval{}"-Block
  167. ########################################################
  168. #*UPnP::ControlPoint::carp = \&UPnP::ControlPoint::croak;
  169. }
  170. ########################################################################################
  171. # Variable Definitions
  172. ########################################################################################
  173. my %gets = (
  174. 'Groups' => ''
  175. );
  176. my %sets = (
  177. 'Groups' => 'groupdefinitions',
  178. 'StopAll' => '',
  179. 'Stop' => '',
  180. 'PauseAll' => '',
  181. 'Pause' => '',
  182. 'Mute' => 'state',
  183. 'MuteOn' => '',
  184. 'MuteOff' => '',
  185. 'RescanNetwork' => '',
  186. 'LoadBookmarks' => '[Groupname]',
  187. 'SaveBookmarks' => '[GroupName]',
  188. 'DisableBookmark' => 'groupname',
  189. 'EnableBookmark' => 'groupname'
  190. );
  191. my %SONOS_ProviderList = ('^http:(\/\/.*)' => 'Radio',
  192. '^aac:(\/\/.*)' => 'Radio',
  193. '^\/\/' => 'Bibliothek',
  194. '^x-sonos-spotify:' => 'Spotify',
  195. '^x-sonos-http:amz' => 'Amazon Music',
  196. '^npsdy:' => 'Napster',
  197. '^x-sonos-http:track%3a' => 'SoundCloud');
  198. my @SONOS_PossibleDefinitions = qw(NAME INTERVAL);
  199. my @SONOS_PossibleAttributes = qw(targetSpeakFileHashCache targetSpeakFileTimestamp targetSpeakDir targetSpeakURL targetSpeakMP3FileDir targetSpeakMP3FileConverter SpeakGoogleURL Speak0 Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone getAlarms disable generateVolumeEvent buttonEvents generateProxyAlbumArtURLs proxyCacheTime bookmarkSaveDir bookmarkTitleDefinition bookmarkPlaylistDefinition);
  200. my @SONOS_PossibleReadings = qw(AlarmList AlarmListIDs UserID_Spotify UserID_Napster location SleepTimerVersion Mute OutputFixed HeadphoneConnected Balance Volume Loudness Bass Treble TruePlay SurroundEnable SurroundLevel SubEnable SubGain SubPolarity AudioDelay AudioDelayLeftRear AudioDelayRightRear NightMode DialogLevel AlarmListVersion ZonePlayerUUIDsInGroup ZoneGroupState ZoneGroupID fieldType ZoneGroupName roomName roomNameAlias roomIcon transportState TransportState LineInConnected presence currentAlbum currentArtist currentTitle GroupVolume GroupMute FavouritesVersion RadiosVersion PlaylistsVersion QueueVersion QueueHash GroupMasterPlayer);
  201. # Obsolete Einstellungen...
  202. my $SONOS_UseTelnetForQuestions = 1;
  203. my $SONOS_UseTelnetForQuestions_Host = 'localhost'; # Wird automatisch durch den anfragenden Host ersetzt
  204. my $SONOS_UseTelnetForQuestions_Port = 7072;
  205. # Communication between the two "levels" of threads
  206. my $SONOS_ComObjectTransportQueue = Thread::Queue->new();
  207. my %SONOS_PlayerRestoreRunningUDN :shared = ();
  208. my $SONOS_PlayerRestoreQueue = Thread::Queue->new();
  209. # For triggering the Main-Thread over Telnet-Session
  210. my $SONOS_Thread :shared = -1;
  211. my $SONOS_Thread_IsAlive :shared = -1;
  212. my $SONOS_Thread_PlayerRestore :shared = -1;
  213. my %SONOS_Thread_IsAlive_Counter;
  214. my $SONOS_Thread_IsAlive_Counter_MaxMerci = 2;
  215. # Some Constants
  216. my @SONOS_PINGTYPELIST = qw(none tcp udp icmp syn);
  217. my $SONOS_DEFAULTPINGTYPE = 'syn';
  218. my $SONOS_SUBSCRIPTIONSRENEWAL = 1800;
  219. my $SONOS_DIDLHeader = '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">';
  220. my $SONOS_DIDLFooter = '</DIDL-Lite>';
  221. my $SONOS_GOOGLETRANSLATOR_URL = 'http://translate.google.com/translate_tts?tl=%1$s&client=tw-ob&q=%2$s'; # 1->Sprache, 2->Text
  222. my $SONOS_GOOGLETRANSLATOR_CHUNKSIZE = 95;
  223. # Basis UPnP-Object und Search-Referenzen
  224. my $SONOS_RestartControlPoint = 0;
  225. my $SONOS_Controlpoint;
  226. my $SONOS_Search;
  227. # Devices merken
  228. my %SONOS_UPnPDevice;
  229. # ControlProxies für spätere Aufrufe für jeden ZonePlayer extra sichern
  230. my %SONOS_AVTransportControlProxy;
  231. my %SONOS_RenderingControlProxy;
  232. my %SONOS_GroupRenderingControlProxy;
  233. my %SONOS_ContentDirectoryControlProxy;
  234. my %SONOS_AlarmClockControlProxy;
  235. my %SONOS_AudioInProxy;
  236. my %SONOS_DevicePropertiesProxy;
  237. my %SONOS_GroupManagementProxy;
  238. my %SONOS_MusicServicesProxy;
  239. my %SONOS_ZoneGroupTopologyProxy;
  240. # Subscriptions müssen für die spätere Erneuerung aufbewahrt werden
  241. my %SONOS_TransportSubscriptions;
  242. my %SONOS_RenderingSubscriptions;
  243. my %SONOS_GroupRenderingSubscriptions;
  244. my %SONOS_ContentDirectorySubscriptions;
  245. my %SONOS_AlarmSubscriptions;
  246. my %SONOS_ZoneGroupTopologySubscriptions;
  247. my %SONOS_DevicePropertiesSubscriptions;
  248. my %SONOS_AudioInSubscriptions;
  249. # Bookmark-Daten
  250. my %SONOS_BookmarkQueueHash;
  251. my %SONOS_BookmarkTitleHash;
  252. my %SONOS_BookmarkQueueDefinition;
  253. my %SONOS_BookmarkTitleDefinition;
  254. my %SONOS_BookmarkSpeicher;
  255. $SONOS_BookmarkSpeicher{OldTracks} = ();
  256. $SONOS_BookmarkSpeicher{NumTracks} = ();
  257. $SONOS_BookmarkSpeicher{OldTrackURIs} = ();
  258. $SONOS_BookmarkSpeicher{OldTitles} = ();
  259. $SONOS_BookmarkSpeicher{OldTrackPositions} = ();
  260. $SONOS_BookmarkSpeicher{OldTrackDurations} = ();
  261. $SONOS_BookmarkSpeicher{OldTransportstate} = ();
  262. # Locations -> UDN der einzelnen Player merken, damit die Event-Verarbeitung schneller geht
  263. my %SONOS_Locations;
  264. # Wenn der Prozess/das Modul nicht von fhem aus gestartet wurde, dann versuchen, den ersten Parameter zu ermitteln
  265. # Für diese Funktionalität werden einige Variablen benötigt
  266. my $SONOS_ListenPort = $ARGV[0] if (lc(substr($0, -7)) ne 'fhem.pl');
  267. my $SONOS_Client_LogLevel :shared = -1;
  268. if ($ARGV[1]) {
  269. $SONOS_Client_LogLevel = $ARGV[1];
  270. }
  271. my $SONOS_mseclog = 0;
  272. if ($ARGV[2]) {
  273. $SONOS_mseclog = $ARGV[2];
  274. }
  275. my $SONOS_StartedOwnUPnPServer = 0;
  276. my $SONOS_Client_Selector;
  277. my %SONOS_Client_Data :shared = ();
  278. my $SONOS_Client_NormalQueueWorking :shared = 1;
  279. my $SONOS_Client_SendQueue = Thread::Queue->new();
  280. my $SONOS_Client_SendQueue_Suspend :shared = 0;
  281. my %SONOS_ButtonPressQueue;
  282. ########################################################################################
  283. #
  284. # SONOS_Initialize
  285. #
  286. # Parameter hash = hash of device addressed
  287. #
  288. ########################################################################################
  289. sub SONOS_Initialize ($) {
  290. my ($hash) = @_;
  291. # Provider
  292. $hash->{Clients} = ':SONOSPLAYER:';
  293. # Normal Defines
  294. $hash->{DefFn} = 'SONOS_Define';
  295. $hash->{UndefFn} = 'SONOS_Undef';
  296. $hash->{DeleteFn} = 'SONOS_Delete';
  297. $hash->{ShutdownFn} = 'SONOS_Shutdown';
  298. $hash->{ReadFn} = "SONOS_Read";
  299. $hash->{ReadyFn} = "SONOS_Ready";
  300. $hash->{GetFn} = 'SONOS_Get';
  301. $hash->{SetFn} = 'SONOS_Set';
  302. $hash->{AttrFn} = 'SONOS_Attribute';
  303. $hash->{NotifyFn} = 'SONOS_Notify';
  304. # CGI
  305. my $name = "sonos";
  306. my $fhem_url = "/" . $name ;
  307. $data{FWEXT}{$fhem_url}{FUNC} = "SONOS_FhemWebCallback";
  308. $data{FWEXT}{$fhem_url}{LINK} = $name;
  309. $data{FWEXT}{$fhem_url}{NAME} = undef;
  310. eval {
  311. no strict;
  312. no warnings;
  313. $hash->{AttrList}= 'disable:1,0 pingType:'.join(',', @SONOS_PINGTYPELIST).' usedonlyIPs ignoredIPs targetSpeakDir targetSpeakURL targetSpeakFileTimestamp:1,0 targetSpeakFileHashCache:1,0 targetSpeakMP3FileDir targetSpeakMP3FileConverter SpeakGoogleURL Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover generateProxyAlbumArtURLs:1,0 proxyCacheTime proxyCacheDir bookmarkSaveDir bookmarkTitleDefinition bookmarkPlaylistDefinition '.$readingFnAttributes;
  314. use strict;
  315. use warnings;
  316. };
  317. $data{RC_layout}{Sonos} = "SONOS_RCLayout";
  318. $data{RC_layout}{SonosSVG_Buttons} = "SONOS_RCLayoutSVG1";
  319. $data{RC_layout}{SonosSVG_Icons} = "SONOS_RCLayoutSVG2";
  320. return undef;
  321. }
  322. ########################################################################################
  323. #
  324. # SONOS_RCLayout - Returns the Standard-Layout-Definition for a RemoteControl-Device
  325. #
  326. ########################################################################################
  327. sub SONOS_RCLayout() {
  328. my @rows = ();
  329. push @rows, "Play:PLAY,Pause:PAUSE,Previous:REWIND,Next:FF,:blank,VolumeD:VOLDOWN,VolumeU:VOLUP,:blank,MuteT:MUTE,ShuffleT:SHUFFLE,RepeatT:REPEAT";
  330. push @rows, "attr rc_iconpath icons/remotecontrol";
  331. push @rows, "attr rc_iconprefix black_btn_";
  332. return @rows;
  333. }
  334. ########################################################################################
  335. #
  336. # SONOS_RCLayoutSVG1 - Returns the Standard-Layout-Definition for a RemoteControl-Device
  337. #
  338. ########################################################################################
  339. sub SONOS_RCLayoutSVG1() {
  340. my @rows = ();
  341. push @rows, "Play:rc_PLAY.svg,Pause:rc_PAUSE.svg,Previous:rc_PREVIOUS.svg,Next:rc_NEXT.svg,:blank,VolumeD:rc_VOLDOWN.svg,VolumeU:rc_VOLUP.svg,:blank,MuteT:rc_MUTE.svg,ShuffleT:rc_SHUFFLE.svg,RepeatT:rc_REPEAT.svg";
  342. push @rows, "attr rc_iconpath icons/remotecontrol";
  343. push @rows, "attr rc_iconprefix black_btn_";
  344. return @rows;
  345. }
  346. ########################################################################################
  347. #
  348. # SONOS_RCLayoutSVG2 - Returns the Standard-Layout-Definition for a RemoteControl-Device
  349. #
  350. ########################################################################################
  351. sub SONOS_RCLayoutSVG2() {
  352. my @rows = ();
  353. push @rows, "Play:audio_play.svg,Pause:audio_pause.svg,Previous:audio_rew.svg,Next:audio_ff.svg,:blank,VolumeD:audio_volume_low.svg,VolumeU:audio_volume_high.svg,:blank,MuteT:audio_volume_mute.svg,ShuffleT:audio_shuffle.svg,RepeatT:audio_repeat.svg";
  354. push @rows, "attr rc_iconpath icons/remotecontrol";
  355. push @rows, "attr rc_iconprefix black_btn_";
  356. return @rows;
  357. }
  358. ########################################################################################
  359. #
  360. # SONOS_LoadExportedSonosBibliothek - Sets the internal Value with the given Name in the given fhem-device with the loaded file given with filename
  361. #
  362. ########################################################################################
  363. sub SONOS_LoadExportedSonosBibliothek($$$) {
  364. my ($fileName, $deviceName, $internalName) = @_;
  365. my $fileInhalt = '';
  366. open FILE, '<'.$fileName;
  367. binmode(FILE, ':encoding(utf-8)');
  368. while (<FILE>) {
  369. $fileInhalt .= $_;
  370. }
  371. close FILE;
  372. $defs{$deviceName}->{$internalName} = eval($fileInhalt);
  373. }
  374. ########################################################################################
  375. #
  376. # SONOS_getCoverTitleRG - Returns the Cover- and Title-Readings for use in a ReadingsGroup
  377. #
  378. ########################################################################################
  379. sub SONOS_getCoverTitleRG($;$$) {
  380. my ($device, $width, $space) = @_;
  381. $width = 500 if (!defined($width));
  382. my $transportState = ReadingsVal($device, 'transportState', '');
  383. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  384. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  385. my $currentRuntime = 1;
  386. my $currentStarttime = 0;
  387. my $currentPosition = 0;
  388. my $normalAudio = ReadingsVal($device, 'currentNormalAudio', 0);
  389. if ($normalAudio) {
  390. $currentRuntime = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackDuration', '0:00:01'));
  391. $currentRuntime = 1 if (!$currentRuntime);
  392. $currentPosition = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackPosition', '0:00:00'));
  393. $currentStarttime = SONOS_GetTimeFromString(ReadingsTimestamp($device, 'currentTrackPosition', SONOS_TimeNow())) - $currentPosition;
  394. }
  395. my $playing = 0;
  396. if ($transportState eq 'PLAYING') {
  397. $playing = 1;
  398. $transportState = FW_makeImage('audio_play', 'Playing', 'SONOS_Transportstate');
  399. }
  400. $transportState = FW_makeImage('audio_pause', 'Paused', 'SONOS_Transportstate') if ($transportState eq 'PAUSED_PLAYBACK');
  401. $transportState = FW_makeImage('audio_stop', 'Stopped', 'SONOS_Transportstate') if ($transportState eq 'STOPPED');
  402. my $fullscreenDiv = '<style type="text/css">.SONOS_Transportstate { height: 0.8em; margin-top: -6px; margin-left: 2px; }</style><div id="cover_current'.$device.'" style="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 10000; background-color: rgb(20,20,20);" onclick="document.getElementById(\'cover_current'.$device.'\').style.display = \'none\'; document.getElementById(\'global_fulldiv_'.$device.'\').innerHTML = \'\';"><div style="width: 100%; top 5px; text-align: center; font-weight: bold; color: lightgray; font-size: 200%;">'.ReadingsVal($device, 'roomName', $device).$transportState.'</div><div style="position: relative; top: 8px; height: 86%; max-width: 100%; text-align: center;"><img style="height: 100%; width: auto; border: 1px solid lightgray;" src="'.((lc($presence) eq 'disappeared') ? '/fhem/sonos/cover/empty.jpg' : ReadingsVal($device, 'currentAlbumArtURL', '')).'"/></div><div style="position: absolute; width: 100%; bottom: 8px; padding: 5px; text-align: center; font-weight: bold; color: lightgray; background-color: rgb(20,20,20); font-size: 120%;">'.((lc($presence) eq 'disappeared') ? 'Player disappeared' : ReadingsVal($device, 'infoSummarize1', '')).'</div><div id="hash_'.$device.'" style="display: none; color: white;">'.md5_hex(ReadingsVal($device, 'roomName', $device).ReadingsVal($device, 'infoSummarize2', '').ReadingsVal($device, 'currentTrackPosition', '').ReadingsVal($device, 'currentAlbumArtURL', '')).'</div>'.(($normalAudio) ? '<div id="prog_runtime_'.$device.'" style="display: none; color: white;">'.$currentRuntime.'</div><div id="prog_starttime_'.$device.'" style="display: none; color: white;">'.$currentStarttime.'</div><div id="prog_playing_'.$device.'" style="display: none; color: white;">'.$playing.'</div><div id="progress'.$device.'" style="position: absolute; bottom: 0px; width: 100%; height: 2px; border: 1px solid #000; overflow: hidden;"><div id="progressbar'.$device.'" style="width: '.(($currentPosition * 100) / $currentRuntime).'%; height: 2px; border-right: 1px solid #000000; background: #d65946;"></div></div>' : '').'</div>';
  403. my $javascriptTimer = 'function refreshTime'.$device.'() {
  404. var playing = document.getElementById("prog_playing_'.$device.'");
  405. if (!playing || (playing && (playing.innerHTML == "0"))) {
  406. return;
  407. }
  408. var runtime = document.getElementById("prog_runtime_'.$device.'");
  409. var starttime = document.getElementById("prog_starttime_'.$device.'");
  410. if (runtime && starttime) {
  411. var now = new Date().getTime();
  412. var percent = (Math.round(now / 10.0) - Math.round(starttime.innerHTML * 100.0)) / runtime.innerHTML;
  413. document.getElementById("progressbar'.$device.'").style.width = percent + "%";
  414. setTimeout(refreshTime'.$device.', 100);
  415. }
  416. }';
  417. my $javascriptText = '<script type="text/javascript">
  418. if (!document.getElementById("global_fulldiv_'.$device.'")) {
  419. var newDiv = document.createElement("div");
  420. newDiv.setAttribute("id", "global_fulldiv_'.$device.'");
  421. document.body.appendChild(newDiv);
  422. var newScript = document.createElement("script");
  423. newScript.setAttribute("type", "text/javascript");
  424. newScript.appendChild(document.createTextNode(\'function refreshFull'.$device.'() {
  425. var fullDiv = document.getElementById("element_fulldiv_'.$device.'");
  426. if (!fullDiv) {
  427. return;
  428. }
  429. var elementHTML = decodeURIComponent(fullDiv.innerHTML);
  430. var global = document.getElementById("global_fulldiv_'.$device.'");
  431. var oldGlobal = global.innerHTML;
  432. var hash = document.getElementById("hash_'.$device.'");
  433. var hashMatch = /<div id="hash_'.$device.'".*?>(.+?)<.div>/i;
  434. hashMatch.exec(elementHTML);
  435. if ((oldGlobal != "") && (!hash || (hash.innerHTML != RegExp.$1))) {
  436. global.innerHTML = elementHTML;
  437. }
  438. if (oldGlobal != "") {
  439. setTimeout(refreshFull'.$device.', 1000);
  440. var playing = document.getElementById("prog_playing_'.$device.'");
  441. if (playing && playing.innerHTML == "1") {
  442. setTimeout(refreshTime'.$device.', 100);
  443. }
  444. }
  445. } '.$javascriptTimer.'\'));
  446. document.body.appendChild(newScript);
  447. }
  448. </script>';
  449. $javascriptText =~ s/\n/ /g;
  450. return $javascriptText.'<div style="float: left;" onclick="document.getElementById(\'global_fulldiv_'.$device.'\').innerHTML = \'&nbsp;\'; refreshFull'.$device.'(); '.($playing ? 'refreshTime'.$device.'();' : '').'">'.SONOS_getCoverRG($device).'</div><div style="display: none;" id="element_fulldiv_'.$device.'">'.SONOS_URI_Escape($fullscreenDiv).'</div><div style="margin-left: 150px; min-width: '.$width.'px;">'.SONOS_getTitleRG($device, $space).'</div>';
  451. }
  452. ########################################################################################
  453. #
  454. # SONOS_getCoverRG - Returns the Cover-Readings for use in a ReadingsGroup
  455. #
  456. ########################################################################################
  457. sub SONOS_getCoverRG($;$) {
  458. my ($device, $height) = @_;
  459. $height = '10.75em' if (!defined($height));
  460. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  461. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  462. return '<img style="margin-right: 5px; border: 1px solid lightgray; height: '.$height.'" src="'.((lc($presence) eq 'disappeared') ? '/fhem/sonos/cover/empty.jpg' : ReadingsVal($device, 'currentAlbumArtURL', '')).'" />';
  463. }
  464. ########################################################################################
  465. #
  466. # SONOS_getTitleRG - Returns the Title-Readings for use in a ReadingsGroup
  467. #
  468. ########################################################################################
  469. sub SONOS_getTitleRG($;$) {
  470. my ($device, $space) = @_;
  471. $space = '1em' if (!defined($space));
  472. $space .= 'px' if (looks_like_number($space));
  473. # Wenn der Player weg ist, nur eine Kurzinfo dazu anzeigen
  474. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  475. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  476. if (lc($presence) eq 'disappeared') {
  477. return '<div style="margin-left: -150px;">Player disappeared</div>';
  478. }
  479. my $infoString = '';
  480. my $transportState = ReadingsVal($device, 'transportState', '');
  481. $transportState = 'Spiele' if ($transportState eq 'PLAYING');
  482. $transportState = 'Pausiere' if ($transportState eq 'PAUSED_PLAYBACK');
  483. $transportState = 'Stop bei' if ($transportState eq 'STOPPED');
  484. # 55
  485. # Läuft Radio oder ein "normaler" Titel
  486. my $currentNormalAudio = ReadingsVal($device, 'currentNormalAudio', 1);
  487. $currentNormalAudio = 1 if (SONOS_Trim($currentNormalAudio) eq '');
  488. if ($currentNormalAudio == 1) {
  489. my $showNext = ReadingsVal($device, 'nextTitle', '') || ReadingsVal($device, 'nextArtist', '') || ReadingsVal($device, 'nextAlbum', '');
  490. $infoString = sprintf('<div style="margin-left: -150px;">%s Titel %s von %s (%s)<br />Titel: <b>%s</b><br />Interpret: <b>%s</b><br />Album: <b>%s</b>'.($showNext ? '<div style="height: %s;"></div>Nächste Wiedergabe (%s):</div><div style="float: left; margin-left: 0px;"><img style="margin: 0px; padding: 0px; margin-right: 5px; border: 1px solid lightgray; height: 3.5em;" border="0" src="%s" /></div><div style="margin-left: 0px;">Titel: %s<br />Interpret: %s<br />Album: %s</div>' : ''),
  491. $transportState,
  492. ReadingsVal($device, 'currentTrack', ''),
  493. ReadingsVal($device, 'numberOfTracks', ''),
  494. ReadingsVal($device, 'currentTrackProvider', ''),
  495. ReadingsVal($device, 'currentTitle', ''),
  496. ReadingsVal($device, 'currentArtist', ''),
  497. ReadingsVal($device, 'currentAlbum', ''),
  498. $space,
  499. ReadingsVal($device, 'nextTrackProvider', ''),
  500. ReadingsVal($device, 'nextAlbumArtURL', ''),
  501. ReadingsVal($device, 'nextTitle', ''),
  502. ReadingsVal($device, 'nextArtist', ''),
  503. ReadingsVal($device, 'nextAlbum', ''));
  504. } else {
  505. $infoString = sprintf('<div style="margin-left: -150px;">%s Radiostream<br />Sender: <b>%s</b><br />Info: <b>%s</b><br />Läuft: <b>%s</b></div>',
  506. $transportState,
  507. ReadingsVal($device, 'currentSender', ''),
  508. ReadingsVal($device, 'currentSenderInfo', ''),
  509. ReadingsVal($device, 'currentSenderCurrent', ''));
  510. }
  511. return $infoString;
  512. }
  513. ########################################################################################
  514. #
  515. # SONOS_getListRG - Returns the approbriate list-Reading for use in a ReadingsGroup
  516. #
  517. ########################################################################################
  518. sub SONOS_getListRG($$;$) {
  519. my ($device, $reading, $ul) = @_;
  520. $ul = 0 if (!defined($ul));
  521. my $resultString = '';
  522. # Manchmal ist es etwas komplizierter mit den Zeichensätzen...
  523. my %elems = %{eval(decode('CP1252', ReadingsVal($device, $reading, '{}')))};
  524. for my $key (keys %elems) {
  525. my $command;
  526. if ($reading eq 'Favourites') {
  527. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartFavourite '.SONOS_URI_Escape($elems{$key}->{Title}));
  528. } elsif ($reading eq 'Playlists') {
  529. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartPlaylist '.SONOS_URI_Escape($elems{$key}->{Title}));
  530. } elsif ($reading eq 'Radios') {
  531. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartRadio '.SONOS_URI_Escape($elems{$key}->{Title}));
  532. }
  533. $command = "FW_cmd('/fhem?XHR=1&$command')";
  534. if ($ul) {
  535. $resultString .= '<li style="list-style-type: none; display: inline;"><a onclick="'.$command.'"><img style="border: solid 1px lightgray; margin: 3px;" width="70" src="'.$elems{$key}->{Cover}.'" /></a></li>';
  536. } else {
  537. $resultString .= '<tr><td><img width="70" src="'.$elems{$key}->{Cover}.'" /></td><td><a onclick="'.$command.'">'.$elems{$key}->{Title}."</a></td></tr>\n";
  538. }
  539. }
  540. if ($ul) {
  541. return '<ul style="margin-left: 0px; padding-left: 0px; list-style-type: none; display: inline;">'.$resultString.'</ul>';
  542. } else {
  543. return '<table>'.$resultString.'</table>';
  544. }
  545. }
  546. ########################################################################################
  547. #
  548. # SONOS_getGroupsRG - Returns a simple group-constellation-list for use in a ReadingsGroup
  549. #
  550. ########################################################################################
  551. sub SONOS_getGroupsRG() {
  552. my $groups = CommandGet(undef, SONOS_getDeviceDefHash(undef)->{NAME}.' Groups');
  553. my $result = '<ul>';
  554. my $i = 0;
  555. while ($groups =~ m/\[(.*?)\]/ig) {
  556. my @member = split(/, /, $1);
  557. @member = map FW_makeImage('icoSONOSPLAYER_icon-'.ReadingsVal($_, 'playerType', '').'.png', '', '').ReadingsVal($_, 'roomNameAlias', $_), @member;
  558. $result .= '<li>'.++$i.'. Gruppe:<ul style="list-style-type: none; padding-left: 0px;"><li>'.join('</li><li>', @member).'</li></ul></li>';
  559. }
  560. return $result.'</ul>';
  561. }
  562. ########################################################################################
  563. #
  564. # SONOS_FhemWebCallback - Implements a Webcallback e.g. a small proxy for Cover-images.
  565. #
  566. ########################################################################################
  567. sub SONOS_FhemWebCallback($) {
  568. my ($URL) = @_;
  569. SONOS_Log undef, 5, 'FhemWebCallback: '.$URL;
  570. # Einfache Grundprüfungen
  571. return ("text/html; charset=UTF8", 'Forbidden call: '.$URL) if ($URL !~ m/^\/sonos\//i);
  572. $URL =~ s/^\/sonos//i;
  573. # Proxy-Features...
  574. if ($URL =~ m/^\/proxy\//i) {
  575. return ("text/html; charset=UTF8", 'No Proxy configured: '.$URL) if (!AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'generateProxyAlbumArtURLs', 0));
  576. my $proxyCacheTime = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'proxyCacheTime', 0);
  577. my $proxyCacheDir = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'proxyCacheDir', '/tmp');
  578. $proxyCacheDir =~ s/\\/\//g;
  579. # Zurückzugebende Adresse ermitteln...
  580. my $albumurl = uri_unescape($1) if ($URL =~ m/^\/proxy\/aa\?url=(.*)/i);
  581. $albumurl =~ s/&apos;/'/ig;
  582. # Nur für Sonos-Player den Proxy spielen (und für Spotify-Links)
  583. my $ip = '';
  584. $ip = $1 if ($albumurl =~ m/^http:\/\/(.*?)[:\/]/i);
  585. for my $player (SONOS_getAllSonosplayerDevices()) {
  586. if (ReadingsVal($player->{NAME}, 'location', '') =~ m/^http:\/\/$ip:/i) {
  587. undef($ip);
  588. last;
  589. }
  590. }
  591. return ("text/html; charset=UTF8", 'Call for Non-Sonos-Player: '.$URL) if (defined($ip) && $albumurl !~ /\.cloudfront.net\//i && $albumurl !~ /\.scdn.co\/image\//i && $albumurl !~ /\/music\/image\?/i);
  592. # Generierter Dateiname für die Cache-Funktionalitaet
  593. my $albumHash;
  594. # Schauen, ob die Datei aus dem Cache bedient werden kann...
  595. if ($proxyCacheTime) {
  596. eval {
  597. require Digest::SHA1;
  598. import Digest::SHA1 qw(sha1_hex);
  599. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc($albumurl)).'.image';
  600. };
  601. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  602. # FallBack auf Digest::SHA durchführen...
  603. eval {
  604. require Digest::SHA;
  605. import Digest::SHA qw(sha1_hex);
  606. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc($albumurl)).'.image';
  607. };
  608. }
  609. if ($@) {
  610. SONOS_Log undef, 1, 'Problem while generating Hashvalue: '.$@;
  611. return(undef, undef);
  612. }
  613. if ((-e $albumHash) && ((stat($albumHash)->mtime) + $proxyCacheTime > gettimeofday())) {
  614. SONOS_Log undef, 5, 'Cover wird aus Cache bedient: '.$albumHash.' ('.$albumurl.')';
  615. $albumHash =~ m/(.*)\/(.*)\.(.*)/;
  616. FW_serveSpecial($2, $3, $1, 1);
  617. return(undef, undef);
  618. }
  619. }
  620. # Bild vom Player holen...
  621. my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11');
  622. my $response = $ua->get($albumurl);
  623. if ($response->is_success) {
  624. SONOS_Log undef, 5, 'Cover wurde neu geladen: '.$albumurl;
  625. my $tempFile;
  626. if ($proxyCacheTime) {
  627. unlink $albumHash if (-e $albumHash);
  628. SONOS_Log undef, 5, 'Cover wird im Cache abgelegt: '.$albumHash.' ('.$albumurl.')';
  629. } else {
  630. # Da wir die Standard-Prozedur 'FW_serveSpecial' aus 'FHEMWEB' verwenden moechten, brauchen wir eine lokale Datei
  631. $tempFile = File::Temp->new(SUFFIX => '.image');
  632. $albumHash = $tempFile->filename;
  633. $albumHash =~ s/\\/\//g;
  634. SONOS_Log undef, 5, 'TempFilename: '.$albumHash;
  635. }
  636. # Either Tempfile or Cachefile...
  637. SONOS_WriteFile($albumHash, $response->content);
  638. $albumHash =~ m/(.*)\/(.*)\.(.*)/;
  639. FW_serveSpecial($2, $3, $1, 1);
  640. return (undef, undef);
  641. } else {
  642. SONOS_Log undef, 1, 'Cover couldn\'t be loaded: '.$albumurl;
  643. FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  644. return (undef, undef);
  645. }
  646. }
  647. # Cover-Features...
  648. if ($URL =~ m/^\/cover\//i) {
  649. $URL =~ s/^\/cover//i;
  650. SONOS_Log undef, 5, 'Cover: '.$URL;
  651. if ($URL =~ m/^\/empty.jpg/i) {
  652. FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  653. return (undef, undef);
  654. }
  655. if ($URL =~ m/^\/playlist.jpg/i) {
  656. FW_serveSpecial('sonos_playlist', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  657. return (undef, undef);
  658. }
  659. if ($URL =~ m/^\/input_default.jpg/i) {
  660. FW_serveSpecial('sonos_input_default', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  661. return (undef, undef);
  662. }
  663. if ($URL =~ m/^\/input_tv.jpg/i) {
  664. FW_serveSpecial('sonos_input_tv', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  665. return (undef, undef);
  666. }
  667. if ($URL =~ m/^\/input_dock.jpg/i) {
  668. FW_serveSpecial('sonos_input_dock', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  669. return (undef, undef);
  670. }
  671. }
  672. ## FolderCover-Features...
  673. #if ($URL =~ m/^\/foldercover\//i) {
  674. # $URL =~ s/^\/foldercover//i;
  675. #
  676. # SONOS_Log undef, 0, 'FolderCover: '.$URL;
  677. #
  678. # # Da wir die Standard-Prozedur 'FW_serveSpecial' aus 'FHEMWEB' verwenden moechten, brauchen wir eine lokale Datei
  679. # my $tempFile = File::Temp->new(SUFFIX => '.image');
  680. # my $filename = $tempFile->filename;
  681. # $filename =~ s/\\/\//g;
  682. # SONOS_Log undef, 5, 'TempFilename: '.$filename;
  683. #
  684. #
  685. #}
  686. # Wenn wir hier ankommen, dann konnte nichts verarbeitet werden...
  687. return ("text/html; charset=UTF8", 'Call failure: '.$URL);
  688. }
  689. ########################################################################################
  690. #
  691. # SONOS_Define - Implements DefFn function
  692. #
  693. # Parameter hash = hash of device addressed
  694. # def = definition string
  695. #
  696. ########################################################################################
  697. sub SONOS_Define($$) {
  698. my ($hash, $def) = @_;
  699. my @a = split("[ \t]+", $def);
  700. # check syntax
  701. return 'Usage: define <name> SONOS [[[[upnplistener] interval] waittime] delaytime]' if($#a < 1 || $#a > 5);
  702. my $name = $a[0];
  703. my $upnplistener;
  704. if ($a[2] && !looks_like_number($a[2])) {
  705. $upnplistener = $a[2];
  706. } else {
  707. $upnplistener = 'localhost:4711';
  708. }
  709. my $interval;
  710. if (looks_like_number($a[3])) {
  711. $interval = $a[3];
  712. if ($interval < 10) {
  713. SONOS_Log undef, 0, 'Interval has to be a minimum of 10 sec. and not: '.$interval;
  714. $interval = 10;
  715. }
  716. } else {
  717. $interval = 30;
  718. }
  719. my $waittime;
  720. if (looks_like_number($a[4])) {
  721. $waittime = $a[4];
  722. } else {
  723. $waittime = 8;
  724. }
  725. my $delaytime;
  726. if (looks_like_number($a[5])) {
  727. $delaytime = $a[5];
  728. } else {
  729. $delaytime = 0;
  730. }
  731. $hash->{NAME} = $name;
  732. $hash->{DeviceName} = $upnplistener;
  733. $hash->{INTERVAL} = $interval;
  734. $hash->{WAITTIME} = $waittime;
  735. $hash->{DELAYTIME} = $delaytime;
  736. $hash->{STATE} = 'waiting for subprocess...';
  737. if (AttrVal($hash->{NAME}, 'disable', 0) == 0) {
  738. if ($hash->{DELAYTIME}) {
  739. InternalTimer(gettimeofday() + $hash->{DELAYTIME}, 'SONOS_DelayStart', $hash, 0);
  740. } else {
  741. InternalTimer(gettimeofday() + 1, 'SONOS_DelayStart', $hash, 0);
  742. }
  743. }
  744. return undef;
  745. }
  746. ########################################################################################
  747. #
  748. # SONOS_DelayStart - Starts the SubProcess with a Delay. Can solute problems with blocked Ports
  749. #
  750. ########################################################################################
  751. sub SONOS_DelayStart($) {
  752. my ($hash) = @_;
  753. return undef if (AttrVal($hash->{NAME}, 'disable', 0));
  754. # Prüfen, ob ein Server erreichbar wäre, und wenn nicht, einen Server starten
  755. SONOS_StartClientProcessIfNeccessary($hash->{DeviceName});
  756. InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
  757. }
  758. ########################################################################################
  759. #
  760. # SONOS_DelayOpenDev - Starts the IO-Connection with a Delay.
  761. #
  762. ########################################################################################
  763. sub SONOS_DelayOpenDev($) {
  764. my ($hash) = @_;
  765. # Die Datenverbindung zu dem gemachten Server hier starten und initialisieren
  766. DevIo_OpenDev($hash, 0, "SONOS_InitClientProcessLater");
  767. }
  768. ########################################################################################
  769. #
  770. # SONOS_Attribute - Implements AttrFn function
  771. #
  772. ########################################################################################
  773. sub SONOS_Attribute($$$@) {
  774. my ($mode, $devName, $attrName, $attrValue) = @_;
  775. my $disableChange = 0;
  776. if ($mode eq 'set') {
  777. if ($attrName eq 'verbose') {
  778. SONOS_DoWork('undef', 'setVerbose', $attrValue);
  779. } elsif ($attrName eq 'disable') {
  780. if ($attrValue && AttrVal($devName, $attrName, 0) != 1) {
  781. SONOS_Log(undef, 5, 'Neu-Disabled');
  782. $disableChange = 1;
  783. }
  784. if (!$attrValue && AttrVal($devName, $attrName, 0) != 0) {
  785. SONOS_Log(undef, 5, 'Neu-Enabled');
  786. $disableChange = 1;
  787. }
  788. }
  789. } elsif ($mode eq 'del') {
  790. if ($attrName eq 'disable') {
  791. if (AttrVal($devName, $attrName, 0) != 0) {
  792. SONOS_Log(undef, 5, 'Deleted-Disabled');
  793. $disableChange = 1;
  794. $attrValue = 0;
  795. }
  796. }
  797. }
  798. if ($disableChange) {
  799. my $hash = SONOS_getDeviceDefHash(undef);
  800. # Wenn der Prozess beendet werden muss...
  801. if ($attrValue) {
  802. SONOS_Log undef, 5, 'Call AttributeFn: Stop SubProcess...';
  803. InternalTimer(gettimeofday() + 1, 'SONOS_StopSubProcess', $hash, 0);
  804. }
  805. # Wenn der Prozess gestartet werden muss...
  806. if (!$attrValue) {
  807. SONOS_Log undef, 5, 'Call AttributeFn: Start SubProcess...';
  808. InternalTimer(gettimeofday() + 1, 'SONOS_DelayStart', $hash, 0);
  809. }
  810. }
  811. return undef;
  812. }
  813. ########################################################################################
  814. #
  815. # SONOS_StopSubProcess - Tries to stop the subprocess
  816. #
  817. ########################################################################################
  818. sub SONOS_StopSubProcess($) {
  819. my ($hash) = @_;
  820. # Den SubProzess beenden, wenn wir ihn selber gestartet haben
  821. if ($SONOS_StartedOwnUPnPServer) {
  822. # DevIo_OpenDev($hash, 1, undef);
  823. DevIo_SimpleWrite($hash, "shutdown\n", 0);
  824. DevIo_CloseDev($hash);
  825. setReadingsVal($hash, "state", 'disabled', TimeNow());
  826. $hash->{STATE} = 'disabled';
  827. # Alle SonosPlayer-Devices disappearen
  828. for my $player (SONOS_getAllSonosplayerDevices()) {
  829. readingsBeginUpdate($player);
  830. SONOS_readingsBulkUpdateIfChanged($player, 'presence', 'disappeared');
  831. SONOS_readingsBulkUpdateIfChanged($player, 'state', 'disappeared');
  832. SONOS_readingsEndUpdate($player, 1);
  833. if (AttrVal($player->{NAME}, 'stateVariable', '') eq 'Presence') {
  834. $player->{STATE} = 'disappeared';
  835. }
  836. }
  837. }
  838. }
  839. ########################################################################################
  840. #
  841. # SONOS_Notify - Implements NotifyFn function
  842. #
  843. ########################################################################################
  844. sub SONOS_Notify() {
  845. my ($hash, $notifyhash) = @_;
  846. # Bei einem globalen save auch die bookmarks sichern...
  847. if (($notifyhash->{NAME} eq 'global') && ($notifyhash->{CHANGED}[0] eq 'SAVE')) {
  848. SONOS_DoWork('SONOS', 'SaveBookmarks', '');
  849. }
  850. return undef;
  851. }
  852. ########################################################################################
  853. #
  854. # SONOS_Ready - Implements ReadyFn function
  855. #
  856. # Parameter hash = hash of device addressed
  857. #
  858. ########################################################################################
  859. sub SONOS_Ready($) {
  860. my ($hash) = @_;
  861. return DevIo_OpenDev($hash, 1, "SONOS_InitClientProcessLater");
  862. }
  863. ########################################################################################
  864. #
  865. # SONOS_Read - Implements ReadFn function
  866. #
  867. # Parameter hash = hash of device addressed
  868. #
  869. ########################################################################################
  870. sub SONOS_Read($) {
  871. my ($hash) = @_;
  872. # Bis zum letzten (damit der Puffer leer ist) Zeilenumbruch einlesen, da SimpleRead immer nur 256-Zeichen-Päckchen einliest.
  873. my $buf = DevIo_DoSimpleRead($hash);
  874. # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden...
  875. if (!defined($buf) || ($buf eq '')) {
  876. if (!AttrVal($hash->{NAME}, 'disable', 0)) {
  877. SONOS_Log undef, 1, 'Nothing could be read from TCP-Channel (the first level) even though the Read-Function was called.';
  878. # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden...
  879. RemoveInternalTimer($hash);
  880. DevIo_SimpleWrite($hash, "disconnect\n", 0);
  881. DevIo_CloseDev($hash);
  882. # Neu anstarten...
  883. SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer);
  884. InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
  885. }
  886. return;
  887. }
  888. # Wenn noch nicht alles gekommen ist, dann hier auf den Rest warten...
  889. while (substr($buf, -1, 1) ne "\n") {
  890. my $newRead = DevIo_SimpleRead($hash);
  891. # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden...
  892. if (!defined($newRead) || ($newRead eq '')) {
  893. if (!AttrVal($hash->{NAME}, 'disable', 0)) {
  894. SONOS_Log undef, 1, 'Nothing could be read from TCP-Channel (the second level) even though the Read-Function was called. The client is now directed to shutdown and the connection should be re-initialized...';
  895. # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden...
  896. RemoveInternalTimer($hash);
  897. DevIo_SimpleWrite($hash, "disconnect\n", 0);
  898. DevIo_CloseDev($hash);
  899. # Neu anstarten...
  900. SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer);
  901. InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
  902. }
  903. return;
  904. }
  905. # Wenn es neue Daten gibt, dann anhängen...
  906. $buf .= $newRead;
  907. }
  908. # Die aktuellen Abspielinformationen werden Schritt für Schritt übertragen, gesammelt und dann in einem Rutsch ausgewertet.
  909. # Dafür eignet sich eine Sub-Statische Variable am Besten.
  910. state %current;
  911. # Hier könnte jetzt eine ganze Liste von Anweisungen enthalten sein, die jedoch einzeln verarbeitet werden müssen
  912. # Dabei kann der Trenner ein Zeilenumbruch sein, oder ein Tab-Zeichen.
  913. foreach my $line (split(/[\n\a]/, $buf)) {
  914. # Abschließende Zeilenumbrüche abschnippeln
  915. $line =~ s/[\r\n]*$//;
  916. SONOS_Log undef, 5, "Received from UPnP-Server: '$line'";
  917. # Hier empfangene Werte verarbeiten
  918. if ($line =~ m/^ReadingsSingleUpdateIfChanged:(.*?):(.*?):(.*)/) {
  919. if (lc($1) eq 'undef') {
  920. SONOS_readingsSingleUpdateIfChanged(SONOS_getDeviceDefHash(undef), $2, $3, 1);
  921. } else {
  922. my $hash = SONOS_getSonosPlayerByUDN($1);
  923. if ($hash) {
  924. SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 1);
  925. } else {
  926. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChanged: $1:$2:$3";
  927. }
  928. }
  929. } elsif ($line =~ m/^ReadingsSingleUpdateIfChangedNoTrigger:(.*?):(.*?):(.*)/) {
  930. if (lc($1) eq 'undef') {
  931. SONOS_readingsSingleUpdateIfChanged(SONOS_getDeviceDefHash(undef), $2, $3, 0);
  932. } else {
  933. my $hash = SONOS_getSonosPlayerByUDN($1);
  934. if ($hash) {
  935. SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 0);
  936. } else {
  937. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChangedNoTrigger: $1:$2:$3";
  938. }
  939. }
  940. } elsif ($line =~ m/^ReadingsSingleUpdate:(.*?):(.*?):(.*)/) {
  941. if (lc($1) eq 'undef') {
  942. readingsSingleUpdate(SONOS_getDeviceDefHash(undef), $2, $3, 1);
  943. } else {
  944. my $hash = SONOS_getSonosPlayerByUDN($1);
  945. if ($hash) {
  946. readingsSingleUpdate($hash, $2, $3, 1);
  947. } else {
  948. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdate: $1:$2:$3";
  949. }
  950. }
  951. } elsif ($line =~ m/^ReadingsBulkUpdate:(.*?):(.*?):(.*)/) {
  952. my $hash = undef;
  953. if (lc($1) eq 'undef') {
  954. $hash = SONOS_getDeviceDefHash(undef);
  955. } else {
  956. $hash = SONOS_getSonosPlayerByUDN($1);
  957. }
  958. if ($hash) {
  959. readingsBulkUpdate($hash, $2, $3);
  960. } else {
  961. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdate: $1:$2:$3";
  962. }
  963. } elsif ($line =~ m/^ReadingsBulkUpdateIfChanged:(.*?):(.*?):(.*)/) {
  964. my $hash = undef;
  965. if (lc($1) eq 'undef') {
  966. $hash = SONOS_getDeviceDefHash(undef);
  967. } else {
  968. $hash = SONOS_getSonosPlayerByUDN($1);
  969. }
  970. if ($hash) {
  971. SONOS_readingsBulkUpdateIfChanged($hash, $2, $3);
  972. } else {
  973. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdateIfChanged: $1:$2:$3";
  974. }
  975. } elsif ($line =~ m/ReadingsBeginUpdate:(.*)/) {
  976. my $hash = undef;
  977. if (lc($1) eq 'undef') {
  978. $hash = SONOS_getDeviceDefHash(undef);
  979. } else {
  980. $hash = SONOS_getSonosPlayerByUDN($1);
  981. }
  982. if ($hash) {
  983. readingsBeginUpdate($hash);
  984. } else {
  985. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBeginUpdate: $1";
  986. }
  987. } elsif ($line =~ m/ReadingsEndUpdate:(.*)/) {
  988. my $hash = undef;
  989. if (lc($1) eq 'undef') {
  990. $hash = SONOS_getDeviceDefHash(undef);
  991. } else {
  992. $hash = SONOS_getSonosPlayerByUDN($1);
  993. }
  994. if ($hash) {
  995. readingsEndUpdate($hash, 1);
  996. } else {
  997. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsEndUpdate: $1";
  998. }
  999. } elsif ($line =~ m/CommandDefine:(.*)/) {
  1000. CommandDefine(undef, $1);
  1001. } elsif ($line =~ m/CommandAttr:(.*)/) {
  1002. CommandAttr(undef, $1);
  1003. } elsif ($line =~ m/CommandAttrWithUDN:(.*?):(.*)/) {
  1004. my $hash = SONOS_getSonosPlayerByUDN($1);
  1005. CommandAttr(undef, $hash->{NAME}.' '.$2);
  1006. } elsif ($line =~ m/CommandDeleteAttr:(.*)/) {
  1007. CommandDeleteAttr(undef, $1);
  1008. } elsif ($line =~ m/deleteCurrentNextTitleInformationAndDisappear:(.*)/) {
  1009. my $hash = SONOS_getSonosPlayerByUDN($1);
  1010. # Start the updating...
  1011. readingsBeginUpdate($hash);
  1012. # Updating...
  1013. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", '');
  1014. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", '');
  1015. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", '');
  1016. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", '');
  1017. SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", 'Disappeared');
  1018. SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", '');
  1019. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", '');
  1020. SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", '');
  1021. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", '');
  1022. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtURL", '/fhem/sonos/cover/empty.jpg');
  1023. SONOS_readingsBulkUpdateIfChanged($hash, "currentSender", '');
  1024. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderCurrent", '');
  1025. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderInfo", '');
  1026. SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", 0);
  1027. SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", 1);
  1028. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", '');
  1029. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", '');
  1030. SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", '');
  1031. SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", '');
  1032. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", '');
  1033. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtist", '');
  1034. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtURL", '/fhem/sonos/cover/empty.jpg');
  1035. SONOS_readingsBulkUpdateIfChanged($hash, "nextOriginalTrackNumber", '');
  1036. # End the Bulk-Update, and trigger events...
  1037. SONOS_readingsEndUpdate($hash, 1);
  1038. } elsif ($line =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) {
  1039. my $hash = SONOS_getSonosPlayerByUDN($1);
  1040. if ($hash) {
  1041. %current = SONOS_GetReadingsToCurrentHash($hash->{NAME}, $2);
  1042. } else {
  1043. SONOS_Log undef, 0, "Fehlerhafter Aufruf von GetReadingsToCurrentHash: $1:$2";
  1044. }
  1045. } elsif ($line =~ m/SetCurrent:(.*?):(.*)/) {
  1046. $current{$1} = $2;
  1047. } elsif ($line =~ m/CurrentBulkUpdate:(.*)/) {
  1048. my $hash = SONOS_getSonosPlayerByUDN($1);
  1049. if ($hash) {
  1050. readingsBeginUpdate($hash);
  1051. # Dekodierung durchführen
  1052. $current{Title} = decode_entities($current{Title});
  1053. $current{Artist} = decode_entities($current{Artist});
  1054. $current{Album} = decode_entities($current{Album});
  1055. $current{AlbumArtist} = decode_entities($current{AlbumArtist});
  1056. $current{Sender} = decode_entities($current{Sender});
  1057. $current{SenderCurrent} = decode_entities($current{SenderCurrent});
  1058. $current{SenderInfo} = decode_entities($current{SenderInfo});
  1059. $current{nextTitle} = decode_entities($current{nextTitle});
  1060. $current{nextArtist} = decode_entities($current{nextArtist});
  1061. $current{nextAlbum} = decode_entities($current{nextAlbum});
  1062. $current{nextAlbumArtist} = decode_entities($current{nextAlbumArtist});
  1063. SONOS_readingsBulkUpdateIfChanged($hash, "transportState", $current{TransportState});
  1064. SONOS_readingsBulkUpdateIfChanged($hash, "Shuffle", $current{Shuffle});
  1065. SONOS_readingsBulkUpdateIfChanged($hash, "Repeat", $current{Repeat});
  1066. SONOS_readingsBulkUpdateIfChanged($hash, "RepeatOne", $current{RepeatOne});
  1067. SONOS_readingsBulkUpdateIfChanged($hash, "CrossfadeMode", $current{CrossfadeMode});
  1068. SONOS_readingsBulkUpdateIfChanged($hash, "SleepTimer", $current{SleepTimer});
  1069. SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunning", $current{AlarmRunning});
  1070. SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunningID", $current{AlarmRunningID});
  1071. SONOS_readingsBulkUpdateIfChanged($hash, "numberOfTracks", $current{NumberOfTracks});
  1072. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", $current{Track});
  1073. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", $current{TrackURI});
  1074. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", $current{TrackDuration});
  1075. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", $current{TrackPosition});
  1076. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProvider", $current{TrackProvider});
  1077. SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", $current{Title});
  1078. SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", $current{Artist});
  1079. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", $current{Album});
  1080. SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", $current{OriginalTrackNumber});
  1081. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", $current{AlbumArtist});
  1082. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtURL", $current{AlbumArtURL});
  1083. SONOS_readingsBulkUpdateIfChanged($hash, "currentSender", $current{Sender});
  1084. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderCurrent", $current{SenderCurrent});
  1085. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderInfo", $current{SenderInfo});
  1086. SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", $current{StreamAudio});
  1087. SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", $current{NormalAudio});
  1088. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", $current{nextTrackDuration});
  1089. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", $current{nextTrackURI});
  1090. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProvider", $current{nextTrackProvider});
  1091. SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", $current{nextTitle});
  1092. SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", $current{nextArtist});
  1093. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", $current{nextAlbum});
  1094. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtist", $current{nextAlbumArtist});
  1095. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtURL", $current{nextAlbumArtURL});
  1096. SONOS_readingsBulkUpdateIfChanged($hash, "nextOriginalTrackNumber", $current{nextOriginalTrackNumber});
  1097. SONOS_readingsBulkUpdateIfChanged($hash, "Volume", $current{Volume});
  1098. SONOS_readingsBulkUpdateIfChanged($hash, "Mute", $current{Mute});
  1099. SONOS_readingsBulkUpdateIfChanged($hash, "Balance", $current{Balance});
  1100. SONOS_readingsBulkUpdateIfChanged($hash, "HeadphoneConnected", $current{HeadphoneConnected});
  1101. my $name = $hash->{NAME};
  1102. # If the SomethingChanged-Event should be triggered, do so. It's useful if one would be triggered if even some changes are made, and it's unimportant to exactly know what
  1103. if (AttrVal($name, 'generateSomethingChangedEvent', 0) == 1) {
  1104. readingsBulkUpdate($hash, "somethingChanged", 1);
  1105. }
  1106. # If the Info-Summarize is configured to be triggered. Here one can define a single information-line with all the neccessary informations according to the type of Audio
  1107. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize1', 1);
  1108. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize2', 1);
  1109. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize3', 1);
  1110. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize4', 1);
  1111. # Zusätzlich noch den STATE und das Reading State mit dem vom Anwender gewünschten Wert aktualisieren, Dabei müssen aber doppelte Anführungszeichen vorher maskiert werden...
  1112. SONOS_readingsBulkUpdateIfChanged($hash, 'state', $current{AttrVal($name, 'stateVariable', 'TransportState')});
  1113. # End the Bulk-Update, and trigger events
  1114. SONOS_readingsEndUpdate($hash, 1);
  1115. # Wenn es ein Dock ist, dann noch jeden abspielenden Player mit aktualisieren
  1116. if (ReadingsVal($hash->{NAME}, 'playerType', '') eq 'WD100') {
  1117. my $shortUDN = $1 if ($hash->{UDN} =~ m/(.*)_MR/);
  1118. for my $elem (SONOS_getAllSonosplayerDevices()) {
  1119. # Wenn es ein Player ist, der gerade das Dock wiedergibt, dann diesen Befüllen...
  1120. if (ReadingsVal($elem->{NAME}, 'currentTrackURI', '') eq 'x-sonos-dock:'.$shortUDN) {
  1121. # Alte Werte holen, muss komplett sein, um infoSummarize füllen zu können
  1122. my %currentElem = SONOS_GetReadingsToCurrentHash($elem->{NAME}, 0);
  1123. $currentElem{Title} = $current{Title};
  1124. $currentElem{Artist} = $current{Artist};
  1125. $currentElem{Album} = $current{Album};
  1126. $currentElem{AlbumArtist} = $current{AlbumArtist};
  1127. $currentElem{Track} = $current{Track};
  1128. $currentElem{NumberOfTracks} = $current{NumberOfTracks};
  1129. $currentElem{TrackDuration} = $current{TrackDuration};
  1130. $currentElem{TrackPosition} = $current{TrackPosition};
  1131. $currentElem{TrackProvider} = $current{TrackProvider};
  1132. # Loslegen
  1133. readingsBeginUpdate($elem);
  1134. # Neue Werte setzen
  1135. SONOS_readingsBulkUpdateIfChanged($elem, "currentTitle", $currentElem{Title});
  1136. SONOS_readingsBulkUpdateIfChanged($elem, "currentArtist", $currentElem{Artist});
  1137. SONOS_readingsBulkUpdateIfChanged($elem, "currentAlbum", $currentElem{Album});
  1138. SONOS_readingsBulkUpdateIfChanged($elem, "currentAlbumArtist", $currentElem{AlbumArtist});
  1139. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrack", $currentElem{Track});
  1140. SONOS_readingsBulkUpdateIfChanged($elem, "numberOfTracks", $currentElem{NumberOfTracks});
  1141. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackDuration", $currentElem{TrackDuration});
  1142. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackPosition", $currentElem{TrackPosition});
  1143. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProvider", $currentElem{TrackProvider});
  1144. if (AttrVal($elem->{NAME}, 'generateSomethingChangedEvent', 0) == 1) {
  1145. readingsBulkUpdate($elem, "somethingChanged", 1);
  1146. }
  1147. # InfoSummarize befüllen
  1148. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize1', 1);
  1149. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize2', 1);
  1150. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize3', 1);
  1151. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize4', 1);
  1152. # State-Reading befüllen
  1153. SONOS_readingsBulkUpdateIfChanged($elem, 'state', $currentElem{AttrVal($elem->{NAME}, 'stateVariable', 'TransportState')});
  1154. # Alles verarbeiten lassen
  1155. SONOS_readingsEndUpdate($elem, 1);
  1156. }
  1157. }
  1158. }
  1159. } else {
  1160. SONOS_Log undef, 0, "Fehlerhafter Aufruf von CurrentBulkUpdate: $1";
  1161. }
  1162. } elsif ($line =~ m/ProcessCover:(.*?):(.*?):(.*?):(.*)/) {
  1163. my $hash = SONOS_getSonosPlayerByUDN($1);
  1164. if ($hash) {
  1165. my $name = $hash->{NAME};
  1166. my $nextReading = 'current';
  1167. my $nextName = '';
  1168. if ($2) {
  1169. $nextReading = 'next';
  1170. $nextName = 'Next';
  1171. }
  1172. my $tempURI = $3;
  1173. my $groundURL = $4;
  1174. my $currentValue;
  1175. my $srcURI = '';
  1176. if (defined($tempURI) && $tempURI ne '') {
  1177. if ($tempURI =~ m/getaa.*?x-sonos-spotify%3aspotify%3atrack%3a(.*)%3f/i) {
  1178. my $infos = SONOS_getSpotifyCoverURL($1);
  1179. if ($infos ne '') {
  1180. $srcURI = $infos;
  1181. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg';
  1182. SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1183. } else {
  1184. $srcURI = $groundURL.$tempURI;
  1185. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI);
  1186. SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download failed. Use normal thumbnail: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1187. }
  1188. } elsif ($tempURI =~ m/^\/fhem\/sonos\/cover\/(.*)/i) {
  1189. $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_'.$1;
  1190. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg';
  1191. SONOS_Log undef, 4, "Transport-Event: Cover-Copy: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1192. } else {
  1193. $srcURI = $groundURL.$tempURI;
  1194. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI);
  1195. SONOS_Log undef, 4, "Transport-Event: Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1196. }
  1197. } else {
  1198. $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_empty.jpg';
  1199. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.png';
  1200. SONOS_Log undef, 4, "Transport-Event: CoverArt konnte nicht gefunden werden. Verwende FHEM-Logo. Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1201. }
  1202. mkpath($attr{global}{modpath}.'/www/images/default/SONOSPLAYER/');
  1203. my $filechanged = SONOS_DownloadReplaceIfChanged($srcURI, $currentValue);
  1204. # Icons neu einlesen lassen, falls die Datei neu ist
  1205. SONOS_RefreshIconsInFHEMWEB('/www/images/default/SONOSPLAYER/') if ($filechanged);
  1206. # Die URL noch beim aktuellen Titel mitspeichern
  1207. my $URL = $srcURI;
  1208. if ($URL =~ m/\/lib\/UPnP\/sonos_(.*)/i) {
  1209. $URL = '/fhem/sonos/cover/'.$1;
  1210. } else {
  1211. my $sonosName = SONOS_getDeviceDefHash(undef)->{NAME};
  1212. $URL = '/fhem/sonos/proxy/aa?url='.SONOS_URI_Escape($URL) if (AttrVal($sonosName, 'generateProxyAlbumArtURLs', 0));
  1213. }
  1214. if ($nextReading eq 'next') {
  1215. $current{nextAlbumArtURL} = $URL;
  1216. } else {
  1217. $current{AlbumArtURL} = $URL;
  1218. }
  1219. # This URI change rarely, but the File itself change nearly with every song, so trigger it everytime the content was different to the old one
  1220. if ($filechanged) {
  1221. readingsSingleUpdate($hash, $nextReading.'AlbumArtURI', $currentValue, 1);
  1222. } else {
  1223. SONOS_readingsSingleUpdateIfChanged($hash, $nextReading.'AlbumArtURI', $currentValue, 1);
  1224. }
  1225. } else {
  1226. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ProcessCover: $1:$2:$3:$4";
  1227. }
  1228. } elsif ($line =~ m/^SetAlarm:(.*?):(.*?);(.*?):(.*)/) {
  1229. my $hash = SONOS_getSonosPlayerByUDN($1);
  1230. my @alarmIDs = split(/,/, $3);
  1231. if ($4) {
  1232. readingsSingleUpdate($hash, 'AlarmList', $4, 0);
  1233. } else {
  1234. readingsSingleUpdate($hash, 'AlarmList', '{}', 0);
  1235. }
  1236. SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListIDs', join(',', sort {$a <=> $b} @alarmIDs), 0);
  1237. SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListVersion', $2, 1);
  1238. } elsif ($line =~ m/QA:(.*?):(.*?):(.*)/) { # Wenn ein QA (Question-Attribut) gefordert wurde, dann auch zurückliefern
  1239. DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0);
  1240. } elsif ($line =~ m/QR:(.*?):(.*?):(.*)/) { # Wenn ein QR (Question-Reading) gefordert wurde, dann auch zurückliefern
  1241. DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0);
  1242. } elsif ($line =~ m/QD:(.*?):(.*?):(.*)/) { # Wenn ein QD (Question-Definition) gefordert wurde, dann auch zurückliefern
  1243. DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0);
  1244. } elsif ($line =~ m/DoWorkAnswer:(.*?):(.*?):(.*)/) {
  1245. my $chash;
  1246. if (lc($1) eq 'undef') {
  1247. $chash = SONOS_getDeviceDefHash(undef);
  1248. } else {
  1249. $chash = SONOS_getSonosPlayerByUDN($1);
  1250. }
  1251. if ($chash) {
  1252. SONOS_Log undef, 4, "DoWorkAnswer arrived for ".$chash->{NAME}."->$2: '$3'";
  1253. readingsSingleUpdate($chash, $2, $3, 1);
  1254. } else {
  1255. SONOS_Log undef, 0, "Fehlerhafter Aufruf von DoWorkAnswer: $1:$2:$3";
  1256. }
  1257. } else {
  1258. SONOS_DoTriggerInternal('Main', $line);
  1259. }
  1260. }
  1261. }
  1262. ########################################################################################
  1263. #
  1264. # SONOS_AnswerQuery - Create the approbriate answer for the given Question
  1265. #
  1266. # Parameter line = The line of Question
  1267. #
  1268. ########################################################################################
  1269. sub SONOS_AnswerQuery($) {
  1270. my ($line) = @_;
  1271. if ($line =~ m/QA:(.*?):(.*?):(.*)/) { # Wenn ein QA (Question-Attribut) gefordert wurde, dann auch zurückliefern
  1272. my $chash;
  1273. if (lc($1) eq 'undef') {
  1274. $chash = SONOS_getDeviceDefHash(undef);
  1275. } else {
  1276. $chash = SONOS_getSonosPlayerByUDN($1);
  1277. }
  1278. if ($chash) {
  1279. SONOS_Log undef, 4, "QA-Anfrage(".$chash->{NAME}."): $1:$2:$3";
  1280. return "A:$1:$2:".AttrVal($chash->{NAME}, $2, $3)."\r\n";
  1281. } else {
  1282. SONOS_Log undef, 1, "Fehlerhafte QA-Anfrage: $1:$2:$3";
  1283. return "A:$1:$2:$3\r\n";
  1284. }
  1285. } elsif ($line =~ m/QR:(.*?):(.*?):(.*)/) { # Wenn ein QR (Question-Reading) gefordert wurde, dann auch zurückliefern
  1286. my $chash;
  1287. if (lc($1) eq 'undef') {
  1288. $chash = SONOS_getDeviceDefHash(undef);
  1289. } else {
  1290. $chash = SONOS_getSonosPlayerByUDN($1);
  1291. }
  1292. if ($chash) {
  1293. SONOS_Log undef, 4, "QR-Anfrage(".$chash->{NAME}."): $1:$2:$3";
  1294. return "R:$1:$2:".ReadingsVal($chash->{NAME}, $2, $3)."\r\n";
  1295. } else {
  1296. SONOS_Log undef, 1, "Fehlerhafte QR-Anfrage: $1:$2:$3";
  1297. return "R:$1:$2:$3\r\n";
  1298. }
  1299. } elsif ($line =~ m/QD:(.*?):(.*?):(.*)/) { # Wenn ein QD (Question-Definition) gefordert wurde, dann auch zurückliefern
  1300. my $chash;
  1301. if (lc($1) eq 'undef') {
  1302. $chash = SONOS_getDeviceDefHash(undef);
  1303. } else {
  1304. $chash = SONOS_getSonosPlayerByUDN($1);
  1305. }
  1306. if ($chash) {
  1307. SONOS_Log undef, 4, "QD-Anfrage(".$chash->{NAME}."): $1:$2:$3";
  1308. if ($chash->{$2}) {
  1309. return "D:$1:$2:".$chash->{$2}."\r\n";
  1310. } else {
  1311. return "D:$1:$2:$3\r\n";
  1312. }
  1313. } else {
  1314. SONOS_Log undef, 1, "Fehlerhafte QD-Anfrage: $1:$2:$3";
  1315. return "D:$1:$2:$3\r\n";
  1316. }
  1317. }
  1318. }
  1319. ########################################################################################
  1320. #
  1321. # SONOS_StartClientProcess - Starts the client-process (in a forked-subprocess), which handles all UPnP-Messages
  1322. #
  1323. # Parameter port = Portnumber to what the client have to listen for
  1324. #
  1325. ########################################################################################
  1326. sub SONOS_StartClientProcessIfNeccessary($) {
  1327. my ($upnplistener) = @_;
  1328. my ($host, $port) = split(/:/, $upnplistener);
  1329. my $socket = new IO::Socket::INET(PeerAddr => $upnplistener, Proto => 'tcp');
  1330. if (!$socket) {
  1331. # Sonos-Device ermitteln...
  1332. my $hash = SONOS_getDeviceDefHash(undef);
  1333. SONOS_Log undef, 1, 'Kein UPnP-Server gefunden... Starte selber einen und warte '.$hash->{WAITTIME}.' Sekunde(n) darauf...';
  1334. $SONOS_StartedOwnUPnPServer = 1;
  1335. if (fork() == 0) {
  1336. # Zuständigen Verbose-Level ermitteln...
  1337. # Allerdings sind die Attribute (momentan) zu diesem Zeitpunkt noch nicht gesetzt, sodass nur das globale Attribut verwendet werden kann...
  1338. my $verboselevel = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'verbose', $attr{global}{verbose});
  1339. # Prozess anstarten...
  1340. exec("$^X $attr{global}{modpath}/FHEM/00_SONOS.pm $port $verboselevel ".(($attr{global}{mseclog}) ? '1' : '0'));
  1341. exit(0);
  1342. }
  1343. } else {
  1344. $socket->sockopt(SO_LINGER, pack("ii", 1, 0));
  1345. # Antwort vom Client weglesen...
  1346. my $answer;
  1347. $socket->recv($answer, 50);
  1348. # Hiermit wird eine etwaig bestehende Thread-Struktur beendet und diese Verbindung selbst geschlossen...
  1349. eval{
  1350. $socket->send("disconnect\n", 0);
  1351. $socket->shutdown(2);
  1352. $socket->close();
  1353. };
  1354. }
  1355. return undef;
  1356. }
  1357. ########################################################################################
  1358. #
  1359. # SONOS_InitClientProcessLater - Initializes the client-process at a later time
  1360. #
  1361. # Parameter hash = The device-hash
  1362. #
  1363. ########################################################################################
  1364. sub SONOS_InitClientProcessLater($) {
  1365. my ($hash) = @_;
  1366. # Begrüßung weglesen...
  1367. my $answer = DevIo_SimpleRead($hash);
  1368. # Verbindung aufbauen...
  1369. InternalTimer(gettimeofday() + 1, 'SONOS_InitClientProcess', $hash, 0);
  1370. return undef;
  1371. }
  1372. ########################################################################################
  1373. #
  1374. # SONOS_InitClientProcess - Initializes the client-process
  1375. #
  1376. # Parameter hash = The device-hash
  1377. #
  1378. ########################################################################################
  1379. sub SONOS_InitClientProcess($) {
  1380. my ($hash) = @_;
  1381. my @playerudn = ();
  1382. my @playername = ();
  1383. foreach my $fhem_dev (sort keys %main::defs) {
  1384. next if($main::defs{$fhem_dev}{TYPE} ne 'SONOSPLAYER');
  1385. push @playerudn, $main::defs{$fhem_dev}{UDN};
  1386. push @playername, $main::defs{$fhem_dev}{NAME};
  1387. }
  1388. # Grundsätzliche Informationen bzgl. der konfigurierten Player übertragen...
  1389. my $setDataString = 'SetData:'.$hash->{NAME}.':'.AttrVal($hash->{NAME}, 'verbose', '3').':'.AttrVal($hash->{NAME}, 'pingType', $SONOS_DEFAULTPINGTYPE).':'.AttrVal($hash->{NAME}, 'usedonlyIPs', '').':'.AttrVal($hash->{NAME}, 'ignoredIPs', '').':'.join(',', @playername).':'.join(',', @playerudn);
  1390. SONOS_Log undef, 5, $setDataString;
  1391. DevIo_SimpleWrite($hash, $setDataString."\n", 0);
  1392. # Gemeldete Attribute, Definitionen und Readings übertragen...
  1393. foreach my $fhem_dev (sort keys %main::defs) {
  1394. if (($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER') || ($main::defs{$fhem_dev}{TYPE} eq 'SONOS')) {
  1395. # Den Namen des Devices ermitteln (normalerweise die UDN, bis auf das zentrale Sonos-Device)
  1396. my $dataName;
  1397. if ($main::defs{$fhem_dev}{TYPE} eq 'SONOS') {
  1398. $dataName = 'SONOS';
  1399. } else {
  1400. $dataName = $main::defs{$fhem_dev}{UDN};
  1401. }
  1402. # Variable für die gesammelten Informationen, die übertragen werden sollen...
  1403. my %valueList = ();
  1404. # Attribute
  1405. foreach my $key (keys %{$main::attr{$fhem_dev}}) {
  1406. if (SONOS_posInList($key, @SONOS_PossibleAttributes) != -1) {
  1407. $valueList{$key} = $main::attr{$fhem_dev}{$key};
  1408. }
  1409. }
  1410. # Definitionen
  1411. foreach my $key (keys %{$main::defs{$fhem_dev}}) {
  1412. if (SONOS_posInList($key, @SONOS_PossibleDefinitions) != -1) {
  1413. $valueList{$key} = $main::defs{$fhem_dev}{$key};
  1414. }
  1415. }
  1416. # Readings
  1417. foreach my $key (keys %{$main::defs{$fhem_dev}{READINGS}}) {
  1418. if (SONOS_posInList($key, @SONOS_PossibleReadings) != -1) {
  1419. $valueList{$key} = $main::defs{$fhem_dev}{READINGS}{$key}{VAL};
  1420. }
  1421. }
  1422. # Werte in Text-Array umwandeln und dabei prüfen, ob überhaupt ein Wert gesetzt werden soll...
  1423. my @values = ();
  1424. foreach my $key (keys %valueList) {
  1425. if (defined($key) && defined($valueList{$key})) {
  1426. push @values, $key.'='.SONOS_URI_Escape($valueList{$key});
  1427. }
  1428. }
  1429. # Übertragen...
  1430. SONOS_Log undef, 5, 'SetValues:'.$dataName.':'.join('|', @values);
  1431. DevIo_SimpleWrite($hash, 'SetValues:'.$dataName.':'.join('|', @values)."\n", 0);
  1432. }
  1433. }
  1434. # Alle Informationen sind drüben, dann Threads dort drüben starten
  1435. DevIo_SimpleWrite($hash, "StartThread\n", 0);
  1436. # Interner Timer für die Überprüfung der Verbindung zum Client (nicht verwechseln mit dem IsAlive-Timer, der die Existenz eines Sonosplayers überprüft)
  1437. InternalTimer(gettimeofday() + ($hash->{INTERVAL} * 2), 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1438. return undef;
  1439. }
  1440. ########################################################################################
  1441. #
  1442. # SONOS_IsSubprocessAliveChecker - Internal checking routine for isAlive of the subprocess
  1443. #
  1444. ########################################################################################
  1445. sub SONOS_IsSubprocessAliveChecker() {
  1446. my ($hash) = @_;
  1447. return undef if (AttrVal($hash->{NAME}, 'disable', 0));
  1448. my $answer;
  1449. my $socket = new IO::Socket::INET(PeerAddr => $hash->{DeviceName}, Proto => 'tcp');
  1450. if ($socket) {
  1451. $socket->sockopt(SO_LINGER, pack("ii", 1, 0));
  1452. $socket->recv($answer, 500);
  1453. $socket->send("hello\n", 0);
  1454. $socket->recv($answer, 500);
  1455. $socket->send("goaway\n", 0);
  1456. $socket->shutdown(2);
  1457. $socket->close();
  1458. }
  1459. if (defined($answer)) {
  1460. $answer =~ s/[\r\n]//g;
  1461. }
  1462. if (!defined($answer) || ($answer ne 'OK')) {
  1463. SONOS_Log undef, 0, 'No Answer from Subprocess. Restart Sonos-Subprocess...';
  1464. # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden...
  1465. RemoveInternalTimer($hash);
  1466. DevIo_SimpleWrite($hash, "disconnect\n", 0);
  1467. DevIo_CloseDev($hash);
  1468. # Neu anstarten...
  1469. SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer);
  1470. InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
  1471. } elsif (defined($answer) && ($answer eq 'OK')) {
  1472. SONOS_Log undef, 4, 'Got correct Answer from Subprocess...';
  1473. InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1474. }
  1475. }
  1476. ########################################################################################
  1477. #
  1478. # SONOS_DoTriggerInternal - Internal working routine for DoTrigger and PeekTriggerQueueInLocalThread
  1479. #
  1480. ########################################################################################
  1481. sub SONOS_DoTriggerInternal($$) {
  1482. my ($triggerType, @lines) = @_;
  1483. # Eval Kommandos ausführen
  1484. my %doTriggerHashParam;
  1485. my @doTriggerArrayParam;
  1486. my $doTriggerScalarParam;
  1487. foreach my $line (@lines) {
  1488. my $reftype = reftype $line;
  1489. if (!defined $reftype) {
  1490. SONOS_Log undef, 5, $triggerType.'Trigger()-Line: '.$line;
  1491. eval $line;
  1492. if ($@) {
  1493. SONOS_Log undef, 2, 'Error during '.$triggerType.'Trigger: '.$@.' - Trying to execute \''.$line.'\'';
  1494. }
  1495. undef(%doTriggerHashParam);
  1496. undef(@doTriggerArrayParam);
  1497. undef($doTriggerScalarParam);
  1498. } elsif($reftype eq 'HASH') {
  1499. %doTriggerHashParam = %{$line};
  1500. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerHashParam: '.SONOS_Stringify(\%doTriggerHashParam);
  1501. } elsif($reftype eq 'ARRAY') {
  1502. @doTriggerArrayParam = @{$line};
  1503. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerArrayParam: '.SONOS_Stringify(\@doTriggerArrayParam);
  1504. } elsif($reftype eq 'SCALAR') {
  1505. $doTriggerScalarParam = ${$line};
  1506. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerScalarParam: '.SONOS_Stringify(\$doTriggerScalarParam);
  1507. }
  1508. }
  1509. }
  1510. ########################################################################################
  1511. #
  1512. # SONOS_Get - Implements GetFn function
  1513. #
  1514. # Parameter hash = hash of the master
  1515. # a = argument array
  1516. #
  1517. ########################################################################################
  1518. sub SONOS_Get($@) {
  1519. my ($hash, @a) = @_;
  1520. my $reading = $a[1];
  1521. my $name = $hash->{NAME};
  1522. # for the ?-selector: which values are possible
  1523. if($a[1] eq '?') {
  1524. my @newGets = ();
  1525. for my $elem (sort keys %gets) {
  1526. push @newGets, $elem.(($gets{$elem} eq '') ? ':noArg' : '');
  1527. }
  1528. return "Unknown argument, choose one of ".join(" ", @newGets);
  1529. }
  1530. # check argument
  1531. my $found = 0;
  1532. for my $elem (keys %gets) {
  1533. if (lc($reading) eq lc($elem)) {
  1534. $reading = $elem; # Korrekte Schreibweise behalten
  1535. $found = 1;
  1536. last;
  1537. }
  1538. }
  1539. return "SONOS: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) if(!$found);
  1540. # some argument needs parameter(s), some not
  1541. return "SONOS: $a[1] needs parameter(s): ".$gets{$a[1]} if (SONOS_CountRequiredParameters($gets{$a[1]}) > scalar(@a) - 2);
  1542. # getter
  1543. if (lc($reading) eq 'groups') {
  1544. return SONOS_ConvertZoneGroupStateToString(SONOS_ConvertZoneGroupState(ReadingsVal($name, 'ZoneGroupState', '')));
  1545. }
  1546. return undef;
  1547. }
  1548. ########################################################################################
  1549. #
  1550. # SONOS_ConvertZoneGroupState - Retrieves the Groupstate in an array (Elements are UDNs)
  1551. #
  1552. ########################################################################################
  1553. sub SONOS_ConvertZoneGroupState($) {
  1554. my ($zoneGroupState) = @_;
  1555. my @groups = ();
  1556. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  1557. my @group = ($1.'_MR');
  1558. my $groupMember = $2;
  1559. while ($groupMember =~ m/<ZoneGroupMember.*?UUID="(.*?)"(.*?)\/>/gi) {
  1560. my $udn = $1;
  1561. my $string = $2;
  1562. push @group, $udn.'_MR' if (!($string =~ m/IsZoneBridge="."/) && !SONOS_isInList($udn.'_MR', @group));
  1563. # Etwaig von vorher enthaltene Bridges wieder entfernen (wenn sie bereits als Koordinator eingesetzt wurde)
  1564. if ($string =~ m/IsZoneBridge="."/) {
  1565. for(my $i = 0; $i <= $#group; $i++) {
  1566. delete $group[$i] if ($group[$i] eq $udn.'_MR');
  1567. }
  1568. }
  1569. }
  1570. # Die Abspielgruppe hinzufügen, wenn sie nicht leer ist (kann bei Bridges passieren)
  1571. if ($#group >= 0) {
  1572. # Playernamen einsetzen...
  1573. @group = map { SONOS_getSonosPlayerByUDN($_)->{NAME} } @group;
  1574. # Die einzelne Gruppe sortieren, dabei den Masterplayer vorne lassen...
  1575. my @newgroup = ($group[0]);
  1576. push @newgroup, sort @group[1..$#group];
  1577. # Zur großen Liste hinzufügen...
  1578. push @groups, \@newgroup;
  1579. }
  1580. }
  1581. # Nach den Masterplayernamen sortieren
  1582. @groups = sort {
  1583. @{$a}[0] cmp @{$b}[0];
  1584. } @groups;
  1585. return @groups;
  1586. }
  1587. ########################################################################################
  1588. #
  1589. # SONOS_ConvertZoneGroupStateToString - Converts the GroupState into a String
  1590. #
  1591. ########################################################################################
  1592. sub SONOS_ConvertZoneGroupStateToString($) {
  1593. my (@groups) = @_;
  1594. # UDNs durch Devicenamen ersetzen und dabei gleich das Ergebnis zusammenbauen
  1595. my $result = '';
  1596. foreach my $gelem (@groups) {
  1597. #$result .= '[';
  1598. #foreach my $elem (@{$gelem}) {
  1599. # $elem = SONOS_getSonosPlayerByUDN($elem)->{NAME};
  1600. #}
  1601. $result .= '['.join(', ', @{$gelem}).'], ';
  1602. }
  1603. return substr($result, 0, -2);
  1604. }
  1605. ########################################################################################
  1606. #
  1607. # SONOS_Set - Implements SetFn function
  1608. #
  1609. # Parameter hash
  1610. # a = argument array
  1611. #
  1612. ########################################################################################
  1613. sub SONOS_Set($@) {
  1614. my ($hash, @a) = @_;
  1615. # %setCopy enthält eine Kopie von %sets, da für eine ?-Anfrage u.U. ein Slider zurückgegeben werden muss...
  1616. my %setcopy;
  1617. if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) {
  1618. foreach my $key (keys %sets) {
  1619. my $oldkey = $key;
  1620. $key = $key.':slider,0,1,100' if (lc($key) eq 'volume');
  1621. $key = $key.':slider,-100,1,100' if (lc($key) eq 'balance');
  1622. $key = $key.':0,1' if ($key =~ m/^mute(all|)$/i);
  1623. $setcopy{$key} = $sets{$oldkey};
  1624. }
  1625. } else {
  1626. %setcopy = %sets;
  1627. }
  1628. # for the ?-selector: which values are possible
  1629. if($a[1] eq '?') {
  1630. my @newSets = ();
  1631. for my $elem (sort keys %setcopy) {
  1632. push @newSets, $elem.(($setcopy{$elem} eq '') ? ':noArg' : '');
  1633. }
  1634. return "Unknown argument, choose one of ".join(" ", @newSets);
  1635. }
  1636. # check argument
  1637. my $found = 0;
  1638. for my $elem (keys %sets) {
  1639. if (lc($a[1]) eq lc($elem)) {
  1640. $a[1] = $elem; # Korrekte Schreibweise behalten
  1641. $found = 1;
  1642. last;
  1643. }
  1644. }
  1645. return "SONOS: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) if(!$found);
  1646. # some argument needs parameter(s), some not
  1647. return "SONOS: $a[1] needs parameter(s): ".$sets{$a[1]} if (SONOS_CountRequiredParameters($sets{$a[1]}) > scalar(@a) - 2);
  1648. # define vars
  1649. my $key = $a[1];
  1650. my $value = $a[2];
  1651. my $value2 = $a[3];
  1652. my $name = $hash->{NAME};
  1653. # setter
  1654. if (lc($key) eq 'groups') {
  1655. my $text = '';
  1656. for(my $i = 2; $i < @a; $i++) {
  1657. $text .= ' '.$a[$i];
  1658. }
  1659. $text =~ s/ //g;
  1660. # Aktuellen Zustand holen
  1661. my @current;
  1662. my $current = SONOS_Get($hash, qw($hash->{NAME} Groups));
  1663. $current =~ s/ //g;
  1664. while ($current =~ m/(\[.*?\])/ig) {
  1665. my @tmp = split(/,/, substr($1, 1, -1));
  1666. push @current, \@tmp;
  1667. }
  1668. if (lc($text) eq 'reset') {
  1669. my $tmpcurrent = $current;
  1670. $tmpcurrent =~ s/[\[\],]/ /g;
  1671. my @list = split(/ /, $tmpcurrent);
  1672. # Alle Player als Standalone-Group festlegen
  1673. for(my $i = 0; $i <= $#list; $i++) {
  1674. next if (!$list[$i]); # Wenn hier ein Leerstring aus dem Split kam, dann überspringen...
  1675. my $elemHash = SONOS_getDeviceDefHash($list[$i]);
  1676. SONOS_DoWork($elemHash->{UDN}, 'makeStandaloneGroup');
  1677. usleep(250_000);
  1678. }
  1679. return undef;
  1680. }
  1681. # Gewünschten Zustand holen
  1682. my @desiredList;
  1683. my @desiredCrowd;
  1684. while ($text =~ m/([\[\{].*?[\}\]])/ig) {
  1685. my @tmp = split(/,/, substr($1, 1, -1));
  1686. if (substr($1, 0, 1) eq '{') {
  1687. push @desiredCrowd, \@tmp;
  1688. } else {
  1689. push @desiredList, \@tmp;
  1690. }
  1691. }
  1692. SONOS_Log undef, 5, "Desired-Crowd: ".Dumper(\@desiredCrowd);
  1693. SONOS_Log undef, 5, "Desired-List: ".Dumper(\@desiredList);
  1694. # Erstmal die Listen sicherstellen
  1695. foreach my $dElem (@desiredList) {
  1696. my @list = @{$dElem};
  1697. for(my $i = 0; $i <= $#list; $i++) { # Die jeweilige Desired-List
  1698. my $elem = $list[$i];
  1699. my $elemHash = SONOS_getDeviceDefHash($elem);
  1700. my $reftype = reftype $elemHash;
  1701. if (!defined($reftype) || $reftype ne 'HASH') {
  1702. SONOS_Log undef, 2, "Hash not found for Device '$elem'. Is it gone away or not known?";
  1703. return undef;
  1704. }
  1705. # Das Element soll ein Gruppenkoordinator sein
  1706. if ($i == 0) {
  1707. my $cPos = -1;
  1708. foreach my $cElem (@current) {
  1709. $cPos = SONOS_posInList($elem, @{$cElem});
  1710. last if ($cPos != -1);
  1711. }
  1712. # Ist es aber nicht... also erstmal dazu machen
  1713. if ($cPos != 0) {
  1714. SONOS_DoWork($elemHash->{UDN}, 'makeStandaloneGroup');
  1715. usleep(250_000);
  1716. }
  1717. } else {
  1718. # Alle weiteren dazufügen
  1719. my $cHash = SONOS_getDeviceDefHash($list[0]);
  1720. SONOS_DoWork($cHash->{UDN}, 'addMember', $elemHash->{UDN});
  1721. usleep(250_000);
  1722. }
  1723. }
  1724. }
  1725. # Jetzt noch die Mengen sicherstellen
  1726. # Dazu aktuellen Zustand nochmal holen
  1727. #@current = ();
  1728. #$current = SONOS_Get($hash, qw($hash->{NAME} Groups));
  1729. #$current =~ s/ //g;
  1730. #while ($current =~ m/(\[.*?\])/ig) {
  1731. # my @tmp = split(/,/, substr($1, 1, -1));
  1732. # push @current, \@tmp;
  1733. #}
  1734. #SONOS_Log undef, 5, "Current after List: ".Dumper(\@current);
  1735. } elsif (lc($key) =~ m/^(Stop|Pause|Mute|MuteOn|MuteOff)(All|)$/i) {
  1736. my $commandType = lc($1);
  1737. my $commandValue = $value;
  1738. $commandValue = 0 if ($commandType ne 'mute');
  1739. $commandValue = 1 if ($commandType eq 'muteon');
  1740. $commandValue = 0 if ($commandType eq 'muteoff');
  1741. $commandType = 'setGroupMute' if (($commandType eq 'mute') || ($commandType eq 'muteon') || ($commandType eq 'muteoff'));
  1742. # Alle Gruppenkoordinatoren zum Stoppen/Pausieren/Muten aufrufen
  1743. foreach my $cElem (@{eval(ReadingsVal($hash->{NAME}, 'MasterPlayer', '[]'))}) {
  1744. SONOS_DoWork(SONOS_getDeviceDefHash($cElem)->{UDN}, $commandType, $commandValue);
  1745. }
  1746. } elsif (lc($key) eq 'rescannetwork') {
  1747. SONOS_DoWork('SONOS', 'rescanNetwork');
  1748. } elsif (lc($key) eq 'savebookmarks') {
  1749. SONOS_DoWork('SONOS', 'SaveBookmarks', $value);
  1750. } elsif (lc($key) eq 'loadbookmarks') {
  1751. SONOS_DoWork('SONOS', 'LoadBookmarks', $value);
  1752. } elsif (lc($key) eq 'disablebookmark') {
  1753. SONOS_DoWork('SONOS', 'DisableBookmark', $value);
  1754. } elsif (lc($key) eq 'enablebookmark') {
  1755. SONOS_DoWork('SONOS', 'EnableBookmark', $value);
  1756. } else {
  1757. return 'Not implemented yet!';
  1758. }
  1759. return (undef, 1);
  1760. }
  1761. ########################################################################################
  1762. #
  1763. # SONOS_CountRequiredParameters - Counta all required parameters in the given string
  1764. #
  1765. ########################################################################################
  1766. sub SONOS_CountRequiredParameters($) {
  1767. my ($params) = @_;
  1768. my $result = 0;
  1769. for my $elem (split(' ', $params)) {
  1770. $result++ if ($elem !~ m/\[.*\]/);
  1771. }
  1772. return $result;
  1773. }
  1774. ########################################################################################
  1775. #
  1776. # SONOS_DoWork - Communicates with the forked Part via Telnet and over there via ComObjectTransportQueue
  1777. #
  1778. # Parameter deviceName = Devicename of the SonosPlayer
  1779. # method = Name der "Methode" die im Thread-Context ausgeführt werden soll
  1780. # params = Parameter for the method
  1781. #
  1782. ########################################################################################
  1783. sub SONOS_DoWork($$;@) {
  1784. my ($udn, $method, @params) = @_;
  1785. if (!@params) {
  1786. @params = ();
  1787. }
  1788. if (!defined($udn)) {
  1789. SONOS_Log undef, 0, "ERROR in DoWork: '$method' -> UDN is undefined - ".Dumper(\@params);
  1790. return;
  1791. }
  1792. # Etwaige optionale Parameter, die sonst undefined wären, löschen
  1793. for(my $i = 0; $i <= $#params; $i++) {
  1794. if (!defined($params[$i])) {
  1795. delete($params[$i]);
  1796. }
  1797. }
  1798. my $hash = SONOS_getDeviceDefHash(undef);
  1799. DevIo_SimpleWrite($hash, 'DoWork:'.$udn.':'.$method.':'.encode_utf8(join('£@£~', @params))."\r\n", 0);
  1800. return undef;
  1801. }
  1802. ########################################################################################
  1803. #
  1804. # SONOS_Discover - Discover SonosPlayer,
  1805. # indirectly autocreate devices if not already present (via callback)
  1806. #
  1807. ########################################################################################
  1808. sub SONOS_Discover() {
  1809. SONOS_Log undef, 3, 'UPnP-Thread gestartet.';
  1810. $SIG{'PIPE'} = 'IGNORE';
  1811. $SIG{'CHLD'} = 'IGNORE';
  1812. # Thread 'cancellation' signal handler
  1813. $SIG{'INT'} = sub {
  1814. # Sendeliste leeren
  1815. while ($SONOS_Client_SendQueue->pending()) {
  1816. $SONOS_Client_SendQueue->dequeue();
  1817. }
  1818. # Empfängerliste leeren
  1819. while ($SONOS_ComObjectTransportQueue->pending()) {
  1820. $SONOS_ComObjectTransportQueue->dequeue();
  1821. }
  1822. # UPnP-Listener beenden
  1823. SONOS_StopControlPoint();
  1824. SONOS_Log undef, 3, 'Controlpoint-Listener wurde beendet.';
  1825. return 1;
  1826. };
  1827. # Thread Signal Handler for doing some work in this thread 'environment'
  1828. $SIG{'HUP'} = sub {
  1829. while ($SONOS_ComObjectTransportQueue->pending()) {
  1830. my $data = $SONOS_ComObjectTransportQueue->peek();
  1831. my $workType = $data->{WorkType};
  1832. my $udn = $data->{UDN};
  1833. my @params = @{$data->{Params}};
  1834. eval {
  1835. if ($workType eq 'setVerbose') {
  1836. $SONOS_Client_LogLevel = $params[0];
  1837. SONOS_Log undef, 0, "Setting LogLevel to new value: $SONOS_Client_LogLevel";
  1838. } elsif ($workType eq 'rescanNetwork') {
  1839. #$SONOS_Controlpoint->_startSearch('urn:schemas-upnp-org:device:ZonePlayer:1');
  1840. $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
  1841. } elsif ($workType eq 'setMinMaxVolumes') {
  1842. $SONOS_Client_Data{Buffer}->{$udn}{$params[0]} = $params[1];
  1843. # Ensures the defined volume-borders
  1844. SONOS_EnsureMinMaxVolumes($udn);
  1845. SONOS_Log undef, 3, "Setting MinMaxVolumes of device '$udn' to a new value ~ $params[0] = $params[1]";
  1846. } elsif ($workType eq 'JumpToBookmark') {
  1847. if ($SONOS_BookmarkTitleDefinition{$params[0]}) {
  1848. }
  1849. } elsif ($workType eq 'LoadBookmarks') {
  1850. SONOS_LoadBookmarkValues($params[0]);
  1851. } elsif ($workType eq 'SaveBookmarks') {
  1852. SONOS_SaveBookmarkValues($params[0]);
  1853. } elsif ($workType eq 'DisableBookmark') {
  1854. $SONOS_BookmarkTitleDefinition{$params[0]}{Disabled} = 1 if ($SONOS_BookmarkTitleDefinition{$params[0]});
  1855. $SONOS_BookmarkQueueDefinition{$params[0]}{Disabled} = 1 if ($SONOS_BookmarkQueueDefinition{$params[0]});
  1856. } elsif ($workType eq 'EnableBookmark') {
  1857. delete($SONOS_BookmarkTitleDefinition{$params[0]}{Disabled}) if ($SONOS_BookmarkTitleDefinition{$params[0]});
  1858. delete($SONOS_BookmarkQueueDefinition{$params[0]}{Disabled}) if ($SONOS_BookmarkQueueDefinition{$params[0]});
  1859. } elsif ($workType eq 'setEQ') {
  1860. my $command = $params[0];
  1861. my $value = $params[1];
  1862. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1863. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).' ('.$command.'): '.SONOS_UPnPAnswerMessage($SONOS_RenderingControlProxy{$udn}->SetEQ(0, $command, $value)));
  1864. }
  1865. } elsif ($workType eq 'setTruePlay') {
  1866. my $value = $params[0];
  1867. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1868. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_RenderingControlProxy{$udn}->SetSonarStatus(0, $value)));
  1869. }
  1870. } elsif ($workType eq 'setName') {
  1871. my $value1 = $params[0];
  1872. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  1873. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes($value1, '', '')));
  1874. }
  1875. } elsif ($workType eq 'setIcon') {
  1876. my $value1 = $params[0];
  1877. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  1878. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes('', 'x-rincon-roomicon:'.$value1, '')));
  1879. }
  1880. } elsif ($workType eq 'getCurrentTrackPosition') {
  1881. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  1882. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime'));
  1883. }
  1884. } elsif ($workType eq 'setCurrentTrackPosition') {
  1885. my $value1 = $params[0];
  1886. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  1887. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', $value1);
  1888. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime'));
  1889. }
  1890. } elsif ($workType eq 'reportUnresponsiveDevice') {
  1891. my $value1 = $params[0];
  1892. if (SONOS_CheckProxyObject($udn, $SONOS_ZoneGroupTopologyProxy{$udn})) {
  1893. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_ZoneGroupTopologyProxy{$udn}->ReportUnresponsiveDevice($value1, 'VerifyThenRemoveSystemwide')));
  1894. }
  1895. } elsif ($workType eq 'setGroupVolume') {
  1896. my $value1 = $params[0];
  1897. my $value2 = $params[1];
  1898. # Wenn ein fixer Wert für alle Gruppenmitglieder gleich gesetzt werden soll...
  1899. if (defined($value2) && lc($value2) eq 'fixed') {
  1900. } else {
  1901. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  1902. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $value1);
  1903. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  1904. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume'));
  1905. }
  1906. }
  1907. } elsif ($workType eq 'setSnapshotGroupVolume') {
  1908. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  1909. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0)));
  1910. }
  1911. } elsif ($workType eq 'setVolume') {
  1912. my $value1 = $params[0];
  1913. my $ramptype = $params[1];
  1914. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1915. if (defined($ramptype)) {
  1916. if ($ramptype == 1) {
  1917. $ramptype = 'SLEEP_TIMER_RAMP_TYPE';
  1918. } elsif ($ramptype == 2) {
  1919. $ramptype = 'AUTOPLAY_RAMP_TYPE';
  1920. } elsif ($ramptype == 3) {
  1921. $ramptype = 'ALARM_RAMP_TYPE';
  1922. }
  1923. my $ramptime = $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, '')->getValue('RampTime');
  1924. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started');
  1925. } else {
  1926. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1);
  1927. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  1928. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'));
  1929. }
  1930. }
  1931. } elsif ($workType eq 'setRelativeGroupVolume') {
  1932. my $value1 = $params[0];
  1933. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  1934. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $value1)->getValue('NewVolume'));
  1935. }
  1936. } elsif ($workType eq 'setRelativeVolume') {
  1937. my $value1 = $params[0];
  1938. my $ramptype = $params[1];
  1939. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1940. if (defined($ramptype)) {
  1941. if ($ramptype == 1) {
  1942. $ramptype = 'SLEEP_TIMER_RAMP_TYPE';
  1943. } elsif ($ramptype == 2) {
  1944. $ramptype = 'AUTOPLAY_RAMP_TYPE';
  1945. } elsif ($ramptype == 3) {
  1946. $ramptype = 'ALARM_RAMP_TYPE';
  1947. }
  1948. # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen
  1949. if ($value1 =~ m/([+-])(\d+)\%/) {
  1950. my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  1951. $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) };
  1952. } else {
  1953. # Hier aus der Relativangabe eine Absolutangabe für den Aufruf von RampToVolume machen
  1954. $value1 = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume') + $value1;
  1955. }
  1956. $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, '');
  1957. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started');
  1958. } else {
  1959. # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen
  1960. if ($value1 =~ m/([+-])(\d+)\%/) {
  1961. my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  1962. $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) };
  1963. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1);
  1964. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  1965. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'));
  1966. } else {
  1967. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->SetRelativeVolume(0, 'Master', $value1)->getValue('NewVolume'));
  1968. }
  1969. }
  1970. }
  1971. } elsif ($workType eq 'setBalance') {
  1972. my $value1 = $params[0];
  1973. # Balancewert auf die beiden Lautstärkeseiten aufteilen...
  1974. my $volumeLeft = 100;
  1975. my $volumeRight = 100;
  1976. if ($value1 < 0) {
  1977. $volumeRight = 100 + $value1;
  1978. } else {
  1979. $volumeLeft = 100 - $value1;
  1980. }
  1981. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1982. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'LF', $volumeLeft);
  1983. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'RF', $volumeRight);
  1984. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  1985. $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume');
  1986. $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume');
  1987. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.((-$volumeLeft) + $volumeRight));
  1988. }
  1989. } elsif ($workType eq 'setLoudness') {
  1990. my $value1 = $params[0];
  1991. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1992. $SONOS_RenderingControlProxy{$udn}->SetLoudness(0, 'Master', SONOS_ConvertWordToNum($value1));
  1993. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  1994. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetLoudness(0, 'Master')->getValue('CurrentLoudness')));
  1995. }
  1996. } elsif ($workType eq 'setBass') {
  1997. my $value1 = $params[0];
  1998. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  1999. $SONOS_RenderingControlProxy{$udn}->SetBass(0, $value1);
  2000. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2001. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetBass(0)->getValue('CurrentBass'));
  2002. }
  2003. } elsif ($workType eq 'setTreble') {
  2004. my $value1 = $params[0];
  2005. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2006. $SONOS_RenderingControlProxy{$udn}->SetTreble(0, $value1);
  2007. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2008. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetTreble(0)->getValue('CurrentTreble'));
  2009. }
  2010. } elsif ($workType eq 'setMute') {
  2011. my $value1 = $params[0];
  2012. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2013. $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1));
  2014. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2015. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute')));
  2016. }
  2017. } elsif ($workType eq 'setOutputFixed') {
  2018. my $value1 = $params[0];
  2019. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2020. $SONOS_RenderingControlProxy{$udn}->SetOutputFixed(0, SONOS_ConvertWordToNum($value1));
  2021. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2022. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetOutputFixed(0)->getValue('CurrentFixed')));
  2023. }
  2024. } elsif ($workType eq 'setResetAttributesToDefault') {
  2025. my $sonosDeviceName = $params[0];
  2026. my $deviceName = $params[1];
  2027. my $value1 = 0;
  2028. $value1 = $params[2] if ($params[2]);
  2029. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  2030. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2031. # Sollen alle Attribute vorher entfernt werden?
  2032. if (SONOS_ConvertWordToNum($value1)) {
  2033. SONOS_Client_Notifier('CommandDeleteAttr:'.$deviceName);
  2034. }
  2035. # Notwendige Daten vom Player ermitteln...
  2036. my ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState) = SONOS_AnalyzeZoneGroupTopology($udn, $udnShort);
  2037. my $roomName = $SONOS_DevicePropertiesProxy{$udn}->GetZoneAttributes()->getValue('CurrentZoneName');
  2038. my $groupName = decode('UTF-8', $roomName);
  2039. eval {
  2040. use utf8;
  2041. $groupName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  2042. };
  2043. $groupName =~ s/[^a-zA-Z0-9]/_/g;
  2044. my $iconPath = decode_entities($1) if ($SONOS_UPnPDevice{$udn}->descriptionDocument() =~ m/<iconList>.*?<icon>.*?<id>0<\/id>.*?<url>(.*?)<\/url>.*?<\/icon>.*?<\/iconList>/sim);
  2045. $iconPath =~ s/.*\/(.*)/icoSONOSPLAYER_$1/i;
  2046. # Standard-Attribute am Player setzen
  2047. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Attributes', $sonosDeviceName, undef, $master, $deviceName, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge)) {
  2048. SONOS_Client_Notifier($elem);
  2049. }
  2050. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Successfully done...');
  2051. }
  2052. } elsif ($workType eq 'setMuteT') {
  2053. my $value1 = 'off';
  2054. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2055. if ($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute') == 0) {
  2056. $value1 = 'on';
  2057. } else {
  2058. $value1 = 'off';
  2059. }
  2060. $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1));
  2061. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2062. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute')));
  2063. }
  2064. } elsif ($workType eq 'setGroupMute') {
  2065. my $value1 = $params[0];
  2066. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  2067. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, SONOS_ConvertWordToNum($value1));
  2068. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2069. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute')));
  2070. }
  2071. } elsif ($workType eq 'setShuffle') {
  2072. my $value1 = undef;
  2073. if ($params[0] ne '~~') {
  2074. $value1 = SONOS_ConvertWordToNum($params[0]);
  2075. }
  2076. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2077. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2078. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2079. $value1 = !$shuffle if (!defined($value1));
  2080. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($value1, $repeat, $repeatOne));
  2081. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2082. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2083. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2084. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($shuffle));
  2085. }
  2086. } elsif ($workType eq 'setRepeat') {
  2087. my $value1 = undef;
  2088. if ($params[0] ne '~~') {
  2089. $value1 = SONOS_ConvertWordToNum($params[0]);
  2090. }
  2091. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2092. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2093. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2094. $value1 = !$repeat if (!defined($value1));
  2095. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($shuffle, $value1, $repeatOne && !$value1));
  2096. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2097. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2098. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2099. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($repeat));
  2100. }
  2101. } elsif ($workType eq 'setRepeatOne') {
  2102. my $value1 = undef;
  2103. if ($params[0] ne '~~') {
  2104. $value1 = SONOS_ConvertWordToNum($params[0]);
  2105. }
  2106. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2107. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2108. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2109. $value1 = !$repeatOne if (!defined($value1));
  2110. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($shuffle, $repeat && !$value1, $value1));
  2111. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2112. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2113. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2114. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($repeatOne));
  2115. }
  2116. } elsif ($workType eq 'setCrossfadeMode') {
  2117. my $value1 = SONOS_ConvertWordToNum($params[0]);
  2118. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2119. $SONOS_AVTransportControlProxy{$udn}->SetCrossfadeMode(0, $value1);
  2120. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2121. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_AVTransportControlProxy{$udn}->GetCrossfadeMode(0)->getValue('CrossfadeMode')));
  2122. }
  2123. } elsif ($workType eq 'setLEDState') {
  2124. my $value1 = (SONOS_ConvertWordToNum($params[0])) ? 'On' : 'Off';
  2125. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2126. $SONOS_DevicePropertiesProxy{$udn}->SetLEDState($value1);
  2127. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2128. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_DevicePropertiesProxy{$udn}->GetLEDState()->getValue('CurrentLEDState')));
  2129. }
  2130. } elsif ($workType eq 'play') {
  2131. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2132. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)));
  2133. }
  2134. } elsif ($workType eq 'stop') {
  2135. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2136. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Stop(0)));
  2137. }
  2138. } elsif ($workType eq 'pause') {
  2139. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2140. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Pause(0)));
  2141. }
  2142. } elsif ($workType eq 'previous') {
  2143. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2144. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Previous(0)));
  2145. }
  2146. } elsif ($workType eq 'next') {
  2147. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2148. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Next(0)));
  2149. }
  2150. } elsif ($workType eq 'setTrack') {
  2151. my $value1 = $params[0];
  2152. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2153. # Abspielliste aktivieren?
  2154. my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI');
  2155. if ($currentURI !~ m/x-rincon-queue:/) {
  2156. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2157. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  2158. }
  2159. if (lc($value1) eq 'random') {
  2160. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', int(rand($SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('NrTracks'))));
  2161. } else {
  2162. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', $value1);
  2163. }
  2164. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track'));
  2165. }
  2166. } elsif ($workType eq 'setCurrentPlaylist') {
  2167. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2168. # Abspielliste aktivieren?
  2169. my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI');
  2170. if ($currentURI !~ m/x-rincon-queue:/) {
  2171. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2172. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '')));
  2173. } else {
  2174. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Not neccessary!');
  2175. }
  2176. }
  2177. } elsif ($workType eq 'getPlaylists') {
  2178. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2179. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2180. my $tmp = $result->getValue('Result');
  2181. my %resultHash;
  2182. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2183. $resultHash{$1} = $2;
  2184. }
  2185. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2186. }
  2187. } elsif ($workType eq 'getPlaylistsWithCovers') {
  2188. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2189. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2190. my $tmp = $result->getValue('Result');
  2191. my %resultHash;
  2192. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/container>/ig) {
  2193. $resultHash{$1}->{Title} = $2;
  2194. $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2195. }
  2196. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2197. }
  2198. } elsif ($workType eq 'getFavourites') {
  2199. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2200. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  2201. my $tmp = $result->getValue('Result');
  2202. my %resultHash;
  2203. while ($tmp =~ m/<item id="(FV:2\/\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/item>/ig) {
  2204. $resultHash{$1} = $2;
  2205. }
  2206. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2207. }
  2208. } elsif ($workType eq 'getFavouritesWithCovers') {
  2209. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2210. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  2211. my $tmp = $result->getValue('Result');
  2212. my %resultHash;
  2213. while ($tmp =~ m/<item id="(FV:2\/\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2214. $resultHash{$1}->{Title} = $2;
  2215. $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2216. }
  2217. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2218. }
  2219. } elsif ($workType eq 'getSearchlistCategories') {
  2220. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2221. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('A:', 'BrowseDirectChildren', '', 0, 0, '');
  2222. my $tmp = $result->getValue('Result');
  2223. SONOS_Log $udn, 5, 'getSearchlistCategories BrowseResult: '.$tmp;
  2224. my %resultHash;
  2225. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2226. $resultHash{$1} = $2;
  2227. }
  2228. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2229. }
  2230. } elsif ($workType eq 'exportSonosBibliothek') {
  2231. my $filename = $params[0];
  2232. # Anfragen durchführen...
  2233. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2234. my $exports = {'Structure' => {}, 'Titles' => {}};
  2235. SONOS_Log undef, 3, 'ExportSonosBibliothek-Start';
  2236. my $startTime = gettimeofday();
  2237. SONOS_RecursiveStructure($udn, 'A:', $exports->{Structure}, $exports->{Titles});
  2238. SONOS_Log undef, 3, 'ExportSonosBibliothek-End. Runtime (in seconds): '.int(gettimeofday() - $startTime);
  2239. my $countTitles = scalar(keys %{$exports->{Titles}});
  2240. # In Datei wegschreiben
  2241. eval {
  2242. open FILE, '>'.$filename;
  2243. binmode(FILE, ':encoding(utf-8)');
  2244. print FILE SONOS_Dumper($exports);
  2245. close FILE;
  2246. };
  2247. if ($@) {
  2248. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during filewriting: '.$@);
  2249. return;
  2250. }
  2251. $exports = undef;
  2252. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Successfully written to file "'.$filename.'", Titles: '.$countTitles.', Duration: '.int(gettimeofday() - $startTime).'s');
  2253. }
  2254. } elsif ($workType eq 'loadSearchlist') {
  2255. # Category holen
  2256. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2257. my $searchlistName = $1 if ($regSearch);
  2258. $searchlistName = uri_unescape($params[0]) if (!$regSearch);
  2259. # RegEx prüfen...
  2260. if ($regSearch) {
  2261. eval { "" =~ m/$searchlistName/ };
  2262. if($@) {
  2263. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad Category RegExp "'.$searchlistName.'": '.$@);
  2264. return;
  2265. }
  2266. }
  2267. # Element holen
  2268. $params[1] = '' if (!$params[1]);
  2269. my $regSearchElement = ($params[1] =~ m/^ *\/(.*)\/ *$/);
  2270. my $searchlistElement = $1 if ($regSearchElement);
  2271. $searchlistElement = uri_unescape($params[1]) if (!$regSearchElement);
  2272. # RegEx prüfen...
  2273. if ($regSearchElement) {
  2274. eval { "" =~ m/$searchlistElement/ };
  2275. if($@) {
  2276. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad CategoryElement RegExp "'.$searchlistElement.'": '.$@);
  2277. return;
  2278. }
  2279. }
  2280. # Filter angegeben?
  2281. my $filter = '//';
  2282. $filter = $params[2] if ($params[2]);
  2283. $filter .= '/' while ((SONOS_CountInString('/', $filter) - SONOS_CountInString('\/', $filter)) < 2);
  2284. my ($filterTitle, $filterAlbum, $filterArtist) = ($1, $3, $5) if ($filter =~ m/((.*?[^\\])|.{0})\/((.*?[^\\])|.{0})\/(.*)/);
  2285. $filterTitle = '.*' if (!$filterTitle);
  2286. $filterAlbum = '.*' if (!$filterAlbum);
  2287. $filterArtist = '.*' if (!$filterArtist);
  2288. SONOS_Log $udn, 4, 'getSearchlist filterTitle: '.$filterTitle;
  2289. SONOS_Log $udn, 4, 'getSearchlist filterAlbum: '.$filterAlbum;
  2290. SONOS_Log $udn, 4, 'getSearchlist filterArtist: '.$filterArtist;
  2291. # RegEx prüfen...
  2292. eval { "" =~ m/$filterTitle/ };
  2293. if($@) {
  2294. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterTitle RegExp "'.$filterTitle.'": '.$@);
  2295. return;
  2296. }
  2297. # RegEx prüfen...
  2298. eval { "" =~ m/$filterAlbum/ };
  2299. if($@) {
  2300. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterAlbum RegExp "'.$filterAlbum.'": '.$@);
  2301. return;
  2302. }
  2303. # RegEx prüfen...
  2304. eval { "" =~ m/$filterArtist/ };
  2305. if($@) {
  2306. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterArtist RegExp "'.$filterArtist.'": '.$@);
  2307. return;
  2308. }
  2309. # Menge angegeben? Hier kann auch mit einem '*' eine zufällige Reihenfolge bestimmt werden...
  2310. my $maxElems = '0-';
  2311. $maxElems = $params[3] if ($params[3]);
  2312. # Anfragen durchführen...
  2313. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2314. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('A:', 'BrowseDirectChildren', '', 0, 0, '');
  2315. my $tmp = $result->getValue('Result');
  2316. SONOS_Log $udn, 5, 'getSearchlistCategories BrowseResult: '.$tmp;
  2317. # Category heraussuchen
  2318. my %resultHash;
  2319. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2320. next if (SONOS_Trim($2) eq ''); # Wenn kein Titel angegeben ist, dann überspringen
  2321. my $name = $2;
  2322. $resultHash{$name} = $1;
  2323. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2324. if ($regSearch) {
  2325. if ($name =~ m/$searchlistName/) {
  2326. $searchlistName = $name;
  2327. $regSearch = 0;
  2328. }
  2329. }
  2330. }
  2331. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2332. if (!$resultHash{$searchlistName} || $regSearch) {
  2333. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Category "'.$searchlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2334. return;
  2335. }
  2336. my $searchlistTitle = $searchlistName;
  2337. $searchlistName = $resultHash{$searchlistName};
  2338. ###############################################
  2339. # Elemente der Category heraussuchen
  2340. ###############################################
  2341. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistName, 'BrowseDirectChildren', '', 0, 0, '');
  2342. $tmp = $result->getValue('Result');
  2343. my $numberReturned = $result->getValue('NumberReturned');
  2344. my $totalMatches = $result->getValue('TotalMatches');
  2345. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements StepInfo_0 - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2346. while ($numberReturned < $totalMatches) {
  2347. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistName, 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2348. $tmp .= $result->getValue('Result');
  2349. $numberReturned += $result->getValue('NumberReturned');
  2350. $totalMatches = $result->getValue('TotalMatches');
  2351. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements StepInfo - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2352. }
  2353. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements Totalmatches: '.$totalMatches;
  2354. SONOS_Log $udn, 5, 'getSearchlistCategoriesElements BrowseResult: '.$tmp;
  2355. # Category heraussuchen
  2356. my $searchlistElementTitle = $searchlistElement;
  2357. if ($tmp =~ m/<container id="(A:.*?)".*?>.*?<\/container>/ig) { # Wenn überhaupt noch was zu suchen ist...
  2358. %resultHash = ();
  2359. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2360. next if (SONOS_Trim($2) eq ''); # Wenn kein Titel angegeben ist, dann überspringen
  2361. my $name = $2;
  2362. $resultHash{$name} = $1;
  2363. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2364. if ($regSearchElement) {
  2365. if ($name =~ m/$searchlistElement/) {
  2366. $searchlistElement = $name;
  2367. $regSearchElement = 0;
  2368. }
  2369. }
  2370. }
  2371. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2372. if (!$resultHash{$searchlistElement} || $regSearchElement) {
  2373. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Element "'.$searchlistElement.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2374. return;
  2375. }
  2376. $searchlistElementTitle = $searchlistElement;
  2377. $searchlistElement = $resultHash{$searchlistElement};
  2378. ###############################################
  2379. # Ziel-Elemente ermitteln und filtern
  2380. ###############################################
  2381. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', 0, 0, '');
  2382. $tmp = $result->getValue('Result');
  2383. # Wenn hier noch eine Schicht Container enthalten ist, dann nochmal tiefer gehen...
  2384. while ($tmp && ($tmp =~ m/<container.*?>.*?<\/container>/i)) {
  2385. $searchlistElement .= '/';
  2386. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', 0, 0, '');
  2387. $tmp = $result->getValue('Result');
  2388. }
  2389. $numberReturned = $result->getValue('NumberReturned');
  2390. $totalMatches = $result->getValue('TotalMatches');
  2391. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl StepInfo_0 - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2392. while ($numberReturned < $totalMatches) {
  2393. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2394. $tmp .= $result->getValue('Result');
  2395. $numberReturned += $result->getValue('NumberReturned');
  2396. $totalMatches = $result->getValue('TotalMatches');
  2397. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl StepInfo - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2398. }
  2399. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl Totalmatches: '.$totalMatches;
  2400. SONOS_Log $udn, 5, 'getSearchlistCategoriesElementsEl BrowseResult: '.$tmp;
  2401. }
  2402. # Elemente heraussuchen
  2403. %resultHash = ();
  2404. my @URIs = ();
  2405. my @Metas = ();
  2406. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  2407. my $item = $2;
  2408. my $uri = $1 if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  2409. $uri =~ s/&apos;/'/gi;
  2410. my $title = '';
  2411. $title = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  2412. my $album = '';
  2413. $album = $1 if ($item =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  2414. my $interpret = '';
  2415. $interpret = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  2416. # Die Matches merken...
  2417. if (($title =~ m/$filterTitle/) && ($album =~ m/$filterAlbum/) && ($interpret =~ m/$filterArtist/)) {
  2418. my ($res, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($uri));
  2419. push(@URIs, $res);
  2420. push(@Metas, $meta);
  2421. }
  2422. }
  2423. my $answer = 'Retrieved all titles of category "'.$searchlistTitle.'" with searchvalue "'.$searchlistElementTitle.'" and filter "'.$filterTitle.'/'.$filterAlbum.'/'.$filterArtist.'" (#'.($#URIs + 1).'). ';
  2424. # Liste u.U. vermischen...
  2425. my @matches = (0..$#URIs);
  2426. if ($maxElems =~ m/^\*/) {
  2427. SONOS_Fisher_Yates_Shuffle(\@matches);
  2428. $answer .= 'Shuffled the searchlist. ';
  2429. }
  2430. # Nicht alle übernehmen?
  2431. if ($maxElems =~ m/^\*{0,1}(\d+)[\+-]{0,1}$/) {
  2432. splice(@matches, $1) if ($1 && ($1 <= $#matches));
  2433. SONOS_Log $udn, 4, 'getSearchlist maxElems('.$maxElems.'): '.$1;
  2434. }
  2435. SONOS_Log $udn, 4, 'getSearchlist Count Matches: '.($#matches + 1);
  2436. # Wenn der AVTransportProxy existiert weitermachen...
  2437. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2438. # Playlist vorher leeren?
  2439. if ($maxElems =~ m/-$/) {
  2440. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue();
  2441. $answer .= 'Queue successfully emptied. ';
  2442. }
  2443. # An das Ende der Playlist oder hinter dem aktuellen Titel einfügen?
  2444. my $currentInsertPos = 0;
  2445. if ($maxElems =~ m/\+$/) {
  2446. $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('NrTracks') + 1;
  2447. } else {
  2448. $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track') + 1;
  2449. }
  2450. # Die Matches in die Playlist laden...
  2451. my $sliceSize = 16;
  2452. my $count = 0;
  2453. SONOS_Log $udn, 4, "Start-Adding: Count ".scalar(@matches)." / $sliceSize";
  2454. if (scalar(@matches)) {
  2455. for my $i (0..int(scalar(@matches) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren
  2456. my $startIndex = $i * $sliceSize;
  2457. my $endIndex = $startIndex + $sliceSize - 1;
  2458. $endIndex = SONOS_Min(scalar(@matches) - 1, $endIndex);
  2459. SONOS_Log $udn, 4, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)";
  2460. my $uri = '';
  2461. my $meta = '';
  2462. for my $index (@matches[$startIndex..$endIndex]) {
  2463. $uri .= ' '.$URIs[$index];
  2464. $meta .= ' '.$Metas[$index];
  2465. }
  2466. $uri = substr($uri, 1) if (length($uri) > 0);
  2467. $meta = substr($meta, 1) if (length($meta) > 0);
  2468. $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, $uri, $meta, '', '', $currentInsertPos, 0);
  2469. if (!$result->isSuccessful()) {
  2470. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  2471. }
  2472. $currentInsertPos += $endIndex - $startIndex + 1;
  2473. $count = $endIndex + 1;
  2474. }
  2475. if ($result->isSuccessful()) {
  2476. $answer .= 'Added '.$count.' entries from searchlist. There are now '.$result->getValue('NewQueueLength').' entries in Queue. ';
  2477. } else {
  2478. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  2479. }
  2480. }
  2481. # Die Liste als aktuelles Abspielstück einstellen, falls etwas anderes als die Playliste läuft...
  2482. my $currentMediaInfo = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  2483. my $currentMediaInfoCurrentURI = $currentMediaInfo->getValue('CurrentURI');
  2484. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2485. if ($queueMetadata->getValue('Result') !~ m/<res.*?>$currentMediaInfoCurrentURI<\/res>/) {
  2486. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  2487. $answer .= 'Startlist: '.SONOS_UPnPAnswerMessage($result).'. ';
  2488. } else {
  2489. $answer .= 'Startlist not neccessary. ';
  2490. }
  2491. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$answer);
  2492. }
  2493. }
  2494. } elsif ($workType eq 'getRadios') {
  2495. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2496. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2497. my $tmp = $result->getValue('Result');
  2498. my %resultHash;
  2499. while ($tmp =~ m/<item id="(R:0\/0\/\d+)".*?><dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2500. $resultHash{$1} = $2;
  2501. }
  2502. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2503. }
  2504. } elsif ($workType eq 'getRadiosWithCovers') {
  2505. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2506. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2507. my $tmp = $result->getValue('Result');
  2508. my %resultHash;
  2509. while ($tmp =~ m/<item id="(R:0\/0\/\d+)".*?><dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2510. $resultHash{$1}->{Title} = $2;
  2511. $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2512. }
  2513. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2514. }
  2515. } elsif ($workType eq 'loadRadio') {
  2516. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2517. my $radioName = $1 if ($regSearch);
  2518. $radioName = uri_unescape($params[0]) if (!$regSearch);
  2519. # RegEx prüfen...
  2520. if ($regSearch) {
  2521. eval { "" =~ m/$radioName/ };
  2522. if($@) {
  2523. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$radioName.'": '.$@);
  2524. return;
  2525. }
  2526. }
  2527. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2528. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2529. my $tmp = $result->getValue('Result');
  2530. SONOS_Log $udn, 5, 'LoadRadio BrowseResult: '.$tmp;
  2531. my %resultHash;
  2532. while ($tmp =~ m/(<item id="(R:0\/0\/\d+)".*?>)<dc:title>(.*?)<\/dc:title>.*?(<upnp:class>.*?<\/upnp:class>).*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2533. my $name = $3;
  2534. $resultHash{$name}{TITLE} = $name;
  2535. $resultHash{$name}{RES} = decode_entities($5);
  2536. $resultHash{$name}{METADATA} = $SONOS_DIDLHeader.$1.'<dc:title>'.$name.'</dc:title>'.$4.'<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON65031_</desc></item>'.$SONOS_DIDLFooter;
  2537. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2538. if ($regSearch) {
  2539. if ($name =~ m/$radioName/) {
  2540. $radioName = $name;
  2541. $regSearch = 0;
  2542. }
  2543. }
  2544. }
  2545. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2546. if (!$resultHash{$radioName} || $regSearch) {
  2547. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Radio "'.$radioName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2548. return;
  2549. }
  2550. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2551. SONOS_Log $udn, 5, 'LoadRadio SetAVTransport-Res: "'.$resultHash{$radioName}{RES}.'", -Meta: "'.$resultHash{$radioName}{METADATA}.'"';
  2552. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$radioName}{RES}, $resultHash{$radioName}{METADATA})));
  2553. }
  2554. }
  2555. } elsif ($workType eq 'startFavourite') {
  2556. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2557. my $favouriteName = $1 if ($regSearch);
  2558. $favouriteName = uri_unescape($params[0]) if (!$regSearch);
  2559. # RegEx prüfen...
  2560. if ($regSearch) {
  2561. eval { "" =~ m/$favouriteName/ };
  2562. if($@) {
  2563. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$favouriteName.'": '.$@);
  2564. return;
  2565. }
  2566. }
  2567. my $nostart = 0;
  2568. if (defined($params[1]) && lc($params[1]) eq 'nostart') {
  2569. $nostart = 1;
  2570. }
  2571. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2572. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  2573. my $tmp = $result->getValue('Result');
  2574. SONOS_Log $udn, 5, 'StartFavourite BrowseResult: '.$tmp;
  2575. my %resultHash;
  2576. while ($tmp =~ m/(<item id="(FV:2\/\d+)".*?>)<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<r:resMD>(.*?)<\/r:resMD>.*?<\/item>/ig) {
  2577. my $name = $3;
  2578. $resultHash{$name}{TITLE} = $name;
  2579. $resultHash{$name}{RES} = decode_entities($4);
  2580. $resultHash{$name}{METADATA} = decode_entities($5);
  2581. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2582. if ($regSearch) {
  2583. if ($name =~ m/$favouriteName/) {
  2584. $favouriteName = $name;
  2585. $regSearch = 0;
  2586. }
  2587. }
  2588. }
  2589. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2590. if (!$resultHash{$favouriteName} || $regSearch) {
  2591. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Favourite "'.$favouriteName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2592. return;
  2593. }
  2594. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2595. # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann
  2596. if ($resultHash{$favouriteName}{METADATA} =~ m/<upnp:class>object\.container.*?<\/upnp:class>/i) {
  2597. SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
  2598. # Queue leeren
  2599. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0);
  2600. # Queue wieder füllen
  2601. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, 0, 1)));
  2602. # Queue aktivieren
  2603. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), '');
  2604. } else {
  2605. SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
  2606. # Stück aktivieren
  2607. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA})));
  2608. }
  2609. # Abspielen starten, wenn nicht absichtlich verhindert
  2610. $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart);
  2611. }
  2612. }
  2613. } elsif ($workType eq 'loadPlaylist') {
  2614. my $answer = '';
  2615. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2616. my $playlistName = $1 if ($regSearch);
  2617. $playlistName = uri_unescape($params[0]) if (!$regSearch);
  2618. # RegEx prüfen...
  2619. if ($regSearch) {
  2620. eval { "" =~ m/$playlistName/ };
  2621. if($@) {
  2622. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$playlistName.'": '.$@);
  2623. return;
  2624. }
  2625. }
  2626. my $overwrite = $params[1];
  2627. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2628. # Queue vorher leeren?
  2629. if ($overwrite) {
  2630. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue();
  2631. $answer .= 'Queue successfully emptied. ';
  2632. }
  2633. my $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track') + 1;
  2634. if ($playlistName =~ /^:m3ufile:(.*)/) {
  2635. my @URIs = ();
  2636. my @Metas = ();
  2637. # Versuche die Datei zu öffnen
  2638. if (!open(FILE, '<'.$1)) {
  2639. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during opening file "'.$1.'": '.$!);
  2640. return;
  2641. };
  2642. binmode(FILE, ':encoding(utf-8)');
  2643. while (<FILE>) {
  2644. if ($_ =~ m/^ *([^#].*) *\n/) {
  2645. next if ($1 eq '');
  2646. my ($res, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($1));
  2647. push(@URIs, $res);
  2648. push(@Metas, $meta);
  2649. }
  2650. }
  2651. close FILE;
  2652. # Elemente an die Queue anhängen
  2653. $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos);
  2654. } elsif ($playlistName =~ /^:device:(.*)/) {
  2655. my $sourceUDN = $1;
  2656. my @URIs = ();
  2657. my @Metas = ();
  2658. # Titel laden
  2659. my $playlistData;
  2660. my $startIndex = 0;
  2661. do {
  2662. $playlistData = $SONOS_ContentDirectoryControlProxy{$sourceUDN}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, '');
  2663. my $tmp = decode('UTF-8', $playlistData->getValue('Result'));
  2664. while ($tmp =~ m/<item.*?>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2665. my ($res, $meta) = SONOS_CreateURIMeta(decode_entities($1));
  2666. next if (!defined($res));
  2667. push(@URIs, $res);
  2668. push(@Metas, $meta);
  2669. }
  2670. $startIndex += $playlistData->getValue('NumberReturned');
  2671. } while ($startIndex < $playlistData->getValue('TotalMatches'));
  2672. # Elemente an die Queue anhängen
  2673. $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos);
  2674. } else {
  2675. my $browseResult = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2676. my $tmp = $browseResult->getValue('Result');
  2677. my %resultHash;
  2678. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2679. my $name = $2;
  2680. $resultHash{$name} = $1;
  2681. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2682. if ($regSearch) {
  2683. if ($name =~ m/$playlistName/) {
  2684. $playlistName = $name;
  2685. $regSearch = 0;
  2686. }
  2687. }
  2688. }
  2689. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2690. if (!$resultHash{$playlistName} || $regSearch) {
  2691. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2692. return;
  2693. }
  2694. # Titel laden
  2695. my $playlistData = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($resultHash{$playlistName}, 'BrowseMetadata', '', 0, 0, '');
  2696. my $playlistRes = SONOS_GetTagData('res', $playlistData->getValue('Result'));
  2697. # Elemente an die Queue anhängen
  2698. my $result = $SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $playlistRes, '', $currentInsertPos, 0);
  2699. $answer .= $result->getValue('NumTracksAdded').' Elems added. '.$result->getValue('NewQueueLength').' Elems in list now. ';
  2700. }
  2701. # Die Liste als aktuelles Abspielstück einstellen
  2702. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2703. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  2704. $answer .= 'Startlist: '.SONOS_UPnPAnswerMessage($result).'. ';
  2705. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$answer);
  2706. }
  2707. } elsif ($workType eq 'setAlarm') {
  2708. my $create = $params[0];
  2709. my @idParams = split(',', $params[1]);
  2710. # Die passenden IDs heraussuchen...
  2711. my @idList = map { SONOS_Trim($_) } split(',', SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListIDs', ''));
  2712. my @id = ();
  2713. foreach my $elem (@idList) {
  2714. if ((lc($idParams[0]) eq 'all') || SONOS_isInList($elem, @idParams)) {
  2715. push @id, $elem;
  2716. }
  2717. }
  2718. # Alle folgenden Parameter weglesen und an den letzten Parameter anhängen
  2719. my $values = {};
  2720. my $val = join(',', @params[2..$#params]);
  2721. if ($val ne '') {
  2722. $values = \%{eval($val)};
  2723. }
  2724. # Wenn keine passenden Elemente gefunden wurden...
  2725. if (scalar(@id) == 0) {
  2726. if ((lc($create) eq 'update') || (lc($create) eq 'delete')) {
  2727. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  2728. }
  2729. }
  2730. # Hier die passenden Änderungen durchführen...
  2731. if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) {
  2732. # Die Room-ID immer fest auf den aktuellen Player eintragen.
  2733. # Hiermit sollte es nicht mehr möglich sein, einen Alarm für einen anderen Player einzutragen. Das kann man auch direkt an dem anderen Player durchführen...
  2734. $values->{RoomUUID} = $1 if ($udn =~ m/(.*?)_MR/i);
  2735. if (lc($create) eq 'update') {
  2736. my $ret = '';
  2737. foreach my $id (@id) {
  2738. my %alarm = %{eval(SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmList', '{}'))->{$id}};
  2739. # Replace old values with the given new ones...
  2740. for my $key (keys %alarm) {
  2741. if (defined($values->{$key})) {
  2742. $alarm{$key} = $values->{$key};
  2743. }
  2744. }
  2745. if (!SONOS_CheckAndCorrectAlarmHash(\%alarm)) {
  2746. $ret .= '#'.$id.': '.SONOS_AnswerMessage(0).', ';
  2747. } else {
  2748. # Send to Zoneplayer
  2749. $ret .= '#'.$id.': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->UpdateAlarm($id, $alarm{StartTime}, $alarm{Duration}, $alarm{Recurrence}, $alarm{Enabled}, $alarm{RoomUUID}, $alarm{ProgramURI}, $alarm{ProgramMetaData}, $alarm{PlayMode}, $alarm{Volume}, $alarm{IncludeLinkedZones})).', ';
  2750. }
  2751. }
  2752. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$ret);
  2753. } elsif (lc($create) eq 'create') {
  2754. # Check if all parameters are given
  2755. if (!SONOS_CheckAndCorrectAlarmHash($values)) {
  2756. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  2757. } else {
  2758. # create here on Zoneplayer
  2759. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AlarmClockControlProxy{$udn}->CreateAlarm($values->{StartTime}, $values->{Duration}, $values->{Recurrence}, $values->{Enabled}, $values->{RoomUUID}, $values->{ProgramURI}, $values->{ProgramMetaData}, $values->{PlayMode}, $values->{Volume}, $values->{IncludeLinkedZones})->getValue('AssignedID'));
  2760. }
  2761. } elsif (lc($create) eq 'delete') {
  2762. my $ret = '';
  2763. foreach my $id (@id) {
  2764. $ret .= '#'.$id.': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->DestroyAlarm($id)).', ';
  2765. }
  2766. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$ret);
  2767. } else {
  2768. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  2769. }
  2770. }
  2771. } elsif ($workType eq 'setSnoozeAlarm') {
  2772. my $time = $params[0];
  2773. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2774. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SnoozeAlarm(0, $time)));
  2775. }
  2776. } elsif ($workType eq 'setDailyIndexRefreshTime') {
  2777. my $time = $params[0];
  2778. if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) {
  2779. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->SetDailyIndexRefreshTime($time)));
  2780. }
  2781. } elsif ($workType eq 'setSleepTimer') {
  2782. my $time = $params[0];
  2783. if ((lc($time) eq 'off') || ($time =~ /0+:0+:0+/)) {
  2784. $time = '';
  2785. }
  2786. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2787. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->ConfigureSleepTimer(0, $time)));
  2788. }
  2789. } elsif ($workType eq 'addMember') {
  2790. my $memberudn = $params[0];
  2791. if (SONOS_CheckProxyObject($memberudn, $SONOS_AVTransportControlProxy{$memberudn}) && SONOS_CheckProxyObject($memberudn, $SONOS_ZoneGroupTopologyProxy{$memberudn})) {
  2792. # Wenn der hinzuzufügende Player Koordinator einer anderen Gruppe ist,
  2793. # dann erst mal ein anderes Gruppenmitglied zum Koordinator machen
  2794. #my @zoneTopology = SONOS_ConvertZoneGroupState($SONOS_ZoneGroupTopologyProxy{$memberudn}->GetZoneGroupState()->getValue('ZoneGroupState'));
  2795. # Hier fehlt noch die Umstellung der bestehenden Gruppe...
  2796. # Sicherstellen, dass der hinzuzufügende Player kein Bestandteil einer Gruppe mehr ist.
  2797. $SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0);
  2798. my $coordinatorUDNShort = $1 if ($udn =~ m/(.*)_MR/);
  2799. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->SetAVTransportURI(0, 'x-rincon:'.$coordinatorUDNShort, '')));
  2800. }
  2801. } elsif ($workType eq 'removeMember') {
  2802. my $memberudn = $params[0];
  2803. if (SONOS_CheckProxyObject($memberudn, $SONOS_AVTransportControlProxy{$memberudn})) {
  2804. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0)));
  2805. }
  2806. } elsif ($workType eq 'makeStandaloneGroup') {
  2807. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2808. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->BecomeCoordinatorOfStandaloneGroup(0)));
  2809. }
  2810. } elsif ($workType eq 'createStereoPair') {
  2811. my $pairString = uri_unescape($params[0]);
  2812. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2813. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->CreateStereoPair($pairString)));
  2814. }
  2815. } elsif ($workType eq 'separateStereoPair') {
  2816. my $pairString = uri_unescape($params[0]);
  2817. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2818. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SeparateStereoPair($pairString)));
  2819. }
  2820. } elsif ($workType eq 'emptyPlaylist') {
  2821. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2822. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue()));
  2823. }
  2824. } elsif ($workType eq 'savePlaylist') {
  2825. my $playlistName = $params[0];
  2826. my $playlistType = $params[1];
  2827. $playlistName =~ s/ $//g;
  2828. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2829. if ($playlistType eq ':m3ufile:') {
  2830. open (FILE, '>'.$playlistName);
  2831. print FILE "#EXTM3U\n";
  2832. my $startIndex = 0;
  2833. my $result;
  2834. my $count = 0;
  2835. do {
  2836. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, '');
  2837. my $queueSongdata = $result->getValue('Result');
  2838. while ($queueSongdata =~ m/<item.*?>(.*?)<\/item>/gi) {
  2839. my $item = $1;
  2840. my $res = uri_unescape(SONOS_GetURIFromQueueValue(decode_entities($1))) if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  2841. my $artist = decode_entities($1) if ($item =~ m/<dc:creator.*?>(.*?)<\/dc:creator>/i);
  2842. my $title = decode_entities($1) if ($item =~ m/<dc:title.*?>(.*?)<\/dc:title>/i);
  2843. my $time = 0;
  2844. $time = SONOS_GetTimeSeconds($1) if ($item =~ m/.*?duration="(.*?)"/);
  2845. # In Datei wegschreiben
  2846. eval {
  2847. print FILE "#EXTINF:$time,($artist) $title\n$res\n";
  2848. };
  2849. $count++;
  2850. }
  2851. $startIndex += $result->getValue('NumberReturned');
  2852. } while ($startIndex < $result->getValue('TotalMatches'));
  2853. close FILE;
  2854. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New M3U-File "'.$playlistName.'" successfully created with '.$count.' entries!');
  2855. } else {
  2856. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2857. my $tmp = $result->getValue('Result');
  2858. my %resultHash;
  2859. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2860. $resultHash{$2} = $1;
  2861. }
  2862. if ($resultHash{$playlistName}) {
  2863. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Existing Playlist "'.$playlistName.'" updated: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, $resultHash{$playlistName})));
  2864. } else {
  2865. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New Playlist '.$playlistName.' created: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, '')));
  2866. }
  2867. }
  2868. }
  2869. } elsif ($workType eq 'deleteFromQueue') {
  2870. $params[0] = uri_unescape($params[0]);
  2871. # Simple Check...
  2872. if ($params[0] !~ m/^[\.\,\d]*$/) {
  2873. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Parameter Error: '.$params[0]);
  2874. return;
  2875. }
  2876. my @elemList = sort { $a <=> $b } SONOS_DeleteDoublettes(eval('('.$params[0].')'));
  2877. SONOS_Log undef, 5, 'DeleteFromQueue: Index-Liste: '.Dumper(\@elemList);
  2878. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2879. # Maximale Indizies bestimmen
  2880. my $maxElems = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '')->getValue('TotalMatches');
  2881. my $deleteCounter = 0;
  2882. foreach my $elem (@elemList) {
  2883. if (($elem > 0) && ($elem <= $maxElems)) {
  2884. $deleteCounter++ if ($SONOS_AVTransportControlProxy{$udn}->RemoveTrackFromQueue(0, 'Q:0/'.($elem - $deleteCounter), 0)->isSuccessful());
  2885. }
  2886. }
  2887. $maxElems = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '')->getValue('TotalMatches');
  2888. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Deleted '.$deleteCounter.' elems. In list are now '.$maxElems.' elems.');
  2889. }
  2890. } elsif ($workType eq 'deletePlaylist') {
  2891. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2892. my $playlistName = $1 if ($regSearch);
  2893. $playlistName = uri_unescape($params[0]) if (!$regSearch);
  2894. # RegEx prüfen...
  2895. if ($regSearch) {
  2896. eval { "" =~ m/$playlistName/ };
  2897. if($@) {
  2898. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$playlistName.'": '.$@);
  2899. return;
  2900. }
  2901. }
  2902. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2903. my $tmp = $result->getValue('Result');
  2904. my %resultHash;
  2905. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2906. my $name = $2;
  2907. $resultHash{$name} = $1;
  2908. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2909. if ($regSearch) {
  2910. if ($name =~ m/$playlistName/) {
  2911. $playlistName = $name;
  2912. $regSearch = 0;
  2913. }
  2914. }
  2915. }
  2916. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2917. if (!$resultHash{$playlistName} || $regSearch) {
  2918. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2919. return;
  2920. }
  2921. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" deleted: '.SONOS_UPnPAnswerMessage($SONOS_ContentDirectoryControlProxy{$udn}->DestroyObject($resultHash{$playlistName})));
  2922. } elsif ($workType eq 'deleteProxyObjects') {
  2923. # Wird vom Sonos-Device selber in IsAlive benötigt
  2924. SONOS_DeleteProxyObjects($udn);
  2925. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(1));
  2926. } elsif ($workType eq 'renewSubscription') {
  2927. if (defined($SONOS_TransportSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_TransportSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2928. eval {
  2929. $SONOS_TransportSubscriptions{$udn}->renew();
  2930. SONOS_Log $udn, 3, 'Transport-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  2931. };
  2932. if ($@) {
  2933. SONOS_Log $udn, 3, 'Error! Transport-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  2934. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  2935. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  2936. if ($@ =~ m/Can.t connect to/) {
  2937. SONOS_DeleteProxyObjects($udn);
  2938. }
  2939. }
  2940. }
  2941. if (defined($SONOS_RenderingSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_RenderingSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2942. eval {
  2943. $SONOS_RenderingSubscriptions{$udn}->renew();
  2944. SONOS_Log $udn, 3, 'Rendering-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  2945. };
  2946. if ($@) {
  2947. SONOS_Log $udn, 3, 'Error! Rendering-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  2948. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  2949. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  2950. if ($@ =~ m/Can.t connect to/) {
  2951. SONOS_DeleteProxyObjects($udn);
  2952. }
  2953. }
  2954. }
  2955. if (defined($SONOS_GroupRenderingSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_GroupRenderingSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2956. eval {
  2957. $SONOS_GroupRenderingSubscriptions{$udn}->renew();
  2958. SONOS_Log $udn, 3, 'GroupRendering-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  2959. };
  2960. if ($@) {
  2961. SONOS_Log $udn, 3, 'Error! GroupRendering-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  2962. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  2963. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  2964. if ($@ =~ m/Can.t connect to/) {
  2965. SONOS_DeleteProxyObjects($udn);
  2966. }
  2967. }
  2968. }
  2969. if (defined($SONOS_ContentDirectorySubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_ContentDirectorySubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2970. eval {
  2971. $SONOS_ContentDirectorySubscriptions{$udn}->renew();
  2972. SONOS_Log $udn, 3, 'ContentDirectory-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  2973. };
  2974. if ($@) {
  2975. SONOS_Log $udn, 3, 'Error! ContentDirectory-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  2976. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  2977. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  2978. if ($@ =~ m/Can.t connect to/) {
  2979. SONOS_DeleteProxyObjects($udn);
  2980. }
  2981. }
  2982. }
  2983. if (defined($SONOS_AlarmSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_AlarmSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2984. eval {
  2985. $SONOS_AlarmSubscriptions{$udn}->renew();
  2986. SONOS_Log $udn, 3, 'Alarm-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  2987. };
  2988. if ($@) {
  2989. SONOS_Log $udn, 3, 'Error! Alarm-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  2990. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  2991. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  2992. if ($@ =~ m/Can.t connect to/) {
  2993. SONOS_DeleteProxyObjects($udn);
  2994. }
  2995. }
  2996. }
  2997. if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_ZoneGroupTopologySubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  2998. eval {
  2999. $SONOS_ZoneGroupTopologySubscriptions{$udn}->renew();
  3000. SONOS_Log $udn, 3, 'ZoneGroupTopology-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  3001. };
  3002. if ($@) {
  3003. SONOS_Log $udn, 3, 'Error! ZoneGroupTopology-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  3004. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  3005. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  3006. if ($@ =~ m/Can.t connect to/) {
  3007. SONOS_DeleteProxyObjects($udn);
  3008. }
  3009. }
  3010. }
  3011. if (defined($SONOS_DevicePropertiesSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_DevicePropertiesSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  3012. eval {
  3013. $SONOS_DevicePropertiesSubscriptions{$udn}->renew();
  3014. SONOS_Log $udn, 3, 'DeviceProperties-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  3015. };
  3016. if ($@) {
  3017. SONOS_Log $udn, 3, 'Error! DeviceProperties-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  3018. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  3019. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  3020. if ($@ =~ m/Can.t connect to/) {
  3021. SONOS_DeleteProxyObjects($udn);
  3022. }
  3023. }
  3024. }
  3025. if (defined($SONOS_AudioInSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_AudioInSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  3026. eval {
  3027. $SONOS_AudioInSubscriptions{$udn}->renew();
  3028. SONOS_Log $udn, 3, 'AudioIn-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  3029. };
  3030. if ($@) {
  3031. SONOS_Log $udn, 3, 'Error! AudioIn-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  3032. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  3033. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  3034. if ($@ =~ m/Can.t connect to/) {
  3035. SONOS_DeleteProxyObjects($udn);
  3036. }
  3037. }
  3038. }
  3039. } elsif ($workType eq 'playURI') {
  3040. my $songURI = SONOS_ExpandURIForQueueing($params[0]);
  3041. SONOS_Log undef, 3, 'songURI: '.$songURI;
  3042. my $volume;
  3043. if ($#params > 0) {
  3044. $volume = $params[1];
  3045. }
  3046. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3047. my ($uri, $meta) = SONOS_CreateURIMeta($songURI);
  3048. SONOS_Log undef, 0, 'URI: '.$uri;
  3049. SONOS_Log undef, 0, 'Meta: '.$meta;
  3050. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $uri, $meta);
  3051. if (defined($volume)) {
  3052. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  3053. $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0);
  3054. if ($volume =~ m/^[+-]{1}/) {
  3055. $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume)
  3056. } else {
  3057. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume);
  3058. }
  3059. }
  3060. }
  3061. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)->isSuccessful));
  3062. }
  3063. } elsif ($workType eq 'playURITemp') {
  3064. my $destURL = $params[0];
  3065. my $volume;
  3066. if ($#params > 0) {
  3067. $volume = $params[1];
  3068. }
  3069. SONOS_PlayURITemp($udn, $destURL, $volume);
  3070. } elsif ($workType eq 'addURIToQueue') {
  3071. my $songURI = SONOS_ExpandURIForQueueing($params[0]);
  3072. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3073. my $track = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track');
  3074. my ($uri, $meta) = SONOS_CreateURIMeta($songURI);
  3075. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $uri, $meta, $track + 1, 1)));
  3076. }
  3077. } elsif ($workType =~ m/speak\d+/i) {
  3078. my $volume = $params[0];
  3079. my $language = $params[1];
  3080. my $text = $params[2];
  3081. for(my $i = 3; $i < @params; $i++) {
  3082. $text .= ','.$params[$i];
  3083. }
  3084. $text =~ s/^ *(.*) *$/$1/g;
  3085. $text = SONOS_Utf8ToLatin1($text);
  3086. my $digest = '';
  3087. if (SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileHashCache', 0) == 1) {
  3088. eval {
  3089. require Digest::SHA1;
  3090. import Digest::SHA1 qw(sha1_hex);
  3091. $digest = '_'.sha1_hex(lc($text));
  3092. };
  3093. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  3094. # Unter Ubuntu gibt es die SHA1-Library nicht mehr, sodass man dort eine andere einbinden muss (SHA)
  3095. eval {
  3096. require Digest::SHA;
  3097. import Digest::SHA qw(sha1_hex);
  3098. $digest = '_'.sha1_hex(lc($text));
  3099. };
  3100. }
  3101. if ($@) {
  3102. SONOS_Log $udn, 2, 'Beim Ermitteln des Hash-Wertes ist ein Fehler aufgetreten: '.$@;
  3103. return;
  3104. }
  3105. }
  3106. my $timestamp = '';
  3107. if (!$digest && SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileTimestamp', 0) == 1) {
  3108. my @timearray = localtime;
  3109. $timestamp = sprintf("_%04d%02d%02d-%02d%02d%02d", $timearray[5]+1900, $timearray[4]+1, $timearray[3], $timearray[2], $timearray[1], $timearray[0]);
  3110. }
  3111. my $fileExtension = SONOS_GetSpeakFileExtension($workType);
  3112. my $dest = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakDir', '.').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension;
  3113. my $destURL = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakURL', '').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension;
  3114. if ($digest && (-e $dest)) {
  3115. SONOS_Log $udn, 3, 'Hole die Durchsage aus dem Cache...';
  3116. } else {
  3117. if (!SONOS_GetSpeakFile($udn, $workType, $language, $text, $dest)) {
  3118. return;
  3119. }
  3120. # MP3-Tags setzen, wenn die entsprechende Library gefunden wurde, und die Ausgabe in ein MP3-Format erfolgte
  3121. if (lc(substr($dest, -3, 3)) eq 'mp3') {
  3122. eval {
  3123. my $mp3GroundPath = SONOS_GetAbsolutePath($0);
  3124. $mp3GroundPath = substr($mp3GroundPath, 0, rindex($mp3GroundPath, '/'));
  3125. require MP3::Tag;
  3126. my $mp3 = MP3::Tag->new($dest);
  3127. $mp3->config(write_v24 => 1);
  3128. $mp3->title_set($text);
  3129. $mp3->artist_set('FHEM ~ Sonos');
  3130. $mp3->album_set('Sprachdurchsagen');
  3131. my $coverPath = SONOS_Client_Data_Retreive('undef', 'attr', ucfirst(lc(($workType =~ /0$/) ? 'speak' : $workType)).'Cover', $mp3GroundPath.'/www/images/default/fhemicon.png');
  3132. my $imgfile = SONOS_ReadFile($coverPath);
  3133. $mp3->set_id3v2_frame('APIC', 0, (($coverPath =~ m/\.png$/) ? 'image/png' : 'image/jpeg'), chr(3), 'Cover Image', $imgfile) if ($imgfile);
  3134. $mp3->update_tags();
  3135. };
  3136. if ($@) {
  3137. SONOS_Log $udn, 2, 'Beim Setzen der MP3-Informationen (ID3TagV2) ist ein Fehler aufgetreten: '.$@;
  3138. }
  3139. }
  3140. }
  3141. SONOS_PlayURITemp($udn, $destURL, $volume);
  3142. } elsif ($workType eq 'restartControlPoint') {
  3143. SONOS_RestartControlPoint();
  3144. } else {
  3145. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DoWork-Syntax ERROR');
  3146. }
  3147. };
  3148. if ($@) {
  3149. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'DoWork-Exception ERROR: '.$@);
  3150. }
  3151. $SONOS_ComObjectTransportQueue->dequeue();
  3152. }
  3153. return 1;
  3154. };
  3155. SONOS_LoadBookmarkValues();
  3156. my $error;
  3157. do {
  3158. $SONOS_RestartControlPoint = 0;
  3159. eval {
  3160. $SONOS_Controlpoint = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, SubscriptionURL => '/fhemmodule', MaxWait => 30, UsedOnlyIP => \%usedonlyIPs, IgnoreIP => \%ignoredIPs);
  3161. $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
  3162. $SONOS_Controlpoint->handle;
  3163. };
  3164. $error = $@;
  3165. # Nur wenn es der Fehler mit der XML-Struktur ist, dann den UPnP-Handler nochmal anstarten...
  3166. if (($error =~ m/multiple roots, wrong element '.*?'/si) || ($error =~ m/junk '.*?' after XML element/si) || ($error =~ m/mismatched tag '.*?'/si) || ($error =~ m/no element found/si) || ($error =~ m/500 Can't connect to/si) || ($error =~ m/not properly closed tag '.*?'/si) || ($error =~ m/Bad arg length for Socket::unpack_sockaddr_in/si)) {
  3167. SONOS_Log undef, 2, "Error during UPnP-Handling, restarting handling: $error";
  3168. SONOS_StopControlPoint();
  3169. } else {
  3170. SONOS_Log undef, 2, "Error during UPnP-Handling: $error";
  3171. SONOS_StopControlPoint();
  3172. undef($error);
  3173. }
  3174. } while ($error || $SONOS_RestartControlPoint);
  3175. SONOS_SaveBookmarkValues();
  3176. SONOS_Log undef, 3, 'UPnP-Thread wurde beendet.';
  3177. $SONOS_Thread = -1;
  3178. return 1;
  3179. }
  3180. ########################################################################################
  3181. #
  3182. # SONOS_RecursiveStructure - Retrieves the structure of the Sonos-Bibliothek
  3183. #
  3184. ########################################################################################
  3185. sub SONOS_RecursiveStructure($$$$) {
  3186. my ($udn, $search, $exportsStruct, $exportsTitles) = @_;
  3187. my $startIndex = 0;
  3188. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, '');
  3189. return if (!defined($result->getValue('NumberReturned')));
  3190. $startIndex += $result->getValue('NumberReturned');
  3191. my $tmp = decode('UTF-8', $result->getValue('Result'));
  3192. # Alle Suchergebnisse vom Player abfragen...
  3193. while ($startIndex < $result->getValue('TotalMatches')) {
  3194. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, '');
  3195. $tmp .= decode('UTF-8', $result->getValue('Result'));
  3196. $startIndex += $result->getValue('NumberReturned');
  3197. }
  3198. # Struktur verarbeiten...
  3199. while ($tmp =~ m/<container id="(.*?)".*?>(.*?)<\/container>/ig) {
  3200. my $id = $1;
  3201. my $item = $2;
  3202. next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen
  3203. $exportsStruct->{$id}->{ID} = $id;
  3204. $exportsStruct->{$id}->{Title} = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  3205. $exportsStruct->{$id}->{Artist} = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  3206. $exportsStruct->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  3207. $exportsStruct->{$id}->{Type} = 'Container';
  3208. $exportsStruct->{$id}->{Children} = {};
  3209. # Wenn hier eine Titel-ID gesucht werden soll, die es bereits lokal gibt, dann nicht mehr anfragen...
  3210. if (!$exportsTitles->{$id}) {
  3211. SONOS_RecursiveStructure($udn, $id, $exportsStruct->{$id}->{Children}, $exportsTitles);
  3212. }
  3213. }
  3214. # Titel verarbeiten...
  3215. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  3216. my $id = $1;
  3217. my $item = $2;
  3218. next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen
  3219. # Titel merken...
  3220. $exportsTitles->{$id}->{ID} = $id;
  3221. $exportsTitles->{$id}->{TrackURI} = SONOS_GetURIFromQueueValue($1) if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  3222. $exportsTitles->{$id}->{Title} = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  3223. $exportsTitles->{$id}->{Artist} = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  3224. $exportsTitles->{$id}->{AlbumArtist} = $1 if ($item =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  3225. $exportsTitles->{$id}->{Album} = $1 if ($item =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  3226. $exportsTitles->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  3227. $exportsTitles->{$id}->{OriginalTrackNumber} = $1 if ($item =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  3228. # Verweis in der Struktur merken...
  3229. $exportsStruct->{$id}->{ID} = $id;
  3230. $exportsStruct->{$id}->{Type} = 'Track';
  3231. }
  3232. }
  3233. ########################################################################################
  3234. #
  3235. # SONOS_AddMultipleURIsToQueue - Adds the given URIs to the current queue of the player
  3236. #
  3237. ########################################################################################
  3238. sub SONOS_AddMultipleURIsToQueue($$$$) {
  3239. my ($udn, $URIs, $Metas, $currentInsertPos) = @_;
  3240. my @URIs = @{$URIs};
  3241. my @Metas = @{$Metas};
  3242. my $sliceSize = 16;
  3243. my $result;
  3244. my $count = 0;
  3245. my $answer = '';
  3246. SONOS_Log $udn, 5, "Start-Adding: Count ".scalar(@URIs)." / $sliceSize";
  3247. for my $i (0..int(scalar(@URIs) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren
  3248. my $startIndex = $i * $sliceSize;
  3249. my $endIndex = $startIndex + $sliceSize - 1;
  3250. $endIndex = SONOS_Min(scalar(@URIs) - 1, $endIndex);
  3251. SONOS_Log $udn, 5, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)";
  3252. SONOS_Log $udn, 5, "Upload($currentInsertPos)-URI: ".join(' ', @URIs[$startIndex..$endIndex]);
  3253. SONOS_Log $udn, 5, "Upload($currentInsertPos)-Meta: ".join(' ', @Metas[$startIndex..$endIndex]);
  3254. $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, join(' ', @URIs[$startIndex..$endIndex]), join(' ', @Metas[$startIndex..$endIndex]), '', '', $currentInsertPos, 0);
  3255. if (!$result->isSuccessful()) {
  3256. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  3257. }
  3258. $currentInsertPos += $endIndex - $startIndex + 1;
  3259. $count = $endIndex + 1;
  3260. }
  3261. if ($result->isSuccessful()) {
  3262. $answer .= 'Added '.$count.' entries from file "'.$1.'". There are now '.$result->getValue('NewQueueLength').' entries in Queue. ';
  3263. } else {
  3264. $answer .= 'Adding: '.SONOS_UPnPAnswerMessage($result).' ';
  3265. }
  3266. return $answer;
  3267. }
  3268. ########################################################################################
  3269. #
  3270. # SONOS_Hex2String - Converts Hex-Representation into String
  3271. #
  3272. ########################################################################################
  3273. sub SONOS_Hex2String($) {
  3274. my $s = shift;
  3275. return pack 'H*', $s;
  3276. }
  3277. ########################################################################################
  3278. #
  3279. # SONOS_String2Hex - Converts a normal String into the Hex-Representation
  3280. #
  3281. ########################################################################################
  3282. sub SONOS_String2Hex($) {
  3283. my $s = shift;
  3284. return unpack("H*", $s);
  3285. }
  3286. ########################################################################################
  3287. #
  3288. # SONOS_Fisher_Yates_Shuffle - Shuffles the given array
  3289. #
  3290. ########################################################################################
  3291. sub SONOS_Fisher_Yates_Shuffle($) {
  3292. my ($deck) = @_; # $deck is a reference to an array
  3293. my $i = @$deck;
  3294. while ($i--) {
  3295. my $j = int rand ($i+1);
  3296. @$deck[$i,$j] = @$deck[$j,$i];
  3297. }
  3298. }
  3299. ########################################################################################
  3300. #
  3301. # SONOS_GetShuffleRepeatStates - Retreives the information according shuffle and repeat
  3302. #
  3303. ########################################################################################
  3304. sub SONOS_GetShuffleRepeatStates($) {
  3305. my ($data) = @_;
  3306. my $shuffle = $data =~ m/SHUFFLE/;
  3307. my $repeat = $data eq 'SHUFFLE' || $data eq 'REPEAT_ALL';
  3308. my $repeatOne = $data =~ m/REPEAT_ONE/;
  3309. return ($shuffle, $repeat, $repeatOne);
  3310. }
  3311. ########################################################################################
  3312. #
  3313. # SONOS_GetShuffleRepeatString - Generates the information string according shuffle and repeat
  3314. #
  3315. ########################################################################################
  3316. sub SONOS_GetShuffleRepeatString($$$) {
  3317. my ($shuffle, $repeat, $repeatOne) = @_;
  3318. my $newMode = 'NORMAL';
  3319. $newMode = 'SHUFFLE' if ($shuffle && $repeat && $repeatOne);
  3320. $newMode = 'SHUFFLE' if ($shuffle && $repeat && !$repeatOne);
  3321. $newMode = 'SHUFFLE_REPEAT_ONE' if ($shuffle && !$repeat && $repeatOne);
  3322. $newMode = 'SHUFFLE_NOREPEAT' if ($shuffle && !$repeat && !$repeatOne);
  3323. $newMode = 'REPEAT_ALL' if (!$shuffle && $repeat && $repeatOne);
  3324. $newMode = 'REPEAT_ALL' if (!$shuffle && $repeat && !$repeatOne);
  3325. $newMode = 'REPEAT_ONE' if (!$shuffle && !$repeat && $repeatOne);
  3326. $newMode = 'NORMAL' if (!$shuffle && !$repeat && !$repeatOne);
  3327. return $newMode;
  3328. }
  3329. ########################################################################################
  3330. #
  3331. # SONOS_DeleteDoublettes - Deletes duplicate entries in the given array
  3332. #
  3333. ########################################################################################
  3334. sub SONOS_DeleteDoublettes{
  3335. return keys %{{ map { $_ => 1 } @_ }};
  3336. }
  3337. ########################################################################################
  3338. #
  3339. # SONOS_Trim - Trim the given string
  3340. #
  3341. ########################################################################################
  3342. sub SONOS_Trim($) {
  3343. my ($str) = @_;
  3344. return $1 if ($str =~ m/^ *(.*?) *$/);
  3345. return $str;
  3346. }
  3347. ########################################################################################
  3348. #
  3349. # SONOS_CountInString - Count the occurences of the first string in the second string
  3350. #
  3351. ########################################################################################
  3352. sub SONOS_CountInString($$) {
  3353. my ($search, $str) = @_;
  3354. my $pos = 0;
  3355. my $matches = 0;
  3356. while (1) {
  3357. $pos = index($str, $search, $pos);
  3358. last if($pos < 0);
  3359. $matches++;
  3360. $pos++;
  3361. }
  3362. return $matches;
  3363. }
  3364. ########################################################################################
  3365. #
  3366. # SONOS_MakeCoverURL - Generates the approbriate cover-url incl. the use of an Fhem-Proxy
  3367. #
  3368. ########################################################################################
  3369. sub SONOS_MakeCoverURL($$) {
  3370. my ($udn, $resURL) = @_;
  3371. SONOS_Log $udn, 5, 'MakeCoverURL-Before: '.$resURL;
  3372. if ($resURL =~ m/^x-rincon-cpcontainer.*?(spotify.*?)(\?|$)/i) {
  3373. $resURL = SONOS_getSpotifyCoverURL($1, 1);
  3374. } elsif ($resURL =~ m/^x-sonos-spotify:spotify%3atrack%3a(.*?)(\?|$)/i) {
  3375. $resURL = SONOS_getSpotifyCoverURL($1);
  3376. } elsif (($resURL =~ m/x-rincon-playlist:.*?#(.*)/i) || ($resURL =~ m/savedqueues.rsq(#\d+)/i)) {
  3377. my $search = $1;
  3378. $search = 'SQ:'.$1 if ($search =~ m/#(\d+)/i);
  3379. # Default, if nothing could be retreived...
  3380. $resURL = '/fhem/sonos/cover/playlist.jpg';
  3381. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3382. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 5, '');
  3383. if ($result) {
  3384. my $tmp = $result->getValue('Result');
  3385. while (defined($tmp) && $tmp =~ m/<container id="(.+?)".*?>.*?<\/container>/i) {
  3386. $search = $1;
  3387. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 5, '');
  3388. if ($result) {
  3389. $tmp = $result->getValue('Result');
  3390. } else {
  3391. undef($tmp);
  3392. }
  3393. }
  3394. }
  3395. if ($result) {
  3396. my $tmp = $result->getValue('Result');
  3397. if (defined($tmp) && $tmp =~ m/<item id=".+?".*?>.*?<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>.*?<\/item>/i) {
  3398. $resURL = $1;
  3399. $resURL =~ s/%25/%/ig;
  3400. # Bei Spotify-URIs, die AlbumURL korrigieren...
  3401. if ($resURL =~ m/getaa.*?x-sonos-spotify%3aspotify%3atrack%3a(.*?)%3f/i) {
  3402. $resURL = SONOS_getSpotifyCoverURL($1);
  3403. } else {
  3404. $resURL = $1.$resURL if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
  3405. }
  3406. }
  3407. }
  3408. }
  3409. } else {
  3410. my $stream = 0;
  3411. $stream = 1 if (($resURL =~ /x-sonosapi-stream/) && ($resURL !~ /x-sonos-http%3aamz/));
  3412. $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.SONOS_URI_Escape($resURL) if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
  3413. }
  3414. # Alles über Fhem als Proxy laufen lassen?
  3415. $resURL = '/fhem/sonos/proxy/aa?url='.SONOS_URI_Escape($resURL) if (($resURL !~ m/^\//) && SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
  3416. SONOS_Log $udn, 5, 'MakeCoverURL-After: '.$resURL;
  3417. return $resURL;
  3418. }
  3419. ########################################################################################
  3420. #
  3421. # SONOS_getSpotifyCoverURL - Generates the approbriate cover-url for Spotify-Cover
  3422. #
  3423. ########################################################################################
  3424. sub SONOS_getSpotifyCoverURL($;$) {
  3425. my ($trackID, $oldStyle) = @_;
  3426. $oldStyle = 0 if (!defined($oldStyle));
  3427. my $infos = '';
  3428. if ($oldStyle) {
  3429. $infos = $1 if (get('https://embed.spotify.com/oembed/?url='.$trackID) =~ m/"thumbnail_url":"(.*?)"/i);
  3430. } else {
  3431. $infos = $1 if (get('https://api.spotify.com/v1/tracks/'.$trackID) =~ m/"images".*?:.*?\[.*?{.*?"height".*?:.*?\d{3},.*?"url".*?:.*?"(.*?)",.*?"width"/is);
  3432. }
  3433. $infos =~ s/\\//g;
  3434. $infos = $1.'original'.$3 if ($infos =~ m/(.*?\/)(cover|default)(\/.*)/i);
  3435. # Falls es ein Standardcover von Spotify geben soll, lieber das Thumbnail von Sonos verwenden...
  3436. return '' if ($infos =~ m/\/static\/img\/defaultCoverL.png/i);
  3437. if ($infos ne '') {
  3438. return $infos;
  3439. }
  3440. return '';
  3441. }
  3442. ########################################################################################
  3443. #
  3444. # SONOS_GetSpeakFileExtension - Retrieves the desired fileextension
  3445. #
  3446. ########################################################################################
  3447. sub SONOS_GetSpeakFileExtension($) {
  3448. my ($workType) = @_;
  3449. if (lc($workType) eq 'speak0') {
  3450. return 'mp3';
  3451. } elsif ($workType =~ m/speak\d+/i) {
  3452. $workType = ucfirst(lc($workType));
  3453. my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0);
  3454. if ($speakDefinition =~ m/(.*?):(.*)/) {
  3455. return $1;
  3456. }
  3457. }
  3458. return '';
  3459. }
  3460. ########################################################################################
  3461. #
  3462. # SONOS_GetSpeakFile - Generates the audiofile according to the given text, language and generator
  3463. #
  3464. ########################################################################################
  3465. sub SONOS_GetSpeakFile($$$$$) {
  3466. my ($udn, $workType, $language, $text, $destFileName) = @_;
  3467. my $targetSpeakMP3FileDir = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakMP3FileDir', '');
  3468. # Parametrisieren...
  3469. my $chunksize = $SONOS_GOOGLETRANSLATOR_CHUNKSIZE;
  3470. my $textescaped = SONOS_URI_Escape($text);
  3471. my $textutf8 = SONOS_Latin1ToUtf8($text);
  3472. my $textutf8escaped = SONOS_URI_Escape($textutf8);
  3473. # Chunks ermitteln...
  3474. # my @textList = ($text =~ m/(?:\b(?:[^ ]+)\W*){0,$SONOS_GOOGLETRANSLATOR_CHUNKSIZE}/g);
  3475. # pop @textList; # Letztes Element ist immer leer, deshalb abschneiden...
  3476. my @textList = ('');
  3477. for my $elem (split(/[ \t]/, $text)) {
  3478. # Files beibehalten...
  3479. if ($elem =~ m/\|(.*)\|/) {
  3480. my $filename = $1;
  3481. $filename = $targetSpeakMP3FileDir.'/'.$filename if ($filename !~ m/^(\/|[a-z]:)/i);
  3482. $filename = $filename.'.mp3' if ($filename !~ m/\.mp3$/i);
  3483. push(@textList, '|'.$filename.'|');
  3484. push(@textList, '');
  3485. next;
  3486. }
  3487. if (length($textList[$#textList].' '.$elem) <= $chunksize) {
  3488. $textList[$#textList] .= ' '.$elem;
  3489. } else {
  3490. push(@textList, $elem);
  3491. }
  3492. }
  3493. SONOS_Log $udn, 5, 'Chunks: '.SONOS_Stringify(\@textList);
  3494. # Generating Speakfiles...
  3495. if (lc($workType) eq 'speak0') {
  3496. # Einzelne Chunks herunterladen...
  3497. my $counter = 0;
  3498. for my $text (@textList) {
  3499. # Leere Einträge überspringen...
  3500. next if ($text eq '');
  3501. $counter++;
  3502. # MP3Files direkt kopieren
  3503. if ($text =~ m/\|(.*)\|/) {
  3504. SONOS_Log $udn, 3, 'Copy MP3-File ('.$counter.'. Element) from "'.$1.'" to "'.$destFileName.$counter.'"';
  3505. copy($1, $destFileName.$counter);
  3506. # Etwaige ID-Tags entfernen...
  3507. eval {
  3508. use MP3::Info;
  3509. remove_mp3tag($destFileName.$counter, 'ALL');
  3510. };
  3511. if ($@) {
  3512. SONOS_Log $udn, 3, 'Copy MP3-File. ERROR during removing of ID3Tag: '.$@;
  3513. }
  3514. next;
  3515. }
  3516. my $url = sprintf(SONOS_Client_Data_Retreive('undef', 'attr', 'SpeakGoogleURL', $SONOS_GOOGLETRANSLATOR_URL), SONOS_URI_Escape(lc($language)), SONOS_URI_Escape($text));
  3517. SONOS_Log $udn, 3, 'Load Google generated MP3 ('.$counter.'. Element) from "'.$url.'" to "'.$destFileName.$counter.'"';
  3518. my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11');
  3519. my $response = $ua->get($url, ':content_file' => $destFileName.$counter);
  3520. if (!$response->is_success) {
  3521. SONOS_Log $udn, 1, 'MP3 Download-Error: '.$response->status_line;
  3522. unlink($destFileName.$counter);
  3523. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during downloading: '.$response->status_line);
  3524. return 0;
  3525. }
  3526. }
  3527. # Heruntergeladene Chunks zusammenführen...
  3528. return SONOS_CombineMP3Files($udn, $workType, $destFileName, $counter);
  3529. } elsif ($workType =~ m/speak\d+/i) {
  3530. $workType = ucfirst(lc($workType));
  3531. SONOS_Log $udn, 3, 'Load '.$workType.' generated SpeakFile to "'.$destFileName.'"';
  3532. my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0);
  3533. if ($speakDefinition =~ m/(.*?):(.*)/) {
  3534. $speakDefinition = $2;
  3535. $speakDefinition =~ s/%language%/$language/gi;
  3536. $speakDefinition =~ s/%filename%/$destFileName/gi;
  3537. $speakDefinition =~ s/%text%/$text/gi;
  3538. $speakDefinition =~ s/%textescaped%/$textescaped/gi;
  3539. $speakDefinition =~ s/%textutf8%/$textutf8/gi;
  3540. $speakDefinition =~ s/%textutf8escaped%/$textutf8escaped/gi;
  3541. SONOS_Log $udn, 5, 'Execute: '.$speakDefinition;
  3542. system($speakDefinition);
  3543. return 1;
  3544. } else {
  3545. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': No Definition found!');
  3546. return 0;
  3547. }
  3548. }
  3549. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Speaking not defined.');
  3550. return 0;
  3551. }
  3552. ########################################################################################
  3553. #
  3554. # SONOS_CombineMP3Files - Combine the loaded mp3-files
  3555. #
  3556. ########################################################################################
  3557. sub SONOS_CombineMP3Files($$$$) {
  3558. my ($udn, $workType, $destFileName, $counter) = @_;
  3559. SONOS_Log $udn, 3, 'Combine loaded chunks into "'.$destFileName.'"';
  3560. # Reinladen
  3561. my $newMP3File = '';
  3562. for(my $i = 1; $i <= $counter; $i++) {
  3563. $newMP3File .= SONOS_ReadFile($destFileName.$i);
  3564. unlink($destFileName.$i);
  3565. }
  3566. # Speichern
  3567. eval {
  3568. open MPFILE, '>'.$destFileName;
  3569. binmode MPFILE ;
  3570. print MPFILE $newMP3File;
  3571. close MPFILE;
  3572. };
  3573. if ($@) {
  3574. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during combining: '.$@);
  3575. return 0;
  3576. }
  3577. # Konvertieren?
  3578. my $targetSpeakMP3FileConverter = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakMP3FileConverter', '');
  3579. if ($targetSpeakMP3FileConverter) {
  3580. SONOS_Log $udn, 3, 'Convert combined file "'.$destFileName.'" with "'.$targetSpeakMP3FileConverter.'"';
  3581. eval {
  3582. my $destFileNameTMP = $destFileName;
  3583. $destFileNameTMP =~ s/^(.*)\/(.*?)$/$1\/TMP_$2/;
  3584. $targetSpeakMP3FileConverter =~ s/%infile%/$destFileName/gi;
  3585. $targetSpeakMP3FileConverter =~ s/%outfile%/$destFileNameTMP/gi;
  3586. SONOS_Log $udn, 5, 'Execute: '.$targetSpeakMP3FileConverter;
  3587. system($targetSpeakMP3FileConverter);
  3588. # "Alte" MP3-Datei entfernen, und die "neue" umbenennen...
  3589. unlink($destFileName);
  3590. move($destFileNameTMP, $destFileName);
  3591. };
  3592. if ($@) {
  3593. SONOS_Log $udn, 2, ucfirst($workType).': MP3-Creation ERROR during converting: '.$@;
  3594. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during converting: '.$@);
  3595. return 0;
  3596. }
  3597. }
  3598. return 1;
  3599. }
  3600. ########################################################################################
  3601. #
  3602. # SONOS_CreateURIMeta - Creates the Meta-Information according to the Song-URI
  3603. #
  3604. # Parameter $res = The URI to the song, for which the Metadata has to be generated
  3605. #
  3606. ########################################################################################
  3607. sub SONOS_CreateURIMeta($) {
  3608. my ($res) = @_;
  3609. my $meta = $SONOS_DIDLHeader.'<item id="" parentID="" restricted="true"><dc:title></dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item>'.$SONOS_DIDLFooter;
  3610. my $userID_Spotify = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Spotify', '-'));
  3611. my $userID_Napster = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Napster', '-'));
  3612. # Wenn es ein Spotify- oder Napster-Titel ist, dann den Benutzernamen extrahieren
  3613. if ($res =~ m/^(x-sonos-spotify:)(.*?)(\?.*)/) {
  3614. if ($userID_Spotify eq '-') {
  3615. SONOS_Log undef, 1, 'There are Spotify-Titles in list, and no Spotify-Username is known. Please empty the main queue and insert a random spotify-title in it for saving this information and do this action again!';
  3616. return;
  3617. }
  3618. $res = $1.SONOS_URI_Escape($2).$3;
  3619. $meta = $SONOS_DIDLHeader.'<item id="00030020'.SONOS_URI_Escape($2).'" parentID="" restricted="true"><dc:title></dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">'.$userID_Spotify.'</desc></item>'.$SONOS_DIDLFooter;
  3620. } elsif ($res =~ m/^(npsdy:)(.*?)(\.mp3)/) {
  3621. if ($userID_Napster eq '-') {
  3622. SONOS_Log undef, 1, 'There are Napster/Rhapsody-Titles in list, and no Napster-Username is known. Please empty the main queue and insert a random napster-title in it for saving this information and do this action again!';
  3623. return;
  3624. }
  3625. $res = $1.SONOS_URI_Escape($2).$3;
  3626. $meta = $SONOS_DIDLHeader.'<item id="RDCPI:GLBTRACK:'.SONOS_URI_Escape($2).'" parentID="" restricted="true"><dc:title></dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">'.$userID_Napster.'</desc></item>'.$SONOS_DIDLFooter;
  3627. } else {
  3628. $res =~ s/ /%20/ig;
  3629. $res =~ s/"/&quot;/ig;
  3630. }
  3631. return ($res, $meta);
  3632. }
  3633. ########################################################################################
  3634. #
  3635. # SONOS_CheckAlarmHash - Checks if the given hash has all neccessary Alarm-Parameters
  3636. # Additionally it converts some parameters for direct use for Zoneplayer-Update
  3637. #
  3638. # Parameter %old = All neccessary informations to check
  3639. #
  3640. ########################################################################################
  3641. sub SONOS_CheckAndCorrectAlarmHash($) {
  3642. my ($hash) = @_;
  3643. # Checks, if a value is missing
  3644. my @keys = keys(%$hash);
  3645. if ((!SONOS_isInList('StartTime', @keys))
  3646. || (!SONOS_isInList('Duration', @keys))
  3647. || (!SONOS_isInList('Recurrence_Once', @keys))
  3648. || (!SONOS_isInList('Recurrence_Monday', @keys))
  3649. || (!SONOS_isInList('Recurrence_Tuesday', @keys))
  3650. || (!SONOS_isInList('Recurrence_Wednesday', @keys))
  3651. || (!SONOS_isInList('Recurrence_Thursday', @keys))
  3652. || (!SONOS_isInList('Recurrence_Friday', @keys))
  3653. || (!SONOS_isInList('Recurrence_Saturday', @keys))
  3654. || (!SONOS_isInList('Recurrence_Sunday', @keys))
  3655. || (!SONOS_isInList('Enabled', @keys))
  3656. || (!SONOS_isInList('RoomUUID', @keys))
  3657. || (!SONOS_isInList('ProgramURI', @keys))
  3658. || (!SONOS_isInList('ProgramMetaData', @keys))
  3659. || (!SONOS_isInList('Shuffle', @keys))
  3660. || (!SONOS_isInList('Repeat', @keys))
  3661. || (!SONOS_isInList('Volume', @keys))
  3662. || (!SONOS_isInList('IncludeLinkedZones', @keys))) {
  3663. return 0;
  3664. }
  3665. # Convert some values
  3666. # Playmode
  3667. $hash->{PlayMode} = 'NORMAL';
  3668. $hash->{PlayMode} = 'SHUFFLE' if ($hash->{Repeat} && $hash->{Shuffle});
  3669. $hash->{PlayMode} = 'SHUFFLE_NOREPEAT' if (!$hash->{Repeat} && $hash->{Shuffle});
  3670. $hash->{PlayMode} = 'REPEAT_ALL' if ($hash->{Repeat} && !$hash->{Shuffle});
  3671. # Recurrence
  3672. if ($hash->{Recurrence_Once}) {
  3673. $hash->{Recurrence} = 'ONCE';
  3674. } else {
  3675. $hash->{Recurrence} = 'ON_';
  3676. $hash->{Recurrence} .= '0' if ($hash->{Recurrence_Sunday});
  3677. $hash->{Recurrence} .= '1' if ($hash->{Recurrence_Monday});
  3678. $hash->{Recurrence} .= '2' if ($hash->{Recurrence_Tuesday});
  3679. $hash->{Recurrence} .= '3' if ($hash->{Recurrence_Wednesday});
  3680. $hash->{Recurrence} .= '4' if ($hash->{Recurrence_Thursday});
  3681. $hash->{Recurrence} .= '5' if ($hash->{Recurrence_Friday});
  3682. $hash->{Recurrence} .= '6' if ($hash->{Recurrence_Saturday});
  3683. # Specials
  3684. $hash->{Recurrence} = 'DAILY' if (($hash->{Recurrence_Monday}) && ($hash->{Recurrence_Tuesday}) && ($hash->{Recurrence_Wednesday}) && ($hash->{Recurrence_Thursday}) && ($hash->{Recurrence_Friday}) && ($hash->{Recurrence_Saturday}) && ($hash->{Recurrence_Sunday}));
  3685. $hash->{Recurrence} = 'WEEKDAYS' if (($hash->{Recurrence_Monday}) && ($hash->{Recurrence_Tuesday}) && ($hash->{Recurrence_Wednesday}) && ($hash->{Recurrence_Thursday}) && ($hash->{Recurrence_Friday}) && (!$hash->{Recurrence_Saturday}) && (!$hash->{Recurrence_Sunday}));
  3686. $hash->{Recurrence} = 'WEEKENDS' if ((!$hash->{Recurrence_Monday}) && (!$hash->{Recurrence_Tuesday}) && (!$hash->{Recurrence_Wednesday}) && (!$hash->{Recurrence_Thursday}) && (!$hash->{Recurrence_Friday}) && ($hash->{Recurrence_Saturday}) && ($hash->{Recurrence_Sunday}));
  3687. }
  3688. # If nothing is given, set 'ONCE'
  3689. if ($hash->{Recurrence} eq 'ON_') {
  3690. $hash->{Recurrence} = 'ONCE';
  3691. }
  3692. return 1;
  3693. }
  3694. ########################################################################################
  3695. #
  3696. # SONOS_RestoreOldPlaystate - Restores the old Position of a playing state
  3697. #
  3698. ########################################################################################
  3699. sub SONOS_RestoreOldPlaystate() {
  3700. SONOS_Log undef, 1, 'Restore-Thread gestartet. Warte auf Arbeit...';
  3701. my $runEndlessLoop = 1;
  3702. my $controlPoint = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, SubscriptionURL => '/fhemmodule', MaxWait => 20, UsedOnlyIP => \%usedonlyIPs, IgnoreIP => \%ignoredIPs);
  3703. $SIG{'PIPE'} = 'IGNORE';
  3704. $SIG{'CHLD'} = 'IGNORE';
  3705. $SIG{'INT'} = sub {
  3706. $runEndlessLoop = 0;
  3707. };
  3708. while ($runEndlessLoop) {
  3709. select(undef, undef, undef, 0.2);
  3710. next if (!$SONOS_PlayerRestoreQueue->pending());
  3711. # Es ist was auf der Queue... versuchen zu verarbeiten...
  3712. my %old = %{$SONOS_PlayerRestoreQueue->peek()};
  3713. # Wenn die Zeit noch nicht reif ist, dann doch wieder übergehen...
  3714. # Dabei die Schleife wieder von vorne beginnen lassen, da noch andere dazwischengeschoben werden könnten.
  3715. # Eine Weile in die Zukunft, da das ermitteln der Proxies Zeit benötigt.
  3716. next if ($old{RestoreTime} > time() + 2);
  3717. # ...sonst das Ding von der Queue nehmen...
  3718. $SONOS_PlayerRestoreQueue->dequeue();
  3719. # Hier die ursprünglichen Proxies wiederherstellen/neu verbinden...
  3720. my $device = $controlPoint->_createDevice($old{location});
  3721. my $AVProxy;
  3722. my $GRProxy;
  3723. my $CCProxy;
  3724. for my $subdevice ($device->children) {
  3725. if ($subdevice->UDN =~ /.*_MR/i) {
  3726. $AVProxy = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1')->controlProxy();
  3727. $GRProxy = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')->controlProxy();
  3728. }
  3729. if ($subdevice->UDN =~ /.*_MS/i) {
  3730. $CCProxy = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')->controlProxy();
  3731. }
  3732. }
  3733. my $udn = $device->UDN.'_MR';
  3734. $udn =~ s/.*?:(.*)/$1/;
  3735. SONOS_Log $udn.'_MR', 3, 'Restorethread has found a job. Waiting for stop playing...';
  3736. # Ist das Ding fertig abgespielt?
  3737. my $result;
  3738. do {
  3739. select(undef, undef, undef, 0.7);
  3740. $result = $AVProxy->GetTransportInfo(0);
  3741. } while ($result->getValue('CurrentTransportState') ne 'STOPPED');
  3742. SONOS_Log $udn, 3, 'Restoring playerstate...';
  3743. SONOS_Log $udn, 5, 'StoredURI: "'.$old{CurrentURI}.'"';
  3744. # Die Liste als aktuelles Abspielstück einstellen, oder den Stream wieder anwerfen
  3745. if ($old{CurrentURI} =~ /^x-.*?-(.*?stream|mp3radio)/) {
  3746. SONOS_Log $udn, 4, 'Restore Stream...';
  3747. $AVProxy->SetAVTransportURI(0, $old{CurrentURI}, $old{CurrentURIMetaData});
  3748. } else {
  3749. SONOS_Log $udn, 4, 'Restore Track #'.$old{Track}.', RelTime: "'.$old{RelTime}.'"...';
  3750. my $queueMetadata = $CCProxy->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  3751. $AVProxy->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  3752. $AVProxy->Seek(0, 'TRACK_NR', $old{Track});
  3753. $AVProxy->Seek(0, 'REL_TIME', $old{RelTime});
  3754. }
  3755. my $oldMute = $GRProxy->GetGroupMute(0)->getValue('CurrentMute');
  3756. $GRProxy->SetGroupMute(0, $old{Mute}) if (defined($old{Mute}) && ($old{Mute} != $oldMute));
  3757. my $oldVolume = $GRProxy->GetGroupVolume(0)->getValue('CurrentVolume');
  3758. $GRProxy->SetGroupVolume(0, $old{Volume}) if (defined($old{Volume}) && ($old{Volume} != $oldVolume));
  3759. if (($old{CurrentTransportState} eq 'PLAYING') || ($old{CurrentTransportState} eq 'TRANSITIONING')) {
  3760. $AVProxy->Play(0, 1);
  3761. } elsif ($old{CurrentTransportState} eq 'PAUSED_PLAYBACK') {
  3762. $AVProxy->Pause(0);
  3763. }
  3764. $SONOS_PlayerRestoreRunningUDN{$udn} = 0;
  3765. SONOS_Log $udn, 3, 'Playerstate restored!';
  3766. }
  3767. undef($controlPoint);
  3768. SONOS_Log undef, 1, 'Restore-Thread wurde beendet.';
  3769. $SONOS_Thread_PlayerRestore = -1;
  3770. }
  3771. ########################################################################################
  3772. #
  3773. # SONOS_PlayURITemp - Plays an URI temporary
  3774. #
  3775. # Parameter $udn = The udn of the SonosPlayer
  3776. # $destURLParam = URI, that has to be played
  3777. # $volumeParam = Volume for playing
  3778. #
  3779. ########################################################################################
  3780. sub SONOS_PlayURITemp($$$) {
  3781. my ($udn, $destURLParam, $volumeParam) = @_;
  3782. my %old;
  3783. $old{DestURIOriginal} = $destURLParam;
  3784. my ($songURI, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($old{DestURIOriginal}));
  3785. # Wenn auf diesem Player bereits eine temporäre Wiedergabe erfolgt, dann hier auf dessen Beendigung warten...
  3786. if (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) {
  3787. SONOS_Log $udn, 3, 'Temporary playing of "'.$old{DestURIOriginal}.'" must wait, because another playing is in work...';
  3788. while (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) {
  3789. select(undef, undef, undef, 0.2);
  3790. }
  3791. }
  3792. $SONOS_PlayerRestoreRunningUDN{$udn} = 1;
  3793. SONOS_Log $udn, 3, 'Start temporary playing of "'.$old{DestURIOriginal}.'"';
  3794. my $volume = $volumeParam;
  3795. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3796. $old{UDN} = $udn;
  3797. my $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0);
  3798. $old{Track} = $result->getValue('Track');
  3799. $old{RelTime} = $result->getValue('RelTime');
  3800. $result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  3801. $old{CurrentURI} = $result->getValue('CurrentURI');
  3802. $old{CurrentURIMetaData} = $result->getValue('CurrentURIMetaData');
  3803. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportInfo(0);
  3804. $old{CurrentTransportState} = $result->getValue('CurrentTransportState');
  3805. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $songURI, $meta);
  3806. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  3807. $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0);
  3808. $old{Mute} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute');
  3809. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, 0) if $old{Mute};
  3810. $old{Volume} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume');
  3811. if (defined($volume)) {
  3812. if ($volume =~ m/^[+-]{1}/) {
  3813. $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume) if $volume;
  3814. } else {
  3815. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume) if ($volume != $old{Volume});
  3816. }
  3817. }
  3818. }
  3819. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'PlayURITemp: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)));
  3820. SONOS_Log $udn, 4, 'All is started successfully. Retreive Positioninfo...';
  3821. $old{SleepTime} = 0;
  3822. eval {
  3823. $old{SleepTime} = SONOS_GetTimeSeconds($SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('TrackDuration'));
  3824. # Wenn es keine Laufzeitangabe gibt, dann muss diese selber berechnet werden, sofern möglich. Sollte dies nicht möglich sein, ist dies vermutlich ein Stream...
  3825. if ($old{SleepTime} == 0) {
  3826. SONOS_Log $udn, 3, 'SleepTimer berechnet die Laufzeit des Titels selber, da keine Wartezeit uebermittelt wurde!';
  3827. eval {
  3828. use MP3::Info;
  3829. my $tag = get_mp3info($old{DestURIOriginal});
  3830. if ($tag) {
  3831. $old{SleepTime} = $tag->{SECS};
  3832. }
  3833. };
  3834. if ($@) {
  3835. SONOS_Log $udn, 2, 'Bei der MP3-Längenermittlung ist ein Fehler aufgetreten: '.$@;
  3836. }
  3837. }
  3838. $old{RestoreTime} = time() + $old{SleepTime} - 1;
  3839. SONOS_Log $udn, 3, 'Laufzeitermittlung abgeschlossen: '.$old{SleepTime}.'s, Restore-Zeit: '.GetTimeString($old{RestoreTime});
  3840. };
  3841. # Location mitsichern, damit die Proxies neu geholt werden können
  3842. my %revUDNs = reverse %SONOS_Locations;
  3843. $old{location} = $revUDNs{$udn};
  3844. # Restore-Daten an der richtigen Stelle auf die Queue legen, damit der Player-Restore-Thread sich darum kümmern kann
  3845. # Aber nur, wenn auch ein Restore erfolgen kann, weil eine Zeit existiert
  3846. if (defined($old{SleepTime}) && ($old{SleepTime} != 0)) {
  3847. my $i;
  3848. for ($i = $SONOS_PlayerRestoreQueue->pending() - 1; $i >= 0; $i--) {
  3849. my %tmpOld = %{$SONOS_PlayerRestoreQueue->peek($i)};
  3850. last if ($old{RestoreTime} > $tmpOld{RestoreTime});
  3851. }
  3852. $SONOS_PlayerRestoreQueue->insert($i + 1, \%old);
  3853. } else {
  3854. SONOS_Log $udn, 1, 'Da keine Endzeit ermittelt werden konnte, wird kein Restoring durchgeführt werden!';
  3855. $SONOS_PlayerRestoreRunningUDN{$udn} = 0;
  3856. }
  3857. }
  3858. }
  3859. ########################################################################################
  3860. #
  3861. # SONOS_GetTrackProvider - Retrieves a textual representation of the Provider of the given URI
  3862. #
  3863. # Parameter $songURI = The URI that has to be converted
  3864. #
  3865. ########################################################################################
  3866. sub SONOS_GetTrackProvider($;$) {
  3867. my ($songURI, $songTitle) = @_;
  3868. # Backslashe umwandeln
  3869. $songURI =~ s/\\/\//g;
  3870. # Gruppen- und LineIn-Wiedergaben bereits hier erkennen
  3871. if ($songURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) {
  3872. return 'Gruppenwiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  3873. } elsif ($songURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) {
  3874. my $elem = 'LineIn';
  3875. $elem = $songTitle if (defined($songTitle) && $songTitle);
  3876. return $elem.'-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  3877. } elsif ($songURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) {
  3878. return 'Dock-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  3879. } elsif ($songURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) {
  3880. return 'SPDIF-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  3881. }
  3882. # Hier die restlichen Erkennungen durchführen...
  3883. for my $elem (keys %SONOS_ProviderList) {
  3884. if ($songURI =~ /$elem/) {
  3885. return $SONOS_ProviderList{$elem};
  3886. }
  3887. }
  3888. return '';
  3889. }
  3890. ########################################################################################
  3891. #
  3892. # SONOS_ExpandURIForQueueing - Expands and corrects a given URI
  3893. #
  3894. # Parameter $songURI = The URI that has to be converted
  3895. #
  3896. ########################################################################################
  3897. sub SONOS_ExpandURIForQueueing($) {
  3898. my ($songURI) = @_;
  3899. # Backslashe umwandeln
  3900. $songURI =~ s/\\/\//g;
  3901. # SongURI erweitern/korrigieren
  3902. $songURI = 'x-file-cifs:'.$songURI if ($songURI =~ m/^\/\//);
  3903. $songURI = 'x-rincon-mp3radio:'.$1 if ($songURI =~ m/^http:(\/\/.*)/);
  3904. return $songURI;
  3905. }
  3906. ########################################################################################
  3907. #
  3908. # SONOS_GetURIFromQueueValue - Gets the URI from current Informations
  3909. #
  3910. # Parameter $songURI = The URI that has to be converted
  3911. #
  3912. ########################################################################################
  3913. sub SONOS_GetURIFromQueueValue($) {
  3914. my ($songURI) = @_;
  3915. # SongURI erweitern/korrigieren
  3916. $songURI = $1 if ($songURI =~ m/^x-file-cifs:(.*)/i);
  3917. $songURI = 'http:'.$1 if ($songURI =~ m/^x-rincon-mp3radio:(.*)/i);
  3918. $songURI = uri_unescape($songURI) if ($songURI =~ m/^x-sonos-spotify:/i);
  3919. return $songURI;
  3920. }
  3921. ########################################################################################
  3922. #
  3923. # SONOS_GetTimeSeconds - Converts a Time-String like '0:04:12' to seconds (e.g. 252)
  3924. #
  3925. # Parameter $timeStr = The timeStr that has to be converted
  3926. #
  3927. ########################################################################################
  3928. sub SONOS_GetTimeSeconds($) {
  3929. my ($timeStr) = @_;
  3930. return SONOS_Max(int($1)*3600 + int($2)*60 + int($3), 1) if ($timeStr =~ m/(\d+):(\d+):(\d+)/);
  3931. return 0;
  3932. }
  3933. ########################################################################################
  3934. #
  3935. # SONOS_ConvertSecondsToTime - Converts seconds (e.g. 252) into a Time-String like '0:04:12'
  3936. #
  3937. # Parameter $seconds = The seconds that have to be converted
  3938. #
  3939. ########################################################################################
  3940. sub SONOS_ConvertSecondsToTime($) {
  3941. my ($seconds) = @_;
  3942. return sprintf('%01d:%02d:%02d', $seconds / 3600, ($seconds%3600) / 60, $seconds%60) if ($seconds > 0);
  3943. return '0:00:00';
  3944. }
  3945. ########################################################################################
  3946. #
  3947. # SONOS_CheckProxyObject - Checks for existence of $proxyObject (=return 1) or not (=return 0). Additionally in case of error it lays an error-answer in the queue
  3948. #
  3949. # Parameter $proxyObject = The Proxy that has to be checked
  3950. #
  3951. ########################################################################################
  3952. sub SONOS_CheckProxyObject($$) {
  3953. my ($udn, $proxyObject) = @_;
  3954. if (defined($proxyObject)) {
  3955. SONOS_Log $udn, 4, 'ProxyObject exists: '.$proxyObject;
  3956. return 1;
  3957. } else {
  3958. SONOS_Log $udn, 3, 'ProxyObject does not exists';
  3959. # Das Aufräumen der ProxyObjects und das Erzeugen des Notify wurde absichtlich nicht hier reingeschrieben, da es besser im IsAlive-Checker aufgehoben ist.
  3960. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'CheckProxyObject-ERROR: SonosPlayer disappeared?');
  3961. return 0;
  3962. }
  3963. }
  3964. ########################################################################################
  3965. #
  3966. # SONOS_MakeSigHandlerReturnValue - Enqueue all necessary elements on upward-queue
  3967. #
  3968. # Parameter $returnValue = The value that has to be laid on the queue.
  3969. #
  3970. ########################################################################################
  3971. sub SONOS_MakeSigHandlerReturnValue($$$) {
  3972. my ($udn, $returnName, $returnValue) = @_;
  3973. #Antwort melden
  3974. SONOS_Client_Notifier('DoWorkAnswer:'.$udn.':'.$returnName.':'.$returnValue);
  3975. }
  3976. ########################################################################################
  3977. #
  3978. # SONOS_RestartControlPoint - Restarts the UPnP-ControlPoint
  3979. #
  3980. ########################################################################################
  3981. sub SONOS_RestartControlPoint() {
  3982. if (defined($SONOS_Controlpoint)) {
  3983. $SONOS_RestartControlPoint = 1;
  3984. $SONOS_Controlpoint->stopSearch($SONOS_Search);
  3985. $SONOS_Controlpoint->stopHandling();
  3986. SONOS_Log undef, 4, 'ControlPoint is successfully stopped for restarting!';
  3987. }
  3988. }
  3989. ########################################################################################
  3990. #
  3991. # SONOS_StopControlPoint - Stops all open Net-Handles and Search-Token of the UPnP Part
  3992. #
  3993. ########################################################################################
  3994. sub SONOS_StopControlPoint {
  3995. if (defined($SONOS_Controlpoint)) {
  3996. $SONOS_Controlpoint->stopSearch($SONOS_Search);
  3997. $SONOS_Controlpoint->stopHandling();
  3998. undef($SONOS_Controlpoint);
  3999. SONOS_Log undef, 4, 'ControlPoint is successfully stopped!';
  4000. }
  4001. }
  4002. ########################################################################################
  4003. #
  4004. # SONOS_GetTagData - Return the content of the given tag in the given string
  4005. #
  4006. # Parameter $tagName = The tag to be searched for
  4007. # $data = The string in which to search for
  4008. #
  4009. ########################################################################################
  4010. sub SONOS_GetTagData($$) {
  4011. my ($tagName, $data) = @_;
  4012. return $1 if ($data =~ m/<$tagName.*?>(.*?)<\/$tagName>/i);
  4013. return '';
  4014. }
  4015. ########################################################################################
  4016. #
  4017. # SONOS_AnswerMessage - Return 'Success' if param is true, 'Error' otherwise
  4018. #
  4019. # Parameter $var = The value to check
  4020. #
  4021. ########################################################################################
  4022. sub SONOS_AnswerMessage($) {
  4023. my ($var) = @_;
  4024. if ($var) {
  4025. return 'Success!';
  4026. } else {
  4027. return 'Error!';
  4028. }
  4029. }
  4030. ########################################################################################
  4031. #
  4032. # SONOS_UPnPAnswerMessage - Return 'Success' if param is true, a complete error-message of the UPnP-answer otherwise
  4033. #
  4034. # Parameter $var = The UPnP-answer to check
  4035. #
  4036. ########################################################################################
  4037. sub SONOS_UPnPAnswerMessage($) {
  4038. my ($var) = @_;
  4039. if ($var->isSuccessful) {
  4040. return 'Success!';
  4041. } else {
  4042. my $faultcode = '-';
  4043. my $faultstring = '-';
  4044. my $faultactor = '-';
  4045. my $faultdetail = '-';
  4046. $faultcode = $var->faultcode if ($var->faultcode);
  4047. $faultstring = $var->faultstring if ($var->faultstring);
  4048. $faultactor = $var->faultactor if ($var->faultactor);
  4049. $faultdetail = $var->faultdetail if ($var->faultdetail);
  4050. return 'Error! UPnP-Fault-Fields: Code: "'.$faultcode.'", String: "'.$faultstring.'", Actor: "'.$faultactor.'", Detail: "'.SONOS_Stringify($faultdetail).'"';
  4051. }
  4052. }
  4053. ########################################################################################
  4054. #
  4055. # SONOS_Dumper - Returns the 'Dumpered' Output of the given Datastructure-Reference
  4056. #
  4057. ########################################################################################
  4058. sub SONOS_Dumper($) {
  4059. my ($varRef) = @_;
  4060. $Data::Dumper::Indent = 0;
  4061. my $text = Dumper($varRef);
  4062. $Data::Dumper::Indent = 2;
  4063. return $text;
  4064. }
  4065. ########################################################################################
  4066. #
  4067. # SONOS_Stringify - Converts a given Value (Array, Hash, Scalar) to a readable string version
  4068. #
  4069. # Parameter $varRef = The value to convert to a readable version
  4070. #
  4071. ########################################################################################
  4072. sub SONOS_Stringify {
  4073. my ($varRef) = @_;
  4074. return 'undef' if (!defined($varRef));
  4075. my $reftype = reftype $varRef;
  4076. if (!defined($reftype) || ($reftype eq '')) {
  4077. if (looks_like_number($varRef)) {
  4078. return $varRef;
  4079. } else {
  4080. $varRef =~ s/'/\\'/g;
  4081. return "'".$varRef."'";
  4082. }
  4083. } elsif ($reftype eq 'HASH') {
  4084. my %var = %{$varRef};
  4085. my @result;
  4086. foreach my $key (keys %var) {
  4087. push(@result, $key.' => '.SONOS_Stringify($var{$key}));
  4088. }
  4089. return '{'.join(', ', @result).'}';
  4090. } elsif ($reftype eq 'ARRAY') {
  4091. my @var = @{$varRef};
  4092. my @result;
  4093. foreach my $value (@var) {
  4094. push(@result, SONOS_Stringify($value));
  4095. }
  4096. return '['.join(', ', @result).']';
  4097. } elsif ($reftype eq 'SCALAR') {
  4098. if (looks_like_number(${$varRef})) {
  4099. return ${$varRef};
  4100. } else {
  4101. ${$varRef} =~ s/'/\\'/g;
  4102. return "'".${$varRef}."'";
  4103. }
  4104. } else {
  4105. return 'Unsupported Type ('.$reftype.') of: '.$varRef;
  4106. }
  4107. }
  4108. ########################################################################################
  4109. #
  4110. # SONOS_UmlautConvert - Converts any umlaut (e.g. ä) to Ascii-conform writing (e.g. ae)
  4111. #
  4112. # Parameter $var = The value to convert
  4113. #
  4114. ########################################################################################
  4115. sub SONOS_UmlautConvert($) {
  4116. eval {
  4117. use utf8;
  4118. my ($var) = @_;
  4119. if ($var eq 'ä') {
  4120. return 'ae';
  4121. } elsif ($var eq 'ö') {
  4122. return 'oe';
  4123. } elsif ($var eq 'ü') {
  4124. return 'ue';
  4125. } elsif ($var eq 'Ä') {
  4126. return 'Ae';
  4127. } elsif ($var eq 'Ö') {
  4128. return 'Oe';
  4129. } elsif ($var eq 'Ü') {
  4130. return 'Ue';
  4131. } elsif ($var eq 'ß') {
  4132. return 'ss';
  4133. } else {
  4134. return '_';
  4135. }
  4136. }
  4137. }
  4138. ########################################################################################
  4139. #
  4140. # SONOS_ConvertUmlautToHtml - Converts any umlaut (e.g. ä) to Html-conform writing (e.g. &auml;)
  4141. #
  4142. # Parameter $var = The value to convert
  4143. #
  4144. ########################################################################################
  4145. sub SONOS_ConvertUmlautToHtml($) {
  4146. my ($var) = @_;
  4147. if ($var eq 'ä') {
  4148. return '&auml;';
  4149. } elsif ($var eq 'ö') {
  4150. return '&ouml;';
  4151. } elsif ($var eq 'ü') {
  4152. return '&uuml;';
  4153. } elsif ($var eq 'Ä') {
  4154. return '&Auml;';
  4155. } elsif ($var eq 'Ö') {
  4156. return '&Ouml;';
  4157. } elsif ($var eq 'Ü') {
  4158. return '&Uuml;';
  4159. } elsif ($var eq 'ß') {
  4160. return '&szlig;';
  4161. } else {
  4162. return $var;
  4163. }
  4164. }
  4165. ########################################################################################
  4166. #
  4167. # SONOS_Latin1ToUtf8 - Converts Latin1 coding to UTF8
  4168. #
  4169. # Parameter $var = The value to convert
  4170. #
  4171. # http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS
  4172. #
  4173. ########################################################################################
  4174. sub SONOS_Latin1ToUtf8($) {
  4175. my ($s)= @_;
  4176. $s =~ s/([\x80-\xFF])/chr(0xC0|ord($1)>>6).chr(0x80|ord($1)&0x3F)/eg;
  4177. return $s;
  4178. }
  4179. ########################################################################################
  4180. #
  4181. # SONOS_Utf8ToLatin1 - Converts UTF8 coding to Latin1
  4182. #
  4183. # Parameter $var = The value to convert
  4184. #
  4185. # http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS
  4186. #
  4187. ########################################################################################
  4188. sub SONOS_Utf8ToLatin1($) {
  4189. my ($s)= @_;
  4190. $s =~ s/([\xC2\xC3])([\x80-\xBF])/chr(ord($1)<<6&0xC0|ord($2)&0x3F)/eg;
  4191. return $s;
  4192. }
  4193. ########################################################################################
  4194. #
  4195. # SONOS_ConvertNumToWord - Converts the values "0, 1" to "off, on"
  4196. #
  4197. # Parameter $var = The value to convert
  4198. #
  4199. ########################################################################################
  4200. sub SONOS_ConvertNumToWord($) {
  4201. my ($var) = @_;
  4202. if (!looks_like_number($var)) {
  4203. return 'on' if (lc($var) ne 'off');
  4204. return 'off';
  4205. }
  4206. if ($var == 0) {
  4207. return 'off';
  4208. } else {
  4209. return 'on';
  4210. }
  4211. }
  4212. ########################################################################################
  4213. #
  4214. # SONOS_ConvertWordToNum - Converts the values "off, on" to "0, 1"
  4215. #
  4216. # Parameter $var = The value to convert
  4217. #
  4218. ########################################################################################
  4219. sub SONOS_ConvertWordToNum($) {
  4220. my ($var) = @_;
  4221. if (looks_like_number($var)) {
  4222. return 1 if ($var != 0);
  4223. return 0;
  4224. }
  4225. if (lc($var) eq 'off') {
  4226. return 0;
  4227. } else {
  4228. return 1;
  4229. }
  4230. }
  4231. ########################################################################################
  4232. #
  4233. # SONOS_ToggleNum - Convert the values "0, 1" to "1, 0"
  4234. #
  4235. # Parameter $var = The value to convert
  4236. #
  4237. ########################################################################################
  4238. sub SONOS_ToggleNum($) {
  4239. my ($var) = @_;
  4240. if ($var == 0) {
  4241. return 1;
  4242. } else {
  4243. return 0;
  4244. }
  4245. }
  4246. ########################################################################################
  4247. #
  4248. # SONOS_ToggleWord - Convert the values "off, on" to "on, off"
  4249. #
  4250. # Parameter $var = The value to convert
  4251. #
  4252. ########################################################################################
  4253. sub SONOS_ToggleWord($) {
  4254. my ($var) = @_;
  4255. if (lc($var) eq 'off') {
  4256. return 'on';
  4257. } else {
  4258. return 'off';
  4259. }
  4260. }
  4261. ########################################################################################
  4262. #
  4263. # SONOS_Discover_Callback - Discover-Callback,
  4264. # autocreate devices if not already present
  4265. #
  4266. # Parameter $search =
  4267. # $device =
  4268. # $action =
  4269. #
  4270. ########################################################################################
  4271. sub SONOS_Discover_Callback($$$) {
  4272. my ($search, $device, $action) = @_;
  4273. # Sicherheitsabfrage, da offensichtlich manchmal falsche Elemente durchkommen...
  4274. if ($device->deviceType() ne 'urn:schemas-upnp-org:device:ZonePlayer:1') {
  4275. SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received!';
  4276. return;
  4277. }
  4278. if ($action eq 'deviceAdded') {
  4279. my $descriptionDocument;
  4280. eval {
  4281. $descriptionDocument = $device->descriptionDocument();
  4282. };
  4283. if ($@) {
  4284. # Das Descriptiondocument konnte nicht abgefragt werden
  4285. SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received! Detected while trying to download the Description-Document from Player.';
  4286. return;
  4287. }
  4288. # Wenn kein Description-Dokument geliefert wurde...
  4289. if (!defined($descriptionDocument) || ($descriptionDocument eq '')) {
  4290. SONOS_Log undef, 2, "Discover-Event: Description-Document is empty. Aborting this deviceadding-process.";
  4291. return;
  4292. }
  4293. # Alles OK, es kann weitergehen
  4294. SONOS_Log undef, 4, "Discover-Event: Description-Document: $descriptionDocument";
  4295. $SONOS_Client_SendQueue_Suspend = 1;
  4296. # Variablen initialisieren
  4297. my $roomName = '';
  4298. my $saveRoomName = '';
  4299. my $modelNumber = '';
  4300. my $displayVersion = '';
  4301. my $serialNum = '';
  4302. my $iconURI = '';
  4303. # Um einen XML-Parser zu vermeiden, werden hier reguläre Ausdrücke für die Ermittlung der Werte eingesetzt...
  4304. # RoomName ermitteln
  4305. $roomName = decode_entities($1) if ($descriptionDocument =~ m/<roomName>(.*?)<\/roomName>/im);
  4306. $saveRoomName = decode('UTF-8', $roomName);
  4307. eval {
  4308. use utf8;
  4309. $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  4310. };
  4311. $saveRoomName =~ s/[^a-zA-Z0-9_ ]//g;
  4312. $saveRoomName = SONOS_Trim($saveRoomName);
  4313. $saveRoomName =~ s/ /_/g;
  4314. my $groupName = $saveRoomName;
  4315. # Modelnumber ermitteln
  4316. $modelNumber = decode_entities($1) if ($descriptionDocument =~ m/<modelNumber>(.*?)<\/modelNumber>/im);
  4317. # DisplayVersion ermitteln
  4318. $displayVersion = decode_entities($1) if ($descriptionDocument =~ m/<displayVersion>(.*?)<\/displayVersion>/im);
  4319. # SerialNum ermitteln
  4320. $serialNum = decode_entities($1) if ($descriptionDocument =~ m/<serialNum>(.*?)<\/serialNum>/im);
  4321. # Icon-URI ermitteln
  4322. $iconURI = decode_entities($1) if ($descriptionDocument =~ m/<iconList>.*?<icon>.*?<id>0<\/id>.*?<url>(.*?)<\/url>.*?<\/icon>.*?<\/iconList>/sim);
  4323. # Kompletten Pfad zum Download des ZonePlayer-Bildchens zusammenbauen
  4324. my $iconOrigPath = $device->location();
  4325. $iconOrigPath =~ s/(http:\/\/.*?)\/.*/$1$iconURI/i;
  4326. # Zieldateiname für das ZonePlayer-Bildchen zusammenbauen
  4327. my $iconPath = $iconURI;
  4328. $iconPath =~ s/.*\/(.*)/icoSONOSPLAYER_$1/i;
  4329. my $udnShort = $device->UDN;
  4330. $udnShort =~ s/.*?://i;
  4331. my $udn = $udnShort.'_MR';
  4332. $SONOS_Locations{$device->location()} = $udn;
  4333. my $name = $SONOS_Client_Data{SonosDeviceName}."_".$saveRoomName;
  4334. # Erkannte Werte ausgeben...
  4335. SONOS_Log undef, 4, "RoomName: '$roomName', SaveRoomName: '$saveRoomName', ModelNumber: '$modelNumber', DisplayVersion: '$displayVersion', SerialNum: '$serialNum', IconURI: '$iconURI', IconOrigPath: '$iconOrigPath', IconPath: '$iconPath'";
  4336. SONOS_Log undef, 2, "Discover Sonosplayer '$roomName' ($modelNumber) Software Revision $displayVersion with ID '$udn'";
  4337. # Device sichern...
  4338. $SONOS_UPnPDevice{$udn} = $device;
  4339. # ServiceProxies für spätere Aufrufe merken
  4340. my $alarmService = $device->getService('urn:schemas-upnp-org:service:AlarmClock:1');
  4341. $SONOS_AlarmClockControlProxy{$udn} = $alarmService->controlProxy if ($alarmService);
  4342. my $audioInService = $device->getService('urn:schemas-upnp-org:service:AudioIn:1');
  4343. $SONOS_AudioInProxy{$udn} = $audioInService->controlProxy if ($audioInService);
  4344. my $devicePropertiesService = $device->getService('urn:schemas-upnp-org:service:DeviceProperties:1');
  4345. $SONOS_DevicePropertiesProxy{$udn} = $devicePropertiesService->controlProxy if ($devicePropertiesService);
  4346. #$SONOS_GroupManagementProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:GroupManagement:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:GroupManagement:1'));
  4347. #$SONOS_MusicServicesProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:MusicServices:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:MusicServices:1'));
  4348. my $zoneGroupTopologyService = $device->getService('urn:schemas-upnp-org:service:ZoneGroupTopology:1');
  4349. $SONOS_ZoneGroupTopologyProxy{$udn} = $zoneGroupTopologyService->controlProxy if ($zoneGroupTopologyService);
  4350. # Bei einem Dock gibt es AVTransport nur am Hauptdevice, deshalb mal schauen, ob wir es hier bekommen können
  4351. my $transportService = $device->getService('urn:schemas-upnp-org:service:AVTransport:1');
  4352. $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService);
  4353. my $renderingService;
  4354. my $groupRenderingService;
  4355. my $contentDirectoryService;
  4356. # Hier die Subdevices durchgehen...
  4357. for my $subdevice ($device->children) {
  4358. SONOS_Log undef, 4, 'SubDevice found: '.$subdevice->UDN;
  4359. if ($subdevice->UDN =~ /.*_MR/i) {
  4360. # Wir haben hier das Media-Renderer Subdevice
  4361. $transportService = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1');
  4362. $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService);
  4363. $renderingService = $subdevice->getService('urn:schemas-upnp-org:service:RenderingControl:1');
  4364. $SONOS_RenderingControlProxy{$udn} = $renderingService->controlProxy if ($renderingService);
  4365. $groupRenderingService = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1');
  4366. $SONOS_GroupRenderingControlProxy{$udn} = $groupRenderingService->controlProxy if ($groupRenderingService);
  4367. }
  4368. if ($subdevice->UDN =~ /.*_MS/i) {
  4369. # Wir haben hier das Media-Server Subdevice
  4370. $contentDirectoryService = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1');
  4371. $SONOS_ContentDirectoryControlProxy{$udn} = $contentDirectoryService->controlProxy if ($contentDirectoryService);
  4372. }
  4373. }
  4374. SONOS_Log undef, 4, 'ControlProxies wurden gesichert';
  4375. # ZoneTopology laden, um die Benennung der Fhem-Devices besser an die Realität anpassen zu können
  4376. my ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState) = SONOS_AnalyzeZoneGroupTopology($udn, $udnShort);
  4377. my @slavePlayerNames = SONOS_AnalyzeTopologyForSlavePlayer($udnShort, $zoneGroupState);
  4378. # Wenn der aktuelle Player der Master ist, dann kein Kürzel anhängen,
  4379. # damit gibt es immer einen Player, der den Raumnamen trägt, und die anderen enthalten Kürzel
  4380. if ($master) {
  4381. $topoType = '';
  4382. }
  4383. # Raumnamen erweitern
  4384. $name .= $topoType;
  4385. $saveRoomName .= $topoType;
  4386. # Volume laden um diese im Reading ablegen zu können
  4387. my $currentVolume = 0;
  4388. my $balance = 0;
  4389. if (!$isZoneBridge) {
  4390. if ($SONOS_RenderingControlProxy{$udn}) {
  4391. eval {
  4392. $currentVolume = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  4393. # Balance ermitteln
  4394. my $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume');
  4395. my $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume');
  4396. $balance = (-$volumeLeft) + $volumeRight;
  4397. SONOS_Log undef, 4, 'Retrieve Current Volumelevels. Master: "'.$currentVolume.'", Balance: "'.$balance.'"';
  4398. };
  4399. if ($@) {
  4400. $currentVolume = 0;
  4401. $balance = 0;
  4402. SONOS_Log undef, 4, 'Couldn\'t retrieve Current Volumelevels: '. $@;
  4403. }
  4404. } else {
  4405. SONOS_Log undef, 4, 'Couldn\'t get any Volume Information due to missing RenderingControlProxy';
  4406. }
  4407. }
  4408. # Load official icon from zoneplayer and copy it to local place for FHEM-use
  4409. SONOS_Client_Notifier('getstore(\''.$iconOrigPath.'\', $attr{global}{modpath}.\'/www/images/default/'.$iconPath."');\n");
  4410. # Icons neu einlesen lassen
  4411. SONOS_Client_Notifier('SONOS_RefreshIconsInFHEMWEB(\'/www/images/default/'.$iconPath.'\');');
  4412. # Transport Informations to FHEM
  4413. # Check if this device is already defined...
  4414. if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerUDNs}})) {
  4415. push @{$SONOS_Client_Data{PlayerUDNs}}, $udn;
  4416. # Wenn der Name schon mal verwendet wurde, dann solange ein Kürzel anhängen, bis ein freier Name gefunden wurde...
  4417. while (SONOS_isInList($name, @{$SONOS_Client_Data{PlayerNames}})) {
  4418. $name .= '_X';
  4419. $saveRoomName .= '_X';
  4420. SONOS_Log undef, 2, "New Fhem-Name neccessary for '$roomName' -> '$name', ID '$udn'";
  4421. }
  4422. push @{$SONOS_Client_Data{PlayerNames}}, $name;
  4423. my %elemValues = ();
  4424. $SONOS_Client_Data{Buffer}->{$udn} = shared_clone(\%elemValues);
  4425. # Define SonosPlayer-Device...
  4426. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER', undef, $udn, undef, $name, undef, undef, undef, undef, undef)) {
  4427. SONOS_Client_Notifier($elem);
  4428. }
  4429. # ...and his attributes
  4430. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Attributes', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge)) {
  4431. SONOS_Client_Notifier($elem);
  4432. }
  4433. # Setting Internal-Data
  4434. if (!$isZoneBridge) {
  4435. SONOS_Client_Data_Refresh('', $udn, 'getAlarms', 1);
  4436. SONOS_Client_Data_Refresh('', $udn, 'minVolume', 0);
  4437. }
  4438. # Define ReadingsGroup
  4439. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_ReadingsGroup', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, undef, undef, $groupName, undef, $isZoneBridge)) {
  4440. SONOS_Client_Notifier($elem);
  4441. }
  4442. # Define ReadingsGroup-Listen
  4443. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_ReadingsGroup_Listen', undef, undef, $master, $name, undef, undef, undef, undef, $isZoneBridge)) {
  4444. SONOS_Client_Notifier($elem);
  4445. }
  4446. # Define RemoteControl
  4447. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Remotecontrol', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, undef, undef, $groupName, undef, $isZoneBridge)) {
  4448. SONOS_Client_Notifier($elem);
  4449. }
  4450. # Name sichern...
  4451. SONOS_Client_Data_Refresh('', $udn, 'NAME', $name);
  4452. SONOS_Log undef, 1, "Successfully autocreated SonosPlayer '$saveRoomName' ($modelNumber) as '$name' with Software Revision $displayVersion and ID '$udn'";
  4453. } else {
  4454. # Wenn das Device schon existiert, dann den dort verwendeten Namen holen
  4455. $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  4456. SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' ($modelNumber) with ID '$udn' is already defined (as '$name') and will only be updated";
  4457. }
  4458. # Wenn der Player noch nicht auf der "Aktiv"-Liste steht, dann draufpacken...
  4459. push @{$SONOS_Client_Data{PlayerAlive}}, $udn if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerAlive}}));
  4460. # Readings aktualisieren
  4461. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  4462. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'presence', 'appeared');
  4463. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume);
  4464. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Balance', $balance);
  4465. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomName', $roomName);
  4466. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  4467. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName);
  4468. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'playerType', $modelNumber);
  4469. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume);
  4470. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'location', $device->location);
  4471. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'softwareRevision', $displayVersion);
  4472. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'serialNum', $serialNum);
  4473. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'fieldType', $fieldType);
  4474. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsMaster', $master ? '1' : '0');
  4475. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'MasterPlayer', $masterPlayerName);
  4476. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayer', SONOS_Dumper(\@slavePlayerNames));
  4477. # Abspielreadings vorab ermitteln, um darauf prüfen zu können...
  4478. if (!$isZoneBridge) {
  4479. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  4480. eval {
  4481. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportInfo(0);
  4482. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'transportState', $result->getValue('CurrentTransportState'));
  4483. $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0);
  4484. my $tmp = $result->getValue('TrackURI');
  4485. $tmp =~ s/&apos;/'/gi;
  4486. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackURI', $tmp);
  4487. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProvider', SONOS_GetTrackProvider($result->getValue('TrackURI')));
  4488. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackDuration', $result->getValue('TrackDuration'));
  4489. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackPosition', $result->getValue('RelTime'));
  4490. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrack', $result->getValue('Track'));
  4491. $result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  4492. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'numberOfTracks', $result->getValue('NrTracks'));
  4493. my $stream = ($result->getValue('CurrentURI') =~ m/^x-(sonosapi|rincon)-(stream|mp3radio):.*?/);
  4494. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentStreamAudio', $stream);
  4495. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentNormalAudio', !$stream);
  4496. };
  4497. if ($@) {
  4498. SONOS_Log undef, 1, 'Couldn\'t retrieve Current Transportsettings during Discovery: '. $@;
  4499. }
  4500. }
  4501. }
  4502. SONOS_Client_Data_Refresh('', $udn, 'LastSubscriptionsRenew', SONOS_TimeNow());
  4503. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  4504. SONOS_Client_Notifier('CommandAttrWithUDN:'.$udn.':model Sonos_'.$modelNumber);
  4505. $SONOS_Client_SendQueue_Suspend = 0;
  4506. SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' is now updated";
  4507. # AVTransport-Subscription
  4508. if (!$isZoneBridge) {
  4509. if ($transportService) {
  4510. $SONOS_TransportSubscriptions{$udn} = $transportService->subscribe(\&SONOS_ServiceCallback);
  4511. if (defined($SONOS_TransportSubscriptions{$udn})) {
  4512. SONOS_Log undef, 2, 'Service-subscribing successful with SID='.$SONOS_TransportSubscriptions{$udn}->SID;
  4513. } else {
  4514. SONOS_Log undef, 1, 'Service-subscribing NOT successful';
  4515. }
  4516. } else {
  4517. undef($SONOS_TransportSubscriptions{$udn});
  4518. SONOS_Log undef, 1, 'Service-subscribing not possible due to missing TransportService';
  4519. }
  4520. }
  4521. # Rendering-Subscription, wenn eine untere oder obere Lautstärkegrenze angegeben wurde, und Lautstärke überhaupt geht
  4522. if ($renderingService && (SONOS_Client_Data_Retreive($udn, 'attr', 'minVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'minVolumeHeadphone', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolumeHeadphone', -1) != -1 )) {
  4523. $SONOS_RenderingSubscriptions{$udn} = $renderingService->subscribe(\&SONOS_RenderingCallback);
  4524. $SONOS_ButtonPressQueue{$udn} = Thread::Queue->new();
  4525. if (defined($SONOS_RenderingSubscriptions{$udn})) {
  4526. SONOS_Log undef, 2, 'Rendering-Service-subscribing successful with SID='.$SONOS_RenderingSubscriptions{$udn}->SID;
  4527. } else {
  4528. SONOS_Log undef, 1, 'Rendering-Service-subscribing NOT successful';
  4529. }
  4530. } else {
  4531. undef($SONOS_RenderingSubscriptions{$udn});
  4532. }
  4533. # GroupRendering-Subscription
  4534. if ($groupRenderingService && (SONOS_Client_Data_Retreive($udn, 'attr', 'minVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'minVolumeHeadphone', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolumeHeadphone', -1) != -1 )) {
  4535. $SONOS_GroupRenderingSubscriptions{$udn} = $groupRenderingService->subscribe(\&SONOS_GroupRenderingCallback);
  4536. if (defined($SONOS_GroupRenderingSubscriptions{$udn})) {
  4537. SONOS_Log undef, 2, 'GroupRendering-Service-subscribing successful with SID='.$SONOS_GroupRenderingSubscriptions{$udn}->SID;
  4538. } else {
  4539. SONOS_Log undef, 1, 'GroupRendering-Service-subscribing NOT successful';
  4540. }
  4541. } else {
  4542. undef($SONOS_GroupRenderingSubscriptions{$udn});
  4543. }
  4544. # ContentDirectory-Subscription
  4545. if ($contentDirectoryService) {
  4546. $SONOS_ContentDirectorySubscriptions{$udn} = $contentDirectoryService->subscribe(\&SONOS_ContentDirectoryCallback);
  4547. if (defined($SONOS_ContentDirectorySubscriptions{$udn})) {
  4548. SONOS_Log undef, 2, 'ContentDirectory-Service-subscribing successful with SID='.$SONOS_ContentDirectorySubscriptions{$udn}->SID;
  4549. } else {
  4550. SONOS_Log undef, 1, 'ContentDirectory-Service-subscribing NOT successful';
  4551. }
  4552. } else {
  4553. undef($SONOS_ContentDirectorySubscriptions{$udn});
  4554. }
  4555. # Alarm-Subscription
  4556. if ($alarmService && (SONOS_Client_Data_Retreive($udn, 'attr', 'getAlarms', 0) != 0)) {
  4557. $SONOS_AlarmSubscriptions{$udn} = $alarmService->subscribe(\&SONOS_AlarmCallback);
  4558. if (defined($SONOS_AlarmSubscriptions{$udn})) {
  4559. SONOS_Log undef, 2, 'Alarm-Service-subscribing successful with SID='.$SONOS_AlarmSubscriptions{$udn}->SID;
  4560. } else {
  4561. SONOS_Log undef, 1, 'Alarm-Service-subscribing NOT successful';
  4562. }
  4563. } else {
  4564. undef($SONOS_AlarmSubscriptions{$udn});
  4565. }
  4566. # ZoneGroupTopology-Subscription
  4567. if ($zoneGroupTopologyService) {
  4568. $SONOS_ZoneGroupTopologySubscriptions{$udn} = $zoneGroupTopologyService->subscribe(\&SONOS_ZoneGroupTopologyCallback);
  4569. if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn})) {
  4570. SONOS_Log undef, 2, 'ZoneGroupTopology-Service-subscribing successful with SID='.$SONOS_ZoneGroupTopologySubscriptions{$udn}->SID;
  4571. } else {
  4572. SONOS_Log undef, 1, 'ZoneGroupTopology-Service-subscribing NOT successful';
  4573. }
  4574. } else {
  4575. undef($SONOS_ZoneGroupTopologySubscriptions{$udn});
  4576. }
  4577. # DeviceProperties-Subscription
  4578. if ($devicePropertiesService) {
  4579. $SONOS_DevicePropertiesSubscriptions{$udn} = $devicePropertiesService->subscribe(\&SONOS_DevicePropertiesCallback);
  4580. if (defined($SONOS_DevicePropertiesSubscriptions{$udn})) {
  4581. SONOS_Log undef, 2, 'DeviceProperties-Service-subscribing successful with SID='.$SONOS_DevicePropertiesSubscriptions{$udn}->SID;
  4582. } else {
  4583. SONOS_Log undef, 1, 'DeviceProperties-Service-subscribing NOT successful';
  4584. }
  4585. } else {
  4586. undef($SONOS_DevicePropertiesSubscriptions{$udn});
  4587. }
  4588. # AudioIn-Subscription
  4589. if ($audioInService) {
  4590. $SONOS_AudioInSubscriptions{$udn} = $audioInService->subscribe(\&SONOS_AudioInCallback);
  4591. if (defined($SONOS_AudioInSubscriptions{$udn})) {
  4592. SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID='.$SONOS_AudioInSubscriptions{$udn}->SID;
  4593. } else {
  4594. SONOS_Log undef, 1, 'AudioIn-Service-subscribing NOT successful';
  4595. }
  4596. } else {
  4597. undef($SONOS_AudioInSubscriptions{$udn});
  4598. }
  4599. SONOS_Log undef, 3, 'Discover: End of discover-event for "'.$roomName.'".';
  4600. } elsif ($action eq 'deviceRemoved') {
  4601. my $udn = $device->UDN;
  4602. $udn =~ s/.*?://i;
  4603. $udn .= '_MR';
  4604. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
  4605. SONOS_Log undef, 2, "Device '$udn' removed. Do nothing special here, cause all is done in another way...";
  4606. }
  4607. return 0;
  4608. }
  4609. ########################################################################################
  4610. #
  4611. # SONOS_GetDefineStringlist - Generates a list of define- or attr-commands acoording to the given desired-device
  4612. #
  4613. ########################################################################################
  4614. sub SONOS_GetDefineStringlist($$$$$$$$$$) {
  4615. my ($devicetype, $sonosDeviceName, $udn, $master, $name, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge) = @_;
  4616. my @defs = ();
  4617. if (lc($devicetype) eq 'sonosplayer') {
  4618. push(@defs, 'CommandDefine:'.$name.' SONOSPLAYER '.$udn);
  4619. } elsif (lc($devicetype) eq 'sonosplayer_attributes') {
  4620. push(@defs, 'CommandAttr:'.$name.' room '.$sonosDeviceName);
  4621. push(@defs, 'CommandAttr:'.$name.' alias '.$roomName.$aliasSuffix);
  4622. push(@defs, 'CommandAttr:'.$name.' group '.$groupName);
  4623. push(@defs, 'CommandAttr:'.$name.' icon '.$iconPath);
  4624. push(@defs, 'CommandAttr:'.$name.' sortby 1');
  4625. if (!$isZoneBridge) {
  4626. push(@defs, 'CommandAttr:'.$name.' userReadings Favourites:LastActionResult.*?GetFavouritesWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, Radios:LastActionResult.*?GetRadiosWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, Playlists:LastActionResult.*?GetPlaylistsWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, currentTrackPosition:LastActionResult.*?GetCurrentTrackPosition.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }');
  4627. push(@defs, 'CommandAttr:'.$name.' generateInfoSummarize1 <NormalAudio><Artist prefix="(" suffix=")"/><Title prefix=" \'" suffix="\'" ifempty="[Keine Musikdatei]"/><Album prefix=" vom Album \'" suffix="\'"/></NormalAudio> <StreamAudio><Sender suffix=":"/><SenderCurrent prefix=" \'" suffix="\' -"/><SenderInfo prefix=" "/></StreamAudio>');
  4628. push(@defs, 'CommandAttr:'.$name.' generateInfoSummarize2 <TransportState/><InfoSummarize1 prefix=" => "/>');
  4629. push(@defs, 'CommandAttr:'.$name.' generateInfoSummarize3 <Volume prefix="Lautstärke: "/><Mute instead=" ~ Kein Ton" ifempty=" ~ Ton An" emptyval="0"/> ~ Balance: <Balance ifempty="Mitte" emptyval="0"/><HeadphoneConnected instead=" ~ Kopfhörer aktiv" ifempty=" ~ Kein Kopfhörer" emptyval="0"/>');
  4630. push(@defs, 'CommandAttr:'.$name.' generateVolumeSlider 1');
  4631. push(@defs, 'CommandAttr:'.$name.' getAlarms 1');
  4632. push(@defs, 'CommandAttr:'.$name.' minVolume 0');
  4633. push(@defs, 'CommandAttr:'.$name.' stateVariable Presence');
  4634. #push(@defs, 'CommandAttr:'.$name.' webCmd Play:Pause:Previous:Next:VolumeD:VolumeU:MuteT');
  4635. } else {
  4636. push(@defs, 'CommandAttr:'.$name.' stateFormat presence');
  4637. }
  4638. } elsif (lc($devicetype) eq 'sonosplayer_readingsgroup') {
  4639. if (!$isZoneBridge) {
  4640. if ($master) {
  4641. push(@defs, 'CommandDefine:'.$name.'RG readingsGroup '.$name.':<{SONOS_getCoverTitleRG($DEVICE)}@infoSummarize2>');
  4642. push(@defs, 'CommandAttr:'.$name.'RG room '.$sonosDeviceName);
  4643. push(@defs, 'CommandAttr:'.$name.'RG group '.$groupName);
  4644. push(@defs, 'CommandAttr:'.$name.'RG sortby 2');
  4645. push(@defs, 'CommandAttr:'.$name.'RG noheading 1');
  4646. push(@defs, 'CommandAttr:'.$name.'RG nonames 1');
  4647. #push(@defs, 'CommandDefine:'.$name.'RG2 readingsGroup '.$name.':infoSummarize2@{SONOSPLAYER_GetMasterPlayerName($DEVICE)}');
  4648. #push(@defs, 'CommandAttr:'.$name.'RG2 valueFormat {" "}');
  4649. #push(@defs, 'CommandAttr:'.$name.'RG2 valuePrefix {SONOS_getCoverTitleRG(SONOSPLAYER_GetMasterPlayerName($DEVICE))}');
  4650. #push(@defs, 'CommandAttr:'.$name.'RG2 room '.$SONOS_Client_Data{SonosDeviceName});
  4651. #push(@defs, 'CommandAttr:'.$name.'RG2 group '.$groupName);
  4652. #push(@defs, 'CommandAttr:'.$name.'RG2 sortby 4');
  4653. #push(@defs, 'CommandAttr:'.$name.'RG2 noheading 1');
  4654. #push(@defs, 'CommandAttr:'.$name.'RG2 nonames 1');
  4655. #push(@defs, 'CommandAttr:'.$name.'RG2 notime 1');
  4656. }
  4657. }
  4658. } elsif (lc($devicetype) eq 'sonosplayer_readingsgroup_listen') {
  4659. if (!$isZoneBridge) {
  4660. if ($master) {
  4661. push(@defs, 'CommandDefine:'.$name.'RG_Favourites readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Favourites",1)}@Favourites>');
  4662. push(@defs, 'CommandDefine:'.$name.'RG_Radios readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Radios",1)}@Radios>');
  4663. push(@defs, 'CommandDefine:'.$name.'RG_Playlists readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Playlists")}@Playlists>');
  4664. }
  4665. }
  4666. } elsif (lc($devicetype) eq 'sonosplayer_remotecontrol') {
  4667. if (!$isZoneBridge) {
  4668. if ($master) {
  4669. push(@defs, 'CommandDefine:'.$name.'RC remotecontrol');
  4670. push(@defs, 'CommandAttr:'.$name.'RC room hidden');
  4671. push(@defs, 'CommandAttr:'.$name.'RC group '.$sonosDeviceName);
  4672. push(@defs, 'CommandAttr:'.$name.'RC rc_iconpath icons/remotecontrol');
  4673. push(@defs, 'CommandAttr:'.$name.'RC rc_iconprefix black_btn_');
  4674. push(@defs, 'CommandAttr:'.$name.'RC row00 Play:rc_PLAY.svg,Pause:rc_PAUSE.svg,Previous:rc_PREVIOUS.svg,Next:rc_NEXT.svg,:blank,VolumeD:rc_VOLDOWN.svg,VolumeU:rc_VOLUP.svg,:blank,MuteT:rc_MUTE.svg,ShuffleT:rc_SHUFFLE.svg,RepeatT:rc_REPEAT.svg');
  4675. push(@defs, 'CommandDefine:'.$name.'RC_Notify notify '.$name.'RC set '.$name.' $EVENT');
  4676. push(@defs, 'CommandDefine:'.$name.'RC_Weblink weblink htmlCode {fhem("get '.$name.'RC htmlcode", 1)}');
  4677. push(@defs, 'CommandAttr:'.$name.'RC_Weblink room '.$sonosDeviceName);
  4678. push(@defs, 'CommandAttr:'.$name.'RC_Weblink group '.$groupName);
  4679. push(@defs, 'CommandAttr:'.$name.'RC_Weblink sortby 3');
  4680. }
  4681. }
  4682. }
  4683. return @defs;
  4684. }
  4685. ########################################################################################
  4686. #
  4687. # SONOS_AnalyzeZoneGroupTopology - Analyzes the current Zoneplayertopology for better naming of the components
  4688. #
  4689. ########################################################################################
  4690. sub SONOS_AnalyzeZoneGroupTopology($$) {
  4691. my ($udn, $udnShort) = @_;
  4692. # ZoneTopology laden, um die Benennung der Fhem-Devices besser an die Realität anpassen zu können
  4693. my $topoType = '';
  4694. my $fieldType = '';
  4695. my $master = 1;
  4696. my $masterPlayerName;
  4697. my $isZoneBridge = 0;
  4698. my $zoneGroupState = '';
  4699. if ($SONOS_ZoneGroupTopologyProxy{$udn}) {
  4700. $zoneGroupState = $SONOS_ZoneGroupTopologyProxy{$udn}->GetZoneGroupState()->getValue('ZoneGroupState');
  4701. SONOS_Log undef, 5, 'ZoneGroupState: '.$zoneGroupState;
  4702. if ($zoneGroupState =~ m/.*(<ZoneGroup Coordinator="(RINCON_[0-9a-f]+)".*?>).*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) {
  4703. my $coordinator = $2;
  4704. my $member = $3;
  4705. $masterPlayerName = SONOS_Client_Data_Retreive($coordinator.'_MR', 'def', 'NAME', $coordinator.'_MR');
  4706. # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten?
  4707. if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) {
  4708. $topoType = '_'.$1;
  4709. }
  4710. # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten?
  4711. if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) {
  4712. $topoType = '_'.$1;
  4713. $topoType =~ s/,/_/g;
  4714. }
  4715. SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType;
  4716. $fieldType = substr($topoType, 1) if ($topoType);
  4717. my $invisible = 0;
  4718. $invisible = 1 if ($member =~ m/Invisible="1"/i);
  4719. $isZoneBridge = 1 if ($member =~ m/IsZoneBridge="1"/i);
  4720. $master = !$invisible || $isZoneBridge;
  4721. }
  4722. }
  4723. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  4724. my $aliasSuffix = '';
  4725. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  4726. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  4727. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  4728. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  4729. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  4730. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  4731. return ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState);
  4732. }
  4733. ########################################################################################
  4734. #
  4735. # SONOS_IsAlive - Checks if the given Device is alive or not and triggers the proper event if status changed
  4736. #
  4737. # Parameter $udn = UDN of the Device in short-form (e.g. RINCON_000E5828D0F401400_MR)
  4738. #
  4739. ########################################################################################
  4740. sub SONOS_IsAlive($) {
  4741. my ($udn) = @_;
  4742. SONOS_Log $udn, 4, "IsAlive-Event UDN=$udn";
  4743. my $result = 1;
  4744. my $doDeleteProxyObjects = 0;
  4745. $SONOS_Client_SendQueue_Suspend = 1;
  4746. my $location = SONOS_Client_Data_Retreive($udn, 'reading', 'location', '');
  4747. if ($location) {
  4748. SONOS_Log $udn, 5, "Location: $location";
  4749. my ($host, $port) = ($1, $2) if ($location =~ m/http:\/\/(.*?):(.*?)\//);
  4750. my $pingType = $SONOS_Client_Data{pingType};
  4751. return 1 if (lc($pingType) eq 'none');
  4752. if ($pingType ~~ @SONOS_PINGTYPELIST) {
  4753. SONOS_Log $udn, 5, "PingType: $pingType";
  4754. } else {
  4755. SONOS_Log $udn, 1, "Wrong pingType given for '$udn': '$pingType'. Choose one of '".join(', ', @SONOS_PINGTYPELIST)."'";
  4756. $pingType = $SONOS_DEFAULTPINGTYPE;
  4757. }
  4758. my $ping = Net::Ping->new($pingType, 1);
  4759. $ping->source_verify(0); # Es ist egal, von welcher Schnittstelle des Zielsystems die Antwort kommt
  4760. $ping->port_number($port) if (lc($pingType) eq 'tcp'); # Wenn TCP verwendet werden soll, dann auf HTTP-Port des Location-Documents (Standard: 1400) des Player verbinden
  4761. if ($ping->ping($host)) {
  4762. # Alive
  4763. SONOS_Log $udn, 4, "$host is alive";
  4764. $result = 1;
  4765. # IsAlive-Negativ-Counter zurücksetzen
  4766. $SONOS_Thread_IsAlive_Counter{$host} = 0;
  4767. } else {
  4768. # Not Alive
  4769. $SONOS_Thread_IsAlive_Counter{$host}++;
  4770. if ($SONOS_Thread_IsAlive_Counter{$host} > $SONOS_Thread_IsAlive_Counter_MaxMerci) {
  4771. SONOS_Log $udn, 3, "$host is REALLY NOT alive (out of merci maxlevel '".$SONOS_Thread_IsAlive_Counter_MaxMerci.'\')';
  4772. $result = 0;
  4773. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
  4774. # Brauchen wir das wirklich? Dabei werden die lokalen Infos nicht aktualisiert...
  4775. #SONOS_Client_Notifier('deleteCurrentNextTitleInformationAndDisappear:'.$udn);
  4776. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'state', 'disappeared');
  4777. $doDeleteProxyObjects = 1;
  4778. } else {
  4779. SONOS_Log $udn, 3, "$host is NOT alive, but in merci level ".$SONOS_Thread_IsAlive_Counter{$host}.'/'.$SONOS_Thread_IsAlive_Counter_MaxMerci.'.';
  4780. }
  4781. }
  4782. $ping->close();
  4783. }
  4784. $SONOS_Client_SendQueue_Suspend = 0;
  4785. # Jetzt, wo das Reading dazu auch gesetzt wurde, hier ausführen
  4786. if ($doDeleteProxyObjects) {
  4787. my %data;
  4788. $data{WorkType} = 'deleteProxyObjects';
  4789. $data{UDN} = $udn;
  4790. my @params = ();
  4791. $data{Params} = \@params;
  4792. $SONOS_ComObjectTransportQueue->enqueue(\%data);
  4793. # Signalhandler aufrufen, wenn er nicht sowieso noch läuft...
  4794. if (defined(threads->object($SONOS_Thread))) {
  4795. threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1);
  4796. }
  4797. }
  4798. return $result;
  4799. }
  4800. ########################################################################################
  4801. #
  4802. # SONOS_DeleteProxyObjects - Deletes all references to the proxy objects of the given zoneplayer
  4803. #
  4804. # Parameter $name = The name of zoneplayerdevice
  4805. #
  4806. ########################################################################################
  4807. sub SONOS_DeleteProxyObjects($) {
  4808. my ($udn) = @_;
  4809. SONOS_Log $udn, 4, "Delete ProxyObjects and SubscriptionObjects for '$udn'";
  4810. delete $SONOS_AVTransportControlProxy{$udn};
  4811. delete $SONOS_RenderingControlProxy{$udn};
  4812. delete $SONOS_ContentDirectoryControlProxy{$udn};
  4813. delete $SONOS_AlarmClockControlProxy{$udn};
  4814. delete $SONOS_AudioInProxy{$udn};
  4815. delete $SONOS_DevicePropertiesProxy{$udn};
  4816. delete $SONOS_GroupManagementProxy{$udn};
  4817. delete $SONOS_MusicServicesProxy{$udn};
  4818. delete $SONOS_ZoneGroupTopologyProxy{$udn};
  4819. delete $SONOS_TransportSubscriptions{$udn};
  4820. delete $SONOS_RenderingSubscriptions{$udn};
  4821. delete $SONOS_GroupRenderingSubscriptions{$udn};
  4822. delete $SONOS_ContentDirectorySubscriptions{$udn};
  4823. delete $SONOS_AlarmSubscriptions{$udn};
  4824. delete $SONOS_ZoneGroupTopologySubscriptions{$udn};
  4825. delete $SONOS_DevicePropertiesSubscriptions{$udn};
  4826. delete $SONOS_AudioInSubscriptions{$udn};
  4827. # Am Ende noch das Device entfernen...
  4828. delete $SONOS_UPnPDevice{$udn};
  4829. SONOS_Log $udn, 4, "Delete of ProxyObjects and SubscriptionObjects DONE for '$udn'";
  4830. }
  4831. ########################################################################################
  4832. #
  4833. # SONOS_GetReadingsToCurrentHash - Get all neccessary readings from named device
  4834. #
  4835. # Parameter $name = The name of the player-device
  4836. #
  4837. ########################################################################################
  4838. sub SONOS_GetReadingsToCurrentHash($$) {
  4839. my ($name, $emptyCurrent) = @_;
  4840. my %current;
  4841. if ($emptyCurrent) {
  4842. # Empty Values for Current Track Readings
  4843. $current{TransportState} = 'ERROR';
  4844. $current{Shuffle} = 0;
  4845. $current{Repeat} = 0;
  4846. $current{RepeatOne} = 0;
  4847. $current{CrossfadeMode} = 0;
  4848. $current{NumberOfTracks} = '';
  4849. $current{Track} = '';
  4850. $current{TrackURI} = '';
  4851. $current{TrackDuration} = '';
  4852. $current{TrackPosition} = '';
  4853. $current{TrackProvider} = '';
  4854. $current{TrackMetaData} = '';
  4855. $current{AlbumArtURI} = '';
  4856. $current{AlbumArtURL} = '';
  4857. $current{Title} = '';
  4858. $current{Artist} = '';
  4859. $current{Album} = '';
  4860. $current{OriginalTrackNumber} = '';
  4861. $current{AlbumArtist} = '';
  4862. $current{Sender} = '';
  4863. $current{SenderCurrent} = '';
  4864. $current{SenderInfo} = '';
  4865. $current{nextTrackDuration} = '';
  4866. $current{nextTrackURI} = '';
  4867. $current{nextAlbumArtURI} = '';
  4868. $current{nextAlbumArtURL} = '';
  4869. $current{nextTitle} = '';
  4870. $current{nextArtist} = '';
  4871. $current{nextAlbum} = '';
  4872. $current{nextAlbumArtist} = '';
  4873. $current{nextOriginalTrackNumber} = '';
  4874. $current{InfoSummarize1} = '';
  4875. $current{InfoSummarize2} = '';
  4876. $current{InfoSummarize3} = '';
  4877. $current{InfoSummarize4} = '';
  4878. $current{StreamAudio} = 0;
  4879. $current{NormalAudio} = 0;
  4880. } else {
  4881. # Insert normal Current Track Readings
  4882. $current{TransportState} = ReadingsVal($name, 'transportState', 'ERROR');
  4883. $current{Shuffle} = ReadingsVal($name, 'Shuffle', 0);
  4884. $current{Repeat} = ReadingsVal($name, 'Repeat', 0);
  4885. $current{RepeatOne} = ReadingsVal($name, 'RepeatOne', 0);
  4886. $current{CrossfadeMode} = ReadingsVal($name, 'CrossfadeMode', 0);
  4887. $current{NumberOfTracks} = ReadingsVal($name, 'numberOfTracks', '');
  4888. $current{Track} = ReadingsVal($name, 'currentTrack', '');
  4889. $current{TrackURI} = ReadingsVal($name, 'currentTrackURI', '');
  4890. $current{TrackDuration} = ReadingsVal($name, 'currentTrackDuration', '');
  4891. $current{TrackPosition} = ReadingsVal($name, 'currentTrackPosition', '');
  4892. $current{TrackProvider} = ReadingsVal($name, 'currentTrackProvider', '');
  4893. #$current{TrackMetaData} = '';
  4894. $current{AlbumArtURI} = ReadingsVal($name, 'currentAlbumArtURI', '');
  4895. $current{AlbumArtURL} = ReadingsVal($name, 'currentAlbumArtURL', '');
  4896. $current{Title} = ReadingsVal($name, 'currentTitle', '');
  4897. $current{Artist} = ReadingsVal($name, 'currentArtist', '');
  4898. $current{Album} = ReadingsVal($name, 'currentAlbum', '');
  4899. $current{OriginalTrackNumber} = ReadingsVal($name, 'currentOriginalTrackNumber', '');
  4900. $current{AlbumArtist} = ReadingsVal($name, 'currentAlbumArtist', '');
  4901. $current{Sender} = ReadingsVal($name, 'currentSender', '');
  4902. $current{SenderCurrent} = ReadingsVal($name, 'currentSenderCurrent', '');
  4903. $current{SenderInfo} = ReadingsVal($name, 'currentSenderInfo', '');
  4904. $current{nextTrackDuration} = ReadingsVal($name, 'nextTrackDuration', '');
  4905. $current{nextTrackURI} = ReadingsVal($name, 'nextTrackURI', '');
  4906. $current{nextTrackProvider} = ReadingsVal($name, 'nextTrackProvider', '');
  4907. $current{nextAlbumArtURI} = ReadingsVal($name, 'nextAlbumArtURI', '');
  4908. $current{nextAlbumArtURL} = ReadingsVal($name, 'nextAlbumArtURL', '');
  4909. $current{nextTitle} = ReadingsVal($name, 'nextTitle', '');
  4910. $current{nextArtist} = ReadingsVal($name, 'nextArtist', '');
  4911. $current{nextAlbum} = ReadingsVal($name, 'nextAlbum', '');
  4912. $current{nextAlbumArtist} = ReadingsVal($name, 'nextAlbumArtist', '');
  4913. $current{nextOriginalTrackNumber} = ReadingsVal($name, 'nextOriginalTrackNumber', '');
  4914. $current{InfoSummarize1} = ReadingsVal($name, 'infoSummarize1', '');
  4915. $current{InfoSummarize2} = ReadingsVal($name, 'infoSummarize2', '');
  4916. $current{InfoSummarize3} = ReadingsVal($name, 'infoSummarize3', '');
  4917. $current{InfoSummarize4} = ReadingsVal($name, 'infoSummarize4', '');
  4918. $current{StreamAudio} = ReadingsVal($name, 'currentStreamAudio', 0);
  4919. $current{NormalAudio} = ReadingsVal($name, 'currentNormalAudio', 0);
  4920. }
  4921. # Insert Variables scanned during Device Detection or other events (for simple Replacing-Option of InfoSummarize)
  4922. $current{Volume} = ReadingsVal($name, 'Volume', 0);
  4923. $current{Mute} = ReadingsVal($name, 'Mute', 0);
  4924. $current{OutputFixed} = ReadingsVal($name, 'OutputFixed', 0);
  4925. $current{Balance} = ReadingsVal($name, 'Balance', 0);
  4926. $current{HeadphoneConnected} = ReadingsVal($name, 'HeadphoneConnected', 0);
  4927. $current{SleepTimer} = ReadingsVal($name, 'SleepTimer', '');
  4928. $current{AlarmRunning} = ReadingsVal($name, 'AlarmRunning', '');
  4929. $current{AlarmRunningID} = ReadingsVal($name, 'AlarmRunningID', '');
  4930. $current{Presence} = ReadingsVal($name, 'presence', '');
  4931. $current{RoomName} = ReadingsVal($name, 'roomName', '');
  4932. $current{RoomNameAlias} = ReadingsVal($name, 'roomNameAlias', '');
  4933. $current{SaveRoomName} = ReadingsVal($name, 'saveRoomName', '');
  4934. $current{PlayerType} = ReadingsVal($name, 'playerType', '');
  4935. $current{Location} = ReadingsVal($name, 'location', '');
  4936. $current{SoftwareRevision} = ReadingsVal($name, 'softwareRevision', '');
  4937. $current{SerialNum} = ReadingsVal($name, 'serialNum', '');
  4938. $current{ZoneGroupID} = ReadingsVal($name, 'ZoneGroupID', '');
  4939. $current{ZoneGroupName} = ReadingsVal($name, 'ZoneGroupName', '');
  4940. $current{ZonePlayerUUIDsInGroup} = ReadingsVal($name, 'ZonePlayerUUIDsInGroup', '');
  4941. return %current;
  4942. }
  4943. ########################################################################################
  4944. #
  4945. # SONOS_ServiceCallback - Service-Callback,
  4946. #
  4947. # Parameter $service = Service-Representing Object
  4948. # $properties = Properties, that have been changed in this event
  4949. #
  4950. ########################################################################################
  4951. sub SONOS_ServiceCallback($$) {
  4952. my ($service, %properties) = @_;
  4953. my $udn = $SONOS_Locations{$service->base};
  4954. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  4955. if (!$udn) {
  4956. SONOS_Log undef, 1, 'Transport-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  4957. return;
  4958. }
  4959. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  4960. # If the Device is disabled, return here...
  4961. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  4962. SONOS_Log $udn, 4, "Transport-Event: device '$name' disabled. No Events/Data will be processed!";
  4963. return;
  4964. }
  4965. SONOS_Log $udn, 3, 'Event: Received Transport-Event for Zone "'.$name.'".';
  4966. # Check if the correct ServiceType
  4967. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AVTransport:1') {
  4968. SONOS_Log $udn, 1, 'Transport-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  4969. return;
  4970. }
  4971. # Check if the Variable called LastChange exists
  4972. if (not defined($properties{LastChange})) {
  4973. SONOS_Log $udn, 1, 'Transport-Event receive error: Property \'LastChange\' does not exists!';
  4974. return;
  4975. }
  4976. SONOS_Log $udn, 4, "Transport-Event: All correct with this service-call till now. UDN='uuid:$udn'";
  4977. $SONOS_Client_SendQueue_Suspend = 1;
  4978. # Determine the base URLs for downloading things from player
  4979. my $groundURL = ($1) if ($service->base =~ m/(http:\/\/.*?:\d+)/i);
  4980. SONOS_Log $udn, 4, "Transport-Event: GroundURL: $groundURL";
  4981. # Variablen initialisieren
  4982. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':1');
  4983. # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden...
  4984. $properties{LastChangeDecoded} = decode_entities($properties{LastChange});
  4985. $properties{LastChangeDecoded} =~ s/[\r\n]//isg; # Komischerweise können hier unmaskierte Newlines auftauchen... wegmachen
  4986. # Verarbeitung starten
  4987. SONOS_Log $udn, 4, 'Transport-Event: LastChange: '.$properties{LastChangeDecoded};
  4988. # Alte Bookmarks aktualisieren, gespeicherte Trackposition bei Bedarf anspringen...
  4989. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  4990. { # Start local area...
  4991. my $bufferedURI = '';
  4992. $bufferedURI = SONOS_GetURIFromQueueValue($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackURI val="(.*?)"\/>/i);
  4993. $bufferedURI =~ s/&apos;/'/gi;
  4994. my $bufferedTrackDuration = 0;
  4995. $bufferedTrackDuration = SONOS_GetTimeSeconds(decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackDuration val="(.*?)"\/>/i);
  4996. my $bufferedTrackPosition = 0;
  4997. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  4998. $bufferedTrackPosition = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
  4999. if ($bufferedTrackPosition !~ /\d+:\d+:\d+/i) { # e.g. NOT_IMPLEMENTED
  5000. $bufferedTrackPosition = '0:00:00';
  5001. }
  5002. $bufferedTrackPosition = SONOS_GetTimeSeconds($bufferedTrackPosition);
  5003. }
  5004. if (($SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} ne $bufferedURI) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5005. my $timestamp = scalar(gettimeofday());
  5006. foreach my $gKey (SONOS_getBookmarkGroupKeys('Title', $udn)) {
  5007. if (defined($SONOS_BookmarkTitleHash{$gKey}{$bufferedURI}) && SONOS_getBookmarkTitleIsRelevant($gKey, $timestamp, $bufferedURI, $bufferedTrackPosition, $bufferedTrackDuration)) {
  5008. my $newTrackposition = $SONOS_BookmarkTitleHash{$gKey}{$bufferedURI}{TrackPosition};
  5009. my $result = $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', SONOS_ConvertSecondsToTime($newTrackposition));
  5010. SONOS_Log $udn, 3, 'Player "'.SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn).'" jumped to the bookmarked trackposition '.SONOS_ConvertSecondsToTime($newTrackposition).' (Group "'.$gKey.'") ~ Bookmarkdata: '.SONOS_Dumper($SONOS_BookmarkTitleHash{$gKey}{$bufferedURI});
  5011. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'JumpToTrackPosition "'.SONOS_ConvertSecondsToTime($newTrackposition).'": '.SONOS_UPnPAnswerMessage($result));
  5012. last;
  5013. }
  5014. }
  5015. }
  5016. }
  5017. # Bulkupdate hier starten...
  5018. #SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  5019. # Check, if this is a SleepTimer-Event
  5020. my $sleepTimerVersion = $1 if ($properties{LastChangeDecoded} =~ m/<r:SleepTimerGeneration val="(.*?)"\/>/i);
  5021. if (defined($sleepTimerVersion) && $sleepTimerVersion ne SONOS_Client_Data_Retreive($udn, 'reading', 'SleepTimerVersion', '')) {
  5022. # Variablen neu initialisieren, und die Original-Werte wieder mit reinholen
  5023. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0');
  5024. # Neuer SleepTimer da!
  5025. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5026. my $result = $SONOS_AVTransportControlProxy{$udn}->GetRemainingSleepTimerDuration();
  5027. my $currentValue = $result->getValue('RemainingSleepTimerDuration');
  5028. # Wenn der Timer abgelaufen ist, wird nur ein Leerstring übergeben. Diesen durch das Wort off ersetzen.
  5029. $currentValue = 'off' if (!defined($currentValue) || ($currentValue eq ''));
  5030. SONOS_Client_Notifier('SetCurrent:SleepTimer:'.$currentValue);
  5031. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SleepTimerVersion', ($result->getValue('CurrentSleepTimerGeneration') ? $result->getValue('CurrentSleepTimerGeneration') : ''));
  5032. }
  5033. }
  5034. # Um einen XML-Parser zu vermeiden, werden hier einige reguläre Ausdrücke für die Ermittlung der Werte eingesetzt...
  5035. # Transportstate ermitteln
  5036. if ($properties{LastChangeDecoded} =~ m/<TransportState val="(.*?)"\/>/i) {
  5037. my $currentValue = decode_entities($1);
  5038. # Wenn der TransportState den neuen Wert 'Transitioning' hat, dann diesen auf Playing umsetzen, da das hier ausreicht.
  5039. $currentValue = 'PLAYING' if ($currentValue =~ m/TRANSITIONING/i);
  5040. SONOS_Client_Notifier('SetCurrent:TransportState:'.$currentValue);
  5041. $SONOS_BookmarkSpeicher{OldTransportstate}{$udn} = $currentValue;
  5042. }
  5043. # Wird hier gerade eine Alarm-Abspielung durchgeführt (oder beendet)?
  5044. SONOS_Client_Notifier('SetCurrent:AlarmRunning:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:AlarmRunning val="(.*?)"\/>/i);
  5045. # Wenn ein Alarm läuft, dann zusätzliche Informationen besorgen, ansonsten das entsprechende Reading leeren
  5046. if (defined($1) && $1 eq '1') {
  5047. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5048. my $alarmID = $SONOS_AVTransportControlProxy{$udn}->GetRunningAlarmProperties(0)->getValue('AlarmID');
  5049. SONOS_Client_Notifier('SetCurrent:AlarmRunningID:'.$alarmID);
  5050. }
  5051. } elsif (defined($1) && $1 eq '0') {
  5052. SONOS_Client_Notifier('SetCurrent:AlarmRunningID:');
  5053. }
  5054. my $isStream = 0;
  5055. # Das nächste nur machen, wenn dieses Event die Track-Informationen auch enthält
  5056. if ($properties{LastChangeDecoded} =~ m/<(AVTransportURI|TransportState) val=".*?"\/>/i) {
  5057. # PlayMode ermitteln
  5058. my $currentPlayMode = 'NORMAL';
  5059. $currentPlayMode = $1 if ($properties{LastChangeDecoded} =~ m/<CurrentPlayMode.*?val="(.*?)".*?\/>/i);
  5060. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($currentPlayMode);
  5061. SONOS_Client_Notifier('SetCurrent:Shuffle:1') if ($shuffle);
  5062. SONOS_Client_Notifier('SetCurrent:Repeat:1') if ($repeat);
  5063. SONOS_Client_Notifier('SetCurrent:RepeatOne:1') if ($repeatOne);
  5064. # CrossfadeMode ermitteln
  5065. SONOS_Client_Notifier('SetCurrent:CrossfadeMode:'.$1) if ($properties{LastChangeDecoded} =~ m/<CurrentCrossfadeMode.*?val="(\d+)".*?\/>/i);
  5066. # Anzahl Tracknumber ermitteln
  5067. SONOS_Client_Notifier('SetCurrent:NumberOfTracks:'.decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<NumberOfTracks val="(.*?)"\/>/i);
  5068. # Current Tracknumber ermitteln
  5069. if ($properties{LastChangeDecoded} =~ m/<CurrentTrack val="(.*?)"\/>/i) {
  5070. SONOS_Client_Notifier('SetCurrent:Track:'.decode_entities($1));
  5071. # Für die Bookmarkverwaltung ablegen
  5072. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = decode_entities($1);
  5073. }
  5074. # Current TrackURI ermitteln
  5075. my $currentTrackURI = SONOS_GetURIFromQueueValue($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackURI val="(.*?)"\/>/i);
  5076. $currentTrackURI =~ s/&apos;/'/gi;
  5077. SONOS_Client_Notifier('SetCurrent:TrackURI:'.$currentTrackURI);
  5078. # Für die Bookmarkverwaltung ablegen
  5079. $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} = $currentTrackURI;
  5080. # Wenn es ein Spotify-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
  5081. if ($currentTrackURI =~ m/^x-sonos-spotify:/i) {
  5082. my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/r:EnqueuedTransportURIMetaData val="(.*?)"\/>/i);
  5083. SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Spotify:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i);
  5084. }
  5085. # Wenn es ein Napster/Rhapsody-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
  5086. if ($currentTrackURI =~ m/^npsdy:/i) {
  5087. my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/r:EnqueuedTransportURIMetaData val="(.*?)"\/>/i);
  5088. SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Napster:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i);
  5089. }
  5090. # Current Trackdauer ermitteln
  5091. if ($properties{LastChangeDecoded} =~ m/<CurrentTrackDuration val="(.*?)"\/>/i) {
  5092. SONOS_Client_Notifier('SetCurrent:TrackDuration:'.decode_entities($1));
  5093. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = SONOS_GetTimeSeconds(decode_entities($1));
  5094. }
  5095. # Current Track Metadaten ermitteln
  5096. my $currentTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackMetaData val="(.*?)"\/>/is);
  5097. SONOS_Log $udn, 4, 'Transport-Event: CurrentTrackMetaData: '.$currentTrackMetaData;
  5098. # Cover herunterladen (Infos dazu in den Track Metadaten)
  5099. my $tempURIground = decode_entities($currentTrackMetaData);
  5100. $tempURIground =~ s/%25/%/ig;
  5101. my $tempURI = '';
  5102. $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  5103. # Wenn in der URI bereits ein kompletter Pfad drinsteht, dann diese Basis verwenden (passiert bei Wiedergabe vom iPad z.B.)
  5104. if ($tempURI =~ m/^(http:\/\/.*?\/)(.*)/) {
  5105. $groundURL = $1;
  5106. $tempURI = $2;
  5107. }
  5108. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.$tempURI.':'.$groundURL);
  5109. # Auch hier den XML-Parser verhindern, und alles per regulärem Ausdruck ermitteln...
  5110. if ($currentTrackMetaData =~ m/<dc:title>x-(sonosapi|rincon)-(stream|mp3radio):.*?<\/dc:title>/) {
  5111. # Wenn es ein Stream ist, dann muss da was anderes erkannt werden
  5112. SONOS_Log $udn, 4, "Transport-Event: Stream erkannt!";
  5113. SONOS_Client_Notifier('SetCurrent:StreamAudio:1');
  5114. $isStream = 1;
  5115. # Sender ermitteln (per SOAP-Request an den SonosPlayer)
  5116. if ($service->controlProxy()->GetMediaInfo(0)->getValue('CurrentURIMetaData') =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5117. SONOS_Client_Notifier('SetCurrent:Sender:'.$1);
  5118. SONOS_Client_Notifier('SetCurrent:TrackProvider:'.SONOS_GetTrackProvider($currentTrackURI, $1));
  5119. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1;
  5120. }
  5121. # Sender-Läuft ermitteln
  5122. SONOS_Client_Notifier('SetCurrent:SenderCurrent:'.$1) if ($currentTrackMetaData =~ m/<r:radioShowMd>(.*?),p\d{6}<\/r:radioShowMd>/i);
  5123. # Sendungs-Informationen ermitteln
  5124. my $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:streamContent>(.*?)<\/r:streamContent>/i);
  5125. $currentValue = '' if (!defined($currentValue));
  5126. # Wenn hier eine Buffering- oder Connecting-Konstante zurückkommt, dann durch vernünftigen Text ersetzen
  5127. $currentValue = 'Verbindung herstellen...' if ($currentValue eq 'ZPSTR_CONNECTING');
  5128. $currentValue = 'Wird gestartet...' if ($currentValue eq 'ZPSTR_BUFFERING');
  5129. # Wenn hier RTL.it seine Infos liefert, diese zurechtschnippeln...
  5130. $currentValue = '' if ($currentValue eq '<songInfo />');
  5131. if ($currentValue =~ m/<class>Music<\/class>.*?<mus_art_name>(.*?)<\/mus_art_name>/i) {
  5132. $currentValue = $1;
  5133. $currentValue =~ s/\[e\]amp\[p\]/&/ig;
  5134. }
  5135. SONOS_Client_Notifier('SetCurrent:SenderInfo:'.encode_entities($currentValue));
  5136. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = 0;
  5137. } else {
  5138. SONOS_Log $udn, 4, "Transport-Event: Normal erkannt!";
  5139. SONOS_Client_Notifier('SetCurrent:NormalAudio:1');
  5140. my $currentArtist = '';
  5141. my $currentTitle = '';
  5142. if ($currentTrackURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) {
  5143. # Gruppenwiedergabe feststellen, und dann andere Informationen anzeigen
  5144. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5145. SONOS_Client_Notifier('SetCurrent:Title:Gruppenwiedergabe');
  5146. SONOS_Client_Notifier('SetCurrent:Artist:');
  5147. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = 'Gruppenwiedergabe von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5148. } elsif ($currentTrackURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) {
  5149. # LineIn-Wiedergabe feststellen, und dann andere Informationen anzeigen
  5150. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5151. # Fallback
  5152. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5153. if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5154. SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_replaceSpecialStringCharacters(decode_entities($1)));
  5155. $currentTitle = $1;
  5156. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1.' von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5157. }
  5158. SONOS_Client_Notifier('SetCurrent:Artist:');
  5159. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_default.jpg:');
  5160. } elsif ($currentTrackURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) {
  5161. # Dock-Wiedergabe feststellen, und dann andere Informationen anzeigen
  5162. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentAlbum', SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)));
  5163. my $tmpTitle = SONOS_replaceSpecialStringCharacters(decode_entities($1)) if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i);
  5164. $tmpTitle = '' if (!defined($tmpTitle));
  5165. SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentTitle', $tmpTitle));
  5166. $currentTitle = $tmpTitle;
  5167. SONOS_Client_Notifier('SetCurrent:Artist:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentArtist', ''));
  5168. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentTitle', $tmpTitle);
  5169. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_dock.jpg:');
  5170. } elsif ($currentTrackURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) {
  5171. # LineIn-Wiedergabe der Playbar feststellen, und dann andere Informationen anzeigen
  5172. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5173. SONOS_Client_Notifier('SetCurrent:Title:SPDIF-Wiedergabe');
  5174. SONOS_Client_Notifier('SetCurrent:Artist:');
  5175. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = 'SPDIF-Wiedergabe von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5176. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_tv.jpg:');
  5177. } else {
  5178. # Titel ermitteln
  5179. if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5180. SONOS_Client_Notifier('SetCurrent:Title:'.$1);
  5181. $currentTitle = $1;
  5182. }
  5183. # Interpret ermitteln
  5184. if ($currentTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i) {
  5185. $currentArtist = decode_entities($1);
  5186. SONOS_Client_Notifier('SetCurrent:Artist:'.encode_entities($currentArtist));
  5187. }
  5188. # Album ermitteln
  5189. SONOS_Client_Notifier('SetCurrent:Album:'.$1) if ($currentTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  5190. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = '('.$currentArtist.') '.$currentTitle;
  5191. }
  5192. SONOS_Client_Notifier('SetCurrent:TrackProvider:'.SONOS_GetTrackProvider($currentTrackURI, $currentTitle));
  5193. # Original Tracknumber ermitteln
  5194. SONOS_Client_Notifier('SetCurrent:OriginalTrackNumber:'.decode_entities($1)) if ($currentTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  5195. # Album Artist ermitteln
  5196. my $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  5197. $currentValue = $currentArtist if (!defined($currentValue) || ($currentValue eq ''));
  5198. SONOS_Client_Notifier('SetCurrent:AlbumArtist:'.encode_entities($currentValue));
  5199. }
  5200. # Next Track Metadaten ermitteln
  5201. my $nextTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<r:NextTrackMetaData val="(.*?)"\/>/i);
  5202. SONOS_Log $udn, 4, 'Transport-Event: NextTrackMetaData: '.$nextTrackMetaData;
  5203. SONOS_Client_Notifier('SetCurrent:nextTrackDuration:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<res.*?duration="(.*?)".*?>/i);
  5204. if ($properties{LastChangeDecoded} =~ m/<r:NextTrackURI val="(.*?)"\/>/i) {
  5205. my $tmp = SONOS_GetURIFromQueueValue($1);
  5206. $tmp =~ s/&apos;/'/gi;
  5207. SONOS_Client_Notifier('SetCurrent:nextTrackURI:'.$tmp);
  5208. SONOS_Client_Notifier('SetCurrent:nextTrackProvider:'.SONOS_GetTrackProvider($tmp));
  5209. }
  5210. $tempURIground = decode_entities($nextTrackMetaData);
  5211. $tempURIground =~ s/%25/%/ig;
  5212. $tempURI = '';
  5213. $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  5214. SONOS_Client_Notifier('ProcessCover:'.$udn.':1:'.$tempURI.':'.$groundURL);
  5215. SONOS_Client_Notifier('SetCurrent:nextTitle:'.$1) if ($nextTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i);
  5216. SONOS_Client_Notifier('SetCurrent:nextArtist:'.$1) if ($nextTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  5217. SONOS_Client_Notifier('SetCurrent:nextAlbum:'.$1) if ($nextTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  5218. SONOS_Client_Notifier('SetCurrent:nextAlbumArtist:'.$1) if ($nextTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  5219. SONOS_Client_Notifier('SetCurrent:nextOriginalTrackNumber:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  5220. } else {
  5221. SONOS_Log undef, 3, 'No trackinformationen found in data: '.$properties{LastChangeDecoded};
  5222. }
  5223. # Current Trackposition ermitteln (durch Abfrage beim Player, bzw. bei Streams statisch)
  5224. if ($isStream) {
  5225. SONOS_Client_Notifier('SetCurrent:TrackPosition:0:00:00');
  5226. } else {
  5227. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5228. my $trackPosition = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
  5229. if ($trackPosition !~ /\d+:\d+:\d+/i) { # e.g. NOT_IMPLEMENTED
  5230. $trackPosition = '0:00:00';
  5231. }
  5232. SONOS_Client_Notifier('SetCurrent:TrackPosition:'.$trackPosition);
  5233. $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn} = SONOS_GetTimeSeconds($trackPosition);
  5234. $SONOS_BookmarkSpeicher{OldTimestamp}{$udn} = scalar(gettimeofday()) - SONOS_GetTimeSeconds($trackPosition);
  5235. }
  5236. }
  5237. # Neue Bookmarks aktualisieren
  5238. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  5239. # Trigger/Transfer the whole bunch and generate InfoSummarize
  5240. SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn);
  5241. SONOS_AnalyzeTopologyForMasterPlayer(SONOS_Client_Data_Retreive('undef', 'reading', 'ZoneGroupState', ''));
  5242. $SONOS_Client_SendQueue_Suspend = 0;
  5243. SONOS_Log $udn, 3, 'Event: End of Transport-Event for Zone "'.$name.'".';
  5244. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  5245. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  5246. SONOS_Log $udn, 1, "Transport-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  5247. SONOS_RestartControlPoint();
  5248. }
  5249. return 0;
  5250. }
  5251. ########################################################################################
  5252. #
  5253. # SONOS_RenderingCallback - Rendering-Callback,
  5254. #
  5255. # Parameter $service = Service-Representing Object
  5256. # $properties = Properties, that have been changed in this event
  5257. #
  5258. ########################################################################################
  5259. sub SONOS_RenderingCallback($$) {
  5260. my ($service, %properties) = @_;
  5261. my $udn = $SONOS_Locations{$service->base};
  5262. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5263. if (!$udn) {
  5264. SONOS_Log undef, 1, 'Rendering-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  5265. return;
  5266. }
  5267. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5268. # If the Device is disabled, return here...
  5269. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5270. SONOS_Log $udn, 3, "Rendering-Event: device '$name' disabled. No Events/Data will be processed!";
  5271. return;
  5272. }
  5273. SONOS_Log $udn, 3, 'Event: Received Rendering-Event for Zone "'.$name.'".';
  5274. $SONOS_Client_SendQueue_Suspend = 1;
  5275. # Check if the correct ServiceType
  5276. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:RenderingControl:1') {
  5277. SONOS_Log $udn, 1, 'Rendering-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5278. return;
  5279. }
  5280. # Check if the Variable called LastChange exists
  5281. if (not defined($properties{LastChange})) {
  5282. SONOS_Log $udn, 1, 'Rendering-Event receive error: Property \'LastChange\' does not exists!';
  5283. return;
  5284. }
  5285. SONOS_Log $udn, 4, "Rendering-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  5286. # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden...
  5287. $properties{LastChangeDecoded} = decode_entities($properties{LastChange});
  5288. SONOS_Log $udn, 4, 'Rendering-Event: LastChange: '.$properties{LastChangeDecoded};
  5289. my $generateVolumeEvent = SONOS_Client_Data_Retreive($udn, 'attr', 'generateVolumeEvent', 0);
  5290. # Mute?
  5291. my $mute = SONOS_Client_Data_Retreive($udn, 'reading', 'Mute', 0);
  5292. if ($properties{LastChangeDecoded} =~ m/<Mute.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  5293. SONOS_AddToButtonQueue($udn, 'M') if ($1 ne $mute);
  5294. $mute = $1;
  5295. if ($generateVolumeEvent) {
  5296. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Mute', $mute);
  5297. } else {
  5298. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Mute', $mute);
  5299. }
  5300. }
  5301. # Headphone?
  5302. my $headphoneConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'HeadphoneConnected', 0);
  5303. if ($properties{LastChangeDecoded} =~ m/<HeadphoneConnected.*?val="(\d+)".*?\/>/i) {
  5304. SONOS_AddToButtonQueue($udn, 'H') if ($1 ne $headphoneConnected);
  5305. $headphoneConnected = $1;
  5306. if ($generateVolumeEvent) {
  5307. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'HeadphoneConnected', $headphoneConnected);
  5308. } else {
  5309. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'HeadphoneConnected', $headphoneConnected);
  5310. }
  5311. }
  5312. # Balance ermitteln
  5313. my $balance = SONOS_Client_Data_Retreive($udn, 'reading', 'Balance', 0);
  5314. if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="LF".*?val="(\d+)".*?\/>/i) {
  5315. my $volumeLeft = $1;
  5316. my $volumeRight = $1 if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="RF".*?val="(\d+)".*?\/>/i);
  5317. $balance = (-$volumeLeft) + $volumeRight if ($volumeLeft && $volumeRight);
  5318. if ($generateVolumeEvent) {
  5319. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Balance', $balance);
  5320. } else {
  5321. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Balance', $balance);
  5322. }
  5323. }
  5324. # Volume ermitteln
  5325. my $currentVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'Volume', 0);
  5326. if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  5327. SONOS_AddToButtonQueue($udn, 'U') if ($1 > $currentVolume);
  5328. SONOS_AddToButtonQueue($udn, 'D') if ($1 < $currentVolume);
  5329. $currentVolume = $1 ;
  5330. if ($generateVolumeEvent) {
  5331. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Volume', $currentVolume);
  5332. } else {
  5333. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Volume', $currentVolume);
  5334. }
  5335. }
  5336. # Loudness?
  5337. my $loudness = SONOS_Client_Data_Retreive($udn, 'reading', 'Loudness', 0);
  5338. if ($properties{LastChangeDecoded} =~ m/<Loudness.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  5339. $loudness = $1;
  5340. if ($generateVolumeEvent) {
  5341. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Loudness', $loudness);
  5342. } else {
  5343. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Loudness', $loudness);
  5344. }
  5345. }
  5346. # Bass?
  5347. my $bass = SONOS_Client_Data_Retreive($udn, 'reading', 'Bass', 0);
  5348. if ($properties{LastChangeDecoded} =~ m/<Bass.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5349. $bass = $1;
  5350. if ($generateVolumeEvent) {
  5351. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Bass', $bass);
  5352. } else {
  5353. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Bass', $bass);
  5354. }
  5355. }
  5356. # Treble?
  5357. my $treble = SONOS_Client_Data_Retreive($udn, 'reading', 'Treble', 0);
  5358. if ($properties{LastChangeDecoded} =~ m/<Treble.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5359. $treble = $1;
  5360. if ($generateVolumeEvent) {
  5361. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Treble', $treble);
  5362. } else {
  5363. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Treble', $treble);
  5364. }
  5365. }
  5366. # TruePlay?
  5367. my $trueplay = SONOS_Client_Data_Retreive($udn, 'reading', 'TruePlay', 0);
  5368. if ($properties{LastChangeDecoded} =~ m/<SonarEnabled.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5369. $trueplay = $1;
  5370. if ($generateVolumeEvent) {
  5371. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'TruePlay', $trueplay);
  5372. } else {
  5373. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'TruePlay', $trueplay);
  5374. }
  5375. }
  5376. # SurroundEnable?
  5377. my $surroundEnable = SONOS_Client_Data_Retreive($udn, 'reading', 'SurroundEnable', 0);
  5378. if ($properties{LastChangeDecoded} =~ m/<SurroundEnable.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5379. $surroundEnable = $1;
  5380. if ($generateVolumeEvent) {
  5381. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SurroundEnable', $surroundEnable);
  5382. } else {
  5383. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SurroundEnable', $surroundEnable);
  5384. }
  5385. }
  5386. # SurroundLevel?
  5387. my $surroundLevel = SONOS_Client_Data_Retreive($udn, 'reading', 'SurroundLevel', 0);
  5388. if ($properties{LastChangeDecoded} =~ m/<SurroundLevel.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5389. $surroundLevel = $1;
  5390. if ($generateVolumeEvent) {
  5391. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SurroundLevel', $surroundLevel);
  5392. } else {
  5393. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SurroundLevel', $surroundLevel);
  5394. }
  5395. }
  5396. # SubEnable?
  5397. my $subEnable = SONOS_Client_Data_Retreive($udn, 'reading', 'SubEnable', 0);
  5398. if ($properties{LastChangeDecoded} =~ m/<SubEnable.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5399. $subEnable = $1;
  5400. if ($generateVolumeEvent) {
  5401. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubEnable', $subEnable);
  5402. } else {
  5403. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubEnable', $subEnable);
  5404. }
  5405. }
  5406. # SubGain?
  5407. my $subGain = SONOS_Client_Data_Retreive($udn, 'reading', 'SubGain', 0);
  5408. if ($properties{LastChangeDecoded} =~ m/<SubGain.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5409. $subGain = $1;
  5410. if ($generateVolumeEvent) {
  5411. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubGain', $subGain);
  5412. } else {
  5413. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubGain', $subGain);
  5414. }
  5415. }
  5416. # SubPolarity?
  5417. my $subPolarity = SONOS_Client_Data_Retreive($udn, 'reading', 'SubPolarity', 0);
  5418. if ($properties{LastChangeDecoded} =~ m/<SubPolarity.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5419. $subPolarity = $1;
  5420. if ($generateVolumeEvent) {
  5421. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubPolarity', $subPolarity);
  5422. } else {
  5423. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubPolarity', $subPolarity);
  5424. }
  5425. }
  5426. # AudioDelay?
  5427. my $audioDelay = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelay', 0);
  5428. if ($properties{LastChangeDecoded} =~ m/<AudioDelay.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5429. $audioDelay = $1;
  5430. if ($generateVolumeEvent) {
  5431. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelay', $audioDelay);
  5432. } else {
  5433. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelay', $audioDelay);
  5434. }
  5435. }
  5436. # AudioDelayLeftRear?
  5437. my $audioDelayLeftRear = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelayLeftRear', 0);
  5438. if ($properties{LastChangeDecoded} =~ m/<AudioDelayLeftRear.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5439. $audioDelayLeftRear = $1;
  5440. if ($generateVolumeEvent) {
  5441. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelayLeftRear', $audioDelayLeftRear);
  5442. } else {
  5443. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelayLeftRear', $audioDelayLeftRear);
  5444. }
  5445. }
  5446. # AudioDelayRightRear?
  5447. my $audioDelayRightRear = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelayRightRear', 0);
  5448. if ($properties{LastChangeDecoded} =~ m/<AudioDelayRightRear.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5449. $audioDelayRightRear = $1;
  5450. if ($generateVolumeEvent) {
  5451. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelayRightRear', $audioDelayRightRear);
  5452. } else {
  5453. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelayRightRear', $audioDelayRightRear);
  5454. }
  5455. }
  5456. # NightMode?
  5457. my $nightMode = SONOS_Client_Data_Retreive($udn, 'reading', 'NightMode', 0);
  5458. if ($properties{LastChangeDecoded} =~ m/<NightMode.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5459. $nightMode = $1;
  5460. if ($generateVolumeEvent) {
  5461. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'NightMode', $nightMode);
  5462. } else {
  5463. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'NightMode', $nightMode);
  5464. }
  5465. }
  5466. # DialogLevel?
  5467. my $dialogLevel = SONOS_Client_Data_Retreive($udn, 'reading', 'DialogLevel', 0);
  5468. if ($properties{LastChangeDecoded} =~ m/<DialogLevel.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5469. $dialogLevel = $1;
  5470. if ($generateVolumeEvent) {
  5471. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'DialogLevel', $dialogLevel);
  5472. } else {
  5473. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'DialogLevel', $dialogLevel);
  5474. }
  5475. }
  5476. # OutputFixed?
  5477. my $outputFixed = SONOS_Client_Data_Retreive($udn, 'reading', 'OutputFixed', 0);
  5478. if ($properties{LastChangeDecoded} =~ m/<OutputFixed.*?val="([-]{0,1}\d+)".*?\/>/i) {
  5479. $outputFixed = $1;
  5480. if ($generateVolumeEvent) {
  5481. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'OutputFixed', $outputFixed);
  5482. } else {
  5483. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'OutputFixed', $outputFixed);
  5484. }
  5485. }
  5486. SONOS_Log $udn, 4, "Rendering-Event: Current Values for '$name' ~ Volume: $currentVolume, HeadphoneConnected: $headphoneConnected, Bass: $bass, Treble: $treble, Balance: $balance, Loudness: $loudness, Mute: $mute";
  5487. # Ensures the defined volume-borders
  5488. if (SONOS_EnsureMinMaxVolumes($udn)) {
  5489. # Variablen initialisieren
  5490. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0');
  5491. SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn);
  5492. }
  5493. # ButtonQueue prüfen
  5494. SONOS_CheckButtonQueue($udn);
  5495. $SONOS_Client_SendQueue_Suspend = 0;
  5496. SONOS_Log $udn, 3, 'Event: End of Rendering-Event for Zone "'.$name.'".';
  5497. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  5498. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  5499. SONOS_Log $udn, 1, "Rendering-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  5500. SONOS_RestartControlPoint();
  5501. }
  5502. return 0;
  5503. }
  5504. ########################################################################################
  5505. #
  5506. # SONOS_EnsureMinMaxVolumes - Ensures the defined volume-borders
  5507. #
  5508. ########################################################################################
  5509. sub SONOS_EnsureMinMaxVolumes($) {
  5510. my ($udn) = @_;
  5511. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5512. my $currentVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'Volume', 0);
  5513. my $headphoneConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'HeadphoneConnected', 0);
  5514. my $mute = SONOS_Client_Data_Retreive($udn, 'reading', 'Mute', 0);
  5515. # Grenzen passend zum verwendeten Tonausgang ermitteln
  5516. # Untere Grenze ermitteln
  5517. my $key = 'minVolume'.($headphoneConnected ? 'Headphone' : '');
  5518. my $minVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 0);
  5519. # Obere Grenze ermitteln
  5520. $key = 'maxVolume'.($headphoneConnected ? 'Headphone' : '');
  5521. my $maxVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 100);
  5522. SONOS_Log $udn, 4, "Rendering-Event: Current Borders for '$name' ~ minVolume: $minVolume, maxVolume: $maxVolume";
  5523. # Fehlerhafte Attributangaben?
  5524. if ($minVolume > $maxVolume) {
  5525. SONOS_Log $udn, 0, 'Min-/MaxVolume check Error: MinVolume('.$minVolume.') > MaxVolume('.$maxVolume.'), using Headphones: '.$headphoneConnected.'!';
  5526. return;
  5527. }
  5528. # Prüfungen und Aktualisierungen durchführen
  5529. if (!$mute && ($minVolume > $currentVolume)) {
  5530. # Grenzen prüfen: Zu Leise
  5531. SONOS_Log $udn, 4, 'Volume to Low. Correct it to "'.$minVolume.'"';
  5532. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $minVolume);
  5533. } elsif (!$mute && ($currentVolume > $maxVolume)) {
  5534. # Grenzen prüfen: Zu Laut
  5535. SONOS_Log $udn, 4, 'Volume to High. Correct it to "'.$maxVolume.'"';
  5536. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $maxVolume);
  5537. } else {
  5538. return 0;
  5539. }
  5540. return 1;
  5541. }
  5542. ########################################################################################
  5543. #
  5544. # SONOS_GroupRenderingCallback - GroupRendering-Callback,
  5545. #
  5546. # Parameter $service = Service-Representing Object
  5547. # $properties = Properties, that have been changed in this event
  5548. #
  5549. ########################################################################################
  5550. sub SONOS_GroupRenderingCallback($$) {
  5551. my ($service, %properties) = @_;
  5552. my $udn = $SONOS_Locations{$service->base};
  5553. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5554. if (!$udn) {
  5555. SONOS_Log undef, 1, 'GroupRendering-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  5556. return;
  5557. }
  5558. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5559. # If the Device is disabled, return here...
  5560. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5561. SONOS_Log $udn, 3, "GroupRendering-Event: device '$name' disabled. No Events/Data will be processed!";
  5562. return;
  5563. }
  5564. SONOS_Log $udn, 3, 'Event: Received GroupRendering-Event for Zone "'.$name.'".';
  5565. $SONOS_Client_SendQueue_Suspend = 1;
  5566. # Check if the correct ServiceType
  5567. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:GroupRenderingControl:1') {
  5568. SONOS_Log $udn, 1, 'GroupRendering-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5569. return;
  5570. }
  5571. SONOS_Log $udn, 4, "GroupRendering-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  5572. my $generateVolumeEvent = SONOS_Client_Data_Retreive($udn, 'attr', 'generateVolumeEvent', 0);
  5573. # GroupVolume...
  5574. my $groupVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'GroupVolume', '~~');
  5575. if (defined($properties{GroupVolume}) && ($properties{GroupVolume} ne $groupVolume)) {
  5576. if ($generateVolumeEvent) {
  5577. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'GroupVolume', $properties{GroupVolume});
  5578. } else {
  5579. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'GroupVolume', $properties{GroupVolume});
  5580. }
  5581. }
  5582. # GroupMute...
  5583. my $groupMute = SONOS_Client_Data_Retreive($udn, 'reading', 'GroupMute', '~~');
  5584. if (defined($properties{GroupMute}) && ($properties{GroupMute} ne $groupMute)) {
  5585. if ($generateVolumeEvent) {
  5586. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'GroupMute', $properties{GroupMute});
  5587. } else {
  5588. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'GroupMute', $properties{GroupMute});
  5589. }
  5590. }
  5591. $SONOS_Client_SendQueue_Suspend = 0;
  5592. SONOS_Log $udn, 3, 'Event: End of GroupRendering-Event for Zone "'.$name.'".';
  5593. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  5594. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  5595. SONOS_Log $udn, 1, "GroupRendering-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  5596. SONOS_RestartControlPoint();
  5597. }
  5598. return 0;
  5599. }
  5600. ########################################################################################
  5601. #
  5602. # SONOS_ContentDirectoryCallback - ContentDirectory-Callback,
  5603. #
  5604. # Parameter $service = Service-Representing Object
  5605. # $properties = Properties, that have been changed in this event
  5606. #
  5607. ########################################################################################
  5608. sub SONOS_ContentDirectoryCallback($$) {
  5609. my ($service, %properties) = @_;
  5610. my $udn = $SONOS_Locations{$service->base};
  5611. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5612. if (!$udn) {
  5613. SONOS_Log undef, 1, 'ContentDirectory-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  5614. return;
  5615. }
  5616. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5617. # If the Device is disabled, return here...
  5618. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5619. SONOS_Log $udn, 3, "ContentDirectory-Event: device '$name' disabled. No Events/Data will be processed!";
  5620. return;
  5621. }
  5622. SONOS_Log $udn, 3, 'Event: Received ContentDirectory-Event for Zone "'.$name.'".';
  5623. $SONOS_Client_SendQueue_Suspend = 1;
  5624. # Check if the correct ServiceType
  5625. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:ContentDirectory:1') {
  5626. SONOS_Log $udn, 1, 'ContentDirectory-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5627. return;
  5628. }
  5629. SONOS_Log $udn, 4, "ContentDirectory-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  5630. #FavoritesUpdateID...
  5631. if (defined($properties{FavoritesUpdateID})) {
  5632. my $containerUpdateIDs = '';
  5633. $containerUpdateIDs = $properties{ContainerUpdateIDs} if ($properties{ContainerUpdateIDs});
  5634. my $favouritesUpdateID = $1 if ($containerUpdateIDs =~ m/FV:2,\d+?/i);
  5635. my $radiosUpdateID = $1 if ($containerUpdateIDs =~ m/R:0,\d+?/i);
  5636. # Wenn beide nicht geliefert wurden, dann beide setzen...
  5637. $containerUpdateIDs = '' if (!defined($favouritesUpdateID) && !defined($radiosUpdateID));
  5638. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'FavouritesVersion', $properties{FavoritesUpdateID}) if (defined($favouritesUpdateID) || ($containerUpdateIDs eq ''));
  5639. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'RadiosVersion', $properties{FavoritesUpdateID}) if (defined($radiosUpdateID) || ($containerUpdateIDs eq ''));
  5640. }
  5641. #QueueUpdateID...
  5642. if (defined($properties{ContainerUpdateIDs})) {
  5643. my $oldVersion = SONOS_Client_Data_Retreive($udn, 'reading', 'QueueVersion', '~~');
  5644. my $newVersion = '';
  5645. $newVersion = $1 if ($properties{ContainerUpdateIDs} =~ m/Q:0,(\d+)/i);
  5646. if ($oldVersion ne $newVersion) {
  5647. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'QueueVersion', $newVersion);
  5648. # Für die Queue-Bookmarkverarbeitung den Queue-Hash neu berechnen und u.U. auf anderen Titel springen...
  5649. SONOS_CalculateQueueHash($udn);
  5650. }
  5651. }
  5652. #SavedQueuesUpdateID...
  5653. my $savedQueuesUpdateID = SONOS_Client_Data_Retreive($udn, 'reading', 'PlaylistsVersion', '~~');
  5654. if (defined($properties{SavedQueuesUpdateID}) && ($properties{SavedQueuesUpdateID} ne $savedQueuesUpdateID)) {
  5655. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'PlaylistsVersion', $properties{SavedQueuesUpdateID});
  5656. }
  5657. $SONOS_Client_SendQueue_Suspend = 0;
  5658. SONOS_Log $udn, 3, 'Event: End of ContentDirectory-Event for Zone "'.$name.'".';
  5659. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  5660. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  5661. SONOS_Log $udn, 1, "ContentDirectory-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  5662. SONOS_RestartControlPoint();
  5663. }
  5664. return 0;
  5665. }
  5666. ########################################################################################
  5667. #
  5668. # SONOS_SaveBookmarkValues - Saves the current queue-values for Bookmarks
  5669. #
  5670. ########################################################################################
  5671. sub SONOS_SaveBookmarkValues(;$$) {
  5672. my ($gKey, $type) = @_;
  5673. my $pathname = SONOS_Client_Data_Retreive('undef', 'attr', 'bookmarkSaveDir', '.');
  5674. SONOS_Log undef, 4, 'Calling SONOS_SaveBookmarkValues("'.(defined($gKey) ? $gKey : 'undef').'", "'.(defined($type) ? $type : 'undef').'") ~ SaveDir: "'.$pathname.'"';
  5675. my @types = ();
  5676. if (defined($type) && ($type ne '')) {
  5677. push(@types, $type);
  5678. } else {
  5679. @types = qw(Queue Title);
  5680. }
  5681. foreach my $type (@types) {
  5682. my @groups = ();
  5683. if (defined($gKey) && ($gKey ne '')) {
  5684. push(@groups, $gKey);
  5685. } else {
  5686. my $hashList = \%SONOS_BookmarkTitleHash;
  5687. $hashList = \%SONOS_BookmarkQueueHash if (lc($type) eq 'queue');
  5688. foreach my $group (keys %{$hashList}) {
  5689. push(@groups, $group);
  5690. }
  5691. }
  5692. foreach my $group (@groups) {
  5693. # ReadOnly-Gruppen niemals speichern
  5694. next if ((lc($type) eq 'queue') && defined($SONOS_BookmarkQueueDefinition{$group}{ReadOnly}) && $SONOS_BookmarkQueueDefinition{$group}{ReadOnly});
  5695. next if ((lc($type) ne 'queue') && defined($SONOS_BookmarkTitleDefinition{$group}{ReadOnly}) && $SONOS_BookmarkTitleDefinition{$group}{ReadOnly});
  5696. my $filename;
  5697. $filename = $pathname.'/SONOS_BookmarksPlaylists_'.$group.'.save' if (lc($type) eq 'queue');
  5698. $filename = $pathname.'/SONOS_BookmarksTitles_'.$group.'.save' if (lc($type) ne 'queue');
  5699. my $data;
  5700. $data = $SONOS_BookmarkQueueHash{$group} if (lc($type) eq 'queue');
  5701. $data = $SONOS_BookmarkTitleHash{$group} if (lc($type) ne 'queue');
  5702. if (defined($data)) {
  5703. eval {
  5704. open FILE, '>'.$filename;
  5705. binmode(FILE, ':encoding(utf-8)');
  5706. print FILE SONOS_Dumper($data);
  5707. close FILE;
  5708. SONOS_Log undef, 3, 'Successfully saved '.$type.'-Bookmarks of group "'.$group.'" to file "'.$filename.'"!';
  5709. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'SaveBookmarks: Success!');
  5710. };
  5711. if ($@) {
  5712. SONOS_Log undef, 2, 'Error during saving '.$type.'-Bookmarks of group "'.$group.'" to file "'.$filename.'": '.$@;
  5713. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'SaveBookmarks: Error! '.$@);
  5714. }
  5715. }
  5716. }
  5717. }
  5718. }
  5719. ########################################################################################
  5720. #
  5721. # SONOS_LoadBookmarkValues - Loads the current queue-values for Bookmarks
  5722. #
  5723. ########################################################################################
  5724. sub SONOS_LoadBookmarkValues(;$$) {
  5725. my ($gKey, $type) = @_;
  5726. my $pathname = SONOS_Client_Data_Retreive('undef', 'attr', 'bookmarkSaveDir', '.');
  5727. SONOS_Log undef, 4, 'Calling SONOS_LoadBookmarkValues("'.(defined($gKey) ? $gKey : 'undef').'", "'.(defined($type) ? $type : 'undef').'") ~ SaveDir: "'.$pathname.'"';
  5728. my @types = ();
  5729. if (defined($type) && ($type ne '')) {
  5730. push(@types, $type);
  5731. } else {
  5732. @types = qw(Queue Title);
  5733. }
  5734. foreach my $type (@types) {
  5735. my @groups = ();
  5736. if (defined($gKey) && ($gKey ne '')) {
  5737. push(@groups, $gKey);
  5738. } else {
  5739. my $hashList = \%SONOS_BookmarkTitleDefinition;
  5740. $hashList = \%SONOS_BookmarkQueueDefinition if (lc($type) eq 'queue');
  5741. foreach my $group (keys %{$hashList}) {
  5742. push(@groups, $group);
  5743. }
  5744. }
  5745. foreach my $group (@groups) {
  5746. my $filename;
  5747. $filename = $pathname.'/SONOS_BookmarksPlaylists_'.$group.'.save' if (lc($type) eq 'queue');
  5748. $filename = $pathname.'/SONOS_BookmarksTitles_'.$group.'.save' if (lc($type) ne 'queue');
  5749. eval {
  5750. if (open FILE, '<'.$filename) {
  5751. binmode(FILE, ':encoding(utf-8)');
  5752. my $fileInhalt = '';
  5753. while (<FILE>) {
  5754. $fileInhalt .= $_;
  5755. }
  5756. close FILE;
  5757. $SONOS_BookmarkQueueHash{$group} = eval($fileInhalt) if (lc($type) eq 'queue');
  5758. $SONOS_BookmarkTitleHash{$group} = eval($fileInhalt) if (lc($type) ne 'queue');
  5759. SONOS_Log undef, 3, 'Successfully loaded '.$type.'-Bookmarks of group "'.$group.'" from file "'.$filename.'"!';
  5760. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'LoadBookmarks: Group "'.$group.'" Success!');
  5761. }
  5762. };
  5763. if ($@) {
  5764. SONOS_Log undef, 2, 'Error during loading '.$type.'-Bookmarks of group "'.$group.'" from file "'.$filename.'": '.$@;
  5765. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'LoadBookmarks: Group "'.$group.'" Error! '.$@);
  5766. }
  5767. }
  5768. }
  5769. }
  5770. ########################################################################################
  5771. #
  5772. # SONOS_CalculateQueueHash - Calculates the Hash over all Queue members and jumps to the saved position
  5773. #
  5774. ########################################################################################
  5775. sub SONOS_CalculateQueueHash($) {
  5776. my ($udn) = @_;
  5777. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  5778. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  5779. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '');
  5780. my $tmp = $result->getValue('Result');
  5781. my $numberReturned = $result->getValue('NumberReturned');
  5782. my $totalMatches = $result->getValue('TotalMatches');
  5783. while ($numberReturned < $totalMatches) {
  5784. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $numberReturned, 0, '');
  5785. $tmp .= $result->getValue('Result');
  5786. $numberReturned += $result->getValue('NumberReturned');
  5787. $totalMatches = $result->getValue('TotalMatches');
  5788. }
  5789. my $hashIn = $totalMatches.':';
  5790. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  5791. my $item = $2;
  5792. my $uri = $1 if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  5793. $uri =~ s/&apos;/'/gi;
  5794. $hashIn .= $uri.':';
  5795. }
  5796. # Neuen Hashwert berechnen
  5797. my $newHash = md5_hex($hashIn);
  5798. # Werte aktualisieren
  5799. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'QueueHash', $newHash);
  5800. # Aktuellen Track ermitteln...
  5801. my $newTrack = 0;
  5802. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5803. $newTrack = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track');
  5804. }
  5805. # Soll was getan werden?
  5806. foreach my $gKey (SONOS_getBookmarkGroupKeys('Queue', $udn)) {
  5807. if (defined($SONOS_BookmarkQueueHash{$gKey}{$newHash}) && SONOS_getBookmarkQueueIsRelevant($gKey, $newHash, scalar(gettimeofday()), $totalMatches)) {
  5808. $newTrack = $SONOS_BookmarkQueueHash{$gKey}{$newHash}{Track};
  5809. # Hier muss jetzt die gespeicherte Position angesprungen werden...
  5810. if (($SONOS_BookmarkSpeicher{OldTracks}{$udn} != $newTrack) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5811. my $result = $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', $newTrack);
  5812. SONOS_Log $udn, 3, 'Player "'.SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn).'" jumped to the bookmarked track #'.$newTrack.' (Group "'.$gKey.'") ~ Bookmarkdata: '.SONOS_Dumper($SONOS_BookmarkQueueHash{$gKey}{$newHash});
  5813. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'JumpToTrack #'.$newTrack.': '.SONOS_UPnPAnswerMessage($result));
  5814. last; # Nur den ersten gültigen Eintrag suchen/ausführen...
  5815. }
  5816. }
  5817. }
  5818. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = $newTrack;
  5819. $SONOS_BookmarkSpeicher{NumTracks}{$udn} = $totalMatches;
  5820. }
  5821. }
  5822. ########################################################################################
  5823. #
  5824. # SONOS_RefreshCurrentBookmarkQueueValues - Saves the current queue-values for Bookmarks
  5825. #
  5826. ########################################################################################
  5827. sub SONOS_RefreshCurrentBookmarkQueueValues($) {
  5828. my ($udn) = @_;
  5829. # Aktuelle Werte im Speicher sicherstellen...
  5830. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTracks}{$udn}));
  5831. $SONOS_BookmarkSpeicher{NumTracks}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{NumTracks}{$udn}));
  5832. $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} = '' if (!defined($SONOS_BookmarkSpeicher{OldTrackURIs}{$udn}));
  5833. $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTrackPositions}{$udn}));
  5834. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn}));
  5835. $SONOS_BookmarkSpeicher{OldTransportstate}{$udn} = 'STOPPED' if (!defined($SONOS_BookmarkSpeicher{OldTransportstate}{$udn}));
  5836. $SONOS_BookmarkSpeicher{OldTimestamp}{$udn} = scalar(gettimeofday()) if (!defined($SONOS_BookmarkSpeicher{OldTimestamp}{$udn}));
  5837. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = '' if (!defined($SONOS_BookmarkSpeicher{OldTitles}{$udn}));
  5838. # Große Logausgabe fürs debugging...
  5839. SONOS_Log $udn, 5, '___________________________________________________________________________';
  5840. SONOS_Log $udn, 5, 'OldTracks: '.$SONOS_BookmarkSpeicher{OldTracks}{$udn};
  5841. SONOS_Log $udn, 5, 'NumTracks: '.$SONOS_BookmarkSpeicher{NumTracks}{$udn};
  5842. SONOS_Log $udn, 5, 'OldTrackURIs: '.$SONOS_BookmarkSpeicher{OldTrackURIs}{$udn};
  5843. SONOS_Log $udn, 5, 'OldTrackPositions: '.$SONOS_BookmarkSpeicher{OldTrackPositions}{$udn};
  5844. SONOS_Log $udn, 5, 'OldTrackDurations: '.$SONOS_BookmarkSpeicher{OldTrackDurations}{$udn};
  5845. SONOS_Log $udn, 5, 'OldTransportstate: '.$SONOS_BookmarkSpeicher{OldTransportstate}{$udn};
  5846. SONOS_Log $udn, 5, 'OldTimestamp: '.$SONOS_BookmarkSpeicher{OldTimestamp}{$udn};
  5847. SONOS_Log $udn, 5, 'OldTitle: '.$SONOS_BookmarkSpeicher{OldTitles}{$udn};
  5848. # Gemeinsamer Zeitstempel...
  5849. my $timestamp = scalar(gettimeofday());
  5850. # Aktuelle Werte für Title sichern...
  5851. my $trackURI = $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn};
  5852. if ($trackURI) {
  5853. foreach my $gKey (SONOS_getBookmarkGroupKeys('Title', $udn)) {
  5854. next if ($SONOS_BookmarkTitleDefinition{$gKey}{Chapter});
  5855. # Passt der Titel zum RegEx-Filter?
  5856. if ($trackURI !~ m/$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}/) {
  5857. SONOS_Log $udn, 5, 'Skipped Title because of no match to m/'.$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}.'/';
  5858. delete($SONOS_BookmarkTitleHash{$gKey}{$trackURI});
  5859. next;
  5860. }
  5861. # Config-Parameter ausgeben...
  5862. SONOS_Log $udn, 5, 'Match Title! Defined group "'.$gKey.'" ~ RemainingLength: '.$SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength}.' ~ MinTitleLength: '.$SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength};
  5863. # U.u. eine Trackposition berechnen...
  5864. my $trackPosition = $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn};
  5865. $trackPosition = ($timestamp - $SONOS_BookmarkSpeicher{OldTimestamp}{$udn}) if ($SONOS_BookmarkSpeicher{OldTransportstate}{$udn} eq 'PLAYING');
  5866. SONOS_Log $udn, 5, 'Used TrackPosition: '.SONOS_ConvertSecondsToTime($trackPosition);
  5867. # Wenn der Titel bereits im Bereich der RemainingTime ist oder die Mindestgröße unterschreitet, dann aus den Bookmarks löschen
  5868. if (($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} - $trackPosition <= $SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength})
  5869. || ($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} < $SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength})) {
  5870. delete($SONOS_BookmarkTitleHash{$gKey}{$trackURI});
  5871. next;
  5872. }
  5873. # Sonst die Werte aktualisieren/hinzufügen...
  5874. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{TrackPosition} = $trackPosition;
  5875. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastAccess} = $timestamp;
  5876. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastPlayer} = $udn;
  5877. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{Title} = $SONOS_BookmarkSpeicher{OldTitles}{$udn}.' - Position 1';
  5878. }
  5879. }
  5880. # Aktuelle Werte für Queue sichern...
  5881. my $listHash = SONOS_Client_Data_Retreive($udn, 'reading', 'QueueHash', '');
  5882. if ($listHash) {
  5883. foreach my $gKey (SONOS_getBookmarkGroupKeys('Queue', $udn)) {
  5884. if (SONOS_getBookmarkQueueIsRelevant($gKey, $listHash, undef, $SONOS_BookmarkSpeicher{NumTracks}{$udn})) {
  5885. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{Track} = $SONOS_BookmarkSpeicher{OldTracks}{$udn};
  5886. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastAccess} = $timestamp;
  5887. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastPlayer} = $udn;
  5888. $SONOS_BookmarkQueueHash{$gKey}{$trackURI}{Title} = $SONOS_BookmarkSpeicher{OldTitles}{$udn};
  5889. }
  5890. }
  5891. }
  5892. SONOS_Log $udn, 5, '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~';
  5893. }
  5894. ########################################################################################
  5895. #
  5896. # SONOS_getBookmarkQueueRelevant - Decide wether or not this Bookmark is relevant
  5897. #
  5898. ########################################################################################
  5899. sub SONOS_getBookmarkQueueIsRelevant($$$$) {
  5900. my ($gKey, $listHash, $timestamp, $listLength) = @_;
  5901. return ((!defined($timestamp) || ($SONOS_BookmarkQueueDefinition{$gKey}{MaxAge} >= ($timestamp - $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastAccess})))
  5902. && ($SONOS_BookmarkQueueDefinition{$gKey}{MinListLength} <= $listLength)
  5903. && ($SONOS_BookmarkQueueDefinition{$gKey}{MaxListLength} >= $listLength));
  5904. }
  5905. ########################################################################################
  5906. #
  5907. # SONOS_getBookmarkTitleRelevant - Decide wether or not this Bookmark is relevant
  5908. #
  5909. ########################################################################################
  5910. sub SONOS_getBookmarkTitleIsRelevant($$$$$) {
  5911. my ($gKey, $timestamp, $trackURI, $titlePosition, $titleLength) = @_;
  5912. return 0 if ($SONOS_BookmarkTitleDefinition{$gKey}{Chapter});
  5913. return (($trackURI =~ m/$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}/)
  5914. && (!defined($timestamp) || ($SONOS_BookmarkTitleDefinition{$gKey}{MaxAge} >= ($timestamp - $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastAccess})))
  5915. && ($SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength} <= $titleLength)
  5916. && ($SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength} <= ($titleLength - $titlePosition)));
  5917. }
  5918. ########################################################################################
  5919. #
  5920. # SONOS_AddToButtonQueue - Adds the given Event-Name to the ButtonQueue
  5921. #
  5922. ########################################################################################
  5923. sub SONOS_AddToButtonQueue($$) {
  5924. my ($udn, $event) = @_;
  5925. my $data = {Action => uc($event), Time => time()};
  5926. $SONOS_ButtonPressQueue{$udn}->enqueue($data);
  5927. }
  5928. ########################################################################################
  5929. #
  5930. # SONOS_CheckButtonQueue - Checks ButtonQueue and triggers events if neccessary
  5931. #
  5932. ########################################################################################
  5933. sub SONOS_CheckButtonQueue($) {
  5934. my ($udn) = @_;
  5935. my $eventDefinitions = SONOS_Client_Data_Retreive($udn, 'attr', 'buttonEvents', '');
  5936. # Wenn keine Events definiert wurden, dann Queue einfach leeren und zurückkehren...
  5937. # Das beschleunigt die Verarbeitung, da im allgemeinen keine (oder eher wenig) Events definiert werden.
  5938. if (!$eventDefinitions) {
  5939. $SONOS_ButtonPressQueue{$udn}->dequeue_nb(10); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen...
  5940. return;
  5941. }
  5942. my $maxElems = 0;
  5943. while ($eventDefinitions =~ m/(\d+):([MHUD]+)/g) {
  5944. $maxElems = SONOS_Max($maxElems, length($2));
  5945. # Sind überhaupt ausreichend Events in der Queue, das dieses ButtonEvent ausgefüllt sein könnte?
  5946. my $ok = $SONOS_ButtonPressQueue{$udn}->pending() >= length($2);
  5947. # Prüfen, ob alle Events in der Queue der Reihenfolge des ButtonEvents entsprechen
  5948. if ($ok) {
  5949. for (my $i = 0; $i < length($2); $i++) {
  5950. if ($SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2) + $i)->{Action} ne substr($2, $i, 1)) {
  5951. $ok = 0;
  5952. }
  5953. }
  5954. }
  5955. # Wenn die Kette stimmt, dann hier prüfen, ob die Maximalzeit eingehalten wurde, und dann u.U. das Event werfen...
  5956. if ($ok) {
  5957. if (time() - $SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2))->{Time} <= $1) {
  5958. # Event here...
  5959. SONOS_Log $udn, 3, 'Generating ButtonEvent for Zone "'.$udn.'": '.$2.'.';
  5960. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'ButtonEvent', $2);
  5961. }
  5962. }
  5963. }
  5964. # Einträge, die "zu viele Elemente" her sind, wieder entfernen, da diese sowieso keine Berücksichtigung mehr finden werden
  5965. if ($SONOS_ButtonPressQueue{$udn}->pending() > $maxElems) {
  5966. $SONOS_ButtonPressQueue{$udn}->extract(0, $SONOS_ButtonPressQueue{$udn}->pending() - $maxElems); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen...
  5967. }
  5968. }
  5969. ########################################################################################
  5970. #
  5971. # SONOS_AlarmCallback - Alarm-Callback,
  5972. #
  5973. # Parameter $service = Service-Representing Object
  5974. # $properties = Properties, that have been changed in this event
  5975. #
  5976. ########################################################################################
  5977. sub SONOS_AlarmCallback($$) {
  5978. my ($service, %properties) = @_;
  5979. my $udn = $SONOS_Locations{$service->base};
  5980. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5981. if (!$udn) {
  5982. SONOS_Log undef, 1, 'Alarm-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  5983. return;
  5984. }
  5985. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5986. # If the Device is disabled, return here...
  5987. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5988. SONOS_Log $udn, 3, "Alarm-Event: device '$name' disabled. No Events/Data will be processed!";
  5989. return;
  5990. }
  5991. SONOS_Log $udn, 3, 'Event: Received Alarm-Event for Zone "'.$name.'".';
  5992. $SONOS_Client_SendQueue_Suspend = 1;
  5993. # Check if the correct ServiceType
  5994. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AlarmClock:1') {
  5995. SONOS_Log $udn, 1, 'Alarm-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5996. return;
  5997. }
  5998. # Check if the Variable called AlarmListVersion or DailyIndexRefreshTime exists
  5999. if (!defined($properties{AlarmListVersion}) && !defined($properties{DailyIndexRefreshTime})) {
  6000. return;
  6001. }
  6002. SONOS_Log $udn, 4, "Alarm-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6003. # If a new AlarmListVersion is available
  6004. my $alarmListVersion = SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListVersion', '~~');
  6005. if (defined($properties{AlarmListVersion}) && ($properties{AlarmListVersion} ne $alarmListVersion)) {
  6006. SONOS_Log $udn, 4, 'Set new Alarm-Data';
  6007. # Retrieve new AlarmList
  6008. my $result = $SONOS_AlarmClockControlProxy{$udn}->ListAlarms();
  6009. my $currentAlarmList = $result->getValue('CurrentAlarmList');
  6010. my %alarms = ();
  6011. my @alarmIDs = ();
  6012. while ($currentAlarmList =~ m/<Alarm (.*?)\/>/gi) {
  6013. my $alarm = $1;
  6014. # Nur die Alarme, die auch für diesen Raum gelten, reinholen...
  6015. if ($alarm =~ m/RoomUUID="$udnShort"/i) {
  6016. my $id = $1 if ($alarm =~ m/ID="(\d+)"/i);
  6017. SONOS_Log $udn, 5, 'Alarm-Event: Alarm: '.SONOS_Stringify($alarm);
  6018. push @alarmIDs, $id;
  6019. $alarms{$id}{StartTime} = $1 if ($alarm =~ m/StartTime="(.*?)"/i);
  6020. $alarms{$id}{Duration} = $1 if ($alarm =~ m/Duration="(.*?)"/i);
  6021. $alarms{$id}{Recurrence_Once} = 0;
  6022. $alarms{$id}{Recurrence_Monday} = 0;
  6023. $alarms{$id}{Recurrence_Tuesday} = 0;
  6024. $alarms{$id}{Recurrence_Wednesday} = 0;
  6025. $alarms{$id}{Recurrence_Thursday} = 0;
  6026. $alarms{$id}{Recurrence_Friday} = 0;
  6027. $alarms{$id}{Recurrence_Saturday} = 0;
  6028. $alarms{$id}{Recurrence_Sunday} = 0;
  6029. $alarms{$id}{Enabled} = $1 if ($alarm =~ m/Enabled="(.*?)"/i);
  6030. $alarms{$id}{RoomUUID} = $1 if ($alarm =~ m/RoomUUID="(.*?)"/i);
  6031. $alarms{$id}{ProgramURI} = decode_entities($1) if ($alarm =~ m/ProgramURI="(.*?)"/i);
  6032. $alarms{$id}{ProgramMetaData} = decode_entities($1) if ($alarm =~ m/ProgramMetaData="(.*?)"/i);
  6033. $alarms{$id}{Shuffle} = 0;
  6034. $alarms{$id}{Repeat} = 0;
  6035. $alarms{$id}{Volume} = $1 if ($alarm =~ m/Volume="(.*?)"/i);
  6036. $alarms{$id}{IncludeLinkedZones} = $1 if ($alarm =~ m/IncludeLinkedZones="(.*?)"/i);
  6037. # PlayMode ermitteln...
  6038. my $currentPlayMode = 'NORMAL';
  6039. $currentPlayMode = $1 if ($alarm =~ m/PlayMode="(.*?)"/i);
  6040. $alarms{$id}{Shuffle} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'SHUFFLE_NOREPEAT');
  6041. $alarms{$id}{Repeat} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'REPEAT_ALL');
  6042. # Recurrence ermitteln...
  6043. my $currentRecurrence = $1 if ($alarm =~ m/Recurrence="(.*?)"/i);
  6044. $alarms{$id}{Recurrence_Once} = 1 if ($currentRecurrence eq 'ONCE');
  6045. $alarms{$id}{Recurrence_Sunday} = 1 if (($currentRecurrence =~ m/^ON_\d*?0/i) || ($currentRecurrence =~ m/^WEEKENDS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6046. $alarms{$id}{Recurrence_Monday} = 1 if (($currentRecurrence =~ m/^ON_\d*?1/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6047. $alarms{$id}{Recurrence_Tuesday} = 1 if (($currentRecurrence =~ m/^ON_\d*?2/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6048. $alarms{$id}{Recurrence_Wednesday} = 1 if (($currentRecurrence =~ m/^ON_\d*?3/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6049. $alarms{$id}{Recurrence_Thursday} = 1 if (($currentRecurrence =~ m/^ON_\d*?4/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6050. $alarms{$id}{Recurrence_Friday} = 1 if (($currentRecurrence =~ m/^ON_\d*?5/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6051. $alarms{$id}{Recurrence_Saturday} = 1 if (($currentRecurrence =~ m/^ON_\d*?6/i) || ($currentRecurrence =~ m/^WEEKENDS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6052. SONOS_Log $udn, 5, 'Alarm-Event: Alarm-Decoded: '.SONOS_Stringify(\%alarms);
  6053. }
  6054. }
  6055. # Sets the approbriate Readings-Value
  6056. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  6057. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmList', SONOS_Dumper(\%alarms));
  6058. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListIDs', join(',', @alarmIDs));
  6059. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListVersion', $result->getValue('CurrentAlarmListVersion'));
  6060. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  6061. }
  6062. if (defined($properties{DailyIndexRefreshTime})) {
  6063. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'DailyIndexRefreshTime', $properties{DailyIndexRefreshTime});
  6064. }
  6065. $SONOS_Client_SendQueue_Suspend = 0;
  6066. SONOS_Log $udn, 3, 'Event: End of Alarm-Event for Zone "'.$name.'".';
  6067. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6068. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6069. SONOS_Log $udn, 1, "Alarm-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6070. SONOS_RestartControlPoint();
  6071. }
  6072. return 0;
  6073. }
  6074. ########################################################################################
  6075. #
  6076. # SONOS_ZoneGroupTopologyCallback - ZoneGroupTopology-Callback,
  6077. #
  6078. # Parameter $service = Service-Representing Object
  6079. # $properties = Properties, that have been changed in this event
  6080. #
  6081. ########################################################################################
  6082. sub SONOS_ZoneGroupTopologyCallback($$) {
  6083. my ($service, %properties) = @_;
  6084. my $udn = $SONOS_Locations{$service->base};
  6085. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6086. if (!$udn) {
  6087. SONOS_Log undef, 1, 'ZoneGroupTopology-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  6088. return;
  6089. }
  6090. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6091. # If the Device is disabled, return here...
  6092. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6093. SONOS_Log $udn, 3, "ZoneGroupTopology-Event: device '$name' disabled. No Events/Data will be processed!";
  6094. return;
  6095. }
  6096. SONOS_Log $udn, 3, 'Event: Received ZoneGroupTopology-Event for Zone "'.$name.'".';
  6097. $SONOS_Client_SendQueue_Suspend = 1;
  6098. # Check if the correct ServiceType
  6099. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:ZoneGroupTopology:1') {
  6100. SONOS_Log $udn, 1, 'ZoneGroupTopology-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6101. return;
  6102. }
  6103. SONOS_Log $udn, 4, "ZoneGroupTopology-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6104. # ZoneGroupState: Gesamtkonstellation
  6105. my $zoneGroupState = '';
  6106. if ($properties{ZoneGroupState}) {
  6107. $zoneGroupState = decode_entities($1) if ($properties{ZoneGroupState} =~ m/(.*)/);
  6108. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', 'undef', 'ZoneGroupState', $zoneGroupState);
  6109. SONOS_AnalyzeTopologyForMasterPlayer($zoneGroupState);
  6110. }
  6111. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  6112. # ZonePlayerUUIDsInGroup: Welche Player befinden sich alle in der gleichen Gruppe wie ich?
  6113. my $zonePlayerUUIDsInGroup = SONOS_Client_Data_Retreive($udn, 'reading', 'ZonePlayerUUIDsInGroup', '');
  6114. if ($properties{ZonePlayerUUIDsInGroup}) {
  6115. $zonePlayerUUIDsInGroup = $properties{ZonePlayerUUIDsInGroup};
  6116. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZonePlayerUUIDsInGroup', $zonePlayerUUIDsInGroup);
  6117. }
  6118. # ZoneGroupID: Welcher Gruppe gehöre ich aktuell an, und hat sich meine Aufgabe innerhalb der Gruppe verändert?
  6119. my $zoneGroupID = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupID', '');
  6120. my $fieldType = SONOS_Client_Data_Retreive($udn, 'reading', 'fieldType', '');
  6121. if ($zoneGroupState =~ m/.*(<ZoneGroup Coordinator="(RINCON_[0-9a-f]+)".*?>).*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) {
  6122. $zoneGroupID = $2;
  6123. my $member = $3;
  6124. my $master = ($zoneGroupID eq $udnShort);
  6125. my $masterPlayerName = SONOS_Client_Data_Retreive($zoneGroupID.'_MR', 'def', 'NAME', $zoneGroupID.'_MR');
  6126. my @slavePlayerNames = SONOS_AnalyzeTopologyForSlavePlayer($udnShort, $zoneGroupState);
  6127. $zoneGroupID .= ':__' if ($zoneGroupID !~ m/:/);
  6128. my $topoType = '';
  6129. # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten?
  6130. if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) {
  6131. $topoType = '_'.$1;
  6132. }
  6133. # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten?
  6134. if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) {
  6135. $topoType = '_'.$1;
  6136. $topoType =~ s/,/_/g;
  6137. }
  6138. SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType;
  6139. if ($topoType ne '') {
  6140. $fieldType = substr($topoType, 1);
  6141. } else {
  6142. $fieldType = '';
  6143. }
  6144. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  6145. my $aliasSuffix = '';
  6146. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  6147. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  6148. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  6149. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  6150. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  6151. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  6152. my $roomName = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', '');
  6153. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  6154. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZoneGroupID', $zoneGroupID);
  6155. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'fieldType', $fieldType);
  6156. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsMaster', $master ? '1' : '0');
  6157. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'MasterPlayer', $masterPlayerName);
  6158. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayer', SONOS_Dumper(\@slavePlayerNames));
  6159. }
  6160. # ZoneGroupName: Welchen Namen hat die aktuelle Gruppe?
  6161. my $zoneGroupName = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupName', '');
  6162. if ($properties{ZoneGroupName}) {
  6163. $zoneGroupName = $properties{ZoneGroupName};
  6164. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZoneGroupName', $zoneGroupName);
  6165. }
  6166. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  6167. $SONOS_Client_SendQueue_Suspend = 0;
  6168. SONOS_Log $udn, 3, 'Event: End of ZoneGroupTopology-Event for Zone "'.$name.'".';
  6169. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6170. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6171. SONOS_Log $udn, 1, "ZoneGroupTopology-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6172. SONOS_RestartControlPoint();
  6173. }
  6174. return 0;
  6175. }
  6176. ########################################################################################
  6177. #
  6178. # SONOS_AnalyzeTopologyForSlavePlayer - Topology analysieren, um die Slaveplayer zu
  6179. # einem Masterplayer zu ermitteln
  6180. #
  6181. ########################################################################################
  6182. sub SONOS_AnalyzeTopologyForSlavePlayer($$) {
  6183. my ($masterUDNShort, $zoneGroupState) = @_;
  6184. my @slavePlayer = ();
  6185. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  6186. next if ($1 ne $masterUDNShort);
  6187. my $member = $2;
  6188. while ($member =~ m/<ZoneGroupMember.*?UUID="(.*?)".*?\/>/gi) {
  6189. next if ($1 eq $masterUDNShort); # Den Master selbst nicht in die Slaveliste reinpacken...
  6190. push @slavePlayer, SONOS_Client_Data_Retreive($1.'_MR', 'def', 'NAME', $1.'_MR');
  6191. }
  6192. }
  6193. return sort @slavePlayer;
  6194. }
  6195. ########################################################################################
  6196. #
  6197. # SONOS_AnalyzeTopologyForMasterPlayer - Topology analysieren, um das Reading "MasterPlayer"
  6198. # sowie die Readings "MasterPlayerPlaying" und
  6199. # "MasterPlayerNotPlaying" zu setzen.
  6200. #
  6201. ########################################################################################
  6202. sub SONOS_AnalyzeTopologyForMasterPlayer($) {
  6203. my ($zoneGroupState) = @_;
  6204. my @playing = ();
  6205. my @notplaying = ();
  6206. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  6207. my $udn = $1.'_MR';
  6208. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6209. if (defined($name)) {
  6210. my $transportState = SONOS_Client_Data_Retreive($udn, 'reading', 'TransportState', '-');
  6211. if ($transportState eq 'PLAYING') {
  6212. push(@playing, $name);
  6213. } else {
  6214. push(@notplaying, $name);
  6215. }
  6216. }
  6217. }
  6218. # Die Listen normalisieren
  6219. @playing = sort @playing;
  6220. @notplaying = sort @notplaying;
  6221. SONOS_Client_Notifier('ReadingsBeginUpdate:undef');
  6222. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerPlaying', SONOS_Dumper(\@playing));
  6223. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerPlayingCount', scalar(@playing));
  6224. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerNotPlaying', SONOS_Dumper(\@notplaying));
  6225. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerNotPlayingCount', scalar(@notplaying));
  6226. push(@playing, @notplaying);
  6227. @playing = sort @playing;
  6228. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayer', SONOS_Dumper(\@playing));
  6229. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerCount', scalar(@playing));
  6230. SONOS_Client_Notifier('ReadingsEndUpdate:undef');
  6231. }
  6232. ########################################################################################
  6233. #
  6234. # SONOS_DevicePropertiesCallback - DeviceProperties-Callback,
  6235. #
  6236. # Parameter $service = Service-Representing Object
  6237. # $properties = Properties, that have been changed in this event
  6238. #
  6239. ########################################################################################
  6240. sub SONOS_DevicePropertiesCallback($$) {
  6241. my ($service, %properties) = @_;
  6242. my $udn = $SONOS_Locations{$service->base};
  6243. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6244. if (!$udn) {
  6245. SONOS_Log undef, 1, 'DeviceProperties-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  6246. return;
  6247. }
  6248. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6249. # If the Device is disabled, return here...
  6250. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6251. SONOS_Log $udn, 3, "DeviceProperties-Event: device '$name' disabled. No Events/Data will be processed!";
  6252. return;
  6253. }
  6254. SONOS_Log $udn, 3, 'Event: Received DeviceProperties-Event for Zone "'.$name.'".';
  6255. $SONOS_Client_SendQueue_Suspend = 1;
  6256. # Check if the correct ServiceType
  6257. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:DeviceProperties:1') {
  6258. SONOS_Log $udn, 1, 'DeviceProperties-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6259. return;
  6260. }
  6261. SONOS_Log $udn, 4, "DeviceProperties-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6262. # Raumname wurde angepasst?
  6263. my $roomName = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', '');
  6264. if (defined($properties{ZoneName}) && $properties{ZoneName} ne '') {
  6265. $roomName = $properties{ZoneName};
  6266. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'roomName', $roomName);
  6267. my $saveRoomName = decode('UTF-8', $roomName);
  6268. eval {
  6269. use utf8;
  6270. $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  6271. };
  6272. $saveRoomName =~ s/[^a-zA-Z0-9_ ]//g;
  6273. $saveRoomName = SONOS_Trim($saveRoomName);
  6274. $saveRoomName =~ s/ /_/g;
  6275. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName);
  6276. my $topoType = '_'.SONOS_Client_Data_Retreive($udn, 'reading', 'fieldType', '');
  6277. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  6278. my $aliasSuffix = '';
  6279. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  6280. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  6281. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  6282. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  6283. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  6284. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  6285. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  6286. }
  6287. # Icon wurde angepasst?
  6288. my $roomIcon = SONOS_Client_Data_Retreive($udn, 'reading', 'roomIcon', '');
  6289. if (defined($properties{Icon}) && $properties{Icon} ne '') {
  6290. $properties{Icon} =~ s/.*?:(.*)/$1/i;
  6291. $roomIcon = $properties{Icon};
  6292. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'roomIcon', $roomIcon);
  6293. }
  6294. $SONOS_Client_SendQueue_Suspend = 0;
  6295. SONOS_Log $udn, 3, 'Event: End of DeviceProperties-Event for Zone "'.$name.'".';
  6296. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6297. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6298. SONOS_Log $udn, 1, "DeviceProperties-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6299. SONOS_RestartControlPoint();
  6300. }
  6301. return 0;
  6302. }
  6303. ########################################################################################
  6304. #
  6305. # SONOS_AudioInCallback - AudioIn-Callback,
  6306. #
  6307. # Parameter $service = Service-Representing Object
  6308. # $properties = Properties, that have been changed in this event
  6309. #
  6310. ########################################################################################
  6311. sub SONOS_AudioInCallback($$) {
  6312. my ($service, %properties) = @_;
  6313. my $udn = $SONOS_Locations{$service->base};
  6314. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6315. if (!$udn) {
  6316. SONOS_Log undef, 1, 'AudioIn-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  6317. return;
  6318. }
  6319. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6320. # If the Device is disabled, return here...
  6321. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6322. SONOS_Log $udn, 3, "AudioIn-Event: device '$name' disabled. No Events/Data will be processed!";
  6323. return;
  6324. }
  6325. SONOS_Log $udn, 3, 'Event: Received AudioIn-Event for Zone "'.$name.'".';
  6326. $SONOS_Client_SendQueue_Suspend = 1;
  6327. # Check if the correct ServiceType
  6328. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AudioIn:1') {
  6329. SONOS_Log $udn, 1, 'AudioIn-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6330. return;
  6331. }
  6332. SONOS_Log $udn, 4, "AudioIn-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6333. # LineInConnected wurde angepasst?
  6334. my $lineInConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'LineInConnected', '');
  6335. if (defined($properties{LineInConnected}) && $properties{LineInConnected} ne '') {
  6336. $lineInConnected = $properties{LineInConnected};
  6337. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'LineInConnected', $lineInConnected);
  6338. }
  6339. $SONOS_Client_SendQueue_Suspend = 0;
  6340. SONOS_Log $udn, 3, 'Event: End of AudioIn-Event for Zone "'.$name.'".';
  6341. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6342. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6343. SONOS_Log $udn, 1, "AudioIn-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6344. SONOS_RestartControlPoint();
  6345. }
  6346. return 0;
  6347. }
  6348. ########################################################################################
  6349. #
  6350. # SONOS_replaceSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal
  6351. #
  6352. # Parameter text = The text, inside that has to be searched and replaced
  6353. #
  6354. ########################################################################################
  6355. sub SONOS_replaceSpecialStringCharacters($) {
  6356. my ($text) = @_;
  6357. $text =~ s/"/'/g;
  6358. return $text;
  6359. }
  6360. ########################################################################################
  6361. #
  6362. # SONOS_maskSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal
  6363. #
  6364. # Parameter text = The text, inside that has to be searched and replaced
  6365. #
  6366. ########################################################################################
  6367. sub SONOS_maskSpecialStringCharacters($) {
  6368. my ($text) = @_;
  6369. $text =~ s/"/\\"/g;
  6370. return $text;
  6371. }
  6372. ########################################################################################
  6373. #
  6374. # SONOS_ProcessInfoSummarize - Process the InfoSummarize-Fields (XML-Alike Structure)
  6375. # Example for Minimal neccesary structure:
  6376. # <NormalAudio></NormalAudio> <StreamAudio></StreamAudio>
  6377. #
  6378. # Complex Example:
  6379. # <NormalAudio><Artist prefix="(" suffix=")"/><Title prefix=" '" suffix="'" ifempty="[Keine Musikdatei]"/><Album prefix=" vom Album '" suffix="'"/></NormalAudio> <StreamAudio><Sender suffix=":"/><SenderCurrent prefix=" '" suffix="'"/><SenderInfo prefix=" - "/></StreamAudio>
  6380. # OR
  6381. # <NormalAudio><TransportState/><InfoSummarize1 prefix=" => "/></NormalAudio> <StreamAudio><TransportState/><InfoSummarize1 prefix=" => "/></StreamAudio>
  6382. #
  6383. # Parameter name = The name of the SonosPlayer-Device
  6384. # current = The Current-Values hashset
  6385. # summarizeVariableName = The variable-name to process (e.g. "InfoSummarize1")
  6386. #
  6387. ########################################################################################
  6388. sub SONOS_ProcessInfoSummarize($$$$) {
  6389. my ($hash, $current, $summarizeVariableName, $bulkUpdate) = @_;
  6390. if (($current->{$summarizeVariableName} = AttrVal($hash->{NAME}, 'generate'.$summarizeVariableName, '')) ne '') {
  6391. # Only pick up the current Audio-Type-Part, if one is available...
  6392. if ($current->{NormalAudio}) {
  6393. $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<NormalAudio>(.*?)<\/NormalAudio>/i);
  6394. } else {
  6395. $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<StreamAudio>(.*?)<\/StreamAudio>/i);
  6396. }
  6397. # Replace placeholder with variables (list defined in 21_SONOSPLAYER ~ stateVariable)
  6398. my $availableVariables = ($2) if (getAllAttr($hash->{NAME}) =~ m/(^|\s+)stateVariable:(.*?)(\s+|$)/);
  6399. foreach (split(/,/, $availableVariables)) {
  6400. $current->{$summarizeVariableName} = SONOS_ReplaceTextToken($current->{$summarizeVariableName}, $_, $current->{$_});
  6401. }
  6402. if ($bulkUpdate) {
  6403. # Enqueue the event
  6404. SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName});
  6405. } else {
  6406. SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName}, 1);
  6407. }
  6408. } else {
  6409. if ($bulkUpdate) {
  6410. # Enqueue the event
  6411. SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), '');
  6412. } else {
  6413. SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), '', 1);
  6414. }
  6415. }
  6416. }
  6417. ########################################################################################
  6418. #
  6419. # SONOS_ReplaceTextToken - Search and replace any occurency of the given tokenName with the value of tokenValue
  6420. #
  6421. # Parameter text = The text, inside that has to be searched and replaced
  6422. # tokenName = The name, that has to be searched for
  6423. # tokenValue = The value, the has to be insert instead of tokenName
  6424. #
  6425. ########################################################################################
  6426. sub SONOS_ReplaceTextToken($$$) {
  6427. my ($text, $tokenName, $tokenValue) = @_;
  6428. # Hier das Token mit Prefix, Suffix, Instead und IfEmpty ersetzen, wenn entsprechend vorhanden
  6429. $text =~ s/<\s*?$tokenName(\s.*?\/|\/)>/SONOS_ReplaceTextTokenRegReplacer($tokenValue, $1)/eig;
  6430. return $text;
  6431. }
  6432. ########################################################################################
  6433. #
  6434. # SONOS_ReplaceTextTokenRegReplacer - Internal procedure for replacing TagValues
  6435. #
  6436. # Parameter tokenValue = The value, the has to be insert instead of tokenName
  6437. # $matcher = The values of the searched and found tag
  6438. #
  6439. ########################################################################################
  6440. sub SONOS_ReplaceTextTokenRegReplacer($$) {
  6441. my ($tokenValue, $matcher) = @_;
  6442. my $emptyVal = SONOS_DealToken($matcher, 'emptyVal', '');
  6443. return SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'prefix', ''), $emptyVal).
  6444. SONOS_ReturnIfEmpty($tokenValue, SONOS_DealToken($matcher, 'ifempty', $emptyVal), $emptyVal).
  6445. SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'instead', $tokenValue), $emptyVal).
  6446. SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'suffix', ''), $emptyVal);
  6447. }
  6448. ########################################################################################
  6449. #
  6450. # SONOS_DealToken - Extracts the content of the given tokenName if exist in checkText
  6451. #
  6452. # Parameter checkText = The text, that has to be search in
  6453. # tokenName = The value, of which the content has to be returned
  6454. #
  6455. ########################################################################################
  6456. sub SONOS_DealToken($$$) {
  6457. my ($checkText, $tokenName, $emptyVal) = @_;
  6458. my $returnText = $1 if($checkText =~ m/$tokenName\s*=\s*"(.*?)"/i);
  6459. return $emptyVal if (not defined($returnText));
  6460. return $returnText;
  6461. }
  6462. ########################################################################################
  6463. #
  6464. # SONOS_ReturnIfEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is* empty
  6465. #
  6466. # Parameter checkText = The text, that has to be checked
  6467. # returnValue = The value, the has to be returned
  6468. #
  6469. ########################################################################################
  6470. sub SONOS_ReturnIfEmpty($$$) {
  6471. my ($checkText, $returnValue, $emptyVal) = @_;
  6472. return '' if not defined($returnValue);
  6473. return $returnValue if ((not defined($checkText)) || $checkText eq $emptyVal);
  6474. return '';
  6475. }
  6476. ########################################################################################
  6477. #
  6478. # SONOS_ReturnIfNotEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is NOT* empty
  6479. #
  6480. # Parameter checkText = The text, that has to be checked
  6481. # returnValue = The value, the has to be returned
  6482. #
  6483. ########################################################################################
  6484. sub SONOS_ReturnIfNotEmpty($$$) {
  6485. my ($checkText, $returnValue, $emptyVal) = @_;
  6486. return '' if not defined($returnValue);
  6487. return $returnValue if (defined($checkText) && $checkText ne $emptyVal);
  6488. return '';
  6489. }
  6490. ########################################################################################
  6491. #
  6492. # SONOS_ImageDownloadTypeExtension - Gives the appropriate extension for the retrieved mimetype of the content of the given url
  6493. #
  6494. # Parameter url = The URL of the content
  6495. #
  6496. ########################################################################################
  6497. sub SONOS_ImageDownloadTypeExtension($) {
  6498. my ($url) = @_;
  6499. # Wenn Spotify, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  6500. if ($url =~ m/x-sonos-spotify/) {
  6501. return 'jpg';
  6502. }
  6503. # Wenn Napster, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  6504. if ($url =~ m/npsdy/) {
  6505. return 'jpg';
  6506. }
  6507. # Wenn Radio, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer GIF
  6508. if ($url =~ m/x-sonosapi-stream/) {
  6509. return 'gif';
  6510. }
  6511. # Wenn Google Music oder Simfy, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  6512. if ($url =~ m/x-sonos-http/) {
  6513. return 'jpg';
  6514. }
  6515. # Server abfragen
  6516. my ($content_type, $document_length, $modified_time, $expires, $server) = head($url);
  6517. return 'ERROR' if (!defined($content_type) || ($content_type =~ m/<head>.*?<\/head>/));
  6518. if ($content_type =~ m/png/) {
  6519. return 'png';
  6520. } elsif (($content_type =~ m/jpeg/) || ($content_type =~ m/jpg/)) {
  6521. return 'jpg';
  6522. } elsif ($content_type =~ m/gif/) {
  6523. return 'gif';
  6524. } else {
  6525. $content_type =~ s/\//-/g;
  6526. return $content_type;
  6527. }
  6528. }
  6529. ########################################################################################
  6530. #
  6531. # SONOS_ImageDownloadMimeType - Retrieves the mimetype of the content of the given url
  6532. #
  6533. # Parameter url = The URL of the content
  6534. #
  6535. ########################################################################################
  6536. sub SONOS_ImageDownloadMimeType($) {
  6537. my ($url) = @_;
  6538. my ($content_type, $document_length, $modified_time, $expires, $server) = head($url);
  6539. return $content_type;
  6540. }
  6541. ########################################################################################
  6542. #
  6543. # SONOS_DownloadReplaceIfChanged - Overwrites the file only if its changed
  6544. #
  6545. # Parameter url = The URL of the new file
  6546. # dest = The local file-uri of the old file
  6547. #
  6548. # Return 1 = New file have been written
  6549. # 0 = nothing happened, because the filecontents are identical or an error has occurred
  6550. #
  6551. ########################################################################################
  6552. sub SONOS_DownloadReplaceIfChanged($$) {
  6553. my ($url, $dest) = @_;
  6554. SONOS_Log undef, 5, 'Call of SONOS_DownloadReplaceIfChanged("'.$url.'", "'.$dest.'")';
  6555. # Be sure URL is absolute
  6556. return 0 if ($url !~ m/^http:\/\//i);
  6557. # Reading new file
  6558. my $newFile = '';
  6559. eval {
  6560. $newFile = get $url;
  6561. if (not defined($newFile)) {
  6562. SONOS_Log undef, 4, 'Couldn\'t retrieve file "'.$url.'" via web. Trying to copy directly...';
  6563. $newFile = SONOS_ReadFile($url);
  6564. if (not defined($newFile)) {
  6565. SONOS_Log undef, 4, 'Couldn\'t even copy file "'.$url.'" directly... exiting...';
  6566. return 0;
  6567. }
  6568. }
  6569. };
  6570. if ($@) {
  6571. SONOS_Log undef, 2, 'Error during SONOS_DownloadReplaceIfChanged("'.$url.'", "'.$dest.'"): '.$@;
  6572. return 0;
  6573. }
  6574. # Wenn keine neue Datei ermittelt wurde, dann abbrechen...
  6575. return 0 if (!defined($newFile) || ($newFile eq ''));
  6576. # Reading old file (if it exists)
  6577. my $oldFile = SONOS_ReadFile($dest);
  6578. $oldFile = '' if (!defined($oldFile));
  6579. # compare those files, and overwrite old file, if it has to be changed
  6580. if ($newFile ne $oldFile) {
  6581. # Hier jetzt alle Dateien dieses Players entfernen, damit nichts überflüssiges rumliegt, falls sich die Endung geändert haben sollte
  6582. if (($dest =~ m/(.*\.).*?/) && ($1 ne '')) {
  6583. unlink(<$1*>);
  6584. }
  6585. # Hier jetzt die neue Datei herunterladen
  6586. SONOS_Log undef, 4, "New filecontent for '$dest'!";
  6587. if (defined(open IMGFILE, '>'.$dest)) {
  6588. binmode IMGFILE ;
  6589. print IMGFILE $newFile;
  6590. close IMGFILE;
  6591. } else {
  6592. SONOS_Log undef, 1, "Error creating file $dest";
  6593. }
  6594. return 1;
  6595. } else {
  6596. SONOS_Log undef, 4, "Identical filecontent for '$dest'!";
  6597. return 0;
  6598. }
  6599. }
  6600. ########################################################################################
  6601. #
  6602. # SONOS_ReadFile - Read the content of the given filename
  6603. #
  6604. # Parameter $fileName = The filename, that has to be read
  6605. #
  6606. ########################################################################################
  6607. sub SONOS_ReadFile($) {
  6608. my ($fileName) = @_;
  6609. if (-e $fileName) {
  6610. my $fileContent = '';
  6611. open IMGFILE, '<'.$fileName;
  6612. binmode IMGFILE;
  6613. while (<IMGFILE>){
  6614. $fileContent .= $_;
  6615. }
  6616. close IMGFILE;
  6617. return $fileContent;
  6618. }
  6619. return undef;
  6620. }
  6621. ########################################################################################
  6622. #
  6623. # SONOS_WriteFile - Write the content to the given filename
  6624. #
  6625. # Parameter $fileName = The filename, that has to be read
  6626. #
  6627. ########################################################################################
  6628. sub SONOS_WriteFile($$) {
  6629. my ($fileName, $data) = @_;
  6630. open IMGFILE, '>'.$fileName;
  6631. binmode IMGFILE;
  6632. print IMGFILE $data;
  6633. close IMGFILE;
  6634. }
  6635. ########################################################################################
  6636. #
  6637. # SONOS_readingsBulkUpdateIfChanged - Wrapper for readingsBulkUpdate. Do only things if value has changed.
  6638. #
  6639. ########################################################################################
  6640. sub SONOS_readingsBulkUpdateIfChanged($$$) {
  6641. my ($hash, $readingName, $readingValue) = @_;
  6642. return if (!defined($hash) || !defined($readingName) || !defined($readingValue));
  6643. readingsBulkUpdate($hash, $readingName, $readingValue) if ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue;
  6644. }
  6645. ########################################################################################
  6646. #
  6647. # SONOS_readingsEndUpdate - Wrapper for readingsEndUpdate.
  6648. #
  6649. ########################################################################################
  6650. sub SONOS_readingsEndUpdate($$) {
  6651. my ($hash, $doTrigger) = @_;
  6652. readingsEndUpdate($hash, $doTrigger);
  6653. }
  6654. ########################################################################################
  6655. #
  6656. # SONOS_readingsSingleUpdateIfChanged - Wrapper for readingsSingleUpdate. Do only things if value has changed.
  6657. #
  6658. ########################################################################################
  6659. sub SONOS_readingsSingleUpdateIfChanged($$$$) {
  6660. my ($hash, $readingName, $readingValue, $doTrigger) = @_;
  6661. readingsSingleUpdate($hash, $readingName, $readingValue, $doTrigger) if ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue;
  6662. }
  6663. ########################################################################################
  6664. #
  6665. # SONOS_RefreshIconsInFHEMWEB - Refreshs Iconcache in all FHEMWEB-Instances
  6666. #
  6667. ########################################################################################
  6668. sub SONOS_RefreshIconsInFHEMWEB($) {
  6669. my ($dir) = @_;
  6670. $dir = $attr{global}{modpath}.$dir;
  6671. foreach my $fhem_dev (sort keys %main::defs) {
  6672. if ($main::defs{$fhem_dev}{TYPE} eq 'FHEMWEB') {
  6673. eval('fhem(\'set '.$main::defs{$fhem_dev}{NAME}.' rereadicons\');');
  6674. last; # Die Icon-Liste ist global, muss also nur einmal neu gelesen werden
  6675. }
  6676. }
  6677. }
  6678. ########################################################################################
  6679. #
  6680. # SONOS_getAllSonosplayerDevices - Retreives all available/defined Sonosplayer-Devices
  6681. #
  6682. ########################################################################################
  6683. sub SONOS_getAllSonosplayerDevices() {
  6684. my @devices = ();
  6685. foreach my $fhem_dev (sort keys %main::defs) {
  6686. push @devices, $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER');
  6687. }
  6688. return @devices;
  6689. }
  6690. ########################################################################################
  6691. #
  6692. # SONOS_getDeviceDefHash - Retrieves the Def-Hash for the SONOS-Device (only one should exists, so this is OK)
  6693. # or, if $devicename is given, the Def-Hash for the SONOSPLAYER with the given name.
  6694. #
  6695. # Parameter $devicename = SONOSPLAYER devicename to be searched for, undef if searching for SONOS instead
  6696. #
  6697. ########################################################################################
  6698. sub SONOS_getDeviceDefHash($) {
  6699. my ($devicename) = @_;
  6700. if (defined($devicename)) {
  6701. foreach my $fhem_dev (sort keys %main::defs) {
  6702. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{NAME} eq $devicename);
  6703. }
  6704. } else {
  6705. foreach my $fhem_dev (sort keys %main::defs) {
  6706. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOS');
  6707. }
  6708. }
  6709. SONOS_Log undef, 1, "The Method 'SONOS_getDeviceDefHash' cannot find the FHEM-Device according to '$devicename'. This should not happen!";
  6710. return undef;
  6711. }
  6712. ########################################################################################
  6713. #
  6714. # SONOS_getSonosPlayerByUDN - Retrieves the Def-Hash for the SONOS-Device with the given UDN
  6715. #
  6716. ########################################################################################
  6717. sub SONOS_getSonosPlayerByUDN($) {
  6718. my ($udn) = @_;
  6719. if (defined($udn)) {
  6720. foreach my $fhem_dev (sort keys %main::defs) {
  6721. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{UDN} eq $udn);
  6722. }
  6723. } else {
  6724. foreach my $fhem_dev (sort keys %main::defs) {
  6725. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOS');
  6726. }
  6727. }
  6728. SONOS_Log undef, 1, "The Method 'SONOS_getSonosPlayerByUDN' cannot find the FHEM-Device according to '$udn'. This should not happen!";
  6729. return undef;
  6730. }
  6731. ########################################################################################
  6732. #
  6733. # SONOS_getSonosPlayerByRoomName - Retrieves the Def-Hash for the SONOS-Device with the given RoomName
  6734. #
  6735. ########################################################################################
  6736. sub SONOS_getSonosPlayerByRoomName($) {
  6737. my ($roomName) = @_;
  6738. foreach my $fhem_dev (sort keys %main::defs) {
  6739. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{READINGS}{roomName}{VAL} eq $roomName);
  6740. }
  6741. SONOS_Log undef, 1, "The Method 'SONOS_getSonosPlayerByRoomName' cannot find the FHEM-Device according to '$roomName'. This should not happen!";
  6742. return undef;
  6743. }
  6744. ########################################################################################
  6745. #
  6746. # SONOS_Undef - Implements UndefFn function
  6747. #
  6748. # Parameter hash = hash of the master, name
  6749. #
  6750. ########################################################################################
  6751. sub SONOS_Undef ($$) {
  6752. my ($hash, $name) = @_;
  6753. RemoveInternalTimer($hash);
  6754. DevIo_SimpleWrite($hash, "disconnect\n", 0);
  6755. DevIo_CloseDev($hash);
  6756. return undef;
  6757. }
  6758. ########################################################################################
  6759. #
  6760. # SONOS_Delete - Implements DeleteFn function
  6761. #
  6762. # Parameter hash = hash of the master, name
  6763. #
  6764. ########################################################################################
  6765. sub SONOS_Delete($$) {
  6766. my ($hash, $name) = @_;
  6767. # Erst alle SonosPlayer-Devices löschen
  6768. for my $player (SONOS_getAllSonosplayerDevices()) {
  6769. CommandDelete(undef, $player->{NAME});
  6770. }
  6771. # Den SubProzess beenden
  6772. # Wenn wir einen eigenen UPnP-Server gestartet haben, diesen hier auch wieder beenden, ansonsten nichts tun...
  6773. if ($SONOS_StartedOwnUPnPServer) {
  6774. # Da die Verbindung bereits durch UndefFn beendet wurde, muss sie hier neu aufgebaut werden, damit ich den Subprozess selbst beenden kann (vorher wurde nur die Verbindung beendet)...
  6775. DevIo_OpenDev($hash, 1, undef);
  6776. DevIo_SimpleWrite($hash, "shutdown\n", 0);
  6777. DevIo_CloseDev($hash);
  6778. }
  6779. # Etwas warten...
  6780. select(undef, undef, undef, 1);
  6781. # Das Entfernen des Sonos-Devices selbst übernimmt Fhem
  6782. return undef;
  6783. }
  6784. ########################################################################################
  6785. #
  6786. # SONOS_Shutdown - Implements ShutdownFn function
  6787. #
  6788. # Parameter hash = hash of the master, name
  6789. #
  6790. ########################################################################################
  6791. sub SONOS_Shutdown ($$) {
  6792. my ($hash) = @_;
  6793. RemoveInternalTimer($hash);
  6794. # Wenn wir einen eigenen UPnP-Server gestartet haben, diesen hier auch wieder beenden,
  6795. # ansonsten nur die Verbindung kappen
  6796. if ($SONOS_StartedOwnUPnPServer) {
  6797. DevIo_SimpleWrite($hash, "shutdown\n", 0);
  6798. } else {
  6799. DevIo_SimpleWrite($hash, "disconnect\n", 0);
  6800. }
  6801. DevIo_CloseDev($hash);
  6802. select(undef, undef, undef, 2);
  6803. return undef;
  6804. }
  6805. ########################################################################################
  6806. #
  6807. # SONOS_isInList - Checks, at which position the given value is in the given list
  6808. # Results in -1 if element not found
  6809. #
  6810. ########################################################################################
  6811. sub SONOS_posInList {
  6812. my($search, @list) = @_;
  6813. for (my $i = 0; $i <= $#list; $i++) {
  6814. return $i if ($list[$i] && $search eq $list[$i]);
  6815. }
  6816. return -1;
  6817. }
  6818. ########################################################################################
  6819. #
  6820. # SONOS_isInList - Checks, if the given value is in the given list
  6821. #
  6822. ########################################################################################
  6823. sub SONOS_isInList {
  6824. my($search, @list) = @_;
  6825. return 1 if SONOS_posInList($search, @list) >= 0;
  6826. return 0;
  6827. }
  6828. ########################################################################################
  6829. #
  6830. # SONOS_Min - Retrieves the minimum of two values
  6831. #
  6832. ########################################################################################
  6833. sub SONOS_Min($$) {
  6834. $_[$_[0] > $_[1]]
  6835. }
  6836. ########################################################################################
  6837. #
  6838. # SONOS_Max - Retrieves the maximum of two values
  6839. #
  6840. ########################################################################################
  6841. sub SONOS_Max($$) {
  6842. $_[$_[0] < $_[1]]
  6843. }
  6844. ########################################################################################
  6845. #
  6846. # SONOS_URI_Escape - Escapes the given string.
  6847. #
  6848. ########################################################################################
  6849. sub SONOS_URI_Escape($) {
  6850. my ($txt) = @_;
  6851. eval {
  6852. $txt = uri_escape($txt);
  6853. };
  6854. if ($@) {
  6855. $txt = uri_escape_utf8($txt);
  6856. };
  6857. return $txt;
  6858. }
  6859. ########################################################################################
  6860. #
  6861. # SONOS_GetRealPath - Retrieves the real (complete and absolute) path of the given file
  6862. # and converts all '\' to '/'
  6863. #
  6864. ########################################################################################
  6865. sub SONOS_GetRealPath($) {
  6866. my ($filename) = @_;
  6867. my $realFilename = realpath($filename);
  6868. $realFilename =~ s/\\/\//g;
  6869. return $realFilename
  6870. }
  6871. ########################################################################################
  6872. #
  6873. # SONOS_GetAbsolutePath - Retreives the absolute path (without filename)
  6874. #
  6875. ########################################################################################
  6876. sub SONOS_GetAbsolutePath($) {
  6877. my ($filename) = @_;
  6878. my $absFilename = SONOS_GetRealPath($filename);
  6879. return substr($absFilename, 0, rindex($absFilename, '/'));
  6880. }
  6881. ########################################################################################
  6882. #
  6883. # SONOS_GetTimeFromString - Parse the given DateTime-String e.g. created by TimeNow().
  6884. #
  6885. ########################################################################################
  6886. sub SONOS_GetTimeFromString($) {
  6887. my ($timeStr) = @_;
  6888. return 0 if (!defined($timeStr));
  6889. eval {
  6890. use Time::Local;
  6891. if($timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})( |_)([0-2]\d):([0-5]\d):([0-5]\d)$/) {
  6892. return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
  6893. }
  6894. }
  6895. }
  6896. ########################################################################################
  6897. #
  6898. # SONOS_GetTimeString - Gets the String for the given time
  6899. #
  6900. ########################################################################################
  6901. sub SONOS_GetTimeString($) {
  6902. my ($time) = @_;
  6903. my @t = localtime($time);
  6904. return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
  6905. }
  6906. ########################################################################################
  6907. #
  6908. # SONOS_TimeNow - Same as FHEM.PL-TimeNow. Neccessary due to forked process...
  6909. #
  6910. ########################################################################################
  6911. sub SONOS_TimeNow() {
  6912. return SONOS_GetTimeString(time());
  6913. }
  6914. ########################################################################################
  6915. #
  6916. # SONOS_Log - Log to the normal Log-command with additional Infomations like Thread-ID and the prefix 'SONOS'
  6917. #
  6918. ########################################################################################
  6919. sub SONOS_Log($$$) {
  6920. my ($udn, $level, $text) = @_;
  6921. if (defined($SONOS_ListenPort)) {
  6922. if ($SONOS_Client_LogLevel >= $level) {
  6923. my ($seconds, $microseconds) = gettimeofday();
  6924. my @t = localtime($seconds);
  6925. my $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d", $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0]);
  6926. if($SONOS_mseclog) {
  6927. $tim .= sprintf(".%03d", $microseconds / 1000);
  6928. }
  6929. print "$tim $level: SONOS".threads->tid().": $text\n";
  6930. }
  6931. } else {
  6932. my $hash = SONOS_getSonosPlayerByUDN($udn);
  6933. eval {
  6934. Log3 $hash->{NAME}, $level, 'SONOS'.threads->tid().': '.$text;
  6935. };
  6936. if ($@) {
  6937. Log $level, 'SONOS'.threads->tid().': '.$text;
  6938. }
  6939. }
  6940. }
  6941. ########################################################################################
  6942. ########################################################################################
  6943. ##
  6944. ## Start of Telnet-Server-Part for Sonos UPnP-Messages
  6945. ##
  6946. ## If SONOS_ListenPort is defined, then we have to start a listening server
  6947. ##
  6948. ########################################################################################
  6949. ########################################################################################
  6950. # Here starts the main-loop of the telnet-server
  6951. ########################################################################################
  6952. if (defined($SONOS_ListenPort)) {
  6953. $| = 1;
  6954. my $runEndlessLoop = 1;
  6955. my $lastRenewSubscriptionCheckTime = time();
  6956. $SIG{'PIPE'} = 'IGNORE';
  6957. $SIG{'CHLD'} = 'IGNORE';
  6958. $SIG{'INT'} = sub {
  6959. # Hauptschleife beenden
  6960. $SONOS_Client_NormalQueueWorking = 0;
  6961. $runEndlessLoop = 0;
  6962. # Sub-Threads beenden, sofern vorhanden
  6963. if (($SONOS_Thread != -1) && defined(threads->object($SONOS_Thread))) {
  6964. threads->object($SONOS_Thread)->kill('INT')->detach();
  6965. }
  6966. if (($SONOS_Thread_IsAlive != -1) && defined(threads->object($SONOS_Thread_IsAlive))) {
  6967. threads->object($SONOS_Thread_IsAlive)->kill('INT')->detach();
  6968. }
  6969. if (($SONOS_Thread_PlayerRestore != -1) && defined(threads->object($SONOS_Thread_PlayerRestore))) {
  6970. threads->object($SONOS_Thread_PlayerRestore)->kill('INT')->detach();
  6971. }
  6972. };
  6973. my $sock;
  6974. my $retryCounter = 10;
  6975. do {
  6976. eval {
  6977. socket($sock, AF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "Could not create socket: $!";
  6978. bind($sock, sockaddr_in($SONOS_ListenPort, INADDR_ANY)) or die "Bind failed: $!";
  6979. setsockopt($sock, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0)) or die "Setsockopt failed: $!";
  6980. listen($sock, 10);
  6981. };
  6982. if ($@) {
  6983. SONOS_Log undef, 0, "Can't bind Port $SONOS_ListenPort: $@";
  6984. SONOS_Log undef, 0, 'Retries left (wait 30s): '.--$retryCounter;
  6985. if (!$retryCounter) {
  6986. die 'Bind failed...';
  6987. }
  6988. select(undef, undef, undef, 30);
  6989. }
  6990. } while ($@);
  6991. SONOS_Log undef, 1, "$0 is listening to Port $SONOS_ListenPort";
  6992. # Accept incoming connections and talk to clients
  6993. $SONOS_Client_Selector = IO::Select->new($sock);
  6994. while ($runEndlessLoop) {
  6995. # NormalQueueWorking wird für die Dauer einer Direkt-Wert-Anfrage deaktiviert, damit hier nicht blockiert und/oder zuviel weggelesen wird.
  6996. if ($SONOS_Client_NormalQueueWorking) {
  6997. # Das ganze blockiert eine kurze Zeit, um nicht 100% CPU-Last zu erzeugen
  6998. # Das bedeutet aber auch, dass Sende-Vorgänge um maximal den Timeout-Wert verzögert werden
  6999. my @ready = $SONOS_Client_Selector->can_read(0.1);
  7000. # Falls wir hier auf eine Antwort reagieren würden, die gar nicht hierfür bestimmt ist, dann übergehen...
  7001. next if (!$SONOS_Client_NormalQueueWorking);
  7002. # Nachschauen, ob Subscriptions erneuert werden müssen
  7003. if (time() - $lastRenewSubscriptionCheckTime > 1800) {
  7004. $lastRenewSubscriptionCheckTime = time ();
  7005. foreach my $udn (@{$SONOS_Client_Data{PlayerUDNs}}) {
  7006. my %data;
  7007. $data{WorkType} = 'renewSubscription';
  7008. $data{UDN} = $udn;
  7009. my @params = ();
  7010. $data{Params} = \@params;
  7011. $SONOS_ComObjectTransportQueue->enqueue(\%data);
  7012. # Signalhandler aufrufen, wenn er nicht sowieso noch läuft...
  7013. threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1);
  7014. }
  7015. }
  7016. # Alle Bereit-Schreibenden verarbeiten
  7017. if ($SONOS_Client_SendQueue->pending() && !$SONOS_Client_SendQueue_Suspend) {
  7018. my @receiver = $SONOS_Client_Selector->can_write(0);
  7019. # Prüfen, ob überhaupt ein Empfänger bereit ist. Sonst würden Befehle verloren gehen...
  7020. if (scalar(@receiver) > 0) {
  7021. while ($SONOS_Client_SendQueue->pending()) {
  7022. my $line = $SONOS_Client_SendQueue->dequeue();
  7023. foreach my $so (@receiver) {
  7024. send($so, $line, 0);
  7025. }
  7026. }
  7027. }
  7028. }
  7029. # Alle Bereit-Lesenden verarbeiten
  7030. foreach my $so (@ready) {
  7031. if ($so == $sock) { # New Connection read
  7032. my $client;
  7033. my $addrinfo = accept($client, $sock);
  7034. setsockopt($client, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0));
  7035. my ($port, $iaddr) = sockaddr_in($addrinfo);
  7036. my $name = gethostbyaddr($iaddr, AF_INET);
  7037. $name = $iaddr if (!defined($name) || $name eq '');
  7038. SONOS_Log undef, 3, "Connection accepted from $name:$port";
  7039. # Von dort kommt die Anfrage, dort finde ich den Telnet-Port von Fhem :-)
  7040. $SONOS_UseTelnetForQuestions_Host = $name;
  7041. # Send Welcome-Message
  7042. send($client, "'This is UPnP-Server calling'\r\n", 0);
  7043. $SONOS_Client_Selector->add($client);
  7044. } else { # Existing client calling
  7045. my $inp = <$so>;
  7046. if (defined($inp)) {
  7047. # Abschließende Zeilenumbrüche abschnippeln
  7048. $inp =~ s/[\r\n]*$//;
  7049. # Consume and send evt. reply
  7050. SONOS_Log undef, 5, "Received: '$inp'";
  7051. SONOS_Client_ConsumeMessage($so, $inp);
  7052. }
  7053. }
  7054. }
  7055. } else {
  7056. # Wenn die Verarbeitung gerade unterbrochen sein soll, dann hier etwas warten, um keine 100% CPU-Last zu erzeugen
  7057. select(undef, undef, undef, 0.5);
  7058. }
  7059. }
  7060. SONOS_Log undef, 0, 'Das Lauschen auf der Schnittstelle wurde beendet. Prozess endet nun auch...';
  7061. # Alle Handles entfernen und schliessen...
  7062. for my $cl ($SONOS_Client_Selector->handles()) {
  7063. $SONOS_Client_Selector->remove($cl);
  7064. shutdown($cl, 2);
  7065. close($cl);
  7066. }
  7067. # Prozess beenden...
  7068. exit(0);
  7069. }
  7070. # Wird für den FHEM-Modulpart benötigt
  7071. 1;
  7072. ########################################################################################
  7073. # SONOS_Client_Thread_Notifier: Notifies all clients with the given message
  7074. ########################################################################################
  7075. sub SONOS_Client_Notifier($) {
  7076. my ($msg) = @_;
  7077. $| = 1;
  7078. state $setCurrentUDN;
  7079. # Wenn hier ein SetCurrent ausgeführt werden soll, dann auch den lokalen Puffer aktualisieren
  7080. if ($msg =~ m/SetCurrent:(.*?):(.*)/) {
  7081. my $udnBuffer = ($setCurrentUDN eq 'undef') ? 'SONOS' : $setCurrentUDN;
  7082. $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$1} = $2;
  7083. } elsif ($msg =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) {
  7084. $setCurrentUDN = $1;
  7085. }
  7086. # Immer ein Zeilenumbruch anfügen...
  7087. $msg .= "\n" if (substr($msg, -1, 1) ne "\n");
  7088. $SONOS_Client_SendQueue->enqueue($msg);
  7089. }
  7090. ########################################################################################
  7091. # SONOS_Client_SendReceive: Send and receive messages
  7092. ########################################################################################
  7093. sub SONOS_Client_SendReceive($) {
  7094. my ($msg) = @_;
  7095. # Immer ein Zeilenumbruch anfügen...
  7096. $msg .= "\n" if (substr($msg, -1, 1) ne "\n");
  7097. my $answer;
  7098. $SONOS_Client_NormalQueueWorking = 0;
  7099. select(undef, undef, undef, 0.1);
  7100. my @sender = $SONOS_Client_Selector->can_write(0);
  7101. foreach my $so (@sender) {
  7102. send($so, $msg, 0);
  7103. do {
  7104. select(undef, undef, undef, 0.1);
  7105. recv($so, $answer, 30000, 0);
  7106. } while (!$answer);
  7107. }
  7108. select(undef, undef, undef, 0.1);
  7109. $SONOS_Client_NormalQueueWorking = 1;
  7110. return $answer;
  7111. }
  7112. ########################################################################################
  7113. # SONOS_Client_SendReceiveTelnet: Send and receive messages
  7114. ########################################################################################
  7115. sub SONOS_Client_SendReceiveTelnet($) {
  7116. my ($msg) = @_;
  7117. SONOS_Log undef, 4, "Telnet-Anfrage: $msg";
  7118. eval {
  7119. require Net::Telnet;
  7120. my $socket = Net::Telnet->new(Timeout => 30);
  7121. $socket->open(Host => $SONOS_UseTelnetForQuestions_Host, Port => $SONOS_UseTelnetForQuestions_Port);
  7122. $socket->telnetmode(0);
  7123. $socket->cmd();
  7124. my @lines = $socket->cmd('{ SONOS_AnswerQuery("'.$msg.'") }');
  7125. my $answer = $lines[0];
  7126. $answer =~ s/[\r\n]*$//;
  7127. $socket->close();
  7128. return $answer;
  7129. };
  7130. if ($@) {
  7131. SONOS_Log undef, 4, "Bei einer Telnet-Anfrage ist ein Fehler aufgetreten, es wird auf Normalanfrage umgestellt: $@";
  7132. $SONOS_UseTelnetForQuestions = 0;
  7133. return $3 if ($msg =~ m/Q.:(.*?):(.*?):(.*)/);
  7134. }
  7135. return "Error during processing: $msg";
  7136. }
  7137. ########################################################################################
  7138. # SONOS_Client_AskAttribute: Asks FHEM for a AttributeValue according to the given Attributename
  7139. ########################################################################################
  7140. sub SONOS_Client_AskAttribute($$$) {
  7141. my ($udn, $name, $default) = @_;
  7142. my $val;
  7143. if ($SONOS_UseTelnetForQuestions) {
  7144. $val = SONOS_Client_SendReceiveTelnet('QA:'.$udn.':'.$name.':'.$default);
  7145. } else {
  7146. $val = SONOS_Client_SendReceive('QA:'.$udn.':'.$name.':'.$default);
  7147. }
  7148. $val =~ s/[\r\n]*$//;
  7149. $val = $1 if ($val =~ m/A:$udn:$name:(.*)/i);
  7150. return $val;
  7151. }
  7152. ########################################################################################
  7153. # SONOS_Client_AskReading: Asks FHEM for a ReadingValue according to the given Readingname
  7154. ########################################################################################
  7155. sub SONOS_Client_AskReading($$$) {
  7156. my ($udn, $name, $default) = @_;
  7157. my $val;
  7158. if ($SONOS_UseTelnetForQuestions) {
  7159. $val = SONOS_Client_SendReceiveTelnet('QR:'.$udn.':'.$name.':'.$default);
  7160. } else {
  7161. $val = SONOS_Client_SendReceive('QR:'.$udn.':'.$name.':'.$default);
  7162. }
  7163. $val =~ s/[\r\n]*$//;
  7164. $val = $1 if ($val =~ m/R:$udn:$name:(.*)/i);
  7165. return $val;
  7166. }
  7167. ########################################################################################
  7168. # SONOS_Client_AskDefinition: Asks FHEM for a DefinitionValue according to the given name
  7169. ########################################################################################
  7170. sub SONOS_Client_AskDefinition($$$) {
  7171. my ($udn, $name, $default) = @_;
  7172. my $val;
  7173. if ($SONOS_UseTelnetForQuestions) {
  7174. $val = SONOS_Client_SendReceiveTelnet('QD:'.$udn.':'.$name.':'.$default);
  7175. } else {
  7176. $val = SONOS_Client_SendReceive('QD:'.$udn.':'.$name.':'.$default);
  7177. }
  7178. $val =~ s/[\r\n]*$//;
  7179. $val = $1 if ($val =~ m/D:$udn:$name:(.*)/i);
  7180. return $val;
  7181. }
  7182. ########################################################################################
  7183. # SONOS_Client_Data_Retreive: Retrieves stored data, and calls AskXX if necessary
  7184. ########################################################################################
  7185. sub SONOS_Client_Data_Retreive($$$$) {
  7186. my ($udn, $reading, $name, $default) = @_;
  7187. my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn;
  7188. # Prüfen, ob die Anforderung überhaupt bedient werden darf
  7189. if ($reading eq 'attr') {
  7190. if (SONOS_posInList($name, @SONOS_PossibleAttributes) == -1) {
  7191. SONOS_Log undef, 0, "Ungültige Attribut-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  7192. exit(1);
  7193. }
  7194. } elsif ($reading eq 'def') {
  7195. if (SONOS_posInList($name, @SONOS_PossibleDefinitions) == -1) {
  7196. SONOS_Log undef, 0, "Ungültige Definitions-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  7197. exit(1);
  7198. }
  7199. } else {
  7200. if (SONOS_posInList($name, @SONOS_PossibleReadings) == -1) {
  7201. SONOS_Log undef, 0, "Ungültige Reading-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  7202. exit(1);
  7203. }
  7204. }
  7205. # Anfrage zulässig, also ausliefern...
  7206. if (defined($SONOS_Client_Data{Buffer}->{$udnBuffer}) && defined($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name})) {
  7207. SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> ".$SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name};
  7208. return $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name};
  7209. } else {
  7210. SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> DEFAULT";
  7211. return $default;
  7212. }
  7213. ##################################################
  7214. # Alter Mechanismus mit Anfrage an Fhem...
  7215. #my $result = do { if (defined($SONOS_Client_Data{Buffer}->{$udnBuffer}) && defined($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name})) {
  7216. # $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name}
  7217. # } else {
  7218. # if ($reading eq 'attr') {
  7219. # SONOS_Client_AskAttribute($udn, $name, $default);
  7220. # } elsif ($reading eq 'def') {
  7221. # SONOS_Client_AskDefinition($udn, $name, $default);
  7222. # } else {
  7223. # SONOS_Client_AskReading($udn, $name, $default);
  7224. # }
  7225. # }
  7226. # };
  7227. #$SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} = $result;
  7228. #
  7229. #return $result;
  7230. }
  7231. ########################################################################################
  7232. # SONOS_Client_Data_Refresh: Send data and refreshs buffer
  7233. ########################################################################################
  7234. sub SONOS_Client_Data_Refresh($$$$) {
  7235. my ($sendCommand, $udn, $name, $value) = @_;
  7236. my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn;
  7237. $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} = $value;
  7238. if ($sendCommand && ($sendCommand ne '')) {
  7239. SONOS_Client_Notifier($sendCommand.':'.$udn.':'.$name.':'.$value);
  7240. }
  7241. }
  7242. ########################################################################################
  7243. # SONOS_Client_ConsumeMessage: Consumes the given message and give an evt. return
  7244. ########################################################################################
  7245. sub SONOS_Client_ConsumeMessage($$) {
  7246. my ($client, $msg) = @_;
  7247. if (lc($msg) eq 'disconnect' || lc($msg) eq 'shutdown') {
  7248. SONOS_Log undef, 3, "Disconnecting client and shutdown server..." if (lc($msg) eq 'shutdown');
  7249. SONOS_Log undef, 3, "Disconnecting client..." if (lc($msg) ne 'shutdown');
  7250. $SONOS_Client_Selector->remove($client);
  7251. if ($SONOS_Thread != -1) {
  7252. my $thr = threads->object($SONOS_Thread);
  7253. if ($thr) {
  7254. SONOS_Log undef, 3, 'Trying to kill Sonos_Thread...';
  7255. $thr->kill('INT')->detach();
  7256. } else {
  7257. SONOS_Log undef, 3, 'Sonos_Thread is already killed!';
  7258. }
  7259. }
  7260. if ($SONOS_Thread_IsAlive != -1) {
  7261. my $thr = threads->object($SONOS_Thread_IsAlive);
  7262. if ($thr) {
  7263. SONOS_Log undef, 3, 'Trying to kill IsAlive_Thread...';
  7264. $thr->kill('INT')->detach();
  7265. } else {
  7266. SONOS_Log undef, 3, 'IsAlive_Thread is already killed!';
  7267. }
  7268. }
  7269. if ($SONOS_Thread_PlayerRestore != -1) {
  7270. my $thr = threads->object($SONOS_Thread_PlayerRestore);
  7271. if ($thr) {
  7272. SONOS_Log undef, 3, 'Trying to kill PlayerRestore_Thread...';
  7273. $thr->kill('INT')->detach();
  7274. } else {
  7275. SONOS_Log undef, 3, 'PlayerRestore_Thread is already killed!';
  7276. }
  7277. }
  7278. shutdown($client, 2);
  7279. close($client);
  7280. threads->self()->kill('INT') if (lc($msg) eq 'shutdown');
  7281. } elsif (lc($msg) eq 'hello') {
  7282. send($client, "OK\r\n", 0);
  7283. } elsif (lc($msg) eq 'goaway') {
  7284. $SONOS_Client_Selector->remove($client);
  7285. shutdown($client, 2);
  7286. close($client);
  7287. } elsif ($msg =~ m/SetData:(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*)/i) {
  7288. $SONOS_Client_Data{SonosDeviceName} = $1;
  7289. $SONOS_Client_LogLevel = $2;
  7290. $SONOS_Client_Data{pingType} = $3;
  7291. my @usedonlyIPs = split(/,/, $4);
  7292. $SONOS_Client_Data{usedonlyIPs} = shared_clone(\@usedonlyIPs);
  7293. for my $elem (@usedonlyIPs) {
  7294. $usedonlyIPs{SONOS_Trim($elem)} = 1;
  7295. }
  7296. my @ignoredIPs = split(/,/, $5);
  7297. $SONOS_Client_Data{ignoredIPs} = shared_clone(\@ignoredIPs);
  7298. for my $elem (@ignoredIPs) {
  7299. $ignoredIPs{SONOS_Trim($elem)} = 1;
  7300. }
  7301. my @names = split(/,/, $6);
  7302. $SONOS_Client_Data{PlayerNames} = shared_clone(\@names);
  7303. my @udns = split(/,/, $7);
  7304. $SONOS_Client_Data{PlayerUDNs} = shared_clone(\@udns);
  7305. my @playeralive = ();
  7306. $SONOS_Client_Data{PlayerAlive} = shared_clone(\@playeralive);
  7307. my %player = ();
  7308. $SONOS_Client_Data{Buffer} = shared_clone(\%player);
  7309. push @udns, 'SONOS';
  7310. foreach my $elem (@udns) {
  7311. my %elemValues = ();
  7312. $SONOS_Client_Data{Buffer}->{$elem} = shared_clone(\%elemValues);
  7313. }
  7314. } elsif ($msg =~ m/SetValues:(.*?):(.*)/i) {
  7315. my $deviceName = $1;
  7316. my $deviceValues = $2;
  7317. my %elemValues = ();
  7318. # Werte aus der Übergabe belegen
  7319. foreach my $elem (split(/\|/, $deviceValues)) {
  7320. if ($elem =~ m/(.*?)=(.*)/) {
  7321. $elemValues{$1} = uri_unescape($2);
  7322. if ($1 eq 'bookmarkPlaylistDefinition') {
  7323. # <Gruppenname>:<PlayerDeviceRegEx>:<MinListLength>:<MaxListLength>:<MaxAge> [...]
  7324. %SONOS_BookmarkQueueDefinition = ();
  7325. my $def = $elemValues{$1};
  7326. foreach my $elem (split(/ /, $def)) {
  7327. # Sicherstellen, das alle Stellen vorhanden sind...
  7328. $elem .= ':' while (SONOS_CountInString(':', $elem) < 5);
  7329. # Zerlegen
  7330. if ($elem =~ m/(.*?):(.*?):(\d*):(\d*):(.*?):(.*)/) {
  7331. my $key = $1;
  7332. $SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx} = ($2 ne '') ? $2 : '.*';
  7333. $SONOS_BookmarkQueueDefinition{$key}{MinListLength} = ($3 ne '') ? $3 : 0;
  7334. $SONOS_BookmarkQueueDefinition{$key}{MaxListLength} = ($4 ne '') ? $4 : 99999;
  7335. $SONOS_BookmarkQueueDefinition{$key}{MaxAge} = ($5 ne '') ? $5 : '28*24*60*60';
  7336. $SONOS_BookmarkQueueDefinition{$key}{ReadOnly} = ($6 ne '') ? $6 : 0;
  7337. # RegEx prüfen
  7338. eval { "" =~ m/$SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx}/ };
  7339. if($@) {
  7340. SONOS_Log undef, 0, 'SetData - bookmarkPlaylistDefinition: Bad PlayerDeviceRegExp "'.$SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx}.'": '.$@;
  7341. delete($SONOS_BookmarkQueueDefinition{$key});
  7342. next;
  7343. }
  7344. # MaxAge berechnen...
  7345. eval { $SONOS_BookmarkQueueDefinition{$key}{MaxAge} = eval($SONOS_BookmarkQueueDefinition{$key}{MaxAge}); };
  7346. if($@) {
  7347. SONOS_Log undef, 0, 'SetData - bookmarkPlaylistDefinition: Bad MaxAge "'.$SONOS_BookmarkQueueDefinition{$key}{MaxAge}.'": '.$@;
  7348. delete($SONOS_BookmarkQueueDefinition{$key});
  7349. next;
  7350. }
  7351. }
  7352. }
  7353. SONOS_Log undef, 4, 'BookmarkPlaylistDefinition: '.Dumper(\%SONOS_BookmarkQueueDefinition);
  7354. }
  7355. if ($1 eq 'bookmarkTitleDefinition') {
  7356. # <Gruppenname>:<PlayerdeviceRegEx>:<TrackURIRegEx>:<MinTitleLength>:<RemainingLength>:<MaxAge>:<ReadOnly> [...]
  7357. %SONOS_BookmarkTitleDefinition = ();
  7358. my $def = $elemValues{$1};
  7359. foreach my $elem (split(/ /, $def)) {
  7360. # Sicherstellen, das alle Stellen vorhanden sind...
  7361. $elem .= ':' while (SONOS_CountInString(':', $elem) < 6);
  7362. # Zerlegen
  7363. if ($elem =~ m/(.*?):(.*?):(.*?):(\d*):(\d*):(.*?):(.*)/) {
  7364. my $key = $1;
  7365. $SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx} = ($2 ne '') ? $2 : '.*';
  7366. $SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx} = ($3 ne '') ? $3 : '.*';
  7367. $SONOS_BookmarkTitleDefinition{$key}{MinTitleLength} = ($4 ne '') ? $4 : 60;
  7368. $SONOS_BookmarkTitleDefinition{$key}{RemainingLength} = ($5 ne '') ? $5 : 10;
  7369. $SONOS_BookmarkTitleDefinition{$key}{MaxAge} = ($6 ne '') ? $6 : '28*24*60*60';
  7370. $SONOS_BookmarkTitleDefinition{$key}{ReadOnly} = 0;
  7371. $SONOS_BookmarkTitleDefinition{$key}{ReadOnly} = 1 if (lc($7) eq 'readonly');
  7372. $SONOS_BookmarkTitleDefinition{$key}{Chapter} = 0;
  7373. $SONOS_BookmarkTitleDefinition{$key}{Chapter} = 1 if (lc($7) eq 'chapter');
  7374. # RegEx prüfen...
  7375. eval { "" =~ m/$SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx}/ };
  7376. if($@) {
  7377. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad PlayerDeviceRegExp "'.$SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx}.'": '.$@;
  7378. delete($SONOS_BookmarkTitleDefinition{$key});
  7379. next;
  7380. }
  7381. # RegEx prüfen...
  7382. eval { "" =~ m/$SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx}/ };
  7383. if($@) {
  7384. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad TrackURIRegEx "'.$SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx}.'": '.$@;
  7385. delete($SONOS_BookmarkTitleDefinition{$key});
  7386. next;
  7387. }
  7388. # MaxAge berechnen...
  7389. eval { $SONOS_BookmarkTitleDefinition{$key}{MaxAge} = eval($SONOS_BookmarkTitleDefinition{$key}{MaxAge}); };
  7390. if($@) {
  7391. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad MaxAge "'.$SONOS_BookmarkTitleDefinition{$key}{MaxAge}.'": '.$@;
  7392. delete($SONOS_BookmarkTitleDefinition{$key});
  7393. next;
  7394. }
  7395. }
  7396. }
  7397. SONOS_Log undef, 4, 'BookmarkTitleDefinition: '.Dumper(\%SONOS_BookmarkTitleDefinition);
  7398. }
  7399. }
  7400. }
  7401. $SONOS_Client_Data{Buffer}->{$deviceName} = shared_clone(\%elemValues);
  7402. } elsif ($msg =~ m/DoWork:(.*?):(.*?):(.*)/i) {
  7403. my %data;
  7404. $data{WorkType} = $2;
  7405. $data{UDN} = $1;
  7406. if (defined($3)) {
  7407. my @params = split(/£@£~/, decode_utf8($3));
  7408. $data{Params} = \@params;
  7409. } else {
  7410. my @params = ();
  7411. $data{Params} = \@params;
  7412. }
  7413. # Auf die Queue legen wenn Thread läuft und Signalhandler aufrufen, wenn er nicht sowieso noch läuft...
  7414. if ($SONOS_Thread != -1) {
  7415. $SONOS_ComObjectTransportQueue->enqueue(\%data);
  7416. threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1);
  7417. }
  7418. } elsif (lc($msg) eq 'startthread') {
  7419. # Discover-Thread
  7420. $SONOS_Thread = threads->create(\&SONOS_Discover)->tid();
  7421. # IsAlive-Checker-Thread
  7422. if (lc($SONOS_Client_Data{pingType}) ne 'none') {
  7423. $SONOS_Thread_IsAlive = threads->create(\&SONOS_Client_IsAlive)->tid();
  7424. }
  7425. # Playerrestore-Thread
  7426. $SONOS_Thread_PlayerRestore = threads->create(\&SONOS_RestoreOldPlaystate)->tid();
  7427. } else {
  7428. SONOS_Log undef, 2, "ConsumMessage: Sorry. I don't understand you - '$msg'.";
  7429. send($client, "Sorry. I don't understand you - '$msg'.\r\n", 0);
  7430. }
  7431. }
  7432. ########################################################################################
  7433. # SONOS_getBookmarkGroupKeys: Retrieves the approbriate GroupKeys to the given UDN
  7434. ########################################################################################
  7435. sub SONOS_getBookmarkGroupKeys($$;$) {
  7436. my ($type, $udn, $disabled) = @_;
  7437. $disabled = 0 if (!defined($disabled));
  7438. my $deviceName = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  7439. my @result = ();
  7440. my $hashList = \%SONOS_BookmarkTitleDefinition;
  7441. $hashList = \%SONOS_BookmarkQueueDefinition if (lc($type) eq 'queue');
  7442. foreach my $key (keys %{$hashList}) {
  7443. if ($deviceName =~ m/$hashList->{$key}{PlayerDeviceRegEx}/) {
  7444. push(@result, $key) if (!$disabled && (!defined($hashList->{$key}{Disabled}) || !$hashList->{$key}{Disabled}));
  7445. push(@result, $key) if ($disabled && defined($hashList->{$key}{Disabled}) && $hashList->{$key}{Disabled});
  7446. }
  7447. }
  7448. return @result;
  7449. }
  7450. ########################################################################################
  7451. # SONOS_Client_IsAlive: Checks of the clients are already available
  7452. ########################################################################################
  7453. sub SONOS_Client_IsAlive() {
  7454. my $interval = SONOS_Max(10, SONOS_Client_Data_Retreive('undef', 'def', 'INTERVAL', 0));
  7455. my $stepInterval = 0.5;
  7456. SONOS_Log undef, 1, 'IsAlive-Thread gestartet. Warte 120 Sekunden und pruefe dann alle '.$interval.' Sekunden...';
  7457. my $runEndlessLoop = 1;
  7458. $SIG{'PIPE'} = 'IGNORE';
  7459. $SIG{'CHLD'} = 'IGNORE';
  7460. $SIG{'INT'} = sub {
  7461. $runEndlessLoop = 0;
  7462. };
  7463. # Erst nach einer Weile wartens anfangen zu arbeiten. Bis dahin sollten alle Player im Netz erkannt, und deren Konfigurationen bekannt sein.
  7464. my $counter = 0;
  7465. do {
  7466. select(undef, undef, undef, 0.5);
  7467. } while (($counter++ < 240) && $runEndlessLoop);
  7468. my $stepCounter = 0;
  7469. while($runEndlessLoop) {
  7470. select(undef, undef, undef, $stepInterval);
  7471. next if (($stepCounter += $stepInterval) < $interval);
  7472. $stepCounter = 0;
  7473. # Alle bekannten Player durchgehen, wenn der Thread nicht beendet werden soll
  7474. if ($runEndlessLoop) {
  7475. my @list = @{$SONOS_Client_Data{PlayerAlive}};
  7476. my @toAnnounce = ();
  7477. for(my $i = 0; $i <= $#list; $i++) {
  7478. next if (!$list[$i]);
  7479. if (!SONOS_IsAlive($list[$i])) {
  7480. # Auf die Entfernen-Meldeliste setzen
  7481. push @toAnnounce, $list[$i];
  7482. # Wenn er nicht mehr am Leben ist, dann auch aus der Aktiven-Liste entfernen
  7483. delete @{$SONOS_Client_Data{PlayerAlive}}[$i];
  7484. }
  7485. }
  7486. # Wenn ein Player gerade verschwunden ist, dann dem (verbleibenden) Sonos-System das mitteilen
  7487. foreach my $toDeleteElem (@toAnnounce) {
  7488. if ($toDeleteElem =~ m/(^.*)_/) {
  7489. $toDeleteElem = $1;
  7490. SONOS_Log undef, 3, 'ReportUnresponsiveDevice: '.$toDeleteElem;
  7491. foreach my $udn (@{$SONOS_Client_Data{PlayerAlive}}) {
  7492. next if (!$udn);
  7493. my %data;
  7494. $data{WorkType} = 'reportUnresponsiveDevice';
  7495. $data{UDN} = $udn;
  7496. my @params = ();
  7497. push @params, $toDeleteElem;
  7498. $data{Params} = \@params;
  7499. $SONOS_ComObjectTransportQueue->enqueue(\%data);
  7500. # Signalhandler aufrufen, wenn er nicht sowieso noch läuft...
  7501. threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1);
  7502. # Da ich das nur an den ersten verfügbaren Player senden muss, kann hier die Schleife direkt beendet werden
  7503. last;
  7504. }
  7505. }
  7506. }
  7507. }
  7508. }
  7509. SONOS_Log undef, 1, 'IsAlive-Thread wurde beendet.';
  7510. $SONOS_Thread_IsAlive = -1;
  7511. }
  7512. ########################################################################################
  7513. ########################################################################################
  7514. ##
  7515. ## End of Telnet-Server-Part for Sonos UPnP-Messages
  7516. ##
  7517. ########################################################################################
  7518. ########################################################################################
  7519. =pod
  7520. =begin html
  7521. <a name="SONOS"></a>
  7522. <h3>SONOS</h3>
  7523. <p>FHEM-Module to communicate with the Sonos-System via UPnP</p>
  7524. <p>For more informations have also a closer look at the wiki at <a href="http://www.fhemwiki.de/wiki/SONOS">http://www.fhemwiki.de/wiki/SONOS</a></p>
  7525. <p>For correct functioning of this module it is neccessary to have some Perl-Modules installed, which are eventually installed already manually:<ul>
  7526. <li><code>LWP::Simple</code></li>
  7527. <li><code>LWP::UserAgent</code></li>
  7528. <li><code>SOAP::Lite</code></li>
  7529. <li><code>HTTP::Request</code></li></ul>
  7530. Installation e.g. as Debian-Packages (via "sudo apt-get install &lt;packagename&gt;"):<ul>
  7531. <li>LWP::Simple-Packagename (incl. LWP::UserAgent and HTTP::Request): libwww-perl</li>
  7532. <li>SOAP::Lite-Packagename: libsoap-lite-perl</li></ul>
  7533. <br />Installation e.g. as Windows ActivePerl (via Perl-Packagemanager)<ul>
  7534. <li>Install Package LWP (incl. LWP::UserAgent and HTTP::Request)</li>
  7535. <li>Install Package SOAP::Lite</li>
  7536. <li>SOAP::Lite-Special for Versions after 5.18:<ul>
  7537. <li>Add another Packagesource from suggestions or manual: Bribes de Perl (http://www.bribes.org/perl/ppm)</li>
  7538. <li>Install Package: SOAP::Lite</li></ul></li></ul>
  7539. <b>Windows ActivePerl 5.20 does currently not work due to missing SOAP::Lite</b></p>
  7540. <p><b>Attention!</b><br />This Module will not work on any platform, because of the use of Threads and the neccessary Perl-modules.</p>
  7541. <p>More information is given in a (german) Wiki-article: <a href="http://www.fhemwiki.de/wiki/SONOS">http://www.fhemwiki.de/wiki/SONOS</a></p>
  7542. <p>The system consists of two different components:<br />
  7543. 1. A UPnP-Client which runs as a standalone process in the background and takes the communications to the sonos-components.<br />
  7544. 2. The FHEM-module itself which connects to the UPnP-client to make fhem able to work with sonos.<br /><br />
  7545. The client will be started by the module itself if not done in another way.<br />
  7546. You can start this client on your own (to let it run instantly and independent from FHEM):<br />
  7547. <code>perl 00_SONOS.pm 4711</code>: Starts a UPnP-Client in an independant way who listens to connections on port 4711. This process can run a long time, FHEM can connect and disconnect to it.</p>
  7548. <h4>Example</h4>
  7549. <p>
  7550. Simplest way to define:<br />
  7551. <b><code>define Sonos SONOS</code></b>
  7552. </p>
  7553. <p>
  7554. Example with control over the used port and the isalive-checker-interval:<br />
  7555. <b><code>define Sonos SONOS localhost:4711 45</code></b>
  7556. </p>
  7557. <a name="SONOSdefine"></a>
  7558. <h4>Define</h4>
  7559. <b><code>define &lt;name&gt; SONOS [upnplistener [interval [waittime [delaytime]]]]</code></b>
  7560. <br /><br /> Define a Sonos interface to communicate with a Sonos-System.<br />
  7561. <p>
  7562. <b><code>[upnplistener]</code></b><br />The name and port of the external upnp-listener. If not given, defaults to <code>localhost:4711</code>. The port has to be a free portnumber on your system. If you don't start a server on your own, the script does itself.<br />If you start it yourself write down the correct informations to connect.</p>
  7563. <p>
  7564. <b><code>[interval]</code></b><br /> The interval is for alive-checking of Zoneplayer-device, because no message come if the host disappear :-)<br />If omitted a value of 10 seconds is the default.</p>
  7565. <p>
  7566. <b><code>[waittime]</code></b><br /> With this value you can configure the waiting time for the starting of the Subprocess.</p>
  7567. <p>
  7568. <b><code>[delaytime]</code></b><br /> With this value you can configure a delay time before starting the network-part.</p>
  7569. <a name="SONOSset"></a>
  7570. <h4>Set</h4>
  7571. <ul>
  7572. <li><b>Common Tasks</b><ul>
  7573. <li><a name="SONOS_setter_RescanNetwork">
  7574. <b><code>RescanNetwork</code></b></a>
  7575. <br />Restarts the player discovery.</li>
  7576. </ul></li>
  7577. <li><b>Control-Commands</b><ul>
  7578. <li><a name="SONOS_setter_Mute">
  7579. <b><code>Mute &lt;state&gt;</code></b></a>
  7580. <br />Sets the mute-state on all players.</li>
  7581. <li><a name="SONOS_setter_PauseAll">
  7582. <b><code>PauseAll</code></b></a>
  7583. <br />Pause all Zoneplayer.</li>
  7584. <li><a name="SONOS_setter_Pause">
  7585. <b><code>Pause</code></b></a>
  7586. <br />Alias for PauseAll.</li>
  7587. <li><a name="SONOS_setter_StopAll">
  7588. <b><code>StopAll</code></b></a>
  7589. <br />Stops all Zoneplayer.</li>
  7590. <li><a name="SONOS_setter_Stop">
  7591. <b><code>Stop</code></b></a>
  7592. <br />Alias for StopAll.</li>
  7593. </ul></li>
  7594. <li><b>Bookmark-Commands</b><ul>
  7595. <li><a name="SONOS_setter_DisableBookmark">
  7596. <b><code>DisableBookmark &lt;Groupname&gt;</code></b></a>
  7597. <br />Disables the group with the given name.</li>
  7598. <li><a name="SONOS_setter_EnableBookmark">
  7599. <b><code>EnableBookmark &lt;Groupname&gt;</code></b></a>
  7600. <br />Enables the group with the given name.</li>
  7601. <li><a name="SONOS_setter_LoadBookmarks">
  7602. <b><code>LoadBookmarks [Groupname]</code></b></a>
  7603. <br />Loads the given group (or all if parameter not set) from the filesystem.</li>
  7604. <li><a name="SONOS_setter_SaveBookmarks">
  7605. <b><code>SaveBookmarks [Groupname]</code></b></a>
  7606. <br />Saves the given group (or all if parameter not set) to the filesystem.</li>
  7607. </ul></li>
  7608. <li><b>Group-Commands</b><ul>
  7609. <li><a name="SONOS_setter_Groups">
  7610. <b><code>Groups &lt;GroupDefinition&gt;</code></b></a>
  7611. <br />Sets the current groups on the whole Sonos-System. The format is the same as retreived by getter 'Groups'.<br >A reserved word is <i>Reset</i>. It can be used to directly extract all players out of their groups.</li>
  7612. </ul></li>
  7613. </ul>
  7614. <a name="SONOSget"></a>
  7615. <h4>Get</h4>
  7616. <ul>
  7617. <li><b>Group-Commands</b><ul>
  7618. <li><a name="SONOS_getter_Groups">
  7619. <b><code>Groups</code></b></a>
  7620. <br />Retreives the current group-configuration of the Sonos-System. The format is a comma-separated List of Lists with devicenames e.g. <code>[Sonos_Kueche], [Sonos_Wohnzimmer, Sonos_Schlafzimmer]</code>. In this example there are two groups: the first consists of one player and the second consists of two players.<br />
  7621. The order in the sublists are important, because the first entry defines the so-called group-coordinator (in this case <code>Sonos_Wohnzimmer</code>), from which the current playlist and the current title playing transferred to the other member(s).</li>
  7622. </ul></li>
  7623. </ul>
  7624. <a name="SONOSattr"></a>
  7625. <h4>Attributes</h4>
  7626. '''Attention'''<br />The most of the attributes can only be used after a restart of fhem, because it must be initially transfered to the subprocess.
  7627. <ul>
  7628. <li><b>Common</b><ul>
  7629. <li><a name="SONOS_attribut_disable"><b><code>disable &lt;value&gt;</code></b>
  7630. </a><br />One of (0,1). With this value you can disable the whole module. Works immediatly. If set to 1 the subprocess will be terminated and no message will be transmitted. If set to 0 the subprocess is again started.<br />It is useful when you install new Sonos-Components and don't want any disgusting devices during the Sonos setup.</li>
  7631. <li><a name="SONOS_attribut_ignoredIPs"><b><code>ignoredIPs &lt;IP-Address&gt;[,IP-Address]</code></b>
  7632. </a><br />With this attribute you can define IP-addresses, which has to be ignored by the UPnP-System of this module. e.g. "192.168.0.11,192.168.0.37"</li>
  7633. <li><a name="SONOS_attribut_pingType"><b><code>pingType &lt;string&gt;</code></b>
  7634. </a><br /> One of (none,tcp,udp,icmp,syn). Defines which pingType for alive-Checking has to be used. If set to 'none' no checks will be done.</li>
  7635. <li><a name="SONOS_attribut_usedonlyIPs"><b><code>usedonlyIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  7636. </a><br />With this attribute you can define IP-addresses, which has to be exclusively used by the UPnP-System of this module. e.g. "192.168.0.11,192.168.0.37"</li>
  7637. </ul></li>
  7638. <li><b>Bookmark Configuration</b><ul>
  7639. <li><a name="SONOS_attribut_bookmarkSaveDir"><b><code>bookmarkSaveDir &lt;path&gt;</code></b>
  7640. </a><br /> Defines a directory where the saved bookmarks can be placed. If not defined, "." will be used.</li>
  7641. <li><a name="SONOS_attribut_bookmarkTitleDefinition"><b><code>bookmarkTitleDefinition &lt;Groupname&gt;:&lt;PlayerdeviceRegEx&gt;:&lt;TrackURIRegEx&gt;:&lt;MinTitleLength&gt;:&lt;RemainingLength&gt;:&lt;MaxAge&gt;:&lt;ReadOnly&gt;</code></b>
  7642. </a><br /> Definition of Bookmarks for titles.</li>
  7643. <li><a name="SONOS_attribut_bookmarkPlaylistDefinition"><b><code>bookmarkPlaylistDefinition &lt;Groupname&gt;:&lt;PlayerdeviceRegEx&gt;:&lt;MinListLength&gt;:&lt;MaxListLength&gt;:&lt;MaxAge&gt;</code></b>
  7644. </a><br /> Definition of bookmarks for playlists.</li>
  7645. </ul></li>
  7646. <li><b>Proxy Configuration</b><ul>
  7647. <li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><b><code>generateProxyAlbumArtURLs &lt;int&gt;</code></b>
  7648. </a><br />One of (0, 1). If defined, all Cover-Links (the readings "currentAlbumArtURL" and "nextAlbumArtURL") are generated as links to the internal Sonos-Module-Proxy. It can be useful if you access Fhem over an external proxy and therefore have no access to the local network (the URLs are direct URLs to the Sonosplayer instead).</li>
  7649. <li><a name="SONOS_attribut_proxyCacheDir"><b><code>proxyCacheDir &lt;Path&gt;</code></b>
  7650. </a><br />Defines a directory where the cached Coverfiles can be placed. If not defined "/tmp" will be used.</li>
  7651. <li><a name="SONOS_attribut_proxyCacheTime"><b><code>proxyCacheTime &lt;int&gt;</code></b>
  7652. </a><br />A time in seconds. With a definition other than "0" the caching mechanism of the internal Sonos-Module-Proxy will be activated. If the filetime of the chached cover is older than this time, it will be reloaded from the Sonosplayer.</li>
  7653. </ul></li>
  7654. <li><b>Speak Configuration</b><ul>
  7655. <li><a name="SONOS_attribut_targetSpeakDir"><b><code>targetSpeakDir &lt;string&gt;</code></b>
  7656. </a><br /> Defines, which Directory has to be used for the Speakfiles</li>
  7657. <li><a name="SONOS_attribut_targetSpeakMP3FileConverter"><b><code>targetSpeakMP3FileConverter &lt;string&gt;</code></b>
  7658. </a><br /> Defines an MP3-File converter, which properly converts the resulting speaking-file. With this option you can avoid timedisplay problems. Please note that the waittime before the speaking starts can increase with this option be set.</li>
  7659. <li><a name="SONOS_attribut_targetSpeakMP3FileDir"><b><code>targetSpeakMP3FileDir &lt;string&gt;</code></b>
  7660. </a><br /> The directory which should be used as a default for text-embedded MP3-Files.</li>
  7661. <li><a name="SONOS_attribut_targetSpeakURL"><b><code>targetSpeakURL &lt;string&gt;</code></b>
  7662. </a><br /> Defines, which URL has to be used for accessing former stored Speakfiles as seen from the SonosPlayer</li>
  7663. <li><a name="SONOS_attribut_targetSpeakFileTimestamp"><b><code>targetSpeakFileTimestamp &lt;int&gt;</code></b>
  7664. </a><br /> One of (0, 1). Defines, if the Speakfile should have a timestamp in his name. That makes it possible to store all historical Speakfiles.</li>
  7665. <li><a name="SONOS_attribut_targetSpeakFileHashCache"><b><code>targetSpeakFileHashCache &lt;int&gt;</code></b>
  7666. </a><br /> One of (0, 1). Defines, if the Speakfile should have a hash-value in his name. If this value is set to one an already generated file with the same hash is re-used and not newly generated.</li>
  7667. <li><a name="SONOS_attribut_Speak1"><b><code>Speak1 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7668. </a><br />Defines a systemcall commandline for generating a speaking file out of the given text. If such an attribute is defined, an associated setter at the Sonosplayer-Device is available. The following placeholders are available:<br />'''%language%''': Will be replaced by the given language-parameter<br />'''%filename%''': Will be replaced by the complete target-filename (incl. fileextension).<br />'''%text%''': Will be replaced with the given text.<br />'''%textescaped%''': Will be replaced with the given url-encoded text.</li>
  7669. <li><a name="SONOS_attribut_Speak2"><b><code>Speak2 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7670. </a><br />See Speak1</li>
  7671. <li><a name="SONOS_attribut_Speak3"><b><code>Speak3 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7672. </a><br />See Speak1</li>
  7673. <li><a name="SONOS_attribut_Speak4"><b><code>Speak4 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7674. </a><br />See Speak1</li>
  7675. <li><a name="SONOS_attribut_SpeakCover"><b><code>SpeakCover &lt;Filename&gt;</code></b>
  7676. </a><br />Defines a Cover for use by the speak generation process. If not defined the Fhem-logo will be used.</li>
  7677. <li><a name="SONOS_attribut_Speak1Cover"><b><code>Speak1Cover &lt;Filename&gt;</code></b>
  7678. </a><br />See SpeakCover</li>
  7679. <li><a name="SONOS_attribut_Speak2Cover"><b><code>Speak2Cover &lt;Filename&gt;</code></b>
  7680. </a><br />See SpeakCover</li>
  7681. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Filename&gt;</code></b>
  7682. </a><br />See SpeakCover</li>
  7683. <li><a name="SONOS_attribut_Speak4Cover"><b><code>Speak4Cover &lt;Filename&gt;</code></b>
  7684. </a><br />See SpeakCover</li>
  7685. <li><a name="SONOS_attribut_SpeakGoogleURL"><b><code>SpeakGoogleURL &lt;GoogleURL&gt;</code></b>
  7686. </a><br />The google-speak-url that has to be used. If empty a default will be used. You have to define placeholders for replacing the language- and text-value: %1$s -> Language, %2$s -> Text<br />The Default-URL is currently: <code>http://translate.google.com/translate_tts?tl=%1$s&client=tw-ob&q=%2$s</code></li>
  7687. </ul></li>
  7688. </ul>
  7689. =end html
  7690. =begin html_DE
  7691. <a name="SONOS"></a>
  7692. <h3>SONOS</h3>
  7693. <p>FHEM-Modul für die Anbindung des Sonos-Systems via UPnP</p>
  7694. <p>Für weitere Hinweise und Beschreibungen bitte auch im Wiki unter <a href="http://www.fhemwiki.de/wiki/SONOS">http://www.fhemwiki.de/wiki/SONOS</a> nachschauen.</p>
  7695. <p>Für die Verwendung sind Perlmodule notwendig, die unter Umständen noch nachinstalliert werden müssen:<ul>
  7696. <li><code>LWP::Simple</code></li>
  7697. <li><code>LWP::UserAgent</code></li>
  7698. <li><code>SOAP::Lite</code></li>
  7699. <li><code>HTTP::Request</code></li></ul>
  7700. Installation z.B. als Debian-Pakete (mittels "sudo apt-get install &lt;packagename&gt;"):<ul>
  7701. <li>LWP::Simple-Packagename (inkl. LWP::UserAgent und HTTP::Request): libwww-perl</li>
  7702. <li>SOAP::Lite-Packagename: libsoap-lite-perl</li></ul>
  7703. <br />Installation z.B. als Windows ActivePerl (mittels Perl-Packagemanager)<ul>
  7704. <li>Package LWP (incl. LWP::UserAgent and HTTP::Request)</li>
  7705. <li>Package SOAP::Lite</li>
  7706. <li>SOAP::Lite-Special für Versionen nach 5.18:<ul>
  7707. <li>Eine andere Paketquelle von den Vorschlägen oder manuell hinzufügen: Bribes de Perl (http://www.bribes.org/perl/ppm)</li>
  7708. <li>Package: SOAP::Lite</li></ul></li></ul>
  7709. <b>Windows ActivePerl 5.20 kann momentan nicht verwendet werden, da es das Paket SOAP::Lite dort momentan nicht gibt.</b></p>
  7710. <p><b>Achtung!</b><br />Das Modul wird nicht auf jeder Plattform lauffähig sein, da Threads und die angegebenen Perl-Module verwendet werden.</p>
  7711. <p>Mehr Informationen im (deutschen) Wiki-Artikel: <a href="http://www.fhemwiki.de/wiki/SONOS">http://www.fhemwiki.de/wiki/SONOS</a></p>
  7712. <p>Das System besteht aus zwei Komponenten:<br />
  7713. 1. Einem UPnP-Client, der als eigener Prozess im Hintergrund ständig läuft, und die Kommunikation mit den Sonos-Geräten übernimmt.<br />
  7714. 2. Dem eigentlichen FHEM-Modul, welches mit dem UPnP-Client zusammenarbeitet, um die Funktionalität in FHEM zu ermöglichen.<br /><br />
  7715. Der Client wird im Notfall automatisch von Modul selbst gestartet.<br />
  7716. Man kann den Server unabhängig von FHEM selbst starten (um ihn dauerhaft und unabh&auml;ngig von FHEM laufen zu lassen):<br />
  7717. <code>perl 00_SONOS.pm 4711</code>: Startet einen unabhängigen Server, der auf Port 4711 auf eingehende FHEM-Verbindungen lauscht. Dieser Prozess kann dauerhaft laufen, FHEM kann sich verbinden und auch wieder trennen.</p>
  7718. <h4>Beispiel</h4>
  7719. <p>
  7720. Einfachste Definition:<br />
  7721. <b><code>define Sonos SONOS</code></b>
  7722. </p>
  7723. <p>
  7724. Definition mit Kontrolle über den verwendeten Port und das Intervall der IsAlive-Prüfung:<br />
  7725. <b><code>define Sonos SONOS localhost:4711 45</code></b>
  7726. </p>
  7727. <a name="SONOSdefine"></a>
  7728. <h4>Definition</h4>
  7729. <b><code>define &lt;name&gt; SONOS [upnplistener [interval [waittime [delaytime]]]]</code></b>
  7730. <br /><br /> Definiert das Sonos interface für die Kommunikation mit dem Sonos-System.<br />
  7731. <p>
  7732. <b><code>[upnplistener]</code></b><br />Name und Port eines externen UPnP-Client. Wenn nicht angegebenen wird <code>localhost:4711</code> festgelegt. Der Port muss eine freie Portnummer ihres Systems sein. <br />Wenn sie keinen externen Client gestartet haben, startet das Skript einen eigenen.<br />Wenn sie einen eigenen Dienst gestartet haben, dann geben sie hier die entsprechenden Informationen an.</p>
  7733. <p>
  7734. <b><code>[interval]</code></b><br /> Das Interval wird für die Überprüfung eines Zoneplayers benötigt. In diesem Interval wird nachgeschaut, ob der Player noch erreichbar ist, da sich ein Player nicht mehr abmeldet, wenn er abgeschaltet wird :-)<br />Wenn nicht angegeben, wird ein Wert von 10 Sekunden angenommen.</p>
  7735. <p>
  7736. <b><code>[waittime]</code></b><br /> Hiermit wird die Wartezeit eingestellt, die nach dem Starten des SubProzesses darauf gewartet wird.</p>
  7737. <p>
  7738. <b><code>[delaytime]</code></b><br /> Hiermit kann eine Verzögerung eingestellt werden, die vor dem Starten des Netzwerks gewartet wird.</p>
  7739. <a name="SONOSset"></a>
  7740. <h4>Set</h4>
  7741. <ul>
  7742. <li><b>Grundsätzliches</b><ul>
  7743. <li><a name="SONOS_setter_RescanNetwork">
  7744. <b><code>RescanNetwork</code></b></a>
  7745. <br />Startet die Erkennung der im Netzwerk vorhandenen Player erneut.</li>
  7746. </ul></li>
  7747. <li><b>Steuerbefehle</b><ul>
  7748. <li><a name="SONOS_setter_Mute">
  7749. <b><code>Mute &lt;state&gt;</code></b></a>
  7750. <br />Setzt den Mute-Zustand bei allen Playern.</li>
  7751. <li><a name="SONOS_setter_PauseAll">
  7752. <b><code>PauseAll</code></b></a>
  7753. <br />Pausiert die Wiedergabe in allen Zonen.</li>
  7754. <li><a name="SONOS_setter_Pause">
  7755. <b><code>Pause</code></b></a>
  7756. <br />Synonym für PauseAll.</li>
  7757. <li><a name="SONOS_setter_StopAll">
  7758. <b><code>StopAll</code></b></a>
  7759. <br />Stoppt die Wiedergabe in allen Zonen.</li>
  7760. <li><a name="SONOS_setter_Stop">
  7761. <b><code>Stop</code></b></a>
  7762. <br />Synonym für StopAll.</li>
  7763. </ul></li>
  7764. <li><b>Bookmark-Befehle</b><ul>
  7765. <li><a name="SONOS_setter_DisableBookmark">
  7766. <b><code>DisableBookmark &lt;Groupname&gt;</code></b></a>
  7767. <br />Deaktiviert die angegebene Gruppe.</li>
  7768. <li><a name="SONOS_setter_EnableBookmark">
  7769. <b><code>EnableBookmark &lt;Groupname&gt;</code></b></a>
  7770. <br />Aktiviert die angegebene Gruppe.</li>
  7771. <li><a name="SONOS_setter_LoadBookmarks">
  7772. <b><code>LoadBookmarks [Groupname]</code></b></a>
  7773. <br />Lädt die angegebene Gruppe (oder alle Gruppen, wenn nicht angegeben) aus den entsprechenden Dateien.</li>
  7774. <li><a name="SONOS_setter_SaveBookmarks">
  7775. <b><code>SaveBookmarks [Groupname]</code></b></a>
  7776. <br />Speichert die angegebene Gruppe (oder alle Gruppen, wenn nicht angegeben) in die entsprechenden Dateien.</li>
  7777. </ul></li>
  7778. <li><b>Gruppenbefehle</b><ul>
  7779. <li><a name="SONOS_setter_Groups">
  7780. <b><code>Groups &lt;GroupDefinition&gt;</code></b></a>
  7781. <br />Setzt die aktuelle Gruppierungskonfiguration der Sonos-Systemlandschaft. Das Format ist jenes, welches auch von dem Get-Befehl 'Groups' geliefert wird.<br >Hier kann als GroupDefinition das Wort <i>Reset</i> verwendet werden, um alle Player aus ihren Gruppen zu entfernen.</li>
  7782. </ul></li>
  7783. </ul>
  7784. <a name="SONOSget"></a>
  7785. <h4>Get</h4>
  7786. <ul>
  7787. <li><b>Gruppenbefehle</b><ul>
  7788. <li><a name="SONOS_getter_Groups">
  7789. <b><code>Groups</code></b></a>
  7790. <br />Liefert die aktuelle Gruppierungskonfiguration der Sonos Systemlandschaft zurück. Das Format ist eine Kommagetrennte Liste von Listen mit Devicenamen, also z.B. <code>[Sonos_Kueche], [Sonos_Wohnzimmer, Sonos_Schlafzimmer]</code>. In diesem Beispiel sind also zwei Gruppen definiert, von denen die erste aus einem Player und die zweite aus Zwei Playern besteht.<br />
  7791. Dabei ist die Reihenfolge innerhalb der Unterlisten wichtig, da der erste Eintrag der sogenannte Gruppenkoordinator ist (in diesem Fall also <code>Sonos_Wohnzimmer</code>), von dem die aktuelle Abspielliste un der aktuelle Titel auf die anderen Gruppenmitglieder übernommen wird.</li>
  7792. </ul></li>
  7793. </ul>
  7794. <a name="SONOSattr"></a>
  7795. <h4>Attribute</h4>
  7796. '''Hinweis'''<br />Die Attribute werden erst bei einem Neustart von Fhem verwendet, da diese dem SubProzess initial zur Verfügung gestellt werden müssen.
  7797. <ul>
  7798. <li><b>Grundsätzliches</b><ul>
  7799. <li><a name="SONOS_attribut_disable"><b><code>disable &lt;value&gt;</code></b>
  7800. </a><br />Eines von (0,1). Hiermit kann das Modul abgeschaltet werden. Wirkt sofort. Bei 1 wird der SubProzess beendet, und somit keine weitere Verarbeitung durchgeführt. Bei 0 wird der Prozess wieder gestartet.<br />Damit kann das Modul temporär abgeschaltet werden, um bei der Neueinrichtung von Sonos-Komponenten keine halben Zustände mitzubekommen.</li>
  7801. <li><a name="SONOS_attribut_ignoredIPs"><b><code>ignoredIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  7802. </a><br />Mit diesem Attribut können IP-Adressen angegeben werden, die vom UPnP-System ignoriert werden sollen. Z.B.: "192.168.0.11,192.168.0.37"</li>
  7803. <li><a name="SONOS_attribut_pingType"><b><code>pingType &lt;string&gt;</code></b>
  7804. </a><br /> Eines von (none,tcp,udp,icmp,syn). Gibt an, welche Methode für die Ping-Überprüfung verwendet werden soll. Wenn 'none' angegeben wird, dann wird keine Überprüfung gestartet.</li>
  7805. <li><a name="SONOS_attribut_usedonlyIPs"><b><code>usedonlyIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  7806. </a><br />Mit diesem Attribut können IP-Adressen angegeben werden, die ausschließlich vom UPnP-System berücksichtigt werden sollen. Z.B.: "192.168.0.11,192.168.0.37"</li>
  7807. </ul></li>
  7808. <li><b>Bookmark-Einstellungen</b><ul>
  7809. <li><a name="SONOS_attribut_bookmarkSaveDir"><b><code>bookmarkSaveDir &lt;path&gt;</code></b>
  7810. </a><br /> Das Verzeichnis, in dem die Dateien für die gespeicherten Bookmarks abgelegt werden sollen. Wenn nicht festgelegt, dann wird "." verwendet.</li>
  7811. <li><a name="SONOS_attribut_bookmarkTitleDefinition"><b><code>bookmarkTitleDefinition &lt;Groupname&gt;:&lt;PlayerdeviceRegEx&gt;:&lt;TrackURIRegEx&gt;:&lt;MinTitleLength&gt;:&lt;RemainingLength&gt;:&lt;MaxAge&gt;:&lt;ReadOnly&gt; [...]</code></b>
  7812. </a><br /> Die Definition für die Verwendung von Bookmarks für Titel.</li>
  7813. <li><a name="SONOS_attribut_bookmarkPlaylistDefinition"><b><code>bookmarkPlaylistDefinition &lt;Groupname&gt;:&lt;PlayerdeviceRegEx&gt;:&lt;MinListLength&gt;:&lt;MaxListLength&gt;:&lt;MaxAge&gt; [...]</code></b>
  7814. </a><br /> Die Definition für die Verwendung von Bookmarks für aktuelle Abspiellisten/Playlisten.</li>
  7815. </ul></li>
  7816. <li><b>Proxy-Einstellungen</b><ul>
  7817. <li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><b><code>generateProxyAlbumArtURLs &lt;int&gt;</code></b>
  7818. </a><br /> Aus (0, 1). Wenn aktiviert, werden alle Cober-Links als Proxy-Aufrufe an Fhem generiert. Dieser Proxy-Server wird vom Sonos-Modul bereitgestellt. In der Grundeinstellung erfolgt kein Caching der Cover, sondern nur eine Durchreichung der Cover von den Sonosplayern (Damit ist der Zugriff durch einen externen Proxyserver auf Fhem möglich).</li>
  7819. <li><a name="SONOS_attribut_proxyCacheDir"><b><code>proxyCacheDir &lt;Path&gt;</code></b>
  7820. </a><br /> Hiermit wird das Verzeichnis festgelegt, in dem die Cober zwischengespeichert werden. Wenn nicht festegelegt, so wird "/tmp" verwendet.</li>
  7821. <li><a name="SONOS_attribut_proxyCacheTime"><b><code>proxyCacheTime &lt;int&gt;</code></b>
  7822. </a><br /> Mit einer Angabe ungleich 0 wird der Caching-Mechanismus des Sonos-Modul-Proxy-Servers aktiviert. Dabei werden Cover, die im Cache älter sind als diese Zeitangabe in Sekunden, neu vom Sonosplayer geladen, alle anderen direkt ausgeliefert, ohne den Player zu fragen.</li>
  7823. </ul></li>
  7824. <li><b>Sprachoptionen</b><ul>
  7825. <li><a name="SONOS_attribut_targetSpeakDir"><b><code>targetSpeakDir &lt;string&gt;</code></b>
  7826. </a><br /> Gibt an, welches Verzeichnis für die Ablage des MP3-Files der Textausgabe verwendet werden soll</li>
  7827. <li><a name="SONOS_attribut_targetSpeakMP3FileConverter"><b><code>targetSpeakMP3FileConverter &lt;string&gt;</code></b>
  7828. </a><br /> Hiermit kann ein MP3-Konverter angegeben werden, da am Ende der Verkettung der Speak-Ansage das resultierende MP3-File nochmal sauber durchkodiert. Damit können Restzeitanzeigeprobleme behoben werden. Dadurch vegrößert sich allerdings u.U. die Ansageverzögerung.</li>
  7829. <li><a name="SONOS_attribut_targetSpeakMP3FileDir"><b><code>targetSpeakMP3FileDir &lt;string&gt;</code></b>
  7830. </a><br /> Das Verzeichnis, welches als Standard für MP3-Fileangaben in Speak-Texten verwendet werden soll. Wird dieses Attribut definiert, können die Angaben bei Speak ohne Verzeichnis erfolgen.</li>
  7831. <li><a name="SONOS_attribut_targetSpeakURL"><b><code>targetSpeakURL &lt;string&gt;</code></b>
  7832. </a><br /> Gibt an, unter welcher Adresse der ZonePlayer das unter targetSpeakDir angegebene Verzeichnis erreichen kann.</li>
  7833. <li><a name="SONOS_attribut_targetSpeakFileTimestamp"><b><code>targetSpeakFileTimestamp &lt;int&gt;</code></b>
  7834. </a><br /> One of (0, 1). Gibt an, ob die erzeugte MP3-Sprachausgabedatei einen Zeitstempel erhalten soll (1) oder nicht (0).</li>
  7835. <li><a name="SONOS_attribut_targetSpeakFileHashCache"><b><code>targetSpeakFileHashCache &lt;int&gt;</code></b>
  7836. </a><br /> One of (0, 1). Gibt an, ob die erzeugte Sprachausgabedatei einen Hashwert erhalten soll (1) oder nicht (0). Wenn dieser Wert gesetzt wird, dann wird eine bereits bestehende Datei wiederverwendet, und nicht neu erzeugt.</li>
  7837. <li><a name="SONOS_attribut_Speak1"><b><code>Speak1 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7838. </a><br />Hiermit kann ein Systemaufruf definiert werden, der zu Erzeugung einer Sprachausgabe verwendet werden kann. Sobald dieses Attribut definiert wurde, ist ein entsprechender Setter am Sonosplayer verfügbar.<br />Es dürfen folgende Platzhalter verwendet werden:<br />'''%language%''': Wird durch die eingegebene Sprache ersetzt<br />'''%filename%''': Wird durch den kompletten Dateinamen (inkl. Dateiendung) ersetzt.<br />'''%text%''': Wird durch den zu übersetzenden Text ersetzt.<br />'''%textescaped%''': Wird durch den URL-Enkodierten zu übersetzenden Text ersetzt.</li>
  7839. <li><a name="SONOS_attribut_Speak2"><b><code>Speak2 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7840. </a><br />Siehe Speak1</li>
  7841. <li><a name="SONOS_attribut_Speak3"><b><code>Speak3 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7842. </a><br />Siehe Speak1</li>
  7843. <li><a name="SONOS_attribut_Speak4"><b><code>Speak4 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  7844. </a><br />Siehe Speak1</li>
  7845. <li><a name="SONOS_attribut_SpeakCover"><b><code>SpeakCover &lt;Absolute-Imagepath&gt;</code></b>
  7846. </a><br />Hiermit kann ein JPG- oder PNG-Bild als Cover für die Sprachdurchsagen definiert werden.</li>
  7847. <li><a name="SONOS_attribut_Speak1Cover"><b><code>Speak1Cover &lt;Absolute-Imagepath&gt;</code></b>
  7848. </a><br />Analog zu SpeakCover für Speak1.</li>
  7849. <li><a name="SONOS_attribut_Speak2Cover"><b><code>Speak2Cover &lt;Absolute-Imagepath&gt;</code></b>
  7850. </a><br />Analog zu SpeakCover für Speak2.</li>
  7851. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Absolute-Imagepath&gt;</code></b>
  7852. </a><br />Analog zu SpeakCover für Speak3.</li>
  7853. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Absolute-Imagepath&gt;</code></b>
  7854. </a><br />Analog zu SpeakCover für Speak3.</li>
  7855. <li><a name="SONOS_attribut_Speak4Cover"><b><code>Speak4Cover &lt;Absolute-Imagepath&gt;</code></b>
  7856. </a><br />Analog zu SpeakCover für Speak4.</li>
  7857. <li><a name="SONOS_attribut_SpeakGoogleURL"><b><code>SpeakGoogleURL &lt;GoogleURL&gt;</code></b>
  7858. </a><br />Die zu verwendende Google-URL. Wenn dieser Parameter nicht angegeben wird, dann wird ein Standard verwendet. Hier müssen Platzhalter für die Ersetzung durch das Modul eingetragen werden: %1$s -> Sprache, %2$s -> Text<br />Die Standard-URL lautet momentan: <code>http://translate.google.com/translate_tts?tl=%1$s&client=tw-ob&q=%2$s</code></li>
  7859. </ul></li>
  7860. </ul>
  7861. =end html_DE
  7862. =cut