00_SONOS.pm 476 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955
  1. ########################################################################################
  2. #
  3. # SONOS.pm (c) by Reiner Leins, March 2018
  4. # rleins at lmsoft dot de
  5. #
  6. # $Id: 00_SONOS.pm 16376 2018-03-10 16:21:04Z Reinerlein $
  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. #
  53. # SVN-History:
  54. # 10.03.2018
  55. # Die PlayBase kann nun auch den SPDIF-Eingang aktivieren (wie die PlayBar)
  56. # Wenn man über Alexa Musik hört, wird das aktuelle Cover nun auch angezeigt.
  57. # Wenn ein Player nach einem Neustart disabled war, und während des Betriebs enabled wird, wird nun versucht ihn wiederzufinden.
  58. # Wenn ein Player disabled oder disappeared ist, wird ein Proxy-Cover-Zugriffsversuch auf diesen Player unterbunden.
  59. # Ein Modify-Befehlsaufruf wird nun am Vorhandensein von $hash->{OLDDEF} erkannt.
  60. # Bei einigen PERL-Installationen stand im Reading 'currentTrackPositionSimulatedSec' eine Kommazahl (da sie von time() aus berechnet wird). Diese Zahl wird nun gerundet.
  61. # 26.02.2018
  62. # ComObjectTransportQueue in Client_ReceiveQueue umbenannt.
  63. # If-Abfrage um die can_read-Schleife im SubProzess eingebaut, Um Signalunterbrechungen zu berücksichtigen.
  64. # Neuer Getter "WifiPortStatus". Liefert Active, wenn das WLAN aktiviert ist, sonst Inactive.
  65. # Drei neue (automatisch ermittelte) Readings "Orientation", "WifiEnabled" und "WirelessMode".
  66. # Warnung mit "unescaped left brace" in Tag.pm wurde korrigiert.
  67. # ExportSonosBibliothek wird nun in einem eigenen Thread (LongJobs-Thread) ausgeführt. Dadurch bleibt das System steuerbar, auch wenn gerade ein langwieriger Export läuft.
  68. # Prüfmethode eingebaut, um verlorengegangene Fhem-Prozessverbindungen (aus Sicht des SubProzesses) zu erkennen, und entsprechende Thread-Bereinigungmaßnahmen durchführen zu können.
  69. # 07.01.2018
  70. # Der Initialwert von LastProcessAnswer (wird beim Start auf 0 gesetzt) wird nun korrekt berücksichtigt
  71. # Bei ignoredIPs und bei usedOnlyIPs kann jetzt für jedes Komma-Getrennte Element auch ein regulärer Ausdruck stehen. Wird mit // umschlossen, und darf keine Doppelpunkte enthalten.
  72. # Logausgabe im UPnP-Modul, welche Devices mit welchen Header-Angaben nun akzeptiert wurden (Ausgabe auf Level 5)
  73. # 23.12.2017
  74. # Subscriptions-Refresh umgebaut.
  75. # Devicenamen mit Punkt (.) funktionieren nun.
  76. # Fehler mit "undefined value $value" behoben.
  77. # GetTrackProvider liefert bei Nichtfinden in der MusicServicesList nun eine leere Angabe, und keine undefined.
  78. # Die Angabe in LastProcessAnswer ist nicht Zeitumstellungsfest. Der Wert wurde nun umgestellt auf epoch-Zeit.
  79. # Der Verweis auf %intAt wurde entfernt. Die Variable wurde sowieso nie verwendet.
  80. # Warnungsunterdrückung von 'mumpitzstuff' eingebaut.
  81. #
  82. ########################################################################################
  83. #
  84. # This programm is free software; you can redistribute it and/or modify
  85. # it under the terms of the GNU General Public License as published by
  86. # the Free Software Foundation; either version 2 of the License, or
  87. # (at your option) any later version.
  88. #
  89. # The GNU General Public License can be found at
  90. # http://www.gnu.org/copyleft/gpl.html.
  91. # A copy is found in the textfile GPL.txt and important notices to the license
  92. # from the author is found in LICENSE.txt distributed with these scripts.
  93. #
  94. # This script is distributed in the hope that it will be useful,
  95. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  96. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  97. # GNU General Public License for more details.
  98. #
  99. ########################################################################################
  100. # Use-Declarations
  101. ########################################################################################
  102. package main;
  103. use strict;
  104. use warnings;
  105. use Cwd qw(realpath);
  106. use LWP::Simple;
  107. use LWP::UserAgent;
  108. use URI::Escape;
  109. use HTML::Entities;
  110. use Net::Ping;
  111. use Socket;
  112. use IO::Select;
  113. use IO::Socket::INET;
  114. use HTTP::Request::Common;
  115. use File::Path;
  116. use File::stat;
  117. use Time::HiRes qw(usleep gettimeofday);
  118. use Scalar::Util qw(reftype looks_like_number);
  119. use PerlIO::encoding;
  120. use Encode;
  121. use feature 'unicode_strings';
  122. use Digest::MD5 qw(md5_hex);
  123. use File::Temp;
  124. use File::Copy;
  125. # use Encode::Guess;
  126. use Data::Dumper;
  127. $Data::Dumper::Terse = 1;
  128. use threads;
  129. use Thread::Queue;
  130. use threads::shared;
  131. use feature 'state';
  132. # SmartMatch-Fehlermeldung unterdrücken...
  133. no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  134. ########################################################
  135. # IP-Adressen, die vom UPnP-Modul ignoriert werden sollen.
  136. # Diese können über ein Attribut gesetzt werden.
  137. ########################################################
  138. my @ignoredIPs = ();
  139. my @usedonlyIPs = ();
  140. my $reusePort = 0;
  141. ########################################################
  142. # Standards aus FHEM einbinden
  143. ########################################################
  144. use vars qw{%attr %modules %defs %data};
  145. ########################################################
  146. # Prozeduren für den Betrieb des Standalone-Parts
  147. ########################################################
  148. sub Log($$);
  149. sub Log3($$$);
  150. sub SONOS_Log($$$);
  151. sub SONOS_StartClientProcessIfNeccessary($);
  152. sub SONOS_Client_Notifier($);
  153. sub SONOS_Client_ConsumeMessage($$);
  154. sub SONOS_RecursiveStructure($$$$);
  155. sub SONOS_RCLayout();
  156. sub SONOS_URI_Escape($);
  157. ########################################################
  158. # Verrenkungen um in allen Situationen das benötigte
  159. # Modul sauber geladen zu bekommen..
  160. ########################################################
  161. my $gPath = '';
  162. BEGIN {
  163. $gPath = substr($0, 0, rindex($0, '/'));
  164. }
  165. if (lc(substr($0, -7)) eq 'fhem.pl') {
  166. $gPath = $attr{global}{modpath}.'/FHEM';
  167. }
  168. use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './', '/usr/local/FHEM/share/fhem/FHEM/lib');
  169. # print 'Current: "'.$0.'", gPath: "'.$gPath."\"\n";
  170. if (lc(substr($0, -7)) eq 'fhem.pl') {
  171. require 'DevIo.pm';
  172. } else {
  173. use UPnP::ControlPoint;
  174. ########################################################
  175. # Change all carp-calls in the UPnP-Module to croak-calls
  176. # This will ensure you can "catch" carp with an enclosing
  177. # "eval{}"-Block
  178. ########################################################
  179. #*UPnP::ControlPoint::carp = \&UPnP::ControlPoint::croak;
  180. }
  181. my $startedbyfhem = SONOS_isInList('startedbyfhem', @ARGV);
  182. ########################################################################################
  183. # Variable Definitions
  184. ########################################################################################
  185. my %gets = (
  186. 'Groups' => ''
  187. );
  188. my %sets = (
  189. 'Groups' => 'groupdefinitions',
  190. 'StopAll' => '',
  191. 'Stop' => '',
  192. 'PauseAll' => '',
  193. 'Pause' => '',
  194. 'Mute' => 'state',
  195. 'MuteOn' => '',
  196. 'MuteOff' => '',
  197. 'RescanNetwork' => '',
  198. 'RefreshShareIndex' => '',
  199. 'LoadBookmarks' => '[Groupname]',
  200. 'SaveBookmarks' => '[GroupName]',
  201. 'DisableBookmark' => 'groupname',
  202. 'EnableBookmark' => 'groupname'
  203. );
  204. my @SONOS_PossibleDefinitions = qw(NAME INTERVAL);
  205. 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 coverLoadTimeout getListsDirectlyToReadings getFavouritesListAtNewVersion getPlaylistsListAtNewVersion getRadiosListAtNewVersion getQueueListAtNewVersion getTitleInfoFromMaster stopSleeptimerInAction saveSleeptimerInAction webname SubProcessLogfileName);
  206. 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 IsBonded ZoneGroupName roomName roomNameAlias roomIcon currentTransportState transportState TransportState LineInConnected presence currentAlbum currentArtist currentTitle currentStreamAudio GroupVolume GroupMute FavouritesVersion RadiosVersion PlaylistsVersion QueueVersion QueueHash GroupMasterPlayer ShareIndexInProgress DirectControlClientID DirectControlIsSuspended DirectControlAccountID IsMaster MasterPlayer SlavePlayer ButtonState ButtonLockState AllPlayer LineInName LineInIcon MusicServicesListVersion MusicServicesList WifiEnabled WirelessMode Orientation);
  207. # Communication between the two "levels" of threads
  208. my $SONOS_Client_ReceiveQueue = Thread::Queue->new();
  209. my %SONOS_PlayerRestoreRunningUDN :shared = ();
  210. my $SONOS_PlayerRestoreQueue = Thread::Queue->new();
  211. my $SONOS_LongJobsQueue = Thread::Queue->new();
  212. my $SONOS_Thread_LongJobs :shared = -1;
  213. my $SONOS_Thread :shared = -1;
  214. my $SONOS_Thread_IsAlive :shared = -1;
  215. my $SONOS_Thread_PlayerRestore :shared = -1;
  216. my %SONOS_Thread_IsAlive_Counter;
  217. my $SONOS_Thread_IsAlive_Counter_MaxMerci = 2;
  218. # Runtime Variables on Module-Level
  219. my %SONOS_Module_BulkUpdateFromSubProcessInWork;
  220. # Some Constants
  221. my @SONOS_PINGTYPELIST = qw(none tcp udp icmp syn);
  222. my $SONOS_DEFAULTPINGTYPE = 'syn';
  223. my $SONOS_SUBSCRIPTIONSRENEWAL = 1800;
  224. my $SONOS_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11';
  225. #my $SONOS_USERAGENT = 'Linux UPnP/1.0 Sonos/35.3-39010 (WDCR:Microsoft Windows NT 6.1.7601 Service Pack 1)';
  226. 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/">';
  227. my $SONOS_DIDLFooter = '</DIDL-Lite>';
  228. my $SONOS_GOOGLETRANSLATOR_URL = 'http://translate.google.com/translate_tts?tl=%1$s&client=tw-ob&q=%2$s'; # 1->Sprache, 2->Text
  229. my $SONOS_GOOGLETRANSLATOR_CHUNKSIZE = 95;
  230. my $SONOS_DEFAULTCOVERLOADTIMEOUT = 5;
  231. my $SONOS_LISTELEMMASK = '[ \(\)\/\+\?\|\*\{\}\$\^\[\]\#]';
  232. # Basis UPnP-Object und Search-Referenzen
  233. my $SONOS_RestartControlPoint = 0;
  234. my $SONOS_Controlpoint;
  235. my $SONOS_Search;
  236. # Devices merken
  237. my %SONOS_UPnPDevice;
  238. # ControlProxies für spätere Aufrufe für jeden ZonePlayer extra sichern
  239. my %SONOS_AVTransportControlProxy;
  240. my %SONOS_RenderingControlProxy;
  241. my %SONOS_GroupRenderingControlProxy;
  242. my %SONOS_ContentDirectoryControlProxy;
  243. my %SONOS_AlarmClockControlProxy;
  244. my %SONOS_AudioInProxy;
  245. my %SONOS_DevicePropertiesProxy;
  246. my %SONOS_GroupManagementProxy;
  247. my %SONOS_MusicServicesProxy;
  248. my %SONOS_ZoneGroupTopologyProxy;
  249. # Subscriptions müssen für die spätere Erneuerung aufbewahrt werden
  250. my %SONOS_TransportSubscriptions;
  251. my %SONOS_RenderingSubscriptions;
  252. my %SONOS_GroupRenderingSubscriptions;
  253. my %SONOS_ContentDirectorySubscriptions;
  254. my %SONOS_AlarmSubscriptions;
  255. my %SONOS_ZoneGroupTopologySubscriptions;
  256. my %SONOS_DevicePropertiesSubscriptions;
  257. my %SONOS_AudioInSubscriptions;
  258. my %SONOS_MusicServicesSubscriptions;
  259. # Bookmark-Daten
  260. my %SONOS_BookmarkQueueHash;
  261. my %SONOS_BookmarkTitleHash;
  262. my %SONOS_BookmarkQueueDefinition;
  263. my %SONOS_BookmarkTitleDefinition;
  264. my %SONOS_BookmarkSpeicher;
  265. $SONOS_BookmarkSpeicher{OldTracks} = ();
  266. $SONOS_BookmarkSpeicher{NumTracks} = ();
  267. $SONOS_BookmarkSpeicher{OldTrackURIs} = ();
  268. $SONOS_BookmarkSpeicher{OldTitles} = ();
  269. $SONOS_BookmarkSpeicher{OldTrackPositions} = ();
  270. $SONOS_BookmarkSpeicher{OldTrackDurations} = ();
  271. $SONOS_BookmarkSpeicher{OldTransportstate} = ();
  272. # Locations -> UDN der einzelnen Player merken, damit die Event-Verarbeitung schneller geht
  273. my %SONOS_Locations;
  274. # Wenn der Prozess/das Modul nicht von fhem aus gestartet wurde, dann versuchen, den ersten Parameter zu ermitteln
  275. # Für diese Funktionalität werden einige Variablen benötigt
  276. my $SONOS_ListenPort = $ARGV[0] if (lc(substr($0, -7)) ne 'fhem.pl');
  277. my $SONOS_Client_LogLevel :shared = -1;
  278. if ($ARGV[1]) {
  279. $SONOS_Client_LogLevel = $ARGV[1];
  280. }
  281. my $SONOS_mseclog = 0;
  282. if ($ARGV[2]) {
  283. $SONOS_mseclog = $ARGV[2];
  284. }
  285. my $SONOS_Client_LogfileName :shared = '-';
  286. my $SONOS_StartedOwnUPnPServer = 0;
  287. my $SONOS_Client_Selector;
  288. my %SONOS_Client_Data :shared = ();
  289. my $SONOS_Client_SendQueue = Thread::Queue->new();
  290. my $SONOS_Client_SendQueue_Suspend :shared = 0;
  291. my %SONOS_ButtonPressQueue;
  292. ########################################################################################
  293. #
  294. # SONOS_Initialize
  295. #
  296. # Parameter hash = hash of device addressed
  297. #
  298. ########################################################################################
  299. sub SONOS_Initialize ($) {
  300. my ($hash) = @_;
  301. # Provider
  302. $hash->{Clients} = ':SONOSPLAYER:';
  303. # Normal Defines
  304. $hash->{DefFn} = 'SONOS_Define';
  305. $hash->{UndefFn} = 'SONOS_Undef';
  306. $hash->{DeleteFn} = 'SONOS_Delete';
  307. $hash->{ShutdownFn} = 'SONOS_Shutdown';
  308. $hash->{ReadFn} = "SONOS_Read";
  309. $hash->{ReadyFn} = "SONOS_Ready";
  310. $hash->{GetFn} = 'SONOS_Get';
  311. $hash->{SetFn} = 'SONOS_Set';
  312. $hash->{AttrFn} = 'SONOS_Attribute';
  313. $hash->{NotifyFn} = 'SONOS_Notify';
  314. # CGI
  315. my $name = "sonos";
  316. my $fhem_url = "/" . $name ;
  317. $data{FWEXT}{$fhem_url}{FUNC} = "SONOS_FhemWebCallback";
  318. $data{FWEXT}{$fhem_url}{LINK} = $name;
  319. $data{FWEXT}{$fhem_url}{NAME} = undef;
  320. eval {
  321. no strict;
  322. no warnings;
  323. $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 coverLoadTimeout:1,2,3,4,5,6,7,8,9,10,15,20,25,30 getListsDirectlyToReadings:1,0 getFavouritesListAtNewVersion:1,0 getPlaylistsListAtNewVersion:1,0 getRadiosListAtNewVersion:1,0 getQueueListAtNewVersion:1,0 deviceRoomView:Both,DeviceLineOnly reusePort:1,0 webname SubProcessLogfileName getLocalCoverArt '.$readingFnAttributes;
  324. use strict;
  325. use warnings;
  326. };
  327. $data{RC_layout}{Sonos} = "SONOS_RCLayout";
  328. $data{RC_layout}{SonosSVG_Buttons} = "SONOS_RCLayoutSVG1";
  329. $data{RC_layout}{SonosSVG_Icons} = "SONOS_RCLayoutSVG2";
  330. return undef;
  331. }
  332. ########################################################################################
  333. #
  334. # SONOS_RCLayout - Returns the Standard-Layout-Definition for a RemoteControl-Device
  335. #
  336. ########################################################################################
  337. sub SONOS_RCLayout() {
  338. my @rows = ();
  339. push @rows, "Play:PLAY,Pause:PAUSE,Previous:REWIND,Next:FF,:blank,VolumeD:VOLDOWN,VolumeU:VOLUP,:blank,MuteT:MUTE,ShuffleT:SHUFFLE,RepeatT:REPEAT";
  340. push @rows, "attr rc_iconpath icons/remotecontrol";
  341. push @rows, "attr rc_iconprefix black_btn_";
  342. return @rows;
  343. }
  344. ########################################################################################
  345. #
  346. # SONOS_RCLayoutSVG1 - Returns the Standard-Layout-Definition for a RemoteControl-Device
  347. #
  348. ########################################################################################
  349. sub SONOS_RCLayoutSVG1() {
  350. my @rows = ();
  351. 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";
  352. push @rows, "attr rc_iconpath icons/remotecontrol";
  353. push @rows, "attr rc_iconprefix black_btn_";
  354. return @rows;
  355. }
  356. ########################################################################################
  357. #
  358. # SONOS_RCLayoutSVG2 - Returns the Standard-Layout-Definition for a RemoteControl-Device
  359. #
  360. ########################################################################################
  361. sub SONOS_RCLayoutSVG2() {
  362. my @rows = ();
  363. 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";
  364. push @rows, "attr rc_iconpath icons/remotecontrol";
  365. push @rows, "attr rc_iconprefix black_btn_";
  366. return @rows;
  367. }
  368. ########################################################################################
  369. #
  370. # SONOS_LoadExportedSonosBibliothek - Sets the internal Value with the given Name in the given fhem-device with the loaded file given with filename
  371. #
  372. ########################################################################################
  373. sub SONOS_LoadExportedSonosBibliothek($$$) {
  374. my ($fileName, $deviceName, $internalName) = @_;
  375. my $fileInhalt = '';
  376. open FILE, '<'.$fileName;
  377. binmode(FILE, ':encoding(utf-8)');
  378. while (<FILE>) {
  379. $fileInhalt .= $_;
  380. }
  381. close FILE;
  382. $defs{$deviceName}->{$internalName} = eval($fileInhalt);
  383. }
  384. ########################################################################################
  385. #
  386. # SONOS_getCoverTitleRG - Returns the Cover- and Title-Readings for use in a ReadingsGroup
  387. #
  388. ########################################################################################
  389. sub SONOS_getCoverTitleRG($;$$) {
  390. my ($device, $width, $space) = @_;
  391. $width = 500 if (!defined($width));
  392. my $saveDevice = $device;
  393. $saveDevice =~ s/[^a-zA-Z0-9]//g;
  394. my $transportState = ReadingsVal($device, 'transportState', '');
  395. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  396. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  397. my $currentRuntime = 1;
  398. my $currentStarttime = 0;
  399. my $currentPosition = 0;
  400. my $normalAudio = ReadingsVal($device, 'currentNormalAudio', 0);
  401. if ($normalAudio) {
  402. $currentRuntime = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackDuration', '0:00:01'));
  403. $currentRuntime = 1 if (!$currentRuntime || $currentRuntime == 0);
  404. $currentPosition = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackPosition', '0:00:00'));
  405. $currentStarttime = SONOS_GetTimeFromString(ReadingsTimestamp($device, 'currentTrackPosition', SONOS_TimeNow())) - $currentPosition;
  406. }
  407. my $playing = 0;
  408. if ($transportState eq 'PLAYING') {
  409. $playing = 1;
  410. $transportState = FW_makeImage('audio_play', 'Playing', 'SONOS_Transportstate');
  411. }
  412. $transportState = FW_makeImage('audio_pause', 'Paused', 'SONOS_Transportstate') if ($transportState eq 'PAUSED_PLAYBACK');
  413. $transportState = FW_makeImage('audio_stop', 'Stopped', 'SONOS_Transportstate') if ($transportState eq 'STOPPED');
  414. my $fullscreenDiv = '<style type="text/css">.SONOS_Transportstate { height: 0.8em; margin-top: -6px; margin-left: 2px; }</style><div id="cover_current'.$saveDevice.'" 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'.$saveDevice.'\').style.display = \'none\'; document.getElementById(\'global_fulldiv_'.$saveDevice.'\').innerHTML = \'\';"><div style="position: absolute; top: 10px; left: 5px; display: inline-block; height: 35px; width: 35px; background-image: url('.ReadingsVal($device, 'currentTrackProviderIconRoundURL', '').'); background-repeat: no-repeat; background-size: contain; background-position: center center;"></div><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;"><div style="display: inline-block; height: calc(100% - 70px); width: 100%; background-image: url('.((lc($presence) eq 'disappeared') ? '/'.AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'webname', 'fhem').'/sonos/cover/empty.jpg' : ReadingsVal($device, 'currentAlbumArtURL', '')).'); background-repeat: no-repeat; background-size: contain; background-position: center center;"/></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_'.$saveDevice.'" 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_'.$saveDevice.'" style="display: none; color: white;">'.$currentRuntime.'</div><div id="prog_starttime_'.$saveDevice.'" style="display: none; color: white;">'.$currentStarttime.'</div><div id="prog_playing_'.$saveDevice.'" style="display: none; color: white;">'.$playing.'</div><div id="progress'.$saveDevice.'" style="position: absolute; bottom: 0px; width: 100%; height: 2px; border: 1px solid #000; overflow: hidden;"><div id="progressbar'.$saveDevice.'" style="width: '.(($currentPosition * 100) / $currentRuntime).'%; height: 2px; border-right: 1px solid #000000; background: #d65946;"></div></div>' : '').'</div>';
  415. my $javascriptTimer = 'function refreshTime'.$saveDevice.'() {
  416. var playing = document.getElementById("prog_playing_'.$saveDevice.'");
  417. if (!playing || (playing && (playing.innerHTML == "0"))) {
  418. return;
  419. }
  420. var runtime = document.getElementById("prog_runtime_'.$saveDevice.'");
  421. var starttime = document.getElementById("prog_starttime_'.$saveDevice.'");
  422. if (runtime && starttime) {
  423. var now = new Date().getTime();
  424. var percent = (Math.round(now / 10.0) - Math.round(starttime.innerHTML * 100.0)) / runtime.innerHTML;
  425. document.getElementById("progressbar'.$saveDevice.'").style.width = percent + "%";
  426. setTimeout(refreshTime'.$saveDevice.', 100);
  427. }
  428. }';
  429. my $javascriptText = '<script type="text/javascript">
  430. if (!document.getElementById("global_fulldiv_'.$saveDevice.'")) {
  431. var newDiv = document.createElement("div");
  432. newDiv.setAttribute("id", "global_fulldiv_'.$saveDevice.'");
  433. document.body.appendChild(newDiv);
  434. var newScript = document.createElement("script");
  435. newScript.setAttribute("type", "text/javascript");
  436. newScript.appendChild(document.createTextNode(\'function refreshFull'.$saveDevice.'() {
  437. var fullDiv = document.getElementById("element_fulldiv_'.$saveDevice.'");
  438. if (!fullDiv) {
  439. return;
  440. }
  441. var elementHTML = decodeURIComponent(fullDiv.innerHTML);
  442. var global = document.getElementById("global_fulldiv_'.$saveDevice.'");
  443. var oldGlobal = global.innerHTML;
  444. var hash = document.getElementById("hash_'.$saveDevice.'");
  445. var hashMatch = /<div id="hash_'.$saveDevice.'".*?>(.+?)<.div>/i;
  446. hashMatch.exec(elementHTML);
  447. if ((oldGlobal != "") && (!hash || (hash.innerHTML != RegExp.$1))) {
  448. global.innerHTML = elementHTML;
  449. }
  450. if (oldGlobal != "") {
  451. setTimeout(refreshFull'.$saveDevice.', 1000);
  452. var playing = document.getElementById("prog_playing_'.$saveDevice.'");
  453. if (playing && playing.innerHTML == "1") {
  454. setTimeout(refreshTime'.$saveDevice.', 100);
  455. }
  456. }
  457. } '.$javascriptTimer.'\'));
  458. document.body.appendChild(newScript);
  459. }
  460. </script>';
  461. $javascriptText =~ s/\n/ /g;
  462. return $javascriptText.'<table cellpadding="0" cellspacing="0" style="padding: 0px; margin: 0px;"><tr><td valign="top" style="padding: 0px; margin: 0px;"><div style="" onclick="document.getElementById(\'global_fulldiv_'.$saveDevice.'\').innerHTML = \'&nbsp;\'; refreshFull'.$saveDevice.'(); '.($playing ? 'refreshTime'.$saveDevice.'();' : '').'">'.SONOS_getCoverRG($device).'</div><div style="display: none;" id="element_fulldiv_'.$saveDevice.'">'.SONOS_URI_Escape($fullscreenDiv).'</div></td><td valign="top" style="padding: 0px; margin: 0px;"><div style="margin-left: 0px; min-width: '.$width.'px;">'.SONOS_getTitleRG($device, $space).'</div></td></tr></table>';
  463. }
  464. ########################################################################################
  465. #
  466. # SONOS_getCoverRG - Returns the Cover-Readings for use in a ReadingsGroup
  467. #
  468. ########################################################################################
  469. sub SONOS_getCoverRG($;$) {
  470. my ($device, $height) = @_;
  471. $height = '10.75em' if (!defined($height));
  472. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  473. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  474. return '<div informid="'.$device.'-display_covertitle"><div style="display: inline-block; margin-right: 5px; border: 1px solid lightgray; height: '.$height.'; width: '.$height.'; background-image: url('.((lc($presence) eq 'disappeared') ? '/'.AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'webname', 'fhem').'/sonos/cover/empty.jpg' : ReadingsVal($device, 'currentAlbumArtURL', '')).'); background-repeat: no-repeat; background-size: contain; background-position: center center;"><div style="position: relative; top: 0px; left: 2px; display: inline-block; height: 15px; width: 15px; background-image: url('.ReadingsVal($device, 'currentTrackProviderIconRoundURL', '').'); background-repeat: no-repeat; background-size: contain; background-position: center center;"></div></div></div>';
  475. }
  476. ########################################################################################
  477. #
  478. # SONOS_getTitleRG - Returns the Title-Readings for use in a ReadingsGroup
  479. #
  480. ########################################################################################
  481. sub SONOS_getTitleRG($;$) {
  482. my ($device, $space) = @_;
  483. $space = '2.35em' if (!defined($space));
  484. $space .= 'px' if (looks_like_number($space));
  485. # Wenn der Player weg ist, nur eine Kurzinfo dazu anzeigen
  486. my $presence = ReadingsVal($device, 'presence', 'disappeared');
  487. $presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
  488. if (lc($presence) eq 'disappeared') {
  489. return '<div style="margin-left: 0px;">Player disappeared</div>';
  490. }
  491. my $infoString = '';
  492. my $transportState = ReadingsVal($device, 'transportState', '');
  493. $transportState = 'Spiele' if ($transportState eq 'PLAYING');
  494. $transportState = 'Pausiere' if ($transportState eq 'PAUSED_PLAYBACK');
  495. $transportState = 'Stop bei' if ($transportState eq 'STOPPED');
  496. my $source = ReadingsVal($device, 'currentSource', '');
  497. # Läuft Radio oder ein "normaler" Titel
  498. my $currentNormalAudio = ReadingsVal($device, 'currentNormalAudio', 1);
  499. $currentNormalAudio = 0 if (SONOS_Trim($currentNormalAudio) eq '');
  500. if ($currentNormalAudio == 1) {
  501. my $showNext = ReadingsVal($device, 'nextTitle', '') || ReadingsVal($device, 'nextArtist', '') || ReadingsVal($device, 'nextAlbum', '');
  502. $infoString = sprintf('<div style="display: inline-block; margin-left: 0px; vertical-align: top;">%1$s Titel %2$s von %3$s'.(($source) ? ' ~ <b>'.$source.'</b>' : '').'<br />Titel: <b>%4$s</b><br />Interpret: <b>%5$s</b><br />Album: <b>%6$s</b>'.($showNext ? '<div style="display: block; height: %7$s; display: table-cell; vertical-align: bottom;">Nächste Wiedergabe:</div><table cellpadding="0px" cellspacing="0px" style="padding: 0px; margin: 0px;"><tr><td valign="top" style="padding: 0px; margin: 0px;"><div style="display: inline-block; margin-left: 0px; margin-right: 5px; border: 1px solid lightgray; height: 3.5em; width: 3.5em; background-image: url(%8$s); background-repeat: no-repeat; background-size: contain; background-position: center center;"><div style="position: relative; top: -5px; left: 2px; display: inline-block; height: 10px; width: 10px; background-image: url(%9$s); background-repeat: no-repeat; background-size: contain; background-position: center center;"></div></div></td><td valign="top" style="padding: 0px; margin: 0px;"><div style="">Titel: %10$s<br />Interpret: %11$s<br />Album: %12$s</div></td></tr></table>' : '').'</div>',
  503. $transportState,
  504. ReadingsVal($device, 'currentTrack', ''),
  505. ReadingsVal($device, 'numberOfTracks', ''),
  506. ReadingsVal($device, 'currentTitle', ''),
  507. ReadingsVal($device, 'currentArtist', ''),
  508. ReadingsVal($device, 'currentAlbum', ''),
  509. $space,
  510. ReadingsVal($device, 'nextAlbumArtURL', ''),
  511. ReadingsVal($device, 'nextTrackProviderIconRoundURL', ''),
  512. ReadingsVal($device, 'nextTitle', ''),
  513. ReadingsVal($device, 'nextArtist', ''),
  514. ReadingsVal($device, 'nextAlbum', ''));
  515. } else {
  516. $infoString = sprintf('<div style="display: inline-block; margin-left: 0px;">%s Radiostream<br />Sender: <b>%s</b><br />Info: <b>%s</b><br />Läuft: <b>%s</b></div>',
  517. $transportState,
  518. ReadingsVal($device, 'currentSender', ''),
  519. ReadingsVal($device, 'currentSenderInfo', ''),
  520. ReadingsVal($device, 'currentSenderCurrent', ''));
  521. }
  522. return $infoString;
  523. }
  524. ########################################################################################
  525. #
  526. # SONOS_getListRG - Returns the approbriate list-Reading for use in a ReadingsGroup
  527. #
  528. ########################################################################################
  529. sub SONOS_getListRG($$;$) {
  530. my ($device, $reading, $ul) = @_;
  531. $ul = 0 if (!defined($ul));
  532. my $resultString = '';
  533. # Manchmal ist es etwas komplizierter mit den Zeichensätzen...
  534. #my %elems = %{eval(decode('CP1252', ReadingsVal($device, $reading, '{}')))};
  535. my %elems = %{eval(ReadingsVal($device, $reading, '{}'))};
  536. for my $key (sort keys %elems) {
  537. my $command = '';
  538. if ($reading eq 'Favourites') {
  539. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartFavourite '.SONOS_URI_Escape($elems{$key}->{Title}));
  540. } elsif ($reading eq 'Playlists') {
  541. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartPlaylist '.SONOS_URI_Escape($elems{$key}->{Title}));
  542. } elsif ($reading eq 'Radios') {
  543. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' StartRadio '.SONOS_URI_Escape($elems{$key}->{Title}));
  544. } elsif ($reading eq 'Queue') {
  545. next if (($key eq 'Duration') || ($key eq 'DurationSec'));
  546. $command = 'cmd.'.$device.SONOS_URI_Escape('=set '.$device.' Track '.$elems{$key}->{Position});
  547. }
  548. $command = "FW_cmd('/fhem?XHR=1&$command')";
  549. if ($ul) {
  550. $resultString .= '<li style="list-style-type: none; display: inline;"><div style="display: inline-block; border: solid 1px lightgray; margin: 3px; width: 70px; height: 70px; background-image: url('.$elems{$key}->{Cover}.'); background-repeat: no-repeat; background-size: contain; background-position: center center;" onclick="'.$command.'"/></li>';
  551. } else {
  552. $resultString .= '<tr><td><div style="list-style-type: none; display: inline;"><div style="border: solid 1px lightgray; margin: 3px; width: 70px; height: 70px; background-image: url('.$elems{$key}->{Cover}.'); background-repeat: no-repeat; background-size: contain; background-position: center center;" /></td><td><a onclick="'.$command.'">'.(($reading eq 'Queue') ? $elems{$key}->{ShowTitle} : $elems{$key}->{Title})."</a></td></tr>\n";
  553. }
  554. }
  555. if ($ul) {
  556. return '<ul style="margin-left: 0px; padding-left: 0px; list-style-type: none; display: inline;">'.$resultString.'</ul>';
  557. } else {
  558. return '<table>'.$resultString.'</table>';
  559. }
  560. }
  561. ########################################################################################
  562. #
  563. # SONOS_getGroupsRG - Returns a simple group-constellation-list for use in a ReadingsGroup
  564. #
  565. ########################################################################################
  566. sub SONOS_getGroupsRG() {
  567. my $groups = CommandGet(undef, SONOS_getSonosPlayerByName()->{NAME}.' Groups');
  568. my $result = '<ul>';
  569. my $i = 0;
  570. while ($groups =~ m/\[(.*?)\]/ig) {
  571. my @member = split(/, /, $1);
  572. @member = map { my $elem = $_; $elem = FW_makeImage('icoSONOSPLAYER_icon-'.ReadingsVal($elem, 'playerType', '').'.png', '', '').ReadingsVal($elem, 'roomNameAlias', $elem); $elem; } @member;
  573. $result .= '<li>'.++$i.'. Gruppe:<ul style="list-style-type: none; padding-left: 0px;"><li>'.join('</li><li>', @member).'</li></ul></li>';
  574. }
  575. return $result.'</ul>';
  576. }
  577. ########################################################################################
  578. #
  579. # SONOS_FhemWebCallback - Implements a Webcallback e.g. a small proxy for Cover-images.
  580. #
  581. ########################################################################################
  582. sub SONOS_FhemWebCallback($) {
  583. my ($URL) = @_;
  584. SONOS_Log undef, 5, 'FhemWebCallback: '.$URL;
  585. # Einfache Grundprüfungen
  586. return ("text/html; charset=UTF8", 'Forbidden call: '.$URL) if ($URL !~ m/^\/sonos\//i);
  587. $URL =~ s/^\/sonos//i;
  588. # Proxy-Features...
  589. if ($URL =~ m/^\/proxy\//i) {
  590. return ("text/html; charset=UTF8", 'No Proxy configured: '.$URL) if (!AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'generateProxyAlbumArtURLs', 0));
  591. my $proxyCacheTime = AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'proxyCacheTime', 0);
  592. my $proxyCacheDir = AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'proxyCacheDir', '/tmp');
  593. $proxyCacheDir =~ s/\\/\//g;
  594. # Zurückzugebende Adresse ermitteln...
  595. my $albumurl = uri_unescape($1) if ($URL =~ m/^\/proxy\/aa\?url=(.*)/i);
  596. $albumurl =~ s/&apos;/'/ig;
  597. SONOS_Log undef, 5, 'FHEMWEB-AlbumURL: '.$albumurl;
  598. # Nur für Sonos-Player den Proxy spielen (und für Spotify-Links)
  599. my $ip = '';
  600. $ip = $1 if ($albumurl =~ m/^http:\/\/(.*?)[:\/]/i);
  601. my $player_found = 0;
  602. my $player_inactive = 0;
  603. for my $player (SONOS_getAllSonosplayerDevices()) {
  604. if (ReadingsVal($player->{NAME}, 'location', '') =~ m/^http:\/\/$ip:/i) {
  605. if ((ReadingsVal($player->{NAME}, 'presence', '') eq 'disappeared') || AttrVal($player->{NAME}, 'disable', 0)) {
  606. $player_inactive = 1;
  607. }
  608. $player_found = 1;
  609. last;
  610. }
  611. }
  612. SONOS_Log undef, 5, 'FHEMWEB-PlayerFound: '.$player_found;
  613. SONOS_Log undef, 5, 'FHEMWEB-PlayerInactive: '.$player_inactive;
  614. # Für Inaktive oder andere Player, einfach das leere Cover liefern...
  615. if (!$player_found) {
  616. if ($albumurl !~ /\.cloudfront.net\//i
  617. && $albumurl !~ /\.scdn.co\//i
  618. && $albumurl !~ /sonos-logo\.ws\.sonos\.com\//i
  619. && $albumurl !~ /\.tunein\.com/i
  620. && $albumurl !~ /\/music\/image\?/i
  621. && $albumurl !~ /media-amazon\.com/i) {
  622. return ("text/html; charset=UTF8", 'Call for Non-Sonos-Player: '.$URL);
  623. }
  624. } elsif ($player_inactive) {
  625. FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  626. return (undef, undef);
  627. }
  628. # Generierter Dateiname für die Cache-Funktionalitaet
  629. my $albumHash;
  630. # Schauen, ob die Datei aus dem Cache bedient werden kann...
  631. if ($proxyCacheTime) {
  632. eval {
  633. require Digest::SHA1;
  634. import Digest::SHA1 qw(sha1_hex);
  635. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc($albumurl)).'.image';
  636. };
  637. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  638. # FallBack auf Digest::SHA durchführen...
  639. eval {
  640. require Digest::SHA;
  641. import Digest::SHA qw(sha1_hex);
  642. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc($albumurl)).'.image';
  643. };
  644. }
  645. if ($@ =~ /Wide character in subroutine entry/i) {
  646. eval {
  647. require Digest::SHA1;
  648. import Digest::SHA1 qw(sha1_hex);
  649. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc(encode("iso-8859-1", $albumurl, 0))).'.image';
  650. };
  651. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  652. eval {
  653. require Digest::SHA;
  654. import Digest::SHA qw(sha1_hex);
  655. $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc(encode("iso-8859-1", $albumurl, 0))).'.image';
  656. };
  657. }
  658. }
  659. if ($@) {
  660. SONOS_Log undef, 1, 'Problem while generating Hashvalue: '.$@;
  661. return(undef, undef);
  662. }
  663. if ((-e $albumHash) && ((stat($albumHash)->mtime) + $proxyCacheTime > gettimeofday())) {
  664. SONOS_Log undef, 5, 'Cover wird aus Cache bedient: '.$albumHash.' ('.$albumurl.')';
  665. $albumHash =~ m/(.*)\/(.*)\.(.*)/;
  666. FW_serveSpecial($2, $3, $1, 1);
  667. return(undef, undef);
  668. }
  669. }
  670. # Bild vom Player holen...
  671. my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
  672. my $response = $ua->get($albumurl);
  673. if ($response->is_success) {
  674. SONOS_Log undef, 5, 'Cover wurde neu geladen: '.$albumurl;
  675. my $tempFile;
  676. if ($proxyCacheTime) {
  677. unlink $albumHash if (-e $albumHash);
  678. SONOS_Log undef, 5, 'Cover wird im Cache abgelegt: '.$albumHash.' ('.$albumurl.')';
  679. } else {
  680. # Da wir die Standard-Prozedur 'FW_serveSpecial' aus 'FHEMWEB' verwenden moechten, brauchen wir eine lokale Datei
  681. $tempFile = File::Temp->new(SUFFIX => '.image');
  682. $albumHash = $tempFile->filename;
  683. $albumHash =~ s/\\/\//g;
  684. SONOS_Log undef, 5, 'TempFilename: '.$albumHash;
  685. }
  686. # Either Tempfile or Cachefile...
  687. SONOS_WriteFile($albumHash, $response->content);
  688. $albumHash =~ m/(.*)\/(.*)\.(.*)/;
  689. FW_serveSpecial($2, $3, $1, 1);
  690. return (undef, undef);
  691. } else {
  692. SONOS_Log undef, 1, 'Cover couldn\'t be loaded "'.$albumurl.'": '.$response->status_line,;
  693. FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  694. return (undef, undef);
  695. }
  696. }
  697. # Cover-Features...
  698. if ($URL =~ m/^\/cover\//i) {
  699. $URL =~ s/^\/cover//i;
  700. SONOS_Log undef, 5, 'Cover: '.$URL;
  701. if ($URL =~ m/^\/leer.gif/i) {
  702. FW_serveSpecial('sonos_leer', 'gif', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  703. return (undef, undef);
  704. }
  705. if ($URL =~ m/^\/tunein_quadratic.jpg/i) {
  706. FW_serveSpecial('sonos_tunein_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  707. return (undef, undef);
  708. }
  709. if ($URL =~ m/^\/tunein_round.png/i) {
  710. FW_serveSpecial('sonos_tunein_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  711. return (undef, undef);
  712. }
  713. if ($URL =~ m/^\/bibliothek_quadratic.jpg/i) {
  714. FW_serveSpecial('sonos_bibliothek_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  715. return (undef, undef);
  716. }
  717. if ($URL =~ m/^\/bibliothek_round.png/i) {
  718. FW_serveSpecial('sonos_bibliothek_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  719. return (undef, undef);
  720. }
  721. if ($URL =~ m/^\/linein_quadratic.jpg/i) {
  722. FW_serveSpecial('sonos_linein_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  723. return (undef, undef);
  724. }
  725. if ($URL =~ m/^\/linein_round.png/i) {
  726. FW_serveSpecial('sonos_linein_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  727. return (undef, undef);
  728. }
  729. if ($URL =~ m/^\/dock_quadratic.jpg/i) {
  730. FW_serveSpecial('sonos_dock_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  731. return (undef, undef);
  732. }
  733. if ($URL =~ m/^\/dock_round.png/i) {
  734. FW_serveSpecial('sonos_dock_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  735. return (undef, undef);
  736. }
  737. if ($URL =~ m/^\/playbar_quadratic.jpg/i) {
  738. FW_serveSpecial('sonos_playbar_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  739. return (undef, undef);
  740. }
  741. if ($URL =~ m/^\/playbar_round.png/i) {
  742. FW_serveSpecial('sonos_playbar_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  743. return (undef, undef);
  744. }
  745. if ($URL =~ m/^\/empty.jpg/i) {
  746. FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  747. return (undef, undef);
  748. }
  749. if ($URL =~ m/^\/playlist.jpg/i) {
  750. FW_serveSpecial('sonos_playlist', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  751. return (undef, undef);
  752. }
  753. if ($URL =~ m/^\/input_default.jpg/i) {
  754. FW_serveSpecial('sonos_input_default', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  755. return (undef, undef);
  756. }
  757. if ($URL =~ m/^\/input_tv.jpg/i) {
  758. FW_serveSpecial('sonos_input_tv', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  759. return (undef, undef);
  760. }
  761. if ($URL =~ m/^\/input_dock.jpg/i) {
  762. FW_serveSpecial('sonos_input_dock', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
  763. return (undef, undef);
  764. }
  765. }
  766. # Wenn wir hier ankommen, dann konnte nichts verarbeitet werden...
  767. return ("text/html; charset=UTF8", 'Call failure: '.$URL);
  768. }
  769. ########################################################################################
  770. #
  771. # SONOS_Define - Implements DefFn function
  772. #
  773. # Parameter hash = hash of device addressed
  774. # def = definition string
  775. #
  776. ########################################################################################
  777. sub SONOS_Define($$) {
  778. my ($hash, $def) = @_;
  779. my @a = split("[ \t]+", $def);
  780. # Check if we just want a modify...
  781. if (defined($hash->{OLDDEF})) {
  782. SONOS_Log undef, 1, 'Modify Device: '.$hash->{NAME};
  783. # Alle Timer entfernen...
  784. RemoveInternalTimer($hash);
  785. # SubProzess beenden, und Verbindung kappen...
  786. SONOS_StopSubProcess($hash);
  787. }
  788. # check syntax
  789. return 'Usage: define <name> SONOS [[[[upnplistener] interval] waittime] delaytime]' if($#a < 1 || $#a > 5);
  790. my $name = $a[0];
  791. my $upnplistener;
  792. if ($a[2] && !looks_like_number($a[2])) {
  793. $upnplistener = $a[2];
  794. } else {
  795. $upnplistener = 'localhost:4711';
  796. }
  797. my $interval;
  798. if (looks_like_number($a[3])) {
  799. $interval = $a[3];
  800. if ($interval < 10) {
  801. SONOS_Log undef, 0, 'Interval has to be a minimum of 10 sec. and not: '.$interval;
  802. $interval = 10;
  803. }
  804. } else {
  805. $interval = 30;
  806. }
  807. my $waittime;
  808. if (looks_like_number($a[4])) {
  809. $waittime = $a[4];
  810. } else {
  811. $waittime = 8;
  812. }
  813. my $delaytime;
  814. if (looks_like_number($a[5])) {
  815. $delaytime = $a[5];
  816. } else {
  817. $delaytime = 0;
  818. }
  819. # Wir brauchen momentan nur die Notifies für global und jeden Sonosplayer...
  820. $hash->{NOTIFYDEV} = 'global';
  821. $hash->{NAME} = $name;
  822. $hash->{DeviceName} = $upnplistener;
  823. $hash->{INTERVAL} = $interval;
  824. $hash->{WAITTIME} = $waittime;
  825. $hash->{DELAYTIME} = $delaytime;
  826. $hash->{STATE} = 'waiting for subprocess...';
  827. if (AttrVal($hash->{NAME}, 'disable', 0) == 0) {
  828. if ($hash->{DELAYTIME}) {
  829. InternalTimer(gettimeofday() + $hash->{DELAYTIME}, 'SONOS_DelayStart', $hash, 0);
  830. } else {
  831. InternalTimer(gettimeofday() + 1, 'SONOS_DelayStart', $hash, 0);
  832. }
  833. }
  834. return undef;
  835. }
  836. ########################################################################################
  837. #
  838. # SONOS_DelayStart - Starts the SubProcess with a Delay. Can solute problems with blocked Ports
  839. #
  840. ########################################################################################
  841. sub SONOS_DelayStart($) {
  842. my ($hash) = @_;
  843. return undef if (AttrVal($hash->{NAME}, 'disable', 0));
  844. # Prüfen, ob ein Server erreichbar wäre, und wenn nicht, einen Server starten
  845. SONOS_StartClientProcessIfNeccessary($hash->{DeviceName});
  846. InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
  847. }
  848. ########################################################################################
  849. #
  850. # SONOS_DelayOpenDev - Starts the IO-Connection with a Delay.
  851. #
  852. ########################################################################################
  853. sub SONOS_DelayOpenDev($) {
  854. my ($hash) = @_;
  855. # Die Datenverbindung zu dem gemachten Server hier starten und initialisieren
  856. DevIo_OpenDev($hash, 0, "SONOS_InitClientProcessLater");
  857. }
  858. ########################################################################################
  859. #
  860. # SONOS_Attribute - Implements AttrFn function
  861. #
  862. ########################################################################################
  863. sub SONOS_Attribute($$$@) {
  864. my ($mode, $devName, $attrName, $attrValue) = @_;
  865. my $disableChange = 0;
  866. my @attrListToSubProcess = qw(getListsDirectlyToReadings);
  867. if ($mode eq 'set') {
  868. if ($attrName eq 'verbose') {
  869. SONOS_DoWork('undef', 'setVerbose', $attrValue);
  870. } elsif ($attrName eq 'SubProcessLogfileName') {
  871. SONOS_DoWork('undef', 'setLogfileName', $attrValue);
  872. } elsif (SONOS_isInList($attrName, @attrListToSubProcess)) {
  873. SONOS_DoWork('undef', 'setAttribute', $attrName, $attrValue);
  874. } elsif ($attrName eq 'disable') {
  875. if ($attrValue && AttrVal($devName, $attrName, 0) != 1) {
  876. SONOS_Log(undef, 5, 'Neu-Disabled');
  877. $disableChange = 1;
  878. }
  879. if (!$attrValue && AttrVal($devName, $attrName, 0) != 0) {
  880. SONOS_Log(undef, 5, 'Neu-Enabled');
  881. $disableChange = 1;
  882. }
  883. } elsif ($attrName eq 'deviceRoomView') {
  884. $modules{SONOSPLAYER}->{FW_addDetailToSummary} = ($attrValue =~ m/(Both)/i) if (defined($modules{SONOSPLAYER}));
  885. }
  886. } elsif ($mode eq 'del') {
  887. if ($attrName eq 'disable') {
  888. if (AttrVal($devName, $attrName, 0) != 0) {
  889. SONOS_Log(undef, 5, 'Deleted-Disabled');
  890. $disableChange = 1;
  891. $attrValue = 0;
  892. }
  893. } elsif ($attrName eq 'deviceRoomView') {
  894. $modules{SONOSPLAYER}->{FW_addDetailToSummary} = 1 if (defined($modules{SONOSPLAYER}));
  895. }
  896. }
  897. if ($disableChange) {
  898. my $hash = SONOS_getSonosPlayerByName();
  899. # Wenn der Prozess beendet werden muss...
  900. if ($attrValue) {
  901. SONOS_Log undef, 5, 'Call AttributeFn: Stop SubProcess...';
  902. InternalTimer(gettimeofday() + 1, 'SONOS_StopSubProcess', $hash, 0);
  903. }
  904. # Wenn der Prozess gestartet werden muss...
  905. if (!$attrValue) {
  906. SONOS_Log undef, 5, 'Call AttributeFn: Start SubProcess...';
  907. InternalTimer(gettimeofday() + 1, 'SONOS_DelayStart', $hash, 0);
  908. }
  909. }
  910. return undef;
  911. }
  912. ########################################################################################
  913. #
  914. # SONOS_StopSubProcess - Tries to stop the subprocess
  915. #
  916. ########################################################################################
  917. sub SONOS_StopSubProcess($) {
  918. my ($hash) = @_;
  919. RemoveInternalTimer($hash);
  920. # Wenn wir einen eigenen UPnP-Server gestartet haben, diesen hier auch wieder beenden,
  921. # ansonsten nur die Verbindung kappen
  922. if ($SONOS_StartedOwnUPnPServer) {
  923. DevIo_SimpleWrite($hash, "shutdown\n", 2);
  924. } else {
  925. DevIo_SimpleWrite($hash, "disconnect\n", 2);
  926. }
  927. DevIo_CloseDev($hash);
  928. setReadingsVal($hash, "state", 'disabled', TimeNow());
  929. $hash->{STATE} = 'disabled';
  930. # Alle SonosPlayer-Devices disappearen
  931. for my $player (SONOS_getAllSonosplayerDevices()) {
  932. SONOS_readingsBeginUpdate($player);
  933. SONOS_readingsBulkUpdateIfChanged($player, 'presence', 'disappeared');
  934. SONOS_readingsBulkUpdateIfChanged($player, 'state', 'disappeared');
  935. SONOS_readingsBulkUpdateIfChanged($player, 'transportState', 'STOPPED');
  936. SONOS_readingsEndUpdate($player, 1);
  937. if (AttrVal($player->{NAME}, 'stateVariable', '') eq 'Presence') {
  938. $player->{STATE} = 'disappeared';
  939. }
  940. }
  941. }
  942. ########################################################################################
  943. #
  944. # SONOS_Notify - Implements NotifyFn function
  945. #
  946. ########################################################################################
  947. sub SONOS_Notify() {
  948. my ($hash, $notifyhash) = @_;
  949. return if (AttrVal($hash->{NAME}, 'disable', 0));
  950. my $events = deviceEvents($notifyhash, 1);
  951. return if(!$events);
  952. foreach my $event (@{$events}) {
  953. next if(!defined($event));
  954. #SONOS_Log $hash->{UDN}, 0, 'Event: '.$notifyhash->{NAME}.'~'.$event;
  955. # Wenn der Benutzer das Kommando 'Save' ausgeführt hat, dann auch die Bookmarks sichern...
  956. if (($notifyhash->{NAME} eq 'global') && ($event eq 'SAVE')) {
  957. SONOS_DoWork('SONOS', 'SaveBookmarks', '');
  958. }
  959. }
  960. return undef;
  961. }
  962. ########################################################################################
  963. #
  964. # SONOS_Ready - Implements ReadyFn function
  965. #
  966. # Parameter hash = hash of device addressed
  967. #
  968. ########################################################################################
  969. sub SONOS_Ready($) {
  970. my ($hash) = @_;
  971. return DevIo_OpenDev($hash, 1, "SONOS_InitClientProcessLater");
  972. }
  973. ########################################################################################
  974. #
  975. # SONOS_Read - Implements ReadFn function
  976. #
  977. # Parameter hash = hash of device addressed
  978. #
  979. ########################################################################################
  980. sub SONOS_Read($) {
  981. my ($hash) = @_;
  982. # Checker erstmal deaktivieren...
  983. RemoveInternalTimer($hash, 'SONOS_IsSubprocessAliveChecker');
  984. return undef if AttrVal($hash->{NAME}, 'disable', 0);
  985. # Bis zum letzten (damit der Puffer leer ist) Zeilenumbruch einlesen, da SimpleRead immer nur kleine Päckchen einliest.
  986. my $buf = DevIo_SimpleRead($hash);
  987. # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden...
  988. if (!defined($buf) || ($buf eq '')) {
  989. # Checker aktivieren...
  990. InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  991. select(undef, undef, undef, 0.001);
  992. return undef;
  993. }
  994. # Wenn noch nicht alles gekommen ist, dann hier auf den Rest warten...
  995. while ($buf !~ m/\n$/) {
  996. my $newRead = DevIo_SimpleRead($hash);
  997. # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden...
  998. if (!defined($newRead) || ($newRead eq '')) {
  999. # Checker aktivieren...
  1000. InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1001. return undef;
  1002. }
  1003. # Wenn es neue Daten gibt, dann anhängen...
  1004. $buf .= $newRead;
  1005. }
  1006. # Die aktuellen Abspielinformationen werden Schritt für Schritt übertragen, gesammelt und dann in einem Rutsch ausgewertet.
  1007. # Dafür eignet sich eine Sub-Statische Variable am Besten.
  1008. state %current;
  1009. # Hier könnte jetzt eine ganze Liste von Anweisungen enthalten sein, die jedoch einzeln verarbeitet werden müssen
  1010. # Dabei kann der Trenner ein Zeilenumbruch sein, oder ein Tab-Zeichen.
  1011. foreach my $line (split(/[\n\a]/, SONOS_Trim($buf))) {
  1012. # Abschließende Zeilenumbrüche abschnippeln
  1013. $line =~ s/[\r\n]*$//;
  1014. SONOS_Log undef, 5, "Received from UPnP-Server: '$line'";
  1015. # Hier empfangene Werte verarbeiten
  1016. if ($line =~ m/^ReadingsSingleUpdateIfChanged:(.*?):(.*?):(.*)/) {
  1017. if (lc($1) eq 'undef') {
  1018. SONOS_readingsSingleUpdateIfChanged(SONOS_getSonosPlayerByName(), $2, $3, 1);
  1019. } else {
  1020. my $hash = SONOS_getSonosPlayerByUDN($1);
  1021. if ($hash) {
  1022. SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 1);
  1023. } else {
  1024. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChanged: $1:$2:$3";
  1025. }
  1026. }
  1027. } elsif ($line =~ m/^ReadingsSingleUpdateIfChangedNoTrigger:(.*?):(.*?):(.*)/) {
  1028. if (lc($1) eq 'undef') {
  1029. SONOS_readingsSingleUpdateIfChanged(SONOS_getSonosPlayerByName(), $2, $3, 0);
  1030. } else {
  1031. my $hash = SONOS_getSonosPlayerByUDN($1);
  1032. if ($hash) {
  1033. SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 0);
  1034. } else {
  1035. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChangedNoTrigger: $1:$2:$3";
  1036. }
  1037. }
  1038. } elsif ($line =~ m/^ReadingsSingleUpdate:(.*?):(.*?):(.*)/) {
  1039. if (lc($1) eq 'undef') {
  1040. SONOS_readingsSingleUpdate(SONOS_getSonosPlayerByName(), $2, $3, 1);
  1041. } else {
  1042. my $hash = SONOS_getSonosPlayerByUDN($1);
  1043. if ($hash) {
  1044. SONOS_readingsSingleUpdate($hash, $2, $3, 1);
  1045. } else {
  1046. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdate: $1:$2:$3";
  1047. }
  1048. }
  1049. } elsif ($line =~ m/^ReadingsBulkUpdate:(.*?):(.*?):(.*)/) {
  1050. my $hash = undef;
  1051. if (lc($1) eq 'undef') {
  1052. $hash = SONOS_getSonosPlayerByName();
  1053. } else {
  1054. $hash = SONOS_getSonosPlayerByUDN($1);
  1055. }
  1056. if ($hash) {
  1057. readingsBulkUpdate($hash, $2, $3);
  1058. } else {
  1059. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdate: $1:$2:$3";
  1060. }
  1061. } elsif ($line =~ m/^ReadingsBulkUpdateIfChanged:(.*?):(.*?):(.*)/) {
  1062. my $hash = undef;
  1063. if (lc($1) eq 'undef') {
  1064. $hash = SONOS_getSonosPlayerByName();
  1065. } else {
  1066. $hash = SONOS_getSonosPlayerByUDN($1);
  1067. }
  1068. if ($hash) {
  1069. SONOS_readingsBulkUpdateIfChanged($hash, $2, $3);
  1070. } else {
  1071. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdateIfChanged: $1:$2:$3";
  1072. }
  1073. } elsif ($line =~ m/ReadingsBeginUpdate:(.*)/) {
  1074. my $hash = undef;
  1075. if (lc($1) eq 'undef') {
  1076. $hash = SONOS_getSonosPlayerByName();
  1077. } else {
  1078. $hash = SONOS_getSonosPlayerByUDN($1);
  1079. }
  1080. if ($hash) {
  1081. SONOS_readingsBeginUpdate($hash, 1);
  1082. } else {
  1083. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBeginUpdate: $1";
  1084. }
  1085. } elsif ($line =~ m/ReadingsEndUpdate:(.*)/) {
  1086. my $hash = undef;
  1087. if (lc($1) eq 'undef') {
  1088. $hash = SONOS_getSonosPlayerByName();
  1089. } else {
  1090. $hash = SONOS_getSonosPlayerByUDN($1);
  1091. }
  1092. if ($hash) {
  1093. SONOS_readingsEndUpdate($hash, 1, 1);
  1094. } else {
  1095. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsEndUpdate: $1";
  1096. }
  1097. } elsif ($line =~ m/CommandDefine:(.*)/) {
  1098. CommandDefine(undef, $1);
  1099. } elsif ($line =~ m/CommandAttr:(.*)/) {
  1100. CommandAttr(undef, $1);
  1101. } elsif ($line =~ m/CommandAttrWithUDN:(.*?):(.*)/) {
  1102. my $hash = SONOS_getSonosPlayerByUDN($1);
  1103. CommandAttr(undef, $hash->{NAME}.' '.$2);
  1104. } elsif ($line =~ m/CommandDeleteAttr:(.*)/) {
  1105. CommandDeleteAttr(undef, $1);
  1106. } elsif ($line =~ m/deleteCurrentNextTitleInformationAndDisappear:(.*)/) {
  1107. my $hash = SONOS_getSonosPlayerByUDN($1);
  1108. # Start the updating...
  1109. SONOS_readingsBeginUpdate($hash);
  1110. # Updating...
  1111. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", '');
  1112. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", '');
  1113. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackHandle", '');
  1114. SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportURI", '');
  1115. SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportHandle", '');
  1116. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", '');
  1117. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDurationSec", '');
  1118. readingsBulkUpdate($hash, "currentTrackPosition", '');
  1119. readingsBulkUpdate($hash, "currentTrackPositionSec", 0);
  1120. SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", 'Disappeared');
  1121. SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", '');
  1122. SONOS_readingsBulkUpdateIfChanged($hash, "currentSource", '');
  1123. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", '');
  1124. SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", '');
  1125. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", '');
  1126. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtURL", '/'.AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'webname', 'fhem').'/sonos/cover/empty.jpg');
  1127. SONOS_readingsBulkUpdateIfChanged($hash, "currentSender", '');
  1128. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderCurrent", '');
  1129. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderInfo", '');
  1130. SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", 0);
  1131. SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", 1);
  1132. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", '');
  1133. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDurationSec", '');
  1134. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", '');
  1135. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackHandle", '');
  1136. SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", '');
  1137. SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", '');
  1138. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", '');
  1139. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtist", '');
  1140. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtURL", '/'.AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'webname', 'fhem').'/sonos/cover/empty.jpg');
  1141. SONOS_readingsBulkUpdateIfChanged($hash, "nextOriginalTrackNumber", '');
  1142. # End the Bulk-Update, and trigger events...
  1143. SONOS_readingsEndUpdate($hash, 1);
  1144. } elsif ($line =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) {
  1145. my $hash = SONOS_getSonosPlayerByUDN($1);
  1146. if ($hash) {
  1147. %current = SONOS_GetReadingsToCurrentHash($hash->{NAME}, $2);
  1148. } else {
  1149. SONOS_Log undef, 0, "Fehlerhafter Aufruf von GetReadingsToCurrentHash: $1:$2";
  1150. }
  1151. } elsif ($line =~ m/SetCurrent:(.*?):(.*)/) {
  1152. $current{$1} = $2;
  1153. } elsif ($line =~ m/CurrentBulkUpdate:(.*)/) {
  1154. my $hash = SONOS_getSonosPlayerByUDN($1);
  1155. if ($hash) {
  1156. SONOS_readingsBeginUpdate($hash);
  1157. my $oldTransportState = ReadingsVal($hash->{NAME}, 'transportState', 0);
  1158. my $oldTrackHandle = ReadingsVal($hash->{NAME}, 'currentTrackHandle', '');
  1159. my $oldTrack = ReadingsVal($hash->{NAME}, 'currentTrack', '');
  1160. my $oldTrackPosition = ReadingsVal($hash->{NAME}, 'currentTrackPosition', '');
  1161. # Wurden für das Device bereits Favoriten geladen? Dann raussuchen, ob gerade ein solcher abgespielt wird...
  1162. $current{FavouriteName} = '';
  1163. eval {
  1164. my $readingsValue = ReadingsVal($hash->{NAME}, 'Favourites', '');
  1165. if ($readingsValue ne '') {
  1166. my %favourites = %{eval($readingsValue)};
  1167. while (my ($key, $value) = each (%favourites)) {
  1168. if (defined($current{EnqueuedTransportURI}) && defined($value->{Ressource})) {
  1169. if ($value->{Ressource} eq $current{EnqueuedTransportURI}) {
  1170. $current{FavouriteName} = $value->{Title};
  1171. }
  1172. }
  1173. }
  1174. }
  1175. };
  1176. if ($@) {
  1177. SONOS_Log $hash->{UDN}, 1, "Error during retreiving of FavouriteName: $@";
  1178. }
  1179. # Wurden für das Device bereits Playlisten geladen? Dann raussuchen, ob gerade eine solche abgespielt wird...
  1180. $current{PlaylistName} = '';
  1181. eval {
  1182. my $readingsValue = ReadingsVal($hash->{NAME}, 'Playlists', '');
  1183. if ($readingsValue ne '') {
  1184. my %playlists = %{eval($readingsValue)};
  1185. while (my ($key, $value) = each (%playlists)) {
  1186. if (defined($current{EnqueuedTransportURI}) && defined($value->{Ressource})) {
  1187. if ($value->{Ressource} eq $current{EnqueuedTransportURI}) {
  1188. $current{PlaylistName} = $value->{Title};
  1189. }
  1190. }
  1191. }
  1192. }
  1193. };
  1194. if ($@) {
  1195. SONOS_Log $hash->{UDN}, 1, "Error during retreiving of PlaylistName: $@";
  1196. }
  1197. # Wurden für das Device bereits Radios geladen? Dann raussuchen, ob gerade ein solches abgespielt wird...
  1198. $current{RadioName} = '';
  1199. eval {
  1200. my $readingsValue = ReadingsVal($hash->{NAME}, 'Radios', '');
  1201. if ($readingsValue ne '') {
  1202. my %radios = %{eval($readingsValue)};
  1203. while (my ($key, $value) = each (%radios)) {
  1204. if (defined($current{EnqueuedTransportURI}) && defined($value->{Ressource})) {
  1205. if ($value->{Ressource} eq $current{EnqueuedTransportURI}) {
  1206. $current{RadioName} = $value->{Title};
  1207. }
  1208. }
  1209. }
  1210. }
  1211. };
  1212. if ($@) {
  1213. SONOS_Log $hash->{UDN}, 1, "Error during retreiving of RadioName: $@";
  1214. }
  1215. # Dekodierung durchführen
  1216. $current{Title} = decode_entities($current{Title});
  1217. $current{Artist} = decode_entities($current{Artist});
  1218. $current{Album} = decode_entities($current{Album});
  1219. $current{AlbumArtist} = decode_entities($current{AlbumArtist});
  1220. $current{Sender} = decode_entities($current{Sender});
  1221. $current{SenderCurrent} = decode_entities($current{SenderCurrent});
  1222. $current{SenderInfo} = decode_entities($current{SenderInfo});
  1223. $current{nextTitle} = decode_entities($current{nextTitle});
  1224. $current{nextArtist} = decode_entities($current{nextArtist});
  1225. $current{nextAlbum} = decode_entities($current{nextAlbum});
  1226. $current{nextAlbumArtist} = decode_entities($current{nextAlbumArtist});
  1227. SONOS_readingsBulkUpdateIfChanged($hash, "transportState", $current{TransportState});
  1228. SONOS_readingsBulkUpdateIfChanged($hash, "Shuffle", $current{Shuffle});
  1229. SONOS_readingsBulkUpdateIfChanged($hash, "Repeat", $current{Repeat});
  1230. SONOS_readingsBulkUpdateIfChanged($hash, "RepeatOne", $current{RepeatOne});
  1231. SONOS_readingsBulkUpdateIfChanged($hash, "CrossfadeMode", $current{CrossfadeMode});
  1232. SONOS_readingsBulkUpdateIfChanged($hash, "SleepTimer", $current{SleepTimer});
  1233. SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunning", $current{AlarmRunning});
  1234. SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunningID", $current{AlarmRunningID});
  1235. SONOS_readingsBulkUpdateIfChanged($hash, "DirectControlClientID", $current{DirectControlClientID});
  1236. SONOS_readingsBulkUpdateIfChanged($hash, "DirectControlIsSuspended", $current{DirectControlIsSuspended});
  1237. SONOS_readingsBulkUpdateIfChanged($hash, "DirectControlAccountID", $current{DirectControlAccountID});
  1238. SONOS_readingsBulkUpdateIfChanged($hash, "numberOfTracks", $current{NumberOfTracks});
  1239. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", $current{Track});
  1240. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", $current{TrackURI});
  1241. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackHandle", $current{TrackHandle});
  1242. SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportURI", $current{EnqueuedTransportURI});
  1243. SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportHandle", $current{EnqueuedTransportHandle});
  1244. SONOS_readingsBulkUpdateIfChanged($hash, "currentFavouriteName", $current{FavouriteName});
  1245. SONOS_readingsBulkUpdateIfChanged($hash, "currentPlaylistName", $current{PlaylistName});
  1246. SONOS_readingsBulkUpdateIfChanged($hash, "currentRadioName", $current{RadioName});
  1247. if (AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'getListsDirectlyToReadings', 0)) {
  1248. my $val = $current{FavouriteName};
  1249. $val =~ s/[ \(\)]/\./g;
  1250. SONOS_readingsBulkUpdateIfChanged($hash, "currentFavouriteNameMasked", $val);
  1251. $val = $current{PlaylistName};
  1252. $val =~ s/[ \(\)]/\./g;
  1253. SONOS_readingsBulkUpdateIfChanged($hash, "currentPlaylistNameMasked", $val);
  1254. $val = $current{RadioName};
  1255. $val =~ s/[ \(\)]/\./g;
  1256. SONOS_readingsBulkUpdateIfChanged($hash, "currentRadioNameMasked", $val);
  1257. }
  1258. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", $current{TrackDuration});
  1259. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDurationSec", $current{TrackDurationSec});
  1260. if ($current{StreamAudio} && ($oldTransportState eq $current{TransportState})) {
  1261. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", '0:00:00');
  1262. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPositionSec", '0');
  1263. } else {
  1264. readingsBulkUpdate($hash, "currentTrackPosition", $current{TrackPosition});
  1265. readingsBulkUpdate($hash, "currentTrackPositionSec", $current{TrackPositionSec});
  1266. }
  1267. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProvider", $current{TrackProvider});
  1268. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProviderIconQuadraticURL", $current{TrackProviderIconQuadraticURL});
  1269. SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProviderIconRoundURL", $current{TrackProviderIconRoundURL});
  1270. SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", $current{Title});
  1271. SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", $current{Artist});
  1272. SONOS_readingsBulkUpdateIfChanged($hash, "currentSource", $current{Source});
  1273. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", $current{Album});
  1274. SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", $current{OriginalTrackNumber});
  1275. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", $current{AlbumArtist});
  1276. SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtURL", $current{AlbumArtURL});
  1277. SONOS_readingsBulkUpdateIfChanged($hash, "currentSender", $current{Sender});
  1278. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderCurrent", $current{SenderCurrent});
  1279. SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderInfo", $current{SenderInfo});
  1280. SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", $current{StreamAudio});
  1281. SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", $current{NormalAudio});
  1282. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", $current{nextTrackDuration});
  1283. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDurationSec", $current{nextTrackDurationSec});
  1284. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", $current{nextTrackURI});
  1285. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackHandle", $current{nextTrackHandle});
  1286. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProvider", $current{nextTrackProvider});
  1287. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProviderIconQuadraticURL", $current{nextTrackProviderIconQuadraticURL});
  1288. SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProviderIconRoundURL", $current{nextTrackProviderIconRoundURL});
  1289. SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", $current{nextTitle});
  1290. SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", $current{nextArtist});
  1291. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", $current{nextAlbum});
  1292. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtist", $current{nextAlbumArtist});
  1293. SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtURL", $current{nextAlbumArtURL});
  1294. SONOS_readingsBulkUpdateIfChanged($hash, "nextOriginalTrackNumber", $current{nextOriginalTrackNumber});
  1295. SONOS_readingsBulkUpdateIfChanged($hash, "Volume", $current{Volume});
  1296. SONOS_readingsBulkUpdateIfChanged($hash, "Mute", $current{Mute});
  1297. SONOS_readingsBulkUpdateIfChanged($hash, "Balance", $current{Balance});
  1298. SONOS_readingsBulkUpdateIfChanged($hash, "HeadphoneConnected", $current{HeadphoneConnected});
  1299. my $name = $hash->{NAME};
  1300. # 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
  1301. if (AttrVal($name, 'generateSomethingChangedEvent', 0) == 1) {
  1302. readingsBulkUpdate($hash, "somethingChanged", 1);
  1303. }
  1304. # 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
  1305. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize1', 1);
  1306. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize2', 1);
  1307. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize3', 1);
  1308. SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize4', 1);
  1309. # 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...
  1310. SONOS_readingsBulkUpdateIfChanged($hash, 'state', $current{AttrVal($name, 'stateVariable', 'TransportState')});
  1311. # End the Bulk-Update, and trigger events
  1312. SONOS_readingsEndUpdate($hash, 1);
  1313. # Wenn es ein Dock ist, dann noch jeden abspielenden Player mit aktualisieren
  1314. if (ReadingsVal($hash->{NAME}, 'playerType', '') eq 'WD100') {
  1315. my $shortUDN = $1 if ($hash->{UDN} =~ m/(.*)_MR/);
  1316. for my $elem (SONOS_getAllSonosplayerDevices()) {
  1317. # Wenn es ein Player ist, der gerade das Dock wiedergibt, dann diesen Befüllen...
  1318. if (ReadingsVal($elem->{NAME}, 'currentTrackURI', '') eq 'x-sonos-dock:'.$shortUDN) {
  1319. # Alte Werte holen, muss komplett sein, um infoSummarize füllen zu können
  1320. my %currentElem = SONOS_GetReadingsToCurrentHash($elem->{NAME}, 0);
  1321. $currentElem{Title} = $current{Title};
  1322. $currentElem{Artist} = $current{Artist};
  1323. $currentElem{Album} = $current{Album};
  1324. $currentElem{AlbumArtist} = $current{AlbumArtist};
  1325. $currentElem{Track} = $current{Track};
  1326. $currentElem{NumberOfTracks} = $current{NumberOfTracks};
  1327. $currentElem{TrackDuration} = $current{TrackDuration};
  1328. $currentElem{TrackDurationSec} = $current{TrackDurationSec};
  1329. $currentElem{TrackPosition} = $current{TrackPosition};
  1330. $currentElem{TrackProvider} = $current{TrackProvider};
  1331. $currentElem{TrackProviderIconQuadraticURL} = $current{TrackProviderIconQuadraticURL};
  1332. $currentElem{TrackProviderIconRoundURL} = $current{TrackProviderIconRoundURL};
  1333. # Loslegen
  1334. SONOS_readingsBeginUpdate($elem);
  1335. # Neue Werte setzen
  1336. SONOS_readingsBulkUpdateIfChanged($elem, "currentTitle", $currentElem{Title});
  1337. SONOS_readingsBulkUpdateIfChanged($elem, "currentArtist", $currentElem{Artist});
  1338. SONOS_readingsBulkUpdateIfChanged($elem, "currentAlbum", $currentElem{Album});
  1339. SONOS_readingsBulkUpdateIfChanged($elem, "currentAlbumArtist", $currentElem{AlbumArtist});
  1340. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrack", $currentElem{Track});
  1341. SONOS_readingsBulkUpdateIfChanged($elem, "numberOfTracks", $currentElem{NumberOfTracks});
  1342. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackDuration", $currentElem{TrackDuration});
  1343. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackDurationSec", $currentElem{TrackDurationSec});
  1344. readingsBulkUpdate($elem, "currentTrackPosition", $currentElem{TrackPosition});
  1345. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProvider", $currentElem{TrackProvider});
  1346. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProviderIconQuadraticURL", $currentElem{TrackProviderIconQuadraticURL});
  1347. SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProviderIconRoundURL", $currentElem{TrackProviderIconRoundURL});
  1348. if (AttrVal($elem->{NAME}, 'generateSomethingChangedEvent', 0) == 1) {
  1349. readingsBulkUpdate($elem, "somethingChanged", 1);
  1350. }
  1351. # InfoSummarize befüllen
  1352. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize1', 1);
  1353. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize2', 1);
  1354. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize3', 1);
  1355. SONOS_ProcessInfoSummarize($elem, \%currentElem, 'InfoSummarize4', 1);
  1356. # State-Reading befüllen
  1357. SONOS_readingsBulkUpdateIfChanged($elem, 'state', $currentElem{AttrVal($elem->{NAME}, 'stateVariable', 'TransportState')});
  1358. # Alles verarbeiten lassen
  1359. SONOS_readingsEndUpdate($elem, 1);
  1360. }
  1361. }
  1362. }
  1363. # SimulatedValues aktualisieren, wenn ein Wechsel des Titels stattgefunden hat, und gerade keine Wiedergabe erfolgt...
  1364. if (($current{TransportState} ne 'PLAYING')
  1365. && (($oldTrackHandle ne $current{TrackHandle})
  1366. || ($oldTrack != $current{Track})
  1367. || ($oldTrackPosition ne $current{TrackPosition}))) {
  1368. SONOSPLAYER_SimulateCurrentTrackPosition($hash);
  1369. }
  1370. } else {
  1371. SONOS_Log undef, 0, "Fehlerhafter Aufruf von CurrentBulkUpdate: $1";
  1372. }
  1373. } elsif ($line =~ m/PropagateTitleInformationsToSlaves:(.*)/) {
  1374. my $hash = SONOS_getSonosPlayerByName($1);
  1375. SONOS_PropagateTitleInformationsToSlaves($hash);
  1376. } elsif ($line =~ m/PropagateTitleInformationsToSlave:(.*?):(.*)/) {
  1377. my $hash = SONOS_getSonosPlayerByName($1);
  1378. SONOS_PropagateTitleInformationsToSlave($hash, $2);
  1379. } elsif ($line =~ m/ProcessCover:(.*?):(.*?):(.*?):(.*)/) {
  1380. my $hash = SONOS_getSonosPlayerByUDN($1);
  1381. if ($hash) {
  1382. my $name = $hash->{NAME};
  1383. my $nextReading = 'current';
  1384. my $nextName = '';
  1385. if ($2) {
  1386. $nextReading = 'next';
  1387. $nextName = 'Next';
  1388. }
  1389. my $tempURI = uri_unescape($3);
  1390. my $groundURL = uri_unescape($4);
  1391. my $currentValue;
  1392. my $srcURI = '';
  1393. my $getLocalCoverArt = AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'getLocalCoverArt', 0);
  1394. if (defined($tempURI) && $tempURI ne '') {
  1395. if ($tempURI =~ m/getaa.*?x-sonos-spotify%3aspotify%3atrack%3a(.*)%3f/i) {
  1396. my $infos = SONOS_getSpotifyCoverURL($1);
  1397. if ($infos ne '') {
  1398. $srcURI = $infos;
  1399. if ($getLocalCoverArt) {
  1400. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg';
  1401. SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1402. }
  1403. } else {
  1404. $srcURI = $groundURL.$tempURI;
  1405. if ($getLocalCoverArt) {
  1406. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI);
  1407. SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download failed. Use normal thumbnail: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1408. }
  1409. }
  1410. } elsif ($tempURI =~ m/getaa.*?x-sonosapi-stream%3a(.+?)%3f/i) {
  1411. $srcURI = SONOS_GetRadioMediaMetadata($hash->{UDN}, $1);
  1412. eval {
  1413. my $result = SONOS_ReadURL($srcURI);
  1414. if (!defined($result) || ($result =~ m/<Error>.*<\/Error>/i)) {
  1415. $srcURI = $groundURL.$tempURI;
  1416. }
  1417. };
  1418. if ($getLocalCoverArt) {
  1419. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.png';
  1420. SONOS_Log undef, 4, "Transport-Event: Radiocover-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1421. }
  1422. } elsif ($tempURI =~ m/^\/fhem\/sonos\/cover\/(.*)/i) {
  1423. $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_'.$1;
  1424. if ($getLocalCoverArt) {
  1425. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg';
  1426. SONOS_Log undef, 4, "Transport-Event: Cover-Copy: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1427. }
  1428. } else {
  1429. $srcURI = $groundURL.$tempURI;
  1430. if ($getLocalCoverArt) {
  1431. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI);
  1432. SONOS_Log undef, 4, "Transport-Event: Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1433. }
  1434. }
  1435. } else {
  1436. $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_empty.jpg';
  1437. if ($getLocalCoverArt) {
  1438. $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.png';
  1439. SONOS_Log undef, 4, "Transport-Event: CoverArt konnte nicht gefunden werden. Verwende FHEM-Logo. Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
  1440. }
  1441. }
  1442. my $filechanged = 0;
  1443. if ($getLocalCoverArt) {
  1444. mkpath($attr{global}{modpath}.'/www/images/default/SONOSPLAYER/');
  1445. $filechanged = SONOS_DownloadReplaceIfChanged($srcURI, $currentValue);
  1446. # Icons neu einlesen lassen, falls die Datei neu ist
  1447. SONOS_RefreshIconsInFHEMWEB('/www/images/default/SONOSPLAYER/') if ($filechanged);
  1448. SONOS_Log undef, 4, 'Transport-Event: CoverArt wurde geladen...';
  1449. } else {
  1450. SONOS_Log undef, 4, 'Transport-Event: CoverArt wurde nicht geladen, weil das Attribut "getLocalCoverArt" nicht gesetzt ist...';
  1451. }
  1452. # Die URL noch beim aktuellen Titel mitspeichern
  1453. my $URL = $srcURI;
  1454. if ($URL =~ m/\/lib\/UPnP\/sonos_(.*)/i) {
  1455. $URL = '/'.AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'webname', 'fhem').'/sonos/cover/'.$1;
  1456. } else {
  1457. my $sonosName = SONOS_getSonosPlayerByName()->{NAME};
  1458. $URL = '/'.AttrVal($sonosName, 'webname', 'fhem').'/sonos/proxy/aa?url='.SONOS_URI_Escape($URL) if (AttrVal($sonosName, 'generateProxyAlbumArtURLs', 0));
  1459. }
  1460. if ($nextReading eq 'next') {
  1461. $current{nextAlbumArtURL} = $URL;
  1462. } else {
  1463. $current{AlbumArtURL} = $URL;
  1464. }
  1465. # 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
  1466. if ($getLocalCoverArt) {
  1467. if ($filechanged) {
  1468. SONOS_readingsSingleUpdate($hash, $nextReading.'AlbumArtURI', $currentValue, 1);
  1469. } else {
  1470. SONOS_readingsSingleUpdateIfChanged($hash, $nextReading.'AlbumArtURI', $currentValue, 1);
  1471. }
  1472. }
  1473. } else {
  1474. SONOS_Log undef, 0, "Fehlerhafter Aufruf von ProcessCover: $1:$2:$3:$4";
  1475. }
  1476. } elsif ($line =~ m/^SetAlarm:(.*?):(.*?);(.*?):(.*)/) {
  1477. my $hash = SONOS_getSonosPlayerByUDN($1);
  1478. my @alarmIDs = split(/,/, $3);
  1479. if ($4) {
  1480. SONOS_readingsSingleUpdate($hash, 'AlarmList', $4, 0);
  1481. } else {
  1482. SONOS_readingsSingleUpdate($hash, 'AlarmList', '{}', 0);
  1483. }
  1484. SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListIDs', join(',', sort {$a <=> $b} @alarmIDs), 0);
  1485. SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListVersion', $2, 1);
  1486. } elsif ($line =~ m/DoWorkAnswer:(.*?):(.*?):(.*)/) {
  1487. my $chash;
  1488. if (lc($1) eq 'undef') {
  1489. $chash = SONOS_getSonosPlayerByName();
  1490. } else {
  1491. $chash = SONOS_getSonosPlayerByUDN($1);
  1492. }
  1493. if ($chash) {
  1494. SONOS_Log undef, 4, "DoWorkAnswer arrived for ".$chash->{NAME}."->$2: '$3'";
  1495. SONOS_readingsSingleUpdate($chash, $2, $3, 1);
  1496. } else {
  1497. SONOS_Log undef, 0, "Fehlerhafter Aufruf von DoWorkAnswer: $1:$2:$3";
  1498. }
  1499. } elsif ($line =~ m/rePing:/) {
  1500. # Zunächst mal nichts weiter tun, hier geht es nur um die Aktualisierung der letzten Prozessantwort...
  1501. } else {
  1502. SONOS_DoTriggerInternal('Main', $line);
  1503. }
  1504. }
  1505. # LastAnswer aktualisieren...
  1506. SONOS_readingsSingleUpdate(SONOS_getSonosPlayerByName(), 'LastProcessAnswer', time(), 1);
  1507. # Checker aktivieren...
  1508. InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1509. }
  1510. ########################################################################################
  1511. #
  1512. # SONOS_PropagateTitleInformationsToSlaves - Propagates the Titleinformations to all Slaveplayers
  1513. #
  1514. # Parameter hash = Hash of the MasterPlayer
  1515. #
  1516. ########################################################################################
  1517. sub SONOS_PropagateTitleInformationsToSlaves($) {
  1518. my ($hash) = @_;
  1519. SONOS_Log $hash->{UDN}, 5, 'Player: '.$hash->{NAME}.' ~ Slaves: '.ReadingsVal($hash->{NAME}, 'SlavePlayer', '[]');
  1520. eval {
  1521. foreach my $slavePlayer (@{eval(ReadingsVal($hash->{NAME}, 'SlavePlayer', '[]'))}) {
  1522. SONOS_PropagateTitleInformationsToSlave($hash, $slavePlayer);
  1523. }
  1524. };
  1525. return undef;
  1526. }
  1527. ########################################################################################
  1528. #
  1529. # SONOS_PropagateTitleInformationsToSlave - Propagates the Titleinformations to one (given) Slaveplayer
  1530. #
  1531. # Parameter hash = Hash of the MasterPlayer
  1532. #
  1533. ########################################################################################
  1534. sub SONOS_PropagateTitleInformationsToSlave($$) {
  1535. my ($hash, $slavePlayer) = @_;
  1536. my $slaveHash = SONOS_getSonosPlayerByName($slavePlayer);
  1537. return if (!defined($hash) || !defined($slaveHash));
  1538. SONOS_Log $hash->{UDN}, 5, 'PropagateTitleInformationsToSlave('.$hash->{NAME}.' => '.$slaveHash->{NAME}.')';
  1539. if (AttrVal($slaveHash->{NAME}, 'getTitleInfoFromMaster', 0)) {
  1540. SONOS_readingsBeginUpdate($slaveHash);
  1541. foreach my $reading (keys %{$defs{$hash->{NAME}}->{READINGS}}) {
  1542. if ($reading =~ /^(current|next).*/) {
  1543. SONOS_readingsBulkUpdateIfChanged($slaveHash, $reading, ReadingsVal($hash->{NAME}, $reading, ''));
  1544. }
  1545. }
  1546. foreach my $reading (qw(transportState GroupMute GroupVolume Repeat RepeatOne Shuffle infoSummarize1 infoSummarize2 infoSummarize3 infoSummarize4 numberOfTracks)) {
  1547. SONOS_readingsBulkUpdateIfChanged($slaveHash, $reading, ReadingsVal($hash->{NAME}, $reading, ''));
  1548. }
  1549. SONOS_readingsEndUpdate($slaveHash, 1);
  1550. }
  1551. return undef;
  1552. }
  1553. ########################################################################################
  1554. #
  1555. # SONOS_StartClientProcess - Starts the client-process (in a forked-subprocess), which handles all UPnP-Messages
  1556. #
  1557. # Parameter port = Portnumber to what the client have to listen for
  1558. #
  1559. ########################################################################################
  1560. sub SONOS_StartClientProcessIfNeccessary($) {
  1561. my ($upnplistener) = @_;
  1562. my ($host, $port) = split(/:/, $upnplistener);
  1563. my $socket = new IO::Socket::INET(PeerAddr => $upnplistener, Proto => 'tcp');
  1564. if (!$socket) {
  1565. # Sonos-Device ermitteln...
  1566. my $hash = SONOS_getSonosPlayerByName();
  1567. SONOS_Log undef, 1, 'Kein UPnP-Server gefunden... Starte selber einen und warte '.$hash->{WAITTIME}.' Sekunde(n) darauf...';
  1568. $SONOS_StartedOwnUPnPServer = 1;
  1569. if (fork() == 0) {
  1570. # Zuständigen Verbose-Level ermitteln...
  1571. # Allerdings sind die Attribute (momentan) zu diesem Zeitpunkt noch nicht gesetzt, sodass nur das globale Attribut verwendet werden kann...
  1572. my $verboselevel = AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'verbose', $attr{global}{verbose});
  1573. # Prozess anstarten...
  1574. exec("$^X $attr{global}{modpath}/FHEM/00_SONOS.pm $port $verboselevel ".(($attr{global}{mseclog}) ? '1' : '0').' startedbyfhem');
  1575. exit(0);
  1576. }
  1577. } else {
  1578. $socket->sockopt(SO_LINGER, pack("ii", 1, 0));
  1579. # Antwort vom Client weglesen...
  1580. my $answer;
  1581. $socket->recv($answer, 5000);
  1582. $socket->send("Test\r\n", 0);
  1583. # Hiermit wird eine etwaig bestehende Thread-Struktur beendet und diese Verbindung selbst geschlossen...
  1584. eval{
  1585. $socket->shutdown(2);
  1586. $socket->close();
  1587. };
  1588. }
  1589. return undef;
  1590. }
  1591. ########################################################################################
  1592. #
  1593. # SONOS_InitClientProcessLater - Initializes the client-process at a later time
  1594. #
  1595. # Parameter hash = The device-hash
  1596. #
  1597. ########################################################################################
  1598. sub SONOS_InitClientProcessLater($) {
  1599. my ($hash) = @_;
  1600. # Begrüßung weglesen...
  1601. my $answer = DevIo_SimpleRead($hash);
  1602. DevIo_SimpleWrite($hash, "Establish connection\r\n", 2);
  1603. # Verbindung aufbauen...
  1604. InternalTimer(gettimeofday() + 1, 'SONOS_InitClientProcess', $hash, 0);
  1605. return undef;
  1606. }
  1607. ########################################################################################
  1608. #
  1609. # SONOS_InitClientProcess - Initializes the client-process
  1610. #
  1611. # Parameter hash = The device-hash
  1612. #
  1613. ########################################################################################
  1614. sub SONOS_InitClientProcess($) {
  1615. my ($hash) = @_;
  1616. my @playerudn = ();
  1617. my @playername = ();
  1618. foreach my $fhem_dev (sort keys %main::defs) {
  1619. next if($main::defs{$fhem_dev}{TYPE} ne 'SONOSPLAYER');
  1620. push @playerudn, $main::defs{$fhem_dev}{UDN};
  1621. push @playername, $main::defs{$fhem_dev}{NAME};
  1622. }
  1623. # Grundsätzliche Informationen bzgl. der konfigurierten Player übertragen...
  1624. my $setDataString = 'SetData:'.$hash->{NAME}.':'.AttrVal($hash->{NAME}, 'verbose', '3').':'.AttrVal($hash->{NAME}, 'SubProcessLogfileName', '-').':'.AttrVal($hash->{NAME}, 'pingType', $SONOS_DEFAULTPINGTYPE).':'.AttrVal($hash->{NAME}, 'usedonlyIPs', '').':'.AttrVal($hash->{NAME}, 'ignoredIPs', '').':'.AttrVal($hash->{NAME}, 'reusePort', 0).':'.join(',', @playername).':'.join(',', @playerudn);
  1625. SONOS_Log undef, 5, $setDataString;
  1626. DevIo_SimpleWrite($hash, $setDataString."\n", 2);
  1627. # Gemeldete Attribute, Definitionen und Readings übertragen...
  1628. foreach my $fhem_dev (sort keys %main::defs) {
  1629. if (($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER') || ($main::defs{$fhem_dev}{TYPE} eq 'SONOS')) {
  1630. # Den Namen des Devices ermitteln (normalerweise die UDN, bis auf das zentrale Sonos-Device)
  1631. my $dataName;
  1632. if ($main::defs{$fhem_dev}{TYPE} eq 'SONOS') {
  1633. $dataName = 'SONOS';
  1634. } else {
  1635. $dataName = $main::defs{$fhem_dev}{UDN};
  1636. }
  1637. # Variable für die gesammelten Informationen, die übertragen werden sollen...
  1638. my %valueList = ();
  1639. # Attribute
  1640. foreach my $key (keys %{$main::attr{$fhem_dev}}) {
  1641. if (SONOS_posInList($key, @SONOS_PossibleAttributes) != -1) {
  1642. $valueList{$key} = $main::attr{$fhem_dev}{$key};
  1643. }
  1644. }
  1645. # Definitionen
  1646. foreach my $key (keys %{$main::defs{$fhem_dev}}) {
  1647. if (SONOS_posInList($key, @SONOS_PossibleDefinitions) != -1) {
  1648. $valueList{$key} = $main::defs{$fhem_dev}{$key};
  1649. }
  1650. }
  1651. # Readings
  1652. foreach my $key (keys %{$main::defs{$fhem_dev}{READINGS}}) {
  1653. if (SONOS_posInList($key, @SONOS_PossibleReadings) != -1) {
  1654. $valueList{$key} = $main::defs{$fhem_dev}{READINGS}{$key}{VAL};
  1655. }
  1656. }
  1657. # Werte in Text-Array umwandeln und dabei prüfen, ob überhaupt ein Wert gesetzt werden soll...
  1658. my @values = ();
  1659. foreach my $key (keys %valueList) {
  1660. if (defined($key) && defined($valueList{$key})) {
  1661. push @values, $key.'='.SONOS_URI_Escape($valueList{$key});
  1662. }
  1663. }
  1664. # Übertragen...
  1665. SONOS_Log undef, 5, 'SetValues:'.$dataName.':'.join('|', @values);
  1666. DevIo_SimpleWrite($hash, 'SetValues:'.$dataName.':'.join('|', @values)."\n", 2);
  1667. }
  1668. }
  1669. # Alle Informationen sind drüben, dann Threads dort drüben starten
  1670. DevIo_SimpleWrite($hash, "StartThread\n", 2);
  1671. # Interner Timer für die Überprüfung der Verbindung zum Client (nicht verwechseln mit dem IsAlive-Timer, der die Existenz eines Sonosplayers überprüft)
  1672. SONOS_readingsSingleUpdate($hash, 'LastProcessAnswer', '0', 0);
  1673. InternalTimer(gettimeofday() + ($hash->{INTERVAL} * 2), 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1674. return undef;
  1675. }
  1676. ########################################################################################
  1677. #
  1678. # SONOS_IsSubprocessAliveChecker - Internal checking routine for isAlive of the subprocess
  1679. #
  1680. ########################################################################################
  1681. sub SONOS_IsSubprocessAliveChecker() {
  1682. my ($hash) = @_;
  1683. return undef if (AttrVal($hash->{NAME}, 'disable', 0));
  1684. my $lastProcessAnswer = ReadingsVal(SONOS_getSonosPlayerByName()->{NAME}, 'LastProcessAnswer', '0');
  1685. # Wenn länger nichts passiert ist, dann eine Aktualisierung anfordern...
  1686. SONOS_DoWork('undef', 'refreshProcessAnswer') if ($lastProcessAnswer < time() - $hash->{INTERVAL});
  1687. # Wenn die letzte Antwort zu lange her ist, dann den SubProzess neustarten...
  1688. if (($lastProcessAnswer != 0) && ($lastProcessAnswer < time() - (4 * $hash->{INTERVAL}))) {
  1689. # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden...
  1690. SONOS_Log $hash->{UDN}, 2, 'LastProcessAnswer way too old (Lastanswer: '.$lastProcessAnswer.' ~ '.SONOS_GetTimeString($lastProcessAnswer).')... try to restart the process and connection...';
  1691. # Letzten Zeitpunkt und Anzahl der Neustarts merken...
  1692. my $sHash = SONOS_getSonosPlayerByName();
  1693. SONOS_readingsBeginUpdate($sHash);
  1694. readingsBulkUpdate($sHash, 'LastProcessRestart', SONOS_TimeNow(), 1);
  1695. my $restarts = ReadingsVal($sHash->{NAME}, 'LastProcessRestartCount', 0);
  1696. $restarts = 0 if (!looks_like_number($restarts));
  1697. readingsBulkUpdate($sHash, 'LastProcessRestartCount', $restarts + 1, 1);
  1698. SONOS_readingsEndUpdate($sHash, 1);
  1699. # Stoppen...
  1700. InternalTimer(gettimeofday() + 1, 'SONOS_StopSubProcess', $hash, 0);
  1701. # Starten...
  1702. InternalTimer(gettimeofday() + 30, 'SONOS_DelayStart', $hash, 0);
  1703. } else {
  1704. RemoveInternalTimer($hash, 'SONOS_IsSubprocessAliveChecker');
  1705. InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
  1706. }
  1707. }
  1708. ########################################################################################
  1709. #
  1710. # SONOS_DoTriggerInternal - Internal working routine for DoTrigger and PeekTriggerQueueInLocalThread
  1711. #
  1712. ########################################################################################
  1713. sub SONOS_DoTriggerInternal($$) {
  1714. my ($triggerType, @lines) = @_;
  1715. # Eval Kommandos ausführen
  1716. my %doTriggerHashParam;
  1717. my @doTriggerArrayParam;
  1718. my $doTriggerScalarParam;
  1719. foreach my $line (@lines) {
  1720. my $reftype = reftype $line;
  1721. if (!defined $reftype) {
  1722. SONOS_Log undef, 5, $triggerType.'Trigger()-Line: '.$line;
  1723. eval $line;
  1724. if ($@) {
  1725. SONOS_Log undef, 2, 'Error during '.$triggerType.'Trigger: '.$@.' - Trying to execute \''.$line.'\'';
  1726. }
  1727. undef(%doTriggerHashParam);
  1728. undef(@doTriggerArrayParam);
  1729. undef($doTriggerScalarParam);
  1730. } elsif($reftype eq 'HASH') {
  1731. %doTriggerHashParam = %{$line};
  1732. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerHashParam: '.SONOS_Stringify(\%doTriggerHashParam);
  1733. } elsif($reftype eq 'ARRAY') {
  1734. @doTriggerArrayParam = @{$line};
  1735. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerArrayParam: '.SONOS_Stringify(\@doTriggerArrayParam);
  1736. } elsif($reftype eq 'SCALAR') {
  1737. $doTriggerScalarParam = ${$line};
  1738. SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerScalarParam: '.SONOS_Stringify(\$doTriggerScalarParam);
  1739. }
  1740. }
  1741. }
  1742. ########################################################################################
  1743. #
  1744. # SONOS_Get - Implements GetFn function
  1745. #
  1746. # Parameter hash = hash of the master
  1747. # a = argument array
  1748. #
  1749. ########################################################################################
  1750. sub SONOS_Get($@) {
  1751. my ($hash, @a) = @_;
  1752. my $reading = $a[1];
  1753. my $name = $hash->{NAME};
  1754. # for the ?-selector: which values are possible
  1755. if($a[1] eq '?') {
  1756. my @newGets = ();
  1757. for my $elem (sort keys %gets) {
  1758. push @newGets, $elem.(($gets{$elem} eq '') ? ':noArg' : '');
  1759. }
  1760. return "Unknown argument, choose one of ".join(" ", @newGets);
  1761. }
  1762. # check argument
  1763. my $found = 0;
  1764. for my $elem (keys %gets) {
  1765. if (lc($reading) eq lc($elem)) {
  1766. $reading = $elem; # Korrekte Schreibweise behalten
  1767. $found = 1;
  1768. last;
  1769. }
  1770. }
  1771. return "SONOS: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) if(!$found);
  1772. # some argument needs parameter(s), some not
  1773. return "SONOS: $a[1] needs parameter(s): ".$gets{$a[1]} if (SONOS_CountRequiredParameters($gets{$a[1]}) > scalar(@a) - 2);
  1774. # getter
  1775. if (lc($reading) eq 'groups') {
  1776. return SONOS_ConvertZoneGroupStateToString(SONOS_ConvertZoneGroupState(ReadingsVal($name, 'ZoneGroupState', '')));
  1777. }
  1778. return undef;
  1779. }
  1780. ########################################################################################
  1781. #
  1782. # SONOS_ConvertZoneGroupState - Retrieves the Groupstate in an array (Elements are UDNs)
  1783. #
  1784. ########################################################################################
  1785. sub SONOS_ConvertZoneGroupState($) {
  1786. my ($zoneGroupState) = @_;
  1787. my @groups = ();
  1788. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  1789. my @group = ($1.'_MR');
  1790. my $groupMember = $2;
  1791. while ($groupMember =~ m/<ZoneGroupMember.*?UUID="(.*?)"(.*?)\/>/gi) {
  1792. my $udn = $1;
  1793. my $string = $2;
  1794. push @group, $udn.'_MR' if (!($string =~ m/IsZoneBridge="."/) && !SONOS_isInList($udn.'_MR', @group));
  1795. # Etwaig von vorher enthaltene Bridges wieder entfernen (wenn sie bereits als Koordinator eingesetzt wurde)
  1796. if ($string =~ m/IsZoneBridge="."/) {
  1797. for(my $i = 0; $i <= $#group; $i++) {
  1798. delete $group[$i] if ($group[$i] eq $udn.'_MR');
  1799. }
  1800. }
  1801. }
  1802. # Die Abspielgruppe hinzufügen, wenn sie nicht leer ist (kann bei Bridges passieren)
  1803. if ($#group >= 0) {
  1804. # Playernamen einsetzen...
  1805. @group = map { SONOS_getSonosPlayerByUDN($_)->{NAME} } @group;
  1806. # Die einzelne Gruppe sortieren, dabei den Masterplayer vorne lassen...
  1807. my @newgroup = ($group[0]);
  1808. push @newgroup, sort @group[1..$#group];
  1809. # Zur großen Liste hinzufügen...
  1810. push @groups, \@newgroup;
  1811. }
  1812. }
  1813. # Nach den Masterplayernamen sortieren
  1814. @groups = sort {
  1815. @{$a}[0] cmp @{$b}[0];
  1816. } @groups;
  1817. return @groups;
  1818. }
  1819. ########################################################################################
  1820. #
  1821. # SONOS_ConvertZoneGroupStateToString - Converts the GroupState into a String
  1822. #
  1823. ########################################################################################
  1824. sub SONOS_ConvertZoneGroupStateToString($) {
  1825. my (@groups) = @_;
  1826. # UDNs durch Devicenamen ersetzen und dabei gleich das Ergebnis zusammenbauen
  1827. my $result = '';
  1828. foreach my $gelem (@groups) {
  1829. #$result .= '[';
  1830. #foreach my $elem (@{$gelem}) {
  1831. # $elem = SONOS_getSonosPlayerByUDN($elem)->{NAME};
  1832. #}
  1833. $result .= '['.join(', ', @{$gelem}).'], ';
  1834. }
  1835. return substr($result, 0, -2);
  1836. }
  1837. ########################################################################################
  1838. #
  1839. # SONOS_Set - Implements SetFn function
  1840. #
  1841. # Parameter hash
  1842. # a = argument array
  1843. #
  1844. ########################################################################################
  1845. sub SONOS_Set($@) {
  1846. my ($hash, @a) = @_;
  1847. # %setCopy enthält eine Kopie von %sets, da für eine ?-Anfrage u.U. ein Slider zurückgegeben werden muss...
  1848. my %setcopy;
  1849. if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) {
  1850. foreach my $key (keys %sets) {
  1851. my $oldkey = $key;
  1852. $key = $key.':slider,0,1,100' if (lc($key) eq 'volume');
  1853. $key = $key.':slider,-100,1,100' if (lc($key) eq 'balance');
  1854. $key = $key.':0,1' if ($key =~ m/^mute(all|)$/i);
  1855. $setcopy{$key} = $sets{$oldkey};
  1856. }
  1857. } else {
  1858. %setcopy = %sets;
  1859. }
  1860. # for the ?-selector: which values are possible
  1861. if($a[1] eq '?') {
  1862. my @newSets = ();
  1863. for my $elem (sort keys %setcopy) {
  1864. push @newSets, $elem.(($setcopy{$elem} eq '') ? ':noArg' : '');
  1865. }
  1866. return "Unknown argument, choose one of ".join(" ", @newSets);
  1867. }
  1868. # check argument
  1869. my $found = 0;
  1870. for my $elem (keys %sets) {
  1871. if (lc($a[1]) eq lc($elem)) {
  1872. $a[1] = $elem; # Korrekte Schreibweise behalten
  1873. $found = 1;
  1874. last;
  1875. }
  1876. }
  1877. return "SONOS: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) if(!$found);
  1878. # some argument needs parameter(s), some not
  1879. return "SONOS: $a[1] needs parameter(s): ".$sets{$a[1]} if (SONOS_CountRequiredParameters($sets{$a[1]}) > scalar(@a) - 2);
  1880. # define vars
  1881. my $key = $a[1];
  1882. my $value = $a[2];
  1883. my $value2 = $a[3];
  1884. my $name = $hash->{NAME};
  1885. # setter
  1886. if (lc($key) eq 'groups') {
  1887. my $text = '';
  1888. for(my $i = 2; $i < @a; $i++) {
  1889. $text .= ' '.$a[$i];
  1890. }
  1891. $text =~ s/ //g;
  1892. # Aktuellen Zustand holen
  1893. my @current;
  1894. my $current = SONOS_Get($hash, qw($hash->{NAME} Groups));
  1895. $current =~ s/ //g;
  1896. while ($current =~ m/(\[.*?\])/ig) {
  1897. my @tmp = split(/,/, substr($1, 1, -1));
  1898. push @current, \@tmp;
  1899. }
  1900. if (lc($text) eq 'reset') {
  1901. my $tmpcurrent = $current;
  1902. $tmpcurrent =~ s/[\[\],]/ /g;
  1903. my @list = split(/ /, $tmpcurrent);
  1904. # Alle Player als Standalone-Group festlegen
  1905. for(my $i = 0; $i <= $#list; $i++) {
  1906. next if (!$list[$i]); # Wenn hier ein Leerstring aus dem Split kam, dann überspringen...
  1907. my $elemHash = SONOS_getSonosPlayerByName($list[$i]);
  1908. SONOS_DoWork($elemHash->{UDN}, 'makeStandaloneGroup');
  1909. usleep(250_000);
  1910. }
  1911. return undef;
  1912. }
  1913. # Gewünschten Zustand holen
  1914. my @desiredList;
  1915. my @desiredCrowd;
  1916. while ($text =~ m/([\[\{].*?[\}\]])/ig) {
  1917. my @tmp = split(/,/, substr($1, 1, -1));
  1918. if (substr($1, 0, 1) eq '{') {
  1919. push @desiredCrowd, \@tmp;
  1920. } else {
  1921. push @desiredList, \@tmp;
  1922. }
  1923. }
  1924. # SONOS_Log undef, 5, "Desired-Crowd: ".Dumper(\@desiredCrowd);
  1925. SONOS_Log undef, 5, "Desired-List: ".Dumper(\@desiredList);
  1926. # Erstmal die Listen sicherstellen
  1927. foreach my $dElem (@desiredList) {
  1928. my @list = @{$dElem};
  1929. for(my $i = 0; $i <= $#list; $i++) { # Die jeweilige Desired-List
  1930. my $elem = $list[$i];
  1931. my $elemHash = SONOS_getSonosPlayerByName($elem);
  1932. my $reftype = reftype $elemHash;
  1933. if (!defined($reftype) || $reftype ne 'HASH') {
  1934. SONOS_Log undef, 2, "Hash not found for Device '$elem'. Is it gone away or not known?";
  1935. return undef;
  1936. }
  1937. # Das Element soll ein Gruppenkoordinator sein
  1938. if ($i == 0) {
  1939. my $cPos = -1;
  1940. foreach my $cElem (@current) {
  1941. $cPos = SONOS_posInList($elem, @{$cElem});
  1942. last if ($cPos != -1);
  1943. }
  1944. # Ist es aber nicht... also erstmal dazu machen
  1945. if ($cPos != 0) {
  1946. SONOS_DoWork($elemHash->{UDN}, 'makeStandaloneGroup');
  1947. usleep(250_000);
  1948. }
  1949. } else {
  1950. # Alle weiteren dazufügen
  1951. my $cHash = SONOS_getSonosPlayerByName($list[0]);
  1952. SONOS_DoWork($cHash->{UDN}, 'addMember', $elemHash->{UDN});
  1953. usleep(250_000);
  1954. }
  1955. }
  1956. }
  1957. } elsif (lc($key) =~ m/^(Stop|Pause|Mute|MuteOn|MuteOff)(All|)$/i) {
  1958. my $commandType = lc($1);
  1959. my $commandValue = $value;
  1960. $commandValue = 0 if ($commandType ne 'mute');
  1961. $commandValue = 1 if ($commandType eq 'muteon');
  1962. $commandValue = 0 if ($commandType eq 'muteoff');
  1963. $commandType = 'setGroupMute' if (($commandType eq 'mute') || ($commandType eq 'muteon') || ($commandType eq 'muteoff'));
  1964. # Alle Gruppenkoordinatoren zum Stoppen/Pausieren/Muten aufrufen
  1965. foreach my $cElem (@{eval(ReadingsVal($hash->{NAME}, 'MasterPlayer', '[]'))}) {
  1966. SONOS_DoWork(SONOS_getSonosPlayerByName($cElem)->{UDN}, $commandType, $commandValue);
  1967. }
  1968. } elsif (lc($key) eq 'rescannetwork') {
  1969. SONOS_DoWork('SONOS', 'rescanNetwork');
  1970. } elsif (lc($key) eq 'refreshshareindex') {
  1971. foreach my $cElem (@{eval(ReadingsVal($hash->{NAME}, 'MasterPlayer', '[]'))}) {
  1972. SONOS_DoWork(SONOS_getSonosPlayerByName($cElem)->{UDN}, 'refreshShareIndex');
  1973. last;
  1974. }
  1975. } elsif (lc($key) eq 'savebookmarks') {
  1976. SONOS_DoWork('SONOS', 'SaveBookmarks', $value);
  1977. } elsif (lc($key) eq 'loadbookmarks') {
  1978. SONOS_DoWork('SONOS', 'LoadBookmarks', $value);
  1979. } elsif (lc($key) eq 'disablebookmark') {
  1980. SONOS_DoWork('SONOS', 'DisableBookmark', $value);
  1981. } elsif (lc($key) eq 'enablebookmark') {
  1982. SONOS_DoWork('SONOS', 'EnableBookmark', $value);
  1983. } else {
  1984. return 'Not implemented yet!';
  1985. }
  1986. return (undef, 1);
  1987. }
  1988. ########################################################################################
  1989. #
  1990. # SONOS_CountRequiredParameters - Counta all required parameters in the given string
  1991. #
  1992. ########################################################################################
  1993. sub SONOS_CountRequiredParameters($) {
  1994. my ($params) = @_;
  1995. my $result = 0;
  1996. for my $elem (split(' ', $params)) {
  1997. $result++ if ($elem !~ m/\[.*\]/);
  1998. }
  1999. return $result;
  2000. }
  2001. ########################################################################################
  2002. #
  2003. # SONOS_DoWork - Communicates with the forked Part via Telnet and over there via ComObjectTransportQueue
  2004. #
  2005. # Parameter deviceName = Devicename of the SonosPlayer
  2006. # method = Name der "Methode" die im Thread-Context ausgeführt werden soll
  2007. # params = Parameter for the method
  2008. #
  2009. ########################################################################################
  2010. sub SONOS_DoWork($$;@) {
  2011. my ($udn, $method, @params) = @_;
  2012. if (!@params) {
  2013. @params = ();
  2014. }
  2015. if (!defined($udn)) {
  2016. SONOS_Log undef, 0, "ERROR in DoWork: '$method' -> UDN is undefined - ".Dumper(\@params);
  2017. return;
  2018. }
  2019. # Etwaige optionale Parameter, die sonst undefined wären, löschen
  2020. for(my $i = 0; $i <= $#params; $i++) {
  2021. if (!defined($params[$i])) {
  2022. delete($params[$i]);
  2023. }
  2024. }
  2025. eval {
  2026. my $hash = SONOS_getSonosPlayerByName();
  2027. if (defined($hash->{TCPDev}) && ($hash->{TCPDev}->connected())) {
  2028. DevIo_SimpleWrite($hash, 'DoWork:'.$udn.':'.$method.':'.encode_utf8(join('--#--', @params))."\r\n", 2);
  2029. }
  2030. };
  2031. if ($@) {
  2032. SONOS_Log undef, 0, 'ERROR in DoWork: '.$@;
  2033. }
  2034. return undef;
  2035. }
  2036. ########################################################################################
  2037. #
  2038. # SONOS_Discover - Discover SonosPlayer,
  2039. # indirectly autocreate devices if not already present (via callback)
  2040. #
  2041. ########################################################################################
  2042. sub SONOS_Discover() {
  2043. SONOS_Log undef, 1, 'UPnP-Thread gestartet.';
  2044. $SIG{'PIPE'} = 'IGNORE';
  2045. $SIG{'CHLD'} = 'IGNORE';
  2046. # Thread 'cancellation' signal handler
  2047. $SIG{'INT'} = sub {
  2048. # Sendeliste leeren
  2049. while ($SONOS_Client_SendQueue->pending()) {
  2050. $SONOS_Client_SendQueue->dequeue();
  2051. }
  2052. # Empfängerliste leeren
  2053. while ($SONOS_Client_ReceiveQueue->pending()) {
  2054. $SONOS_Client_ReceiveQueue->dequeue();
  2055. }
  2056. # UPnP-Listener beenden
  2057. SONOS_StopControlPoint();
  2058. SONOS_Log undef, 3, 'Controlpoint-Listener wurde beendet.';
  2059. return 1;
  2060. };
  2061. SONOS_LoadBookmarkValues();
  2062. my $error;
  2063. do {
  2064. $SONOS_RestartControlPoint = 0;
  2065. eval {
  2066. $SONOS_Controlpoint = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, SubscriptionURL => '/fhemmodule', MaxWait => 30, LogLevel => $SONOS_Client_LogLevel, UsedOnlyIP => \@usedonlyIPs, IgnoreIP => \@ignoredIPs, ReusePort => $reusePort);
  2067. $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
  2068. #$SONOS_Controlpoint->handle;
  2069. my @mysockets = $SONOS_Controlpoint->sockets();
  2070. my $select = IO::Select->new(@mysockets);
  2071. while (!$SONOS_RestartControlPoint) {
  2072. # UPnP-Sockets abfragen...
  2073. if (my @sockets = $select->can_read(0.01)) {
  2074. for my $sock (@sockets) {
  2075. $SONOS_Controlpoint->handleOnce($sock);
  2076. }
  2077. }
  2078. # Befehlsqueue abfragen...
  2079. while ($SONOS_Client_ReceiveQueue->pending()) {
  2080. SONOS_Discover_DoQueue($SONOS_Client_ReceiveQueue->dequeue());
  2081. }
  2082. }
  2083. };
  2084. $error = $@;
  2085. # Nur wenn es der Fehler mit der XML-Struktur ist, dann den UPnP-Handler nochmal anstarten...
  2086. 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)) {
  2087. SONOS_Log undef, 2, "Error during UPnP-Handling, restarting handling: $error";
  2088. SONOS_StopControlPoint();
  2089. } else {
  2090. SONOS_Log undef, 2, "Error during UPnP-Handling: $error";
  2091. SONOS_StopControlPoint();
  2092. undef($error);
  2093. }
  2094. } while ($error || $SONOS_RestartControlPoint);
  2095. SONOS_SaveBookmarkValues();
  2096. SONOS_Log undef, 1, 'UPnP-Thread wurde beendet.';
  2097. $SONOS_Thread = -1;
  2098. return 1;
  2099. }
  2100. ########################################################################################
  2101. #
  2102. # SONOS_Discover_DoQueue - Do the working job (command from Fhem -> Sonosplayer)
  2103. #
  2104. ########################################################################################
  2105. sub SONOS_Discover_DoQueue($) {
  2106. my ($data) = @_;
  2107. my $workType = $data->{WorkType};
  2108. return if (!defined($workType));
  2109. my $udn = $data->{UDN};
  2110. my @params = ();
  2111. @params = @{$data->{Params}} if (defined($data->{Params}));
  2112. eval {
  2113. if ($workType eq 'setVerbose') {
  2114. $SONOS_Client_LogLevel = $params[0];
  2115. SONOS_Log undef, $SONOS_Client_LogLevel, "Setting LogLevel to new value: $SONOS_Client_LogLevel";
  2116. } elsif ($workType eq 'setLogfileName') {
  2117. $SONOS_Client_LogfileName = $params[0]
  2118. } elsif ($workType eq 'refreshProcessAnswer') {
  2119. SONOS_Client_Notifier('rePing:undef::');
  2120. } elsif ($workType eq 'setAttribute') {
  2121. SONOS_Client_Data_Refresh('', $udn, $params[0], $params[1]);
  2122. } elsif ($workType eq 'deleteAttribute') {
  2123. SONOS_Client_Data_Refresh('', $udn, $params[0], undef);
  2124. } elsif ($workType eq 'rescanNetwork') {
  2125. $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
  2126. } elsif ($workType eq 'refreshShareIndex') {
  2127. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2128. my $albumArtistDisplayOption = $SONOS_ContentDirectoryControlProxy{$udn}->GetAlbumArtistDisplayOption()->getValue('AlbumArtistDisplayOption');
  2129. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_ContentDirectoryControlProxy{$udn}->RefreshShareIndex($albumArtistDisplayOption)));
  2130. }
  2131. } elsif ($workType eq 'setMinMaxVolumes') {
  2132. $SONOS_Client_Data{Buffer}->{$udn}{$params[0]} = $params[1];
  2133. # Ensures the defined volume-borders
  2134. SONOS_EnsureMinMaxVolumes($udn);
  2135. SONOS_Log undef, 3, "Setting MinMaxVolumes of device '$udn' to a new value ~ $params[0] = $params[1]";
  2136. } elsif ($workType eq 'JumpToBookmark') {
  2137. if ($SONOS_BookmarkTitleDefinition{$params[0]}) {
  2138. }
  2139. } elsif ($workType eq 'LoadBookmarks') {
  2140. SONOS_LoadBookmarkValues($params[0]);
  2141. } elsif ($workType eq 'SaveBookmarks') {
  2142. SONOS_SaveBookmarkValues($params[0]);
  2143. } elsif ($workType eq 'DisableBookmark') {
  2144. $SONOS_BookmarkTitleDefinition{$params[0]}{Disabled} = 1 if ($SONOS_BookmarkTitleDefinition{$params[0]});
  2145. $SONOS_BookmarkQueueDefinition{$params[0]}{Disabled} = 1 if ($SONOS_BookmarkQueueDefinition{$params[0]});
  2146. } elsif ($workType eq 'EnableBookmark') {
  2147. delete($SONOS_BookmarkTitleDefinition{$params[0]}{Disabled}) if ($SONOS_BookmarkTitleDefinition{$params[0]});
  2148. delete($SONOS_BookmarkQueueDefinition{$params[0]}{Disabled}) if ($SONOS_BookmarkQueueDefinition{$params[0]});
  2149. } elsif ($workType eq 'setEQ') {
  2150. my $command = $params[0];
  2151. my $value = $params[1];
  2152. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2153. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).' ('.$command.'): '.SONOS_UPnPAnswerMessage($SONOS_RenderingControlProxy{$udn}->SetEQ(0, $command, $value)));
  2154. }
  2155. } elsif ($workType eq 'setTruePlay') {
  2156. my $value = $params[0];
  2157. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2158. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_RenderingControlProxy{$udn}->SetSonarStatus(0, $value)));
  2159. }
  2160. } elsif ($workType eq 'setName') {
  2161. my $value1 = $params[0];
  2162. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2163. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes($value1, '', '')));
  2164. }
  2165. } elsif ($workType eq 'setIcon') {
  2166. my $value1 = $params[0];
  2167. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2168. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes('', 'x-rincon-roomicon:'.$value1, '')));
  2169. }
  2170. } elsif ($workType eq 'getCurrentTrackPosition') {
  2171. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2172. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2173. my $position = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
  2174. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2175. my $modus = 'ReadingsBulkUpdate'.((SONOS_Client_Data_Retreive($udn, 'reading', 'currentStreamAudio', 0)) ? 'IfChanged' : '');
  2176. SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPosition', $position);
  2177. SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($position));
  2178. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2179. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2180. } else {
  2181. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime'));
  2182. }
  2183. }
  2184. } elsif ($workType eq 'setCurrentTrackPosition') {
  2185. my $value1 = $params[0];
  2186. # Wenn eine Sekundenangabe gemacht wurde, dann in einen Zeitstring umwandeln, damit der Rest so bleiben kann
  2187. $value1 = $1.SONOS_ConvertSecondsToTime($2).$3 if ($value1 =~ m/^([+-]{0,1})(\d+)(\%{0,1})$/);
  2188. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2189. if ($value1 =~ m/([+-])(\d+:\d+:\d+|\d+:\d+|\d+)(\%{0,1})/) {
  2190. # Relative-(Prozent)-Angabe
  2191. my $value1Sec = SONOS_GetTimeSeconds(SONOS_ExpandTimeString($2));
  2192. # Positionswerte abfragen...
  2193. my $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0);
  2194. my $pos = SONOS_GetTimeSeconds($result->getValue('RelTime'));
  2195. my $duration = SONOS_GetTimeSeconds($result->getValue('TrackDuration'));
  2196. # Neue Position berechnen...
  2197. my $newPos = 0;
  2198. if ($3 eq '') {
  2199. $newPos = ($pos + $value1Sec) if ($1 eq '+');
  2200. $newPos = ($pos - $value1Sec) if ($1 eq '-');
  2201. } else {
  2202. $newPos = ($pos + ($value1Sec * $duration / 100)) if ($1 eq '+');
  2203. $newPos = ($pos - ($value1Sec * $duration / 100)) if ($1 eq '-');
  2204. }
  2205. # Sicherstellen, dass wir im Bereich des Titels bleiben...
  2206. $newPos = 0 if ($newPos < 0);
  2207. $newPos = $duration if ($newPos > $duration);
  2208. # Neue Position setzen
  2209. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', SONOS_ConvertSecondsToTime($newPos));
  2210. } else {
  2211. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', $value1);
  2212. }
  2213. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime'));
  2214. }
  2215. } elsif ($workType eq 'reportUnresponsiveDevice') {
  2216. my $value1 = $params[0];
  2217. if (SONOS_CheckProxyObject($udn, $SONOS_ZoneGroupTopologyProxy{$udn})) {
  2218. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_ZoneGroupTopologyProxy{$udn}->ReportUnresponsiveDevice($value1, 'VerifyThenRemoveSystemwide')));
  2219. }
  2220. } elsif ($workType eq 'setGroupVolume') {
  2221. my $value1 = $params[0];
  2222. my $value2 = $params[1];
  2223. # Wenn ein fixer Wert für alle Gruppenmitglieder gleich gesetzt werden soll...
  2224. if (defined($value2) && lc($value2) eq 'fixed') {
  2225. } else {
  2226. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  2227. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $value1);
  2228. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2229. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume'));
  2230. }
  2231. }
  2232. } elsif ($workType eq 'setSnapshotGroupVolume') {
  2233. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  2234. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0)));
  2235. }
  2236. } elsif ($workType eq 'setVolume') {
  2237. my $value1 = $params[0];
  2238. my $ramptype = $params[1];
  2239. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2240. if (defined($ramptype)) {
  2241. if ($ramptype == 1) {
  2242. $ramptype = 'SLEEP_TIMER_RAMP_TYPE';
  2243. } elsif ($ramptype == 2) {
  2244. $ramptype = 'AUTOPLAY_RAMP_TYPE';
  2245. } elsif ($ramptype == 3) {
  2246. $ramptype = 'ALARM_RAMP_TYPE';
  2247. }
  2248. my $ramptime = $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, '')->getValue('RampTime');
  2249. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started');
  2250. } else {
  2251. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1);
  2252. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2253. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'));
  2254. }
  2255. }
  2256. } elsif ($workType eq 'setRelativeGroupVolume') {
  2257. my $value1 = $params[0];
  2258. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  2259. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $value1)->getValue('NewVolume'));
  2260. }
  2261. } elsif ($workType eq 'setRelativeVolume') {
  2262. my $value1 = $params[0];
  2263. my $ramptype = $params[1];
  2264. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2265. if (defined($ramptype)) {
  2266. if ($ramptype == 1) {
  2267. $ramptype = 'SLEEP_TIMER_RAMP_TYPE';
  2268. } elsif ($ramptype == 2) {
  2269. $ramptype = 'AUTOPLAY_RAMP_TYPE';
  2270. } elsif ($ramptype == 3) {
  2271. $ramptype = 'ALARM_RAMP_TYPE';
  2272. }
  2273. # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen
  2274. if ($value1 =~ m/([+-])(\d+)\%/) {
  2275. my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  2276. $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) };
  2277. } else {
  2278. # Hier aus der Relativangabe eine Absolutangabe für den Aufruf von RampToVolume machen
  2279. $value1 = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume') + $value1;
  2280. }
  2281. $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, '');
  2282. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started');
  2283. } else {
  2284. # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen
  2285. if ($value1 =~ m/([+-])(\d+)\%/) {
  2286. my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  2287. $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) };
  2288. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1);
  2289. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2290. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'));
  2291. } else {
  2292. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->SetRelativeVolume(0, 'Master', $value1)->getValue('NewVolume'));
  2293. }
  2294. }
  2295. }
  2296. } elsif ($workType eq 'setBalance') {
  2297. my $value1 = $params[0];
  2298. # Balancewert auf die beiden Lautstärkeseiten aufteilen...
  2299. my $volumeLeft = 100;
  2300. my $volumeRight = 100;
  2301. if ($value1 < 0) {
  2302. $volumeRight = 100 + $value1;
  2303. } else {
  2304. $volumeLeft = 100 - $value1;
  2305. }
  2306. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2307. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'LF', $volumeLeft);
  2308. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'RF', $volumeRight);
  2309. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2310. $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume');
  2311. $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume');
  2312. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.((-$volumeLeft) + $volumeRight));
  2313. }
  2314. } elsif ($workType eq 'setLoudness') {
  2315. my $value1 = $params[0];
  2316. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2317. $SONOS_RenderingControlProxy{$udn}->SetLoudness(0, 'Master', SONOS_ConvertWordToNum($value1));
  2318. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2319. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetLoudness(0, 'Master')->getValue('CurrentLoudness')));
  2320. }
  2321. } elsif ($workType eq 'setBass') {
  2322. my $value1 = $params[0];
  2323. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2324. $SONOS_RenderingControlProxy{$udn}->SetBass(0, $value1);
  2325. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2326. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetBass(0)->getValue('CurrentBass'));
  2327. }
  2328. } elsif ($workType eq 'setTreble') {
  2329. my $value1 = $params[0];
  2330. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2331. $SONOS_RenderingControlProxy{$udn}->SetTreble(0, $value1);
  2332. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2333. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetTreble(0)->getValue('CurrentTreble'));
  2334. }
  2335. } elsif ($workType eq 'setMute') {
  2336. my $value1 = $params[0];
  2337. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2338. $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1));
  2339. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2340. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute')));
  2341. }
  2342. } elsif ($workType eq 'setOutputFixed') {
  2343. my $value1 = $params[0];
  2344. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2345. $SONOS_RenderingControlProxy{$udn}->SetOutputFixed(0, SONOS_ConvertWordToNum($value1));
  2346. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2347. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetOutputFixed(0)->getValue('CurrentFixed')));
  2348. }
  2349. } elsif ($workType eq 'setButtonLockState') {
  2350. my $value1 = $params[0];
  2351. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2352. $SONOS_DevicePropertiesProxy{$udn}->SetButtonLockState(0, lcfirst(SONOS_ConvertNumToWord($value1)));
  2353. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2354. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertWordToNum(lc($SONOS_DevicePropertiesProxy{$udn}->GetButtonLockState(0)->getValue('CurrentButtonLockState'))));
  2355. }
  2356. } elsif ($workType eq 'setResetAttributesToDefault') {
  2357. my $sonosDeviceName = $params[0];
  2358. my $deviceName = $params[1];
  2359. my $value1 = 0;
  2360. $value1 = $params[2] if ($params[2]);
  2361. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  2362. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2363. # Sollen alle Attribute vorher entfernt werden?
  2364. if (SONOS_ConvertWordToNum($value1)) {
  2365. SONOS_Client_Notifier('CommandDeleteAttr:'.$deviceName);
  2366. }
  2367. # Notwendige Daten vom Player ermitteln...
  2368. my ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState) = SONOS_AnalyzeZoneGroupTopology($udn, $udnShort);
  2369. my $roomName = $SONOS_DevicePropertiesProxy{$udn}->GetZoneAttributes()->getValue('CurrentZoneName');
  2370. my $groupName = decode('UTF-8', $roomName);
  2371. eval {
  2372. use utf8;
  2373. $groupName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  2374. };
  2375. $groupName =~ s/[^a-zA-Z0-9]/_/g;
  2376. my $iconPath = decode_entities($1) if ($SONOS_UPnPDevice{$udn}->descriptionDocument() =~ m/<iconList>.*?<icon>.*?<id>0<\/id>.*?<url>(.*?)<\/url>.*?<\/icon>.*?<\/iconList>/sim);
  2377. $iconPath =~ s/.*\/(.*)/icoSONOSPLAYER_$1/i;
  2378. # Standard-Attribute am Player setzen
  2379. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Attributes', $sonosDeviceName, undef, $master, $deviceName, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge)) {
  2380. SONOS_Client_Notifier($elem);
  2381. }
  2382. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Successfully done...');
  2383. }
  2384. } elsif ($workType eq 'setMuteT') {
  2385. my $value1 = 'off';
  2386. if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) {
  2387. if ($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute') == 0) {
  2388. $value1 = 'on';
  2389. } else {
  2390. $value1 = 'off';
  2391. }
  2392. $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1));
  2393. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2394. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute')));
  2395. }
  2396. } elsif ($workType eq 'setGroupMute') {
  2397. my $value1 = $params[0];
  2398. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  2399. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, SONOS_ConvertWordToNum($value1));
  2400. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2401. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute')));
  2402. }
  2403. } elsif ($workType eq 'setShuffle') {
  2404. my $value1 = undef;
  2405. if ($params[0] ne '~~') {
  2406. $value1 = SONOS_ConvertWordToNum($params[0]);
  2407. }
  2408. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2409. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2410. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2411. $value1 = !$shuffle if (!defined($value1));
  2412. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($value1, $repeat, $repeatOne));
  2413. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2414. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2415. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2416. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($shuffle));
  2417. }
  2418. } elsif ($workType eq 'setRepeat') {
  2419. my $value1 = undef;
  2420. if ($params[0] ne '~~') {
  2421. $value1 = SONOS_ConvertWordToNum($params[0]);
  2422. }
  2423. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2424. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2425. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2426. $value1 = !$repeat if (!defined($value1));
  2427. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($shuffle, $value1, $repeatOne && !$value1));
  2428. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2429. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2430. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2431. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($repeat));
  2432. }
  2433. } elsif ($workType eq 'setRepeatOne') {
  2434. my $value1 = undef;
  2435. if ($params[0] ne '~~') {
  2436. $value1 = SONOS_ConvertWordToNum($params[0]);
  2437. }
  2438. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2439. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2440. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2441. $value1 = !$repeatOne if (!defined($value1));
  2442. $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, SONOS_GetShuffleRepeatString($shuffle, $repeat && !$value1, $value1));
  2443. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2444. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode');
  2445. ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($result);
  2446. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($repeatOne));
  2447. }
  2448. } elsif ($workType eq 'setCrossfadeMode') {
  2449. my $value1 = SONOS_ConvertWordToNum($params[0]);
  2450. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2451. $SONOS_AVTransportControlProxy{$udn}->SetCrossfadeMode(0, $value1);
  2452. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2453. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_AVTransportControlProxy{$udn}->GetCrossfadeMode(0)->getValue('CrossfadeMode')));
  2454. }
  2455. } elsif ($workType eq 'setLEDState') {
  2456. my $value1 = (SONOS_ConvertWordToNum($params[0])) ? 'On' : 'Off';
  2457. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  2458. $SONOS_DevicePropertiesProxy{$udn}->SetLEDState($value1);
  2459. # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können
  2460. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_DevicePropertiesProxy{$udn}->GetLEDState()->getValue('CurrentLEDState')));
  2461. }
  2462. } elsif ($workType eq 'play') {
  2463. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2464. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)));
  2465. }
  2466. } elsif ($workType eq 'stop') {
  2467. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2468. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Stop(0)));
  2469. }
  2470. } elsif ($workType eq 'pause') {
  2471. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2472. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Pause(0)));
  2473. }
  2474. } elsif ($workType eq 'previous') {
  2475. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2476. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Previous(0)));
  2477. }
  2478. } elsif ($workType eq 'next') {
  2479. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2480. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Next(0)));
  2481. }
  2482. } elsif ($workType eq 'setTrack') {
  2483. my $value1 = $params[0];
  2484. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2485. # Abspielliste aktivieren?
  2486. my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI');
  2487. if ($currentURI !~ m/x-rincon-queue:/) {
  2488. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2489. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  2490. }
  2491. if (lc($value1) eq 'random') {
  2492. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', int(rand($SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('NrTracks'))));
  2493. } else {
  2494. $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', $value1);
  2495. }
  2496. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track'));
  2497. }
  2498. } elsif ($workType eq 'setCurrentPlaylist') {
  2499. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2500. # Abspielliste aktivieren?
  2501. my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI');
  2502. if ($currentURI !~ m/x-rincon-queue:/) {
  2503. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2504. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '')));
  2505. } else {
  2506. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Not neccessary!');
  2507. }
  2508. }
  2509. } elsif ($workType eq 'getPlaylists') {
  2510. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2511. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2512. my $tmp = $result->getValue('Result');
  2513. my %resultHash;
  2514. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2515. $resultHash{$1} = $2;
  2516. }
  2517. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2518. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2519. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'PlaylistsListAlias', join('|', sort values %resultHash));
  2520. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'PlaylistsList', join('|', map { $_ =~ s/$SONOS_LISTELEMMASK/\./g; $_ } sort values %resultHash));
  2521. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2522. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2523. } else {
  2524. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2525. }
  2526. }
  2527. } elsif ($workType eq 'getPlaylistsWithCovers') {
  2528. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2529. my %resultHash = %{SONOS_GetBrowseStructuredResult($udn, 'SQ:', 1, $workType, 1, 1)};
  2530. #my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  2531. #my $tmp = $result->getValue('Result');
  2532. #
  2533. #my %resultHash;
  2534. #while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/container>/ig) {
  2535. # $resultHash{$1}->{Title} = $2;
  2536. # $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2537. # $resultHash{$1}->{Ressource} = decode_entities($3);
  2538. #}
  2539. #
  2540. #if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2541. # SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Playlists', SONOS_Dumper(\%resultHash));
  2542. #
  2543. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2544. #} else {
  2545. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2546. #}
  2547. }
  2548. } elsif ($workType eq 'getFavourites') {
  2549. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2550. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  2551. my $tmp = $result->getValue('Result');
  2552. my %resultHash;
  2553. while ($tmp =~ m/<item id="(FV:2\/\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/item>/ig) {
  2554. $resultHash{$1} = $2;
  2555. }
  2556. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2557. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2558. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'FavouritesListAlias', join('|', sort values %resultHash));
  2559. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'FavouritesList', join('|', map { $_ =~ s/$SONOS_LISTELEMMASK/\./g; $_ } sort values %resultHash));
  2560. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2561. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2562. } else {
  2563. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2564. }
  2565. }
  2566. } elsif ($workType eq 'getFavouritesWithCovers') {
  2567. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2568. SONOS_GetBrowseStructuredResult($udn, 'FV:2', 1, $workType, 1);
  2569. #my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  2570. #my $tmp = $result->getValue('Result');
  2571. #
  2572. #my %resultHash;
  2573. #while ($tmp =~ m/<item id="(FV:2\/\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2574. # $resultHash{$1}->{Title} = $2;
  2575. # $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2576. # $resultHash{$1}->{Ressource} = decode_entities($3);
  2577. #}
  2578. #
  2579. #if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2580. # SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Favourites', SONOS_Dumper(\%resultHash));
  2581. #
  2582. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2583. #} else {
  2584. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2585. #}
  2586. }
  2587. } elsif ($workType eq 'getSearchlistCategories') {
  2588. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2589. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('A:', 'BrowseDirectChildren', '', 0, 0, '');
  2590. my $tmp = $result->getValue('Result');
  2591. SONOS_Log $udn, 5, 'getSearchlistCategories BrowseResult: '.$tmp;
  2592. my %resultHash;
  2593. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2594. $resultHash{$1} = $2;
  2595. }
  2596. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2597. }
  2598. } elsif ($workType eq 'exportSonosBibliothek') {
  2599. SONOS_AddToLongJobsQueue($udn, $workType, \@params);
  2600. } elsif ($workType eq 'loadSearchlist') {
  2601. # Category holen
  2602. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2603. my $searchlistName = $1 if ($regSearch);
  2604. $searchlistName = uri_unescape($params[0]) if (!$regSearch);
  2605. # RegEx prüfen...
  2606. if ($regSearch) {
  2607. eval { "" =~ m/$searchlistName/ };
  2608. if($@) {
  2609. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad Category RegExp "'.$searchlistName.'": '.$@);
  2610. return;
  2611. }
  2612. }
  2613. # Element holen
  2614. $params[1] = '' if (!$params[1]);
  2615. my $regSearchElement = ($params[1] =~ m/^ *\/(.*)\/ *$/);
  2616. my $searchlistElement = $1 if ($regSearchElement);
  2617. $searchlistElement = uri_unescape($params[1]) if (!$regSearchElement);
  2618. # RegEx prüfen...
  2619. if ($regSearchElement) {
  2620. eval { "" =~ m/$searchlistElement/ };
  2621. if($@) {
  2622. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad CategoryElement RegExp "'.$searchlistElement.'": '.$@);
  2623. return;
  2624. }
  2625. }
  2626. # Filter angegeben?
  2627. my $filter = '//';
  2628. $filter = $params[2] if ($params[2]);
  2629. $filter .= '/' while ((SONOS_CountInString('/', $filter) - SONOS_CountInString('\/', $filter)) < 2);
  2630. my ($filterTitle, $filterAlbum, $filterArtist) = ($1, $3, $5) if ($filter =~ m/((.*?[^\\])|.{0})\/((.*?[^\\])|.{0})\/(.*)/);
  2631. $filterTitle = '.*' if (!$filterTitle);
  2632. $filterAlbum = '.*' if (!$filterAlbum);
  2633. $filterArtist = '.*' if (!$filterArtist);
  2634. SONOS_Log $udn, 4, 'getSearchlist filterTitle: '.$filterTitle;
  2635. SONOS_Log $udn, 4, 'getSearchlist filterAlbum: '.$filterAlbum;
  2636. SONOS_Log $udn, 4, 'getSearchlist filterArtist: '.$filterArtist;
  2637. # RegEx prüfen...
  2638. eval { "" =~ m/$filterTitle/ };
  2639. if($@) {
  2640. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterTitle RegExp "'.$filterTitle.'": '.$@);
  2641. return;
  2642. }
  2643. # RegEx prüfen...
  2644. eval { "" =~ m/$filterAlbum/ };
  2645. if($@) {
  2646. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterAlbum RegExp "'.$filterAlbum.'": '.$@);
  2647. return;
  2648. }
  2649. # RegEx prüfen...
  2650. eval { "" =~ m/$filterArtist/ };
  2651. if($@) {
  2652. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad FilterArtist RegExp "'.$filterArtist.'": '.$@);
  2653. return;
  2654. }
  2655. # Menge angegeben? Hier kann auch mit einem '*' eine zufällige Reihenfolge bestimmt werden...
  2656. my $maxElems = '0-';
  2657. $maxElems = $params[3] if ($params[3]);
  2658. # Anfragen durchführen...
  2659. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2660. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('A:', 'BrowseDirectChildren', '', 0, 0, '');
  2661. my $tmp = $result->getValue('Result');
  2662. SONOS_Log $udn, 5, 'getSearchlistCategories BrowseResult: '.$tmp;
  2663. # Category heraussuchen
  2664. my %resultHash;
  2665. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2666. next if (SONOS_Trim($2) eq ''); # Wenn kein Titel angegeben ist, dann überspringen
  2667. my $name = $2;
  2668. $resultHash{$name} = $1;
  2669. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2670. if ($regSearch) {
  2671. if ($name =~ m/$searchlistName/) {
  2672. $searchlistName = $name;
  2673. $regSearch = 0;
  2674. }
  2675. }
  2676. }
  2677. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2678. if (!$resultHash{$searchlistName} || $regSearch) {
  2679. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Category "'.$searchlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2680. return;
  2681. }
  2682. my $searchlistTitle = $searchlistName;
  2683. $searchlistName = $resultHash{$searchlistName};
  2684. ###############################################
  2685. # Elemente der Category heraussuchen
  2686. ###############################################
  2687. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistName, 'BrowseDirectChildren', '', 0, 0, '');
  2688. $tmp = $result->getValue('Result');
  2689. my $numberReturned = $result->getValue('NumberReturned');
  2690. my $totalMatches = $result->getValue('TotalMatches');
  2691. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements StepInfo_0 - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2692. while ($numberReturned < $totalMatches) {
  2693. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistName, 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2694. $tmp .= $result->getValue('Result');
  2695. $numberReturned += $result->getValue('NumberReturned');
  2696. $totalMatches = $result->getValue('TotalMatches');
  2697. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements StepInfo - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2698. }
  2699. SONOS_Log $udn, 4, 'getSearchlistCategoriesElements Totalmatches: '.$totalMatches;
  2700. SONOS_Log $udn, 5, 'getSearchlistCategoriesElements BrowseResult: '.$tmp;
  2701. # Category heraussuchen
  2702. my $searchlistElementTitle = $searchlistElement;
  2703. if ($tmp =~ m/<container id="(A:.*?)".*?>.*?<\/container>/ig) { # Wenn überhaupt noch was zu suchen ist...
  2704. %resultHash = ();
  2705. while ($tmp =~ m/<container id="(A:.*?)".*?><dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  2706. next if (SONOS_Trim($2) eq ''); # Wenn kein Titel angegeben ist, dann überspringen
  2707. my $name = $2;
  2708. $resultHash{$name} = $1;
  2709. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  2710. if ($regSearchElement) {
  2711. if ($name =~ m/$searchlistElement/) {
  2712. $searchlistElement = $name;
  2713. $regSearchElement = 0;
  2714. }
  2715. }
  2716. }
  2717. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  2718. if (!$resultHash{$searchlistElement} || $regSearchElement) {
  2719. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Element "'.$searchlistElement.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  2720. return;
  2721. }
  2722. $searchlistElementTitle = $searchlistElement;
  2723. $searchlistElement = $resultHash{$searchlistElement};
  2724. ###############################################
  2725. # Ziel-Elemente ermitteln und filtern
  2726. ###############################################
  2727. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', 0, 0, '');
  2728. $tmp = $result->getValue('Result');
  2729. # Wenn hier noch eine Schicht Container enthalten ist, dann nochmal tiefer gehen...
  2730. while ($tmp && ($tmp =~ m/<container.*?>.*?<\/container>/i)) {
  2731. $searchlistElement .= '/';
  2732. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', 0, 0, '');
  2733. $tmp = $result->getValue('Result');
  2734. }
  2735. $numberReturned = $result->getValue('NumberReturned');
  2736. $totalMatches = $result->getValue('TotalMatches');
  2737. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl StepInfo_0 - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2738. while ($numberReturned < $totalMatches) {
  2739. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchlistElement, 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2740. $tmp .= $result->getValue('Result');
  2741. $numberReturned += $result->getValue('NumberReturned');
  2742. $totalMatches = $result->getValue('TotalMatches');
  2743. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl StepInfo - NumberReturned: '.$numberReturned.' - Totalmatches: '.$totalMatches;
  2744. }
  2745. SONOS_Log $udn, 4, 'getSearchlistCategoriesElementsEl Totalmatches: '.$totalMatches;
  2746. SONOS_Log $udn, 5, 'getSearchlistCategoriesElementsEl BrowseResult: '.$tmp;
  2747. }
  2748. # Elemente heraussuchen
  2749. %resultHash = ();
  2750. my @URIs = ();
  2751. my @Metas = ();
  2752. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  2753. my $item = $2;
  2754. my $uri = $1 if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  2755. $uri =~ s/&apos;/'/gi;
  2756. my $title = '';
  2757. $title = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  2758. my $album = '';
  2759. $album = $1 if ($item =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  2760. my $interpret = '';
  2761. $interpret = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  2762. # Die Matches merken...
  2763. if (($title =~ m/$filterTitle/) && ($album =~ m/$filterAlbum/) && ($interpret =~ m/$filterArtist/)) {
  2764. my ($res, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($uri));
  2765. push(@URIs, $res);
  2766. push(@Metas, $meta);
  2767. }
  2768. }
  2769. my $answer = 'Retrieved all titles of category "'.$searchlistTitle.'" with searchvalue "'.$searchlistElementTitle.'" and filter "'.$filterTitle.'/'.$filterAlbum.'/'.$filterArtist.'" (#'.($#URIs + 1).'). ';
  2770. # Liste u.U. vermischen...
  2771. my @matches = (0..$#URIs);
  2772. if ($maxElems =~ m/^\*/) {
  2773. SONOS_Fisher_Yates_Shuffle(\@matches);
  2774. $answer .= 'Shuffled the searchlist. ';
  2775. }
  2776. # Nicht alle übernehmen?
  2777. if ($maxElems =~ m/^\*{0,1}(\d+)[\+-]{0,1}$/) {
  2778. splice(@matches, $1) if ($1 && ($1 <= $#matches));
  2779. SONOS_Log $udn, 4, 'getSearchlist maxElems('.$maxElems.'): '.$1;
  2780. }
  2781. SONOS_Log $udn, 4, 'getSearchlist Count Matches: '.($#matches + 1);
  2782. # Wenn der AVTransportProxy existiert weitermachen...
  2783. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  2784. # Playlist vorher leeren?
  2785. if ($maxElems =~ m/-$/) {
  2786. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue();
  2787. $answer .= 'Queue successfully emptied. ';
  2788. }
  2789. # An das Ende der Playlist oder hinter dem aktuellen Titel einfügen?
  2790. my $currentInsertPos = 0;
  2791. if ($maxElems =~ m/\+$/) {
  2792. $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('NrTracks') + 1;
  2793. } else {
  2794. $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track') + 1;
  2795. }
  2796. # Die Matches in die Playlist laden...
  2797. my $sliceSize = 16;
  2798. my $count = 0;
  2799. SONOS_Log $udn, 4, "Start-Adding: Count ".scalar(@matches)." / $sliceSize";
  2800. if (scalar(@matches)) {
  2801. for my $i (0..int(scalar(@matches) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren
  2802. my $startIndex = $i * $sliceSize;
  2803. my $endIndex = $startIndex + $sliceSize - 1;
  2804. $endIndex = SONOS_Min(scalar(@matches) - 1, $endIndex);
  2805. SONOS_Log $udn, 4, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)";
  2806. my $uri = '';
  2807. my $meta = '';
  2808. for my $index (@matches[$startIndex..$endIndex]) {
  2809. $uri .= ' '.$URIs[$index];
  2810. $meta .= ' '.$Metas[$index];
  2811. }
  2812. $uri = substr($uri, 1) if (length($uri) > 0);
  2813. $meta = substr($meta, 1) if (length($meta) > 0);
  2814. $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, $uri, $meta, '', '', $currentInsertPos, 0);
  2815. if (!$result->isSuccessful()) {
  2816. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  2817. }
  2818. $currentInsertPos += $endIndex - $startIndex + 1;
  2819. $count = $endIndex + 1;
  2820. }
  2821. if ($result->isSuccessful()) {
  2822. $answer .= 'Added '.$count.' entries from searchlist. There are now '.$result->getValue('NewQueueLength').' entries in Queue. ';
  2823. } else {
  2824. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  2825. }
  2826. }
  2827. # Die Liste als aktuelles Abspielstück einstellen, falls etwas anderes als die Playliste läuft...
  2828. my $currentMediaInfo = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  2829. my $currentMediaInfoCurrentURI = $currentMediaInfo->getValue('CurrentURI');
  2830. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  2831. if ($queueMetadata->getValue('Result') !~ m/<res.*?>$currentMediaInfoCurrentURI<\/res>/) {
  2832. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  2833. $answer .= 'Startlist: '.SONOS_UPnPAnswerMessage($result).'. ';
  2834. } else {
  2835. $answer .= 'Startlist not neccessary. ';
  2836. }
  2837. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$answer);
  2838. }
  2839. }
  2840. } elsif ($workType eq 'getRadios') {
  2841. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2842. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2843. my $tmp = $result->getValue('Result');
  2844. my %resultHash;
  2845. while ($tmp =~ m/<item id="(R:0\/0\/\d+)".*?><dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2846. $resultHash{$1} = $2;
  2847. }
  2848. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2849. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2850. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'RadiosListAlias', join('|', sort values %resultHash));
  2851. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'RadiosList', join('|', map { $_ =~ s/$SONOS_LISTELEMMASK/\./g; $_ } sort values %resultHash));
  2852. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2853. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2854. } else {
  2855. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"');
  2856. }
  2857. }
  2858. } elsif ($workType eq 'getRadiosWithCovers') {
  2859. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2860. my %resultHash = %{SONOS_GetBrowseStructuredResult($udn, 'R:0/0', 1, $workType, 1)};
  2861. #my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2862. #my $tmp = $result->getValue('Result');
  2863. #
  2864. #my %resultHash;
  2865. #while ($tmp =~ m/<item id="(R:0\/0\/\d+)".*?><dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2866. # $resultHash{$1}->{Title} = $2;
  2867. # $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  2868. # $resultHash{$1}->{Ressource} = decode_entities($3);
  2869. #}
  2870. #
  2871. #if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2872. # SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Radios', SONOS_Dumper(\%resultHash));
  2873. #
  2874. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2875. #} else {
  2876. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2877. #}
  2878. }
  2879. } elsif ($workType eq 'getQueue') {
  2880. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2881. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '');
  2882. my $tmp = $result->getValue('Result');
  2883. my $numberReturned = $result->getValue('NumberReturned');
  2884. my $totalMatches = $result->getValue('TotalMatches');
  2885. while ($numberReturned < $totalMatches) {
  2886. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2887. $tmp .= $result->getValue('Result');
  2888. $numberReturned += $result->getValue('NumberReturned');
  2889. $totalMatches = $result->getValue('TotalMatches');
  2890. }
  2891. my @inputArray;
  2892. while ($tmp =~ m/(<item id="Q:0\/\d+".*?>.*?<\/item>)/ig) {
  2893. push(@inputArray, $1);
  2894. }
  2895. my @resultArray;
  2896. my $position = 0;
  2897. foreach my $line (@inputArray) {
  2898. my $id = $1 if ($line =~ m/<item id="(Q:0\/\d+)".*?>.*?<\/item>/i);
  2899. my $duration = $1 if ($line =~ m/<item id="Q:0\/\d+".*?>.*?<res.*?duration="(.+?)".*?>.*?<\/item>/i);
  2900. my $res = $1 if ($line =~ m/<item id="Q:0\/\d+".*?>.*?<res.*?>(.*?)<\/res>.*?<\/item>/i);
  2901. my $title = $1 if ($line =~ m/<item id="Q:0\/\d+".*?>.*?<dc:title>(.*?)<\/dc:title>.*?<\/item>/i);
  2902. my $artist = $1 if ($line =~ m/<item id="Q:0\/\d+".*?>.*?<dc:creator>(.*?)<\/dc:creator>.*?<\/item>/i);
  2903. my $album = $1 if ($line =~ m/<item id="Q:0\/\d+".*?>.*?<upnp:album>(.*?)<\/upnp:album>.*?<\/item>/i);
  2904. if ($duration) {
  2905. push(@resultArray, ++$position.'. ('.$artist.') '.$title.' ['.$duration.']');
  2906. } else {
  2907. push(@resultArray, ++$position.'. ('.$artist.') '.$title.' [k.A.]');
  2908. }
  2909. }
  2910. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2911. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2912. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueListAlias', join('|', @resultArray));
  2913. # Elemente um 1 verschieben...
  2914. unshift(@resultArray, ''); @resultArray = keys @resultArray; shift(@resultArray);
  2915. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueList', join('|', @resultArray));
  2916. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2917. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2918. } else {
  2919. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', @resultArray).'"');
  2920. }
  2921. }
  2922. } elsif ($workType eq 'getQueueWithCovers') {
  2923. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2924. SONOS_GetQueueStructuredResult($udn, 1, 'getQueueWithCovers');
  2925. #my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '');
  2926. #my $tmp = $result->getValue('Result');
  2927. #
  2928. #my $numberReturned = $result->getValue('NumberReturned');
  2929. #my $totalMatches = $result->getValue('TotalMatches');
  2930. #while ($numberReturned < $totalMatches) {
  2931. # $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $numberReturned, 0, '');
  2932. # $tmp .= $result->getValue('Result');
  2933. #
  2934. # $numberReturned += $result->getValue('NumberReturned');
  2935. # $totalMatches = $result->getValue('TotalMatches');
  2936. #}
  2937. #
  2938. #my %resultHash;
  2939. #$resultHash{DurationSec} = 0;
  2940. #$resultHash{Duration} = '0:00:00';
  2941. #my $position = 0;
  2942. #while ($tmp =~ m/<item id="(Q:0\/)(\d+)".*?>.*?<res.*?(duration="(.+?)"|).*?>(.*?)<\/res>.*?<dc:title>(.*?)<\/dc:title>.*?<dc:creator>(.*?)<\/dc:creator>.*?<upnp:album>(.*?)<\/upnp:album>.*?<\/item>/ig) {
  2943. # my $key = $1.sprintf("%04d", $2);
  2944. # $resultHash{$key}->{Position} = ++$position;
  2945. # $resultHash{$key}->{Title} = $6;
  2946. # $resultHash{$key}->{Artist} = $7;
  2947. # $resultHash{$key}->{Album} = $8;
  2948. # if (defined($4)) {
  2949. # $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' ['.$4.']';
  2950. # $resultHash{$key}->{Duration} = $4;
  2951. # $resultHash{$key}->{DurationSec} = SONOS_GetTimeSeconds($4);
  2952. # $resultHash{DurationSec} += SONOS_GetTimeSeconds($4);
  2953. # } else {
  2954. # $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' [k.A.]';
  2955. # $resultHash{$key}->{Duration} = '0:00:00';
  2956. # $resultHash{$key}->{DurationSec} = 0;
  2957. # }
  2958. # $resultHash{$key}->{Cover} = SONOS_MakeCoverURL($udn, $5);
  2959. # $resultHash{$key}->{Ressource} = decode_entities($5);
  2960. #}
  2961. #$resultHash{Duration} = SONOS_ConvertSecondsToTime($resultHash{DurationSec});
  2962. #
  2963. #if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  2964. # SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  2965. # SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueDuration', $resultHash{Duration});
  2966. # SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueDurationSec', $resultHash{DurationSec});
  2967. # SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Queue', SONOS_Dumper(\%resultHash));
  2968. # SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  2969. #
  2970. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
  2971. #} else {
  2972. # SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash));
  2973. #}
  2974. }
  2975. } elsif ($workType eq 'loadRadio') {
  2976. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  2977. my $radioName = $1 if ($regSearch);
  2978. $radioName = uri_unescape($params[0]) if (!$regSearch);
  2979. # Alle übergebenen Anführungszeichen in die HTML Entity übersetzen, da es so auch von Sonos geliefert wird.
  2980. $radioName =~ s/'/&apos;/g;
  2981. # RegEx prüfen...
  2982. if ($regSearch) {
  2983. eval { "" =~ m/$radioName/ };
  2984. if($@) {
  2985. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$radioName.'": '.$@);
  2986. return;
  2987. }
  2988. }
  2989. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  2990. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, '');
  2991. my $tmp = $result->getValue('Result');
  2992. SONOS_Log $udn, 5, 'LoadRadio BrowseResult: '.$tmp;
  2993. my %resultHash;
  2994. while ($tmp =~ m/(<item id="(R:0\/0\/\d+)".*?>)<dc:title>(.*?)<\/dc:title>.*?(<upnp:class>.*?<\/upnp:class>).*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  2995. my $name = $3;
  2996. $resultHash{$name}{TITLE} = $name;
  2997. $resultHash{$name}{RES} = decode_entities($5);
  2998. $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;
  2999. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  3000. if ($regSearch) {
  3001. if ($name =~ m/$radioName/) {
  3002. $radioName = $name;
  3003. $regSearch = 0;
  3004. }
  3005. }
  3006. }
  3007. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  3008. if (!$resultHash{$radioName} || $regSearch) {
  3009. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Radio "'.$radioName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  3010. return;
  3011. }
  3012. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3013. SONOS_Log $udn, 5, 'LoadRadio SetAVTransport-Res: "'.$resultHash{$radioName}{RES}.'", -Meta: "'.$resultHash{$radioName}{METADATA}.'"';
  3014. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$radioName}{RES}, $resultHash{$radioName}{METADATA})));
  3015. }
  3016. }
  3017. } elsif ($workType eq 'startFavourite') {
  3018. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  3019. my $favouriteName = $1 if ($regSearch);
  3020. $favouriteName = uri_unescape($params[0]) if (!$regSearch);
  3021. # Alle übergebenen Anführungszeichen in die HTML Entity übersetzen, da es so auch von Sonos geliefert wird.
  3022. $favouriteName =~ s/'/&apos;/g;
  3023. # RegEx prüfen...
  3024. if ($regSearch) {
  3025. eval { "" =~ m/$favouriteName/ };
  3026. if($@) {
  3027. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$favouriteName.'": '.$@);
  3028. return;
  3029. }
  3030. }
  3031. my $nostart = 0;
  3032. if (defined($params[1]) && lc($params[1]) eq 'nostart') {
  3033. $nostart = 1;
  3034. }
  3035. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3036. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, '');
  3037. my $tmp = $result->getValue('Result');
  3038. SONOS_Log $udn, 5, 'StartFavourite BrowseResult: '.$tmp;
  3039. my %resultHash;
  3040. while ($tmp =~ m/(<item id="(FV:2\/\d+)".*?>)<dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<r:resMD>(.*?)<\/r:resMD>.*?<\/item>/ig) {
  3041. my $name = $3;
  3042. $resultHash{$name}{TITLE} = $name;
  3043. $resultHash{$name}{RES} = decode_entities($4);
  3044. $resultHash{$name}{METADATA} = decode_entities($5);
  3045. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  3046. if ($regSearch) {
  3047. if ($name =~ m/$favouriteName/) {
  3048. $favouriteName = $name;
  3049. $regSearch = 0;
  3050. }
  3051. }
  3052. }
  3053. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  3054. if (!$resultHash{$favouriteName} || $regSearch) {
  3055. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Favourite "'.$favouriteName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  3056. return;
  3057. }
  3058. SONOS_StartMetadata($workType, $udn, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, $nostart);
  3059. }
  3060. } elsif ($workType eq 'loadPlaylist') {
  3061. my $answer = '';
  3062. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  3063. my $playlistName = $1 if ($regSearch);
  3064. $playlistName = uri_unescape($params[0]) if (!$regSearch);
  3065. # Alle übergebenen Anführungszeichen in die HTML Entity übersetzen, da es so auch von Sonos geliefert wird.
  3066. $playlistName =~ s/'/&apos;/g;
  3067. # RegEx prüfen...
  3068. if ($regSearch) {
  3069. eval { "" =~ m/$playlistName/ };
  3070. if($@) {
  3071. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$playlistName.'": '.$@);
  3072. return;
  3073. }
  3074. }
  3075. my $overwrite = $params[1];
  3076. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3077. # Queue vorher leeren?
  3078. if ($overwrite) {
  3079. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue();
  3080. $answer .= 'Queue successfully emptied. ';
  3081. }
  3082. my $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track') + 1;
  3083. if ($playlistName =~ /^:m3ufile:(.*)/) {
  3084. my @URIs = ();
  3085. my @Metas = ();
  3086. # Versuche die Datei zu öffnen
  3087. if (!open(FILE, '<'.$1)) {
  3088. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during opening file "'.$1.'": '.$!);
  3089. return;
  3090. };
  3091. binmode(FILE, ':encoding(utf-8)');
  3092. while (<FILE>) {
  3093. if ($_ =~ m/^ *([^#].*) *\n/) {
  3094. next if ($1 eq '');
  3095. my ($res, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($1));
  3096. push(@URIs, $res);
  3097. push(@Metas, $meta);
  3098. }
  3099. }
  3100. close FILE;
  3101. # Elemente an die Queue anhängen
  3102. $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos);
  3103. } elsif ($playlistName =~ /^:device:(.*)/) {
  3104. my $sourceUDN = $1;
  3105. my @URIs = ();
  3106. my @Metas = ();
  3107. # Titel laden
  3108. my $playlistData;
  3109. my $startIndex = 0;
  3110. do {
  3111. $playlistData = $SONOS_ContentDirectoryControlProxy{$sourceUDN}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, '');
  3112. my $tmp = decode('UTF-8', $playlistData->getValue('Result'));
  3113. while ($tmp =~ m/<item.*?>.*?<res.*?>(.*?)<\/res>.*?<\/item>/ig) {
  3114. my ($res, $meta) = SONOS_CreateURIMeta(decode_entities($1));
  3115. next if (!defined($res));
  3116. push(@URIs, $res);
  3117. push(@Metas, $meta);
  3118. }
  3119. $startIndex += $playlistData->getValue('NumberReturned');
  3120. } while ($startIndex < $playlistData->getValue('TotalMatches'));
  3121. # Elemente an die Queue anhängen
  3122. $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos);
  3123. } else {
  3124. my $browseResult = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  3125. my $tmp = $browseResult->getValue('Result');
  3126. my %resultHash;
  3127. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  3128. my $name = $2;
  3129. $resultHash{$name} = $1;
  3130. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  3131. if ($regSearch) {
  3132. if ($name =~ m/$playlistName/) {
  3133. $playlistName = $name;
  3134. $regSearch = 0;
  3135. }
  3136. }
  3137. }
  3138. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  3139. if (!$resultHash{$playlistName} || $regSearch) {
  3140. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  3141. return;
  3142. }
  3143. # Titel laden
  3144. my $playlistData = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($resultHash{$playlistName}, 'BrowseMetadata', '', 0, 0, '');
  3145. my $playlistRes = SONOS_GetTagData('res', $playlistData->getValue('Result'));
  3146. # Elemente an die Queue anhängen
  3147. my $result = $SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $playlistRes, '', $currentInsertPos, 0);
  3148. $answer .= $result->getValue('NumTracksAdded').' Elems added. '.$result->getValue('NewQueueLength').' Elems in list now. ';
  3149. }
  3150. # Die Liste als aktuelles Abspielstück einstellen
  3151. my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  3152. my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  3153. $answer .= 'Startlist: '.SONOS_UPnPAnswerMessage($result).'. ';
  3154. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$answer);
  3155. }
  3156. } elsif ($workType eq 'setAlarm') {
  3157. my $create = $params[0];
  3158. my @idParams = split(',', $params[1]);
  3159. # Die passenden IDs heraussuchen...
  3160. my @idList = map { SONOS_Trim($_) } split(',', SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListIDs', ''));
  3161. my @id = ();
  3162. foreach my $elem (@idList) {
  3163. if ((lc($idParams[0]) eq 'all') || SONOS_isInList($elem, @idParams)) {
  3164. push @id, $elem;
  3165. }
  3166. }
  3167. # Alle folgenden Parameter weglesen und an den letzten Parameter anhängen
  3168. my $values = {};
  3169. my $val = join(',', @params[2..$#params]);
  3170. if ($val ne '') {
  3171. $values = \%{eval($val)};
  3172. }
  3173. # Wenn keine passenden Elemente gefunden wurden...
  3174. if (scalar(@id) == 0) {
  3175. if ((lc($create) eq 'update') || (lc($create) eq 'delete')) {
  3176. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  3177. }
  3178. }
  3179. # Hier die passenden Änderungen durchführen...
  3180. if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) {
  3181. # Die Room-ID immer fest auf den aktuellen Player eintragen.
  3182. # 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...
  3183. $values->{RoomUUID} = $1 if ($udn =~ m/(.*?)_MR/i);
  3184. if (lc($create) eq 'update') {
  3185. my $ret = '';
  3186. foreach my $id (@id) {
  3187. my %alarm = %{eval(SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmList', '{}'))->{$id}};
  3188. # Replace old values with the given new ones...
  3189. for my $key (keys %alarm) {
  3190. if (defined($values->{$key})) {
  3191. $alarm{$key} = $values->{$key};
  3192. }
  3193. }
  3194. if (!SONOS_CheckAndCorrectAlarmHash(\%alarm)) {
  3195. $ret .= '#'.$id.': '.SONOS_AnswerMessage(0).', ';
  3196. } else {
  3197. # Send to Zoneplayer
  3198. $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})).', ';
  3199. }
  3200. }
  3201. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$ret);
  3202. } elsif (lc($create) eq 'create') {
  3203. # Check if all parameters are given
  3204. if (!SONOS_CheckAndCorrectAlarmHash($values)) {
  3205. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  3206. } else {
  3207. # create here on Zoneplayer
  3208. 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'));
  3209. }
  3210. } elsif (lc($create) eq 'delete') {
  3211. my $ret = '';
  3212. foreach my $id (@id) {
  3213. $ret .= '#'.$id.': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->DestroyAlarm($id)).', ';
  3214. }
  3215. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$ret);
  3216. } else {
  3217. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0));
  3218. }
  3219. }
  3220. } elsif ($workType eq 'setSnoozeAlarm') {
  3221. my $time = $params[0];
  3222. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3223. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SnoozeAlarm(0, $time)));
  3224. }
  3225. } elsif ($workType eq 'setDailyIndexRefreshTime') {
  3226. my $time = $params[0];
  3227. if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) {
  3228. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->SetDailyIndexRefreshTime($time)));
  3229. }
  3230. } elsif ($workType eq 'setSleepTimer') {
  3231. my $time = $params[0];
  3232. if ((lc($time) eq 'off') || ($time =~ /0+:0+:0+/)) {
  3233. $time = '';
  3234. }
  3235. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3236. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->ConfigureSleepTimer(0, $time)));
  3237. }
  3238. } elsif ($workType eq 'addMember') {
  3239. my $memberudn = $params[0];
  3240. if (SONOS_CheckProxyObject($memberudn, $SONOS_AVTransportControlProxy{$memberudn}) && SONOS_CheckProxyObject($memberudn, $SONOS_ZoneGroupTopologyProxy{$memberudn})) {
  3241. # Wenn der hinzuzufügende Player Koordinator einer anderen Gruppe ist,
  3242. # dann erst mal ein anderes Gruppenmitglied zum Koordinator machen
  3243. #my @zoneTopology = SONOS_ConvertZoneGroupState($SONOS_ZoneGroupTopologyProxy{$memberudn}->GetZoneGroupState()->getValue('ZoneGroupState'));
  3244. # Hier fehlt noch die Umstellung der bestehenden Gruppe...
  3245. # Sicherstellen, dass der hinzuzufügende Player kein Bestandteil einer Gruppe mehr ist.
  3246. $SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0);
  3247. my $coordinatorUDNShort = $1 if ($udn =~ m/(.*)_MR/);
  3248. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->SetAVTransportURI(0, 'x-rincon:'.$coordinatorUDNShort, '')));
  3249. }
  3250. } elsif ($workType eq 'removeMember') {
  3251. my $memberudn = $params[0];
  3252. if (SONOS_CheckProxyObject($memberudn, $SONOS_AVTransportControlProxy{$memberudn})) {
  3253. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0)));
  3254. }
  3255. } elsif ($workType eq 'makeStandaloneGroup') {
  3256. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3257. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->BecomeCoordinatorOfStandaloneGroup(0)));
  3258. }
  3259. } elsif ($workType eq 'createStereoPair') {
  3260. my $pairString = uri_unescape($params[0]);
  3261. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  3262. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->CreateStereoPair($pairString)));
  3263. }
  3264. } elsif ($workType eq 'separateStereoPair') {
  3265. my $pairString = uri_unescape($params[0]);
  3266. if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) {
  3267. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SeparateStereoPair($pairString)));
  3268. }
  3269. } elsif ($workType eq 'emptyPlaylist') {
  3270. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3271. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue()));
  3272. }
  3273. } elsif ($workType eq 'savePlaylist') {
  3274. my $playlistName = $params[0];
  3275. my $playlistType = $params[1];
  3276. $playlistName =~ s/ $//g;
  3277. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3278. if ($playlistType eq ':m3ufile:') {
  3279. open (FILE, '>'.$playlistName);
  3280. print FILE "#EXTM3U\n";
  3281. my $startIndex = 0;
  3282. my $result;
  3283. my $count = 0;
  3284. do {
  3285. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, '');
  3286. my $queueSongdata = $result->getValue('Result');
  3287. while ($queueSongdata =~ m/<item.*?>(.*?)<\/item>/gi) {
  3288. my $item = $1;
  3289. my $res = uri_unescape(SONOS_GetURIFromQueueValue(decode_entities($1))) if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  3290. my $artist = decode_entities($1) if ($item =~ m/<dc:creator.*?>(.*?)<\/dc:creator>/i);
  3291. my $title = decode_entities($1) if ($item =~ m/<dc:title.*?>(.*?)<\/dc:title>/i);
  3292. my $time = 0;
  3293. $time = SONOS_GetTimeSeconds($1) if ($item =~ m/.*?duration="(.*?)"/);
  3294. # In Datei wegschreiben
  3295. eval {
  3296. print FILE "#EXTINF:$time,($artist) $title\n$res\n";
  3297. };
  3298. $count++;
  3299. }
  3300. $startIndex += $result->getValue('NumberReturned');
  3301. } while ($startIndex < $result->getValue('TotalMatches'));
  3302. close FILE;
  3303. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New M3U-File "'.$playlistName.'" successfully created with '.$count.' entries!');
  3304. } else {
  3305. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  3306. my $tmp = $result->getValue('Result');
  3307. my %resultHash;
  3308. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  3309. $resultHash{$2} = $1;
  3310. }
  3311. if ($resultHash{$playlistName}) {
  3312. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Existing Playlist "'.$playlistName.'" updated: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, $resultHash{$playlistName})));
  3313. } else {
  3314. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New Playlist '.$playlistName.' created: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, '')));
  3315. }
  3316. }
  3317. }
  3318. } elsif ($workType eq 'deleteFromQueue') {
  3319. $params[0] = uri_unescape($params[0]);
  3320. # Simple Check...
  3321. if ($params[0] !~ m/^[\.\,\d]*$/) {
  3322. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Parameter Error: '.$params[0]);
  3323. return;
  3324. }
  3325. my @elemList = sort { $a <=> $b } SONOS_DeleteDoublettes(eval('('.$params[0].')'));
  3326. SONOS_Log undef, 5, 'DeleteFromQueue: Index-Liste: '.Dumper(\@elemList);
  3327. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3328. # Maximale Indizies bestimmen
  3329. my $maxElems = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '')->getValue('TotalMatches');
  3330. my $deleteCounter = 0;
  3331. foreach my $elem (@elemList) {
  3332. if (($elem > 0) && ($elem <= $maxElems)) {
  3333. $deleteCounter++ if ($SONOS_AVTransportControlProxy{$udn}->RemoveTrackFromQueue(0, 'Q:0/'.($elem - $deleteCounter), 0)->isSuccessful());
  3334. }
  3335. }
  3336. $maxElems = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '')->getValue('TotalMatches');
  3337. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Deleted '.$deleteCounter.' elems. In list are now '.$maxElems.' elems.');
  3338. }
  3339. } elsif ($workType eq 'deletePlaylist') {
  3340. my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/);
  3341. my $playlistName = $1 if ($regSearch);
  3342. $playlistName = uri_unescape($params[0]) if (!$regSearch);
  3343. # RegEx prüfen...
  3344. if ($regSearch) {
  3345. eval { "" =~ m/$playlistName/ };
  3346. if($@) {
  3347. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$playlistName.'": '.$@);
  3348. return;
  3349. }
  3350. }
  3351. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, '');
  3352. my $tmp = $result->getValue('Result');
  3353. my %resultHash;
  3354. while ($tmp =~ m/<container id="(SQ:\d+)".*?<dc:title>(.*?)<\/dc:title>.*?<\/container>/ig) {
  3355. my $name = $2;
  3356. $resultHash{$name} = $1;
  3357. # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken...
  3358. if ($regSearch) {
  3359. if ($name =~ m/$playlistName/) {
  3360. $playlistName = $name;
  3361. $regSearch = 0;
  3362. }
  3363. }
  3364. }
  3365. # Wenn RegSearch gesetzt war, und nichts gefunden wurde...
  3366. if (!$resultHash{$playlistName} || $regSearch) {
  3367. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"');
  3368. return;
  3369. }
  3370. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" deleted: '.SONOS_UPnPAnswerMessage($SONOS_ContentDirectoryControlProxy{$udn}->DestroyObject($resultHash{$playlistName})));
  3371. } elsif ($workType eq 'deleteProxyObjects') {
  3372. # Wird vom Sonos-Device selber in IsAlive benötigt
  3373. SONOS_DeleteProxyObjects($udn);
  3374. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(1));
  3375. } elsif ($workType eq 'renewSubscription') {
  3376. SONOS_ProcessRenew($udn, 'Transport', \%SONOS_TransportSubscriptions);
  3377. SONOS_ProcessRenew($udn, 'Rendering', \%SONOS_RenderingSubscriptions);
  3378. SONOS_ProcessRenew($udn, 'GroupRendering', \%SONOS_GroupRenderingSubscriptions);
  3379. SONOS_ProcessRenew($udn, 'ContentDirectory', \%SONOS_ContentDirectorySubscriptions);
  3380. SONOS_ProcessRenew($udn, 'Alarm', \%SONOS_AlarmSubscriptions);
  3381. SONOS_ProcessRenew($udn, 'ZoneGroupTopology', \%SONOS_ZoneGroupTopologySubscriptions);
  3382. SONOS_ProcessRenew($udn, 'DeviceProperties', \%SONOS_DevicePropertiesSubscriptions);
  3383. SONOS_ProcessRenew($udn, 'AudioIn', \%SONOS_AudioInSubscriptions);
  3384. SONOS_ProcessRenew($udn, 'MusicServices', \%SONOS_MusicServicesSubscriptions);
  3385. } elsif ($workType eq 'startHandle') {
  3386. if ($params[0] =~ m/^(.+)\|(.+)$/) {
  3387. my $songURI = $1;
  3388. my $songMeta = $2;
  3389. SONOS_Log undef, 4, 'songURI: '.$songURI;
  3390. SONOS_Log undef, 4, 'songMeta: '.$songMeta;
  3391. SONOS_Log undef, 4, 'nostart: '.$params[1];
  3392. SONOS_StartMetadata($workType, $udn, $songURI, $songMeta, $params[1]);
  3393. } else {
  3394. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Wrong Handle: '.$params[0]);
  3395. }
  3396. } elsif ($workType eq 'playURI') {
  3397. my $songURI = SONOS_ExpandURIForQueueing($params[0]);
  3398. SONOS_Log undef, 4, 'songURI: '.$songURI;
  3399. my $volume;
  3400. if ($#params > 0) {
  3401. $volume = $params[1];
  3402. }
  3403. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3404. my ($uri, $meta) = SONOS_CreateURIMeta($songURI);
  3405. SONOS_Log undef, 4, 'URI: '.$uri;
  3406. SONOS_Log undef, 4, 'Meta: '.$meta;
  3407. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $uri, $meta);
  3408. if (defined($volume)) {
  3409. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  3410. $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0);
  3411. if ($volume =~ m/^[+-]{1}/) {
  3412. $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume)
  3413. } else {
  3414. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume);
  3415. }
  3416. }
  3417. }
  3418. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)->isSuccessful));
  3419. }
  3420. } elsif ($workType eq 'playURITemp') {
  3421. my $destURL = $params[0];
  3422. my $volume;
  3423. if ($#params > 0) {
  3424. $volume = $params[1];
  3425. }
  3426. SONOS_PlayURITemp($udn, $destURL, $volume);
  3427. } elsif ($workType eq 'addURIToQueue') {
  3428. my $songURI = SONOS_ExpandURIForQueueing($params[0]);
  3429. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3430. my $track = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track');
  3431. my ($uri, $meta) = SONOS_CreateURIMeta($songURI);
  3432. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $uri, $meta, $track + 1, 1)));
  3433. }
  3434. } elsif ($workType =~ m/speak\d+/i) {
  3435. my $volume = $params[0];
  3436. my $language = $params[1];
  3437. my $text = $params[2];
  3438. for(my $i = 3; $i < @params; $i++) {
  3439. $text .= ','.$params[$i];
  3440. }
  3441. $text =~ s/^ *(.*) *$/$1/g;
  3442. $text = SONOS_Utf8ToLatin1($text);
  3443. my $digest = '';
  3444. if (SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileHashCache', 0) == 1) {
  3445. eval {
  3446. require Digest::SHA1;
  3447. import Digest::SHA1 qw(sha1_hex);
  3448. $digest = '_'.sha1_hex(lc($text));
  3449. };
  3450. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  3451. # Unter Ubuntu gibt es die SHA1-Library nicht mehr, sodass man dort eine andere einbinden muss (SHA)
  3452. eval {
  3453. require Digest::SHA;
  3454. import Digest::SHA qw(sha1_hex);
  3455. $digest = '_'.sha1_hex(lc($text));
  3456. };
  3457. }
  3458. if ($@ =~ /Wide character in subroutine entry/i) {
  3459. eval {
  3460. require Digest::SHA1;
  3461. import Digest::SHA1 qw(sha1_hex);
  3462. $digest = '_'.sha1_hex(lc(encode("iso-8859-1", $text, 0)));
  3463. };
  3464. if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) {
  3465. eval {
  3466. require Digest::SHA;
  3467. import Digest::SHA qw(sha1_hex);
  3468. $digest = '_'.sha1_hex(lc(encode("iso-8859-1", $text, 0)));
  3469. };
  3470. }
  3471. }
  3472. if ($@) {
  3473. SONOS_Log $udn, 2, 'Beim Ermitteln des Hash-Wertes ist ein Fehler aufgetreten: '.$@;
  3474. return;
  3475. }
  3476. }
  3477. my $timestamp = '';
  3478. if (!$digest && SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileTimestamp', 0) == 1) {
  3479. my @timearray = localtime;
  3480. $timestamp = sprintf("_%04d%02d%02d-%02d%02d%02d", $timearray[5]+1900, $timearray[4]+1, $timearray[3], $timearray[2], $timearray[1], $timearray[0]);
  3481. }
  3482. my $fileExtension = SONOS_GetSpeakFileExtension($workType);
  3483. my $dest = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakDir', '.').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension;
  3484. my $destURL = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakURL', '').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension;
  3485. if ($digest && (-e $dest)) {
  3486. SONOS_Log $udn, 3, 'Hole die Durchsage aus dem Cache...';
  3487. } else {
  3488. if (!SONOS_GetSpeakFile($udn, $workType, $language, $text, $dest)) {
  3489. return;
  3490. }
  3491. # MP3-Tags setzen, wenn die entsprechende Library gefunden wurde, und die Ausgabe in ein MP3-Format erfolgte
  3492. if (lc(substr($dest, -3, 3)) eq 'mp3') {
  3493. eval {
  3494. my $mp3GroundPath = SONOS_GetAbsolutePath($0);
  3495. $mp3GroundPath = substr($mp3GroundPath, 0, rindex($mp3GroundPath, '/'));
  3496. require MP3::Tag;
  3497. my $mp3 = MP3::Tag->new($dest);
  3498. $mp3->config(write_v24 => 1);
  3499. $mp3->title_set($text);
  3500. $mp3->artist_set('FHEM ~ Sonos');
  3501. $mp3->album_set('Sprachdurchsagen');
  3502. my $coverPath = SONOS_Client_Data_Retreive('undef', 'attr', ucfirst(lc(($workType =~ /0$/) ? 'speak' : $workType)).'Cover', $mp3GroundPath.'/www/images/default/fhemicon.png');
  3503. my $imgfile = SONOS_ReadFile($coverPath);
  3504. $mp3->set_id3v2_frame('APIC', 0, (($coverPath =~ m/\.png$/) ? 'image/png' : 'image/jpeg'), chr(3), 'Cover Image', $imgfile) if ($imgfile);
  3505. $mp3->update_tags();
  3506. };
  3507. if ($@) {
  3508. SONOS_Log $udn, 2, 'Beim Setzen der MP3-Informationen (ID3TagV2) ist ein Fehler aufgetreten: '.$@;
  3509. }
  3510. }
  3511. }
  3512. SONOS_PlayURITemp($udn, $destURL, $volume);
  3513. } elsif ($workType eq 'restartControlPoint') {
  3514. SONOS_RestartControlPoint();
  3515. } else {
  3516. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DoWork-Syntax ERROR');
  3517. }
  3518. };
  3519. if ($@) {
  3520. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'DoWork-Exception ERROR: '.$@);
  3521. }
  3522. }
  3523. ########################################################################################
  3524. #
  3525. # SONOS_ProcessRenew - Process the renewal of sbscriptions
  3526. #
  3527. ########################################################################################
  3528. sub SONOS_ProcessRenew($$$) {
  3529. my ($udn, $subscriptionName, $subscriptionHash) = @_;
  3530. if (defined($subscriptionHash->{$udn}) && (Time::HiRes::time() - $subscriptionHash->{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
  3531. eval {
  3532. $SIG{__WARN__} = sub { $_ = shift; };
  3533. $subscriptionHash->{$udn}->renew();
  3534. SONOS_Log $udn, 3, $subscriptionName.'-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
  3535. };
  3536. if ($@) {
  3537. SONOS_Log $udn, 3, 'Error! '.$subscriptionName.'-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
  3538. # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
  3539. # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
  3540. if ($@ =~ m/Can.t connect to/i) {
  3541. SONOS_DeleteProxyObjects($udn);
  3542. # Player-Informationen aktualisieren...
  3543. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
  3544. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'state', 'disappeared');
  3545. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'transportState', 'STOPPED');
  3546. # Discovery neu anstarten, falls der Player irgendwie doch noch erreichbar sein sollte...
  3547. $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
  3548. }
  3549. }
  3550. }
  3551. }
  3552. ########################################################################################
  3553. #
  3554. # SONOS_GetBrowseStructuredResult - Browse and give the result back
  3555. #
  3556. ########################################################################################
  3557. sub SONOS_GetBrowseStructuredResult($$@) {
  3558. my ($udn, $searchValue, $withLastActionResult, $workType, $singleUpdate, $container) = @_;
  3559. $withLastActionResult = 0 if (!defined($withLastActionResult));
  3560. $workType = '' if (!defined($workType));
  3561. $singleUpdate = 0 if (!defined($singleUpdate));
  3562. if (defined($container)) {
  3563. $container = 'container';
  3564. } else {
  3565. $container = 'item';
  3566. }
  3567. my %resultHash = ();
  3568. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3569. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($searchValue, 'BrowseDirectChildren', '', 0, 0, '');
  3570. my $tmp = $result->getValue('Result');
  3571. while ($tmp =~ m/<$container id="(.+?)".*?><dc:title>(.*?)<\/dc:title>.*?<res.*?>(.*?)<\/res>.*?<\/$container>/ig) {
  3572. $resultHash{$1}->{Title} = $2;
  3573. $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3);
  3574. $resultHash{$1}->{Ressource} = decode_entities($3);
  3575. }
  3576. }
  3577. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  3578. my $type = $1 if ($workType =~ m/get(.*?)WithCovers/);
  3579. SONOS_Client_Data_Refresh('Readings'.(($singleUpdate) ? 'Single' : 'Bulk').'UpdateIfChanged', $udn, $type, SONOS_Dumper(\%resultHash));
  3580. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet') if ($withLastActionResult);
  3581. } else {
  3582. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash)) if ($withLastActionResult);
  3583. }
  3584. return \%resultHash;
  3585. }
  3586. ########################################################################################
  3587. #
  3588. # SONOS_GetQueueStructuredResult - Browse Queue and give the result back
  3589. #
  3590. ########################################################################################
  3591. sub SONOS_GetQueueStructuredResult($$$) {
  3592. my ($udn, $withLastActionResult, $workType) = @_;
  3593. my %resultHash = ();
  3594. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3595. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '');
  3596. my $tmp = $result->getValue('Result');
  3597. my $numberReturned = $result->getValue('NumberReturned');
  3598. my $totalMatches = $result->getValue('TotalMatches');
  3599. while ($numberReturned < $totalMatches) {
  3600. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $numberReturned, 0, '');
  3601. $tmp .= $result->getValue('Result');
  3602. $numberReturned += $result->getValue('NumberReturned');
  3603. $totalMatches = $result->getValue('TotalMatches');
  3604. }
  3605. $resultHash{DurationSec} = 0;
  3606. $resultHash{Duration} = '0:00:00';
  3607. my $position = 0;
  3608. while ($tmp =~ m/<item id="(Q:0\/)(\d+)".*?>.*?<res.*?(duration="(.+?)"|).*?>(.*?)<\/res>.*?<dc:title>(.*?)<\/dc:title>.*?<dc:creator>(.*?)<\/dc:creator>.*?<upnp:album>(.*?)<\/upnp:album>.*?<\/item>/ig) {
  3609. my $key = $1.sprintf("%04d", $2);
  3610. $resultHash{$key}->{Position} = ++$position;
  3611. $resultHash{$key}->{Title} = $6;
  3612. $resultHash{$key}->{Artist} = $7;
  3613. $resultHash{$key}->{Album} = $8;
  3614. if (defined($4)) {
  3615. $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' ['.$4.']';
  3616. $resultHash{$key}->{Duration} = $4;
  3617. $resultHash{$key}->{DurationSec} = SONOS_GetTimeSeconds($4);
  3618. $resultHash{DurationSec} += SONOS_GetTimeSeconds($4);
  3619. } else {
  3620. $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' [k.A.]';
  3621. $resultHash{$key}->{Duration} = '0:00:00';
  3622. $resultHash{$key}->{DurationSec} = 0;
  3623. }
  3624. $resultHash{$key}->{Cover} = SONOS_MakeCoverURL($udn, $5);
  3625. $resultHash{$key}->{Ressource} = decode_entities($5);
  3626. }
  3627. $resultHash{Duration} = SONOS_ConvertSecondsToTime($resultHash{DurationSec});
  3628. }
  3629. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  3630. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  3631. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueDuration', $resultHash{Duration});
  3632. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'QueueDurationSec', $resultHash{DurationSec});
  3633. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Queue', SONOS_Dumper(\%resultHash));
  3634. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  3635. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet') if ($withLastActionResult);
  3636. } else {
  3637. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_Dumper(\%resultHash)) if ($withLastActionResult);
  3638. }
  3639. return \%resultHash;
  3640. }
  3641. ########################################################################################
  3642. #
  3643. # SONOS_StartMetadata - Starts any kind of Metadata
  3644. #
  3645. ########################################################################################
  3646. sub SONOS_StartMetadata($$$$) {
  3647. my ($workType, $udn, $res, $meta, $nostart) = @_;
  3648. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  3649. # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann
  3650. if ($meta =~ m/<upnp:class>object\.container.*?<\/upnp:class>/i) {
  3651. SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$res.'", -Meta: "'.$meta.'"';
  3652. # Queue leeren
  3653. $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0);
  3654. # Queue wieder füllen
  3655. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $res, $meta, 0, 1)));
  3656. # Queue aktivieren
  3657. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), '');
  3658. } else {
  3659. SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$res.'", -Meta: "'.$meta.'"';
  3660. # Stück aktivieren
  3661. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $res, $meta)));
  3662. }
  3663. # Abspielen starten, wenn nicht absichtlich verhindert
  3664. $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart);
  3665. }
  3666. }
  3667. ########################################################################################
  3668. #
  3669. # SONOS_RecursiveStructure - Retrieves the structure of the Sonos-Bibliothek
  3670. #
  3671. ########################################################################################
  3672. sub SONOS_RecursiveStructure($$$$) {
  3673. my ($udn, $search, $exportsStruct, $exportsTitles) = @_;
  3674. my $startIndex = 0;
  3675. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, '');
  3676. return if (!defined($result->getValue('NumberReturned')));
  3677. $startIndex += $result->getValue('NumberReturned');
  3678. my $tmp = decode('UTF-8', $result->getValue('Result'));
  3679. # Alle Suchergebnisse vom Player abfragen...
  3680. while ($startIndex < $result->getValue('TotalMatches')) {
  3681. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, '');
  3682. $tmp .= decode('UTF-8', $result->getValue('Result'));
  3683. $startIndex += $result->getValue('NumberReturned');
  3684. }
  3685. # Struktur verarbeiten...
  3686. while ($tmp =~ m/<container id="(.*?)".*?>(.*?)<\/container>/ig) {
  3687. my $id = $1;
  3688. my $item = $2;
  3689. next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen
  3690. $exportsStruct->{$id}->{ID} = $id;
  3691. $exportsStruct->{$id}->{Title} = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  3692. $exportsStruct->{$id}->{Artist} = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  3693. $exportsStruct->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  3694. $exportsStruct->{$id}->{Type} = 'Container';
  3695. $exportsStruct->{$id}->{Children} = {};
  3696. # Wenn hier eine Titel-ID gesucht werden soll, die es bereits lokal gibt, dann nicht mehr anfragen...
  3697. if (!$exportsTitles->{$id}) {
  3698. SONOS_RecursiveStructure($udn, $id, $exportsStruct->{$id}->{Children}, $exportsTitles);
  3699. }
  3700. }
  3701. # Titel verarbeiten...
  3702. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  3703. my $id = $1;
  3704. my $item = $2;
  3705. next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen
  3706. # Titel merken...
  3707. $exportsTitles->{$id}->{ID} = $id;
  3708. $exportsTitles->{$id}->{TrackURI} = SONOS_GetURIFromQueueValue($1) if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  3709. $exportsTitles->{$id}->{Title} = $1 if ($item =~ m/<dc:title>(.*?)<\/dc:title>/i);
  3710. $exportsTitles->{$id}->{Artist} = $1 if ($item =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  3711. $exportsTitles->{$id}->{AlbumArtist} = $1 if ($item =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  3712. $exportsTitles->{$id}->{Album} = $1 if ($item =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  3713. $exportsTitles->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  3714. $exportsTitles->{$id}->{OriginalTrackNumber} = $1 if ($item =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  3715. # Verweis in der Struktur merken...
  3716. $exportsStruct->{$id}->{ID} = $id;
  3717. $exportsStruct->{$id}->{Type} = 'Track';
  3718. }
  3719. }
  3720. ########################################################################################
  3721. #
  3722. # SONOS_AddMultipleURIsToQueue - Adds the given URIs to the current queue of the player
  3723. #
  3724. ########################################################################################
  3725. sub SONOS_AddMultipleURIsToQueue($$$$) {
  3726. my ($udn, $URIs, $Metas, $currentInsertPos) = @_;
  3727. my @URIs = @{$URIs};
  3728. my @Metas = @{$Metas};
  3729. my $sliceSize = 16;
  3730. my $result;
  3731. my $count = 0;
  3732. my $answer = '';
  3733. SONOS_Log $udn, 5, "Start-Adding: Count ".scalar(@URIs)." / $sliceSize";
  3734. for my $i (0..int(scalar(@URIs) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren
  3735. my $startIndex = $i * $sliceSize;
  3736. my $endIndex = $startIndex + $sliceSize - 1;
  3737. $endIndex = SONOS_Min(scalar(@URIs) - 1, $endIndex);
  3738. SONOS_Log $udn, 5, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)";
  3739. SONOS_Log $udn, 5, "Upload($currentInsertPos)-URI: ".join(' ', @URIs[$startIndex..$endIndex]);
  3740. SONOS_Log $udn, 5, "Upload($currentInsertPos)-Meta: ".join(' ', @Metas[$startIndex..$endIndex]);
  3741. $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, join(' ', @URIs[$startIndex..$endIndex]), join(' ', @Metas[$startIndex..$endIndex]), '', '', $currentInsertPos, 0);
  3742. if (!$result->isSuccessful()) {
  3743. $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' ';
  3744. }
  3745. $currentInsertPos += $endIndex - $startIndex + 1;
  3746. $count = $endIndex + 1;
  3747. }
  3748. if ($result->isSuccessful()) {
  3749. $answer .= 'Added '.$count.' entries from file "'.$1.'". There are now '.$result->getValue('NewQueueLength').' entries in Queue. ';
  3750. } else {
  3751. $answer .= 'Adding: '.SONOS_UPnPAnswerMessage($result).' ';
  3752. }
  3753. return $answer;
  3754. }
  3755. ########################################################################################
  3756. #
  3757. # SONOS_Hex2String - Converts Hex-Representation into String
  3758. #
  3759. ########################################################################################
  3760. sub SONOS_Hex2String($) {
  3761. my $s = shift;
  3762. return pack 'H*', $s;
  3763. }
  3764. ########################################################################################
  3765. #
  3766. # SONOS_String2Hex - Converts a normal String into the Hex-Representation
  3767. #
  3768. ########################################################################################
  3769. sub SONOS_String2Hex($) {
  3770. my $s = shift;
  3771. return unpack("H*", $s);
  3772. }
  3773. ########################################################################################
  3774. #
  3775. # SONOS_Fisher_Yates_Shuffle - Shuffles the given array
  3776. #
  3777. ########################################################################################
  3778. sub SONOS_Fisher_Yates_Shuffle($) {
  3779. my ($deck) = @_; # $deck is a reference to an array
  3780. my $i = @$deck;
  3781. while ($i--) {
  3782. my $j = int rand ($i+1);
  3783. @$deck[$i,$j] = @$deck[$j,$i];
  3784. }
  3785. }
  3786. ########################################################################################
  3787. #
  3788. # SONOS_GetShuffleRepeatStates - Retreives the information according shuffle and repeat
  3789. #
  3790. ########################################################################################
  3791. sub SONOS_GetShuffleRepeatStates($) {
  3792. my ($data) = @_;
  3793. my $shuffle = $data =~ m/SHUFFLE/;
  3794. my $repeat = $data eq 'SHUFFLE' || $data eq 'REPEAT_ALL';
  3795. my $repeatOne = $data =~ m/REPEAT_ONE/;
  3796. return ($shuffle, $repeat, $repeatOne);
  3797. }
  3798. ########################################################################################
  3799. #
  3800. # SONOS_GetShuffleRepeatString - Generates the information string according shuffle and repeat
  3801. #
  3802. ########################################################################################
  3803. sub SONOS_GetShuffleRepeatString($$$) {
  3804. my ($shuffle, $repeat, $repeatOne) = @_;
  3805. my $newMode = 'NORMAL';
  3806. $newMode = 'SHUFFLE' if ($shuffle && $repeat && $repeatOne);
  3807. $newMode = 'SHUFFLE' if ($shuffle && $repeat && !$repeatOne);
  3808. $newMode = 'SHUFFLE_REPEAT_ONE' if ($shuffle && !$repeat && $repeatOne);
  3809. $newMode = 'SHUFFLE_NOREPEAT' if ($shuffle && !$repeat && !$repeatOne);
  3810. $newMode = 'REPEAT_ALL' if (!$shuffle && $repeat && $repeatOne);
  3811. $newMode = 'REPEAT_ALL' if (!$shuffle && $repeat && !$repeatOne);
  3812. $newMode = 'REPEAT_ONE' if (!$shuffle && !$repeat && $repeatOne);
  3813. $newMode = 'NORMAL' if (!$shuffle && !$repeat && !$repeatOne);
  3814. return $newMode;
  3815. }
  3816. ########################################################################################
  3817. #
  3818. # SONOS_DeleteDoublettes - Deletes duplicate entries in the given array
  3819. #
  3820. ########################################################################################
  3821. sub SONOS_DeleteDoublettes{
  3822. return keys %{{ map { $_ => 1 } @_ }};
  3823. }
  3824. ########################################################################################
  3825. #
  3826. # SONOS_Trim - Trim the given string
  3827. #
  3828. ########################################################################################
  3829. sub SONOS_Trim($) {
  3830. my ($str) = @_;
  3831. return $1 if ($str =~ m/^ *(.*?) *$/);
  3832. return $str;
  3833. }
  3834. ########################################################################################
  3835. #
  3836. # SONOS_CountInString - Count the occurences of the first string in the second string
  3837. #
  3838. ########################################################################################
  3839. sub SONOS_CountInString($$) {
  3840. my ($search, $str) = @_;
  3841. my $pos = 0;
  3842. my $matches = 0;
  3843. while (1) {
  3844. $pos = index($str, $search, $pos);
  3845. last if($pos < 0);
  3846. $matches++;
  3847. $pos++;
  3848. }
  3849. return $matches;
  3850. }
  3851. ########################################################################################
  3852. #
  3853. # SONOS_MakeCoverURL - Generates the approbriate cover-url incl. the use of a Fhem-Proxy
  3854. #
  3855. ########################################################################################
  3856. sub SONOS_MakeCoverURL($$) {
  3857. my ($udn, $resURL) = @_;
  3858. SONOS_Log $udn, 5, 'MakeCoverURL-Before: '.$resURL;
  3859. if ($resURL =~ m/^x-rincon-cpcontainer.*?(spotify.*?)(\?|$)/i) {
  3860. $resURL = SONOS_getSpotifyCoverURL($1, 1);
  3861. } elsif ($resURL =~ m/^x-sonos-spotify:spotify%3atrack%3a(.*?)(\?|$)/i) {
  3862. $resURL = SONOS_getSpotifyCoverURL($1);
  3863. } elsif ($resURL =~ m/^x-sonosapi-stream:(.+?)\?/i) {
  3864. my $resURLtemp = SONOS_GetRadioMediaMetadata($udn, $1);
  3865. eval {
  3866. my $result = SONOS_ReadURL($resURLtemp);
  3867. if (!defined($result) || ($result =~ m/<Error>.*<\/Error>/i)) {
  3868. $resURLtemp = $1.'/getaa?s=1&u='.SONOS_URI_Escape($resURL) if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
  3869. }
  3870. };
  3871. $resURL = $resURLtemp;
  3872. } elsif (($resURL =~ m/x-rincon-playlist:.*?#(.*)/i) || ($resURL =~ m/savedqueues.rsq(#\d+)/i)) {
  3873. my $search = $1;
  3874. $search = 'SQ:'.$1 if ($search =~ m/#(\d+)/i);
  3875. # Default, if nothing could be retreived...
  3876. $resURL = '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/playlist.jpg';
  3877. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  3878. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 15, '');
  3879. if ($result) {
  3880. my $tmp = $result->getValue('Result');
  3881. while (defined($tmp) && $tmp =~ m/<container id="(.+?)".*?>.*?<\/container>/i) {
  3882. $search = $1;
  3883. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 15, '');
  3884. if ($result) {
  3885. $tmp = $result->getValue('Result');
  3886. } else {
  3887. undef($tmp);
  3888. }
  3889. }
  3890. }
  3891. if ($result) {
  3892. my $tmp = $result->getValue('Result');
  3893. my $coverOK = 0;
  3894. while (!$coverOK && defined($tmp) && $tmp =~ m/<item id=".+?".*?>.*?<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>.*?<\/item>/ig) {
  3895. $resURL = $1;
  3896. $resURL =~ s/%25/%/ig;
  3897. # Bei Spotify-URIs, die AlbumURL korrigieren...
  3898. if ($resURL =~ m/getaa.*?x-sonos-spotify%3aspotify%3atrack%3a(.*?)%3f/i) {
  3899. $resURL = SONOS_getSpotifyCoverURL($1);
  3900. } else {
  3901. $resURL = $1.$resURL if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
  3902. }
  3903. my $loadedCover = SONOS_ReadURL($resURL);
  3904. $coverOK = (defined($loadedCover) && ($loadedCover !~ m/<Error>.*<\/Error>/i))
  3905. }
  3906. }
  3907. }
  3908. } else {
  3909. my $stream = 0;
  3910. $stream = 1 if (($resURL =~ /x-sonosapi-stream/) && ($resURL !~ /x-sonos-http%3aamz/));
  3911. $stream = 1 if (!$stream && (($resURL =~ /x-sonosapi-hls-static/) || ($resURL =~ /x-sonos-http:track%3a/)));
  3912. $resURL = SONOS_URI_Escape($resURL);
  3913. SONOS_Log undef, 5, 'resURL-1: '.$resURL;
  3914. $resURL =~ s/%26apos%3B/&apos;/ig;
  3915. $resURL =~ s/%26amp%3B/%26/ig;
  3916. SONOS_Log undef, 5, 'resURL-2: '.$resURL;
  3917. $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.$resURL if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
  3918. SONOS_Log undef, 5, 'resURL-3: '.$resURL;
  3919. }
  3920. # Alles über Fhem als Proxy laufen lassen?
  3921. $resURL = '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/proxy/aa?url='.SONOS_URI_Escape($resURL) if (($resURL !~ m/^\//) && SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
  3922. SONOS_Log $udn, 5, 'MakeCoverURL-After: '.$resURL;
  3923. return $resURL;
  3924. }
  3925. ########################################################################################
  3926. #
  3927. # SONOS_getSpotifyCoverURL - Generates the approbriate cover-url for Spotify-Cover
  3928. #
  3929. ########################################################################################
  3930. sub SONOS_getSpotifyCoverURL($;$) {
  3931. my ($trackID, $oldStyle) = @_;
  3932. $oldStyle = 0 if (!defined($oldStyle));
  3933. my $infos = '';
  3934. if ($oldStyle) {
  3935. my $result = get('https://embed.spotify.com/oembed/?url='.$trackID);
  3936. $infos = $1 if ($result && ($result =~ m/"thumbnail_url":"(.*?)"/i));
  3937. } else {
  3938. my $result = get('https://api.spotify.com/v1/tracks/'.$trackID);
  3939. $infos = $1 if ($result && ($result =~ m/"images".*?:.*?\[.*?{.*?"height".*?:.*?\d{3},.*?"url".*?:.*?"(.*?)",.*?"width"/is));
  3940. }
  3941. $infos =~ s/\\//g;
  3942. #$infos = $1.'original'.$3 if ($infos =~ m/(.*?\/)(cover|default)(\/.*)/i);
  3943. # Falls es ein Standardcover von Spotify geben soll, lieber das Thumbnail von Sonos verwenden...
  3944. return '' if ($infos =~ m/\/static\/img\/defaultCoverL.png/i);
  3945. if ($infos ne '') {
  3946. return $infos;
  3947. }
  3948. return '';
  3949. }
  3950. ########################################################################################
  3951. #
  3952. # SONOS_GetSpeakFileExtension - Retrieves the desired fileextension
  3953. #
  3954. ########################################################################################
  3955. sub SONOS_GetSpeakFileExtension($) {
  3956. my ($workType) = @_;
  3957. if (lc($workType) eq 'speak0') {
  3958. return 'mp3';
  3959. } elsif ($workType =~ m/speak\d+/i) {
  3960. $workType = ucfirst(lc($workType));
  3961. my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0);
  3962. if ($speakDefinition =~ m/(.*?):(.*)/) {
  3963. return $1;
  3964. }
  3965. }
  3966. return '';
  3967. }
  3968. ########################################################################################
  3969. #
  3970. # SONOS_GetSpeakFile - Generates the audiofile according to the given text, language and generator
  3971. #
  3972. ########################################################################################
  3973. sub SONOS_GetSpeakFile($$$$$) {
  3974. my ($udn, $workType, $language, $text, $destFileName) = @_;
  3975. my $targetSpeakMP3FileDir = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakMP3FileDir', '');
  3976. # Parametrisieren...
  3977. my $chunksize = $SONOS_GOOGLETRANSLATOR_CHUNKSIZE;
  3978. my $textescaped = SONOS_URI_Escape($text);
  3979. my $textutf8 = SONOS_Latin1ToUtf8($text);
  3980. my $textutf8escaped = SONOS_URI_Escape($textutf8);
  3981. # Chunks ermitteln...
  3982. # my @textList = ($text =~ m/(?:\b(?:[^ ]+)\W*){0,$SONOS_GOOGLETRANSLATOR_CHUNKSIZE}/g);
  3983. # pop @textList; # Letztes Element ist immer leer, deshalb abschneiden...
  3984. my @textList = ('');
  3985. for my $elem (split(/[ \t]/, $text)) {
  3986. # Files beibehalten...
  3987. if ($elem =~ m/\|(.*)\|/) {
  3988. my $filename = $1;
  3989. $filename = $targetSpeakMP3FileDir.'/'.$filename if ($filename !~ m/^(\/|[a-z]:)/i);
  3990. $filename = $filename.'.mp3' if ($filename !~ m/\.mp3$/i);
  3991. push(@textList, '|'.$filename.'|');
  3992. push(@textList, '');
  3993. next;
  3994. }
  3995. if (length($textList[$#textList].' '.$elem) <= $chunksize) {
  3996. $textList[$#textList] .= ' '.$elem;
  3997. } else {
  3998. push(@textList, $elem);
  3999. }
  4000. }
  4001. SONOS_Log $udn, 5, 'Chunks: '.SONOS_Stringify(\@textList);
  4002. # Generating Speakfiles...
  4003. if (lc($workType) eq 'speak0') {
  4004. # Einzelne Chunks herunterladen...
  4005. my $counter = 0;
  4006. for my $text (@textList) {
  4007. # Leere Einträge überspringen...
  4008. next if ($text eq '');
  4009. $counter++;
  4010. # MP3Files direkt kopieren
  4011. if ($text =~ m/\|(.*)\|/) {
  4012. SONOS_Log $udn, 3, 'Copy MP3-File ('.$counter.'. Element) from "'.$1.'" to "'.$destFileName.$counter.'"';
  4013. copy($1, $destFileName.$counter);
  4014. # Etwaige ID-Tags entfernen...
  4015. eval {
  4016. use MP3::Info;
  4017. remove_mp3tag($destFileName.$counter, 'ALL');
  4018. };
  4019. if ($@) {
  4020. SONOS_Log $udn, 3, 'Copy MP3-File. ERROR during removing of ID3Tag: '.$@;
  4021. }
  4022. next;
  4023. }
  4024. my $url = sprintf(SONOS_Client_Data_Retreive('undef', 'attr', 'SpeakGoogleURL', $SONOS_GOOGLETRANSLATOR_URL), SONOS_URI_Escape(lc($language)), SONOS_URI_Escape($text));
  4025. SONOS_Log $udn, 3, 'Load Google generated MP3 ('.$counter.'. Element) from "'.$url.'" to "'.$destFileName.$counter.'"';
  4026. my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
  4027. my $response = $ua->get($url, ':content_file' => $destFileName.$counter);
  4028. if (!$response->is_success) {
  4029. SONOS_Log $udn, 1, 'MP3 Download-Error: '.$response->status_line;
  4030. unlink($destFileName.$counter);
  4031. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during downloading: '.$response->status_line);
  4032. return 0;
  4033. }
  4034. }
  4035. # Heruntergeladene Chunks zusammenführen...
  4036. return SONOS_CombineMP3Files($udn, $workType, $destFileName, $counter);
  4037. } elsif ($workType =~ m/speak\d+/i) {
  4038. $workType = ucfirst(lc($workType));
  4039. SONOS_Log $udn, 3, 'Load '.$workType.' generated SpeakFile to "'.$destFileName.'"';
  4040. my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0);
  4041. if ($speakDefinition =~ m/(.*?):(.*)/) {
  4042. $speakDefinition = $2;
  4043. $speakDefinition =~ s/%language%/$language/gi;
  4044. $speakDefinition =~ s/%filename%/$destFileName/gi;
  4045. $speakDefinition =~ s/%text%/$text/gi;
  4046. $speakDefinition =~ s/%textescaped%/$textescaped/gi;
  4047. $speakDefinition =~ s/%textutf8%/$textutf8/gi;
  4048. $speakDefinition =~ s/%textutf8escaped%/$textutf8escaped/gi;
  4049. SONOS_Log $udn, 5, 'Execute: '.$speakDefinition;
  4050. system($speakDefinition);
  4051. return 1;
  4052. } else {
  4053. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': No Definition found!');
  4054. return 0;
  4055. }
  4056. }
  4057. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Speaking not defined.');
  4058. return 0;
  4059. }
  4060. ########################################################################################
  4061. #
  4062. # SONOS_CombineMP3Files - Combine the loaded mp3-files
  4063. #
  4064. ########################################################################################
  4065. sub SONOS_CombineMP3Files($$$$) {
  4066. my ($udn, $workType, $destFileName, $counter) = @_;
  4067. SONOS_Log $udn, 3, 'Combine loaded chunks into "'.$destFileName.'"';
  4068. # Reinladen
  4069. my $newMP3File = '';
  4070. for(my $i = 1; $i <= $counter; $i++) {
  4071. $newMP3File .= SONOS_ReadFile($destFileName.$i);
  4072. unlink($destFileName.$i);
  4073. }
  4074. # Speichern
  4075. eval {
  4076. open MPFILE, '>'.$destFileName;
  4077. binmode MPFILE ;
  4078. print MPFILE $newMP3File;
  4079. close MPFILE;
  4080. };
  4081. if ($@) {
  4082. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during combining: '.$@);
  4083. return 0;
  4084. }
  4085. # Konvertieren?
  4086. my $targetSpeakMP3FileConverter = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakMP3FileConverter', '');
  4087. if ($targetSpeakMP3FileConverter) {
  4088. SONOS_Log $udn, 3, 'Convert combined file "'.$destFileName.'" with "'.$targetSpeakMP3FileConverter.'"';
  4089. eval {
  4090. my $destFileNameTMP = $destFileName;
  4091. $destFileNameTMP =~ s/^(.*)\/(.*?)$/$1\/TMP_$2/;
  4092. $targetSpeakMP3FileConverter =~ s/%infile%/$destFileName/gi;
  4093. $targetSpeakMP3FileConverter =~ s/%outfile%/$destFileNameTMP/gi;
  4094. SONOS_Log $udn, 5, 'Execute: '.$targetSpeakMP3FileConverter;
  4095. system($targetSpeakMP3FileConverter);
  4096. # "Alte" MP3-Datei entfernen, und die "neue" umbenennen...
  4097. unlink($destFileName);
  4098. move($destFileNameTMP, $destFileName);
  4099. };
  4100. if ($@) {
  4101. SONOS_Log $udn, 2, ucfirst($workType).': MP3-Creation ERROR during converting: '.$@;
  4102. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during converting: '.$@);
  4103. return 0;
  4104. }
  4105. }
  4106. return 1;
  4107. }
  4108. ########################################################################################
  4109. #
  4110. # SONOS_CreateURIMeta - Creates the Meta-Information according to the Song-URI
  4111. #
  4112. # Parameter $res = The URI to the song, for which the Metadata has to be generated
  4113. #
  4114. ########################################################################################
  4115. sub SONOS_CreateURIMeta($) {
  4116. my ($res) = @_;
  4117. 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;
  4118. my $userID_Spotify = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Spotify', '-'));
  4119. my $userID_Napster = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Napster', '-'));
  4120. # Wenn es ein Spotify- oder Napster-Titel ist, dann den Benutzernamen extrahieren
  4121. if ($res =~ m/^(x-sonos-spotify:)(.*?)(\?.*)/) {
  4122. if ($userID_Spotify eq '-') {
  4123. 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!';
  4124. return;
  4125. }
  4126. $res = $1.SONOS_URI_Escape($2).$3;
  4127. $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;
  4128. } elsif ($res =~ m/^(npsdy:)(.*?)(\.mp3)/) {
  4129. if ($userID_Napster eq '-') {
  4130. 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!';
  4131. return;
  4132. }
  4133. $res = $1.SONOS_URI_Escape($2).$3;
  4134. $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;
  4135. } else {
  4136. $res =~ s/ /%20/ig;
  4137. $res =~ s/"/&quot;/ig;
  4138. }
  4139. return ($res, $meta);
  4140. }
  4141. ########################################################################################
  4142. #
  4143. # SONOS_CheckAlarmHash - Checks if the given hash has all neccessary Alarm-Parameters
  4144. # Additionally it converts some parameters for direct use for Zoneplayer-Update
  4145. #
  4146. # Parameter %old = All neccessary informations to check
  4147. #
  4148. ########################################################################################
  4149. sub SONOS_CheckAndCorrectAlarmHash($) {
  4150. my ($hash) = @_;
  4151. # Checks, if a value is missing
  4152. my @keys = keys(%$hash);
  4153. if ((!SONOS_isInList('StartTime', @keys))
  4154. || (!SONOS_isInList('Duration', @keys))
  4155. || (!SONOS_isInList('Recurrence_Once', @keys))
  4156. || (!SONOS_isInList('Recurrence_Monday', @keys))
  4157. || (!SONOS_isInList('Recurrence_Tuesday', @keys))
  4158. || (!SONOS_isInList('Recurrence_Wednesday', @keys))
  4159. || (!SONOS_isInList('Recurrence_Thursday', @keys))
  4160. || (!SONOS_isInList('Recurrence_Friday', @keys))
  4161. || (!SONOS_isInList('Recurrence_Saturday', @keys))
  4162. || (!SONOS_isInList('Recurrence_Sunday', @keys))
  4163. || (!SONOS_isInList('Enabled', @keys))
  4164. || (!SONOS_isInList('RoomUUID', @keys))
  4165. || (!SONOS_isInList('ProgramURI', @keys))
  4166. || (!SONOS_isInList('ProgramMetaData', @keys))
  4167. || (!SONOS_isInList('Shuffle', @keys))
  4168. || (!SONOS_isInList('Repeat', @keys))
  4169. || (!SONOS_isInList('Volume', @keys))
  4170. || (!SONOS_isInList('IncludeLinkedZones', @keys))) {
  4171. return 0;
  4172. }
  4173. # Convert some values
  4174. # Playmode
  4175. $hash->{PlayMode} = 'NORMAL';
  4176. $hash->{PlayMode} = 'SHUFFLE' if ($hash->{Repeat} && $hash->{Shuffle});
  4177. $hash->{PlayMode} = 'SHUFFLE_NOREPEAT' if (!$hash->{Repeat} && $hash->{Shuffle});
  4178. $hash->{PlayMode} = 'REPEAT_ALL' if ($hash->{Repeat} && !$hash->{Shuffle});
  4179. # Recurrence
  4180. if ($hash->{Recurrence_Once}) {
  4181. $hash->{Recurrence} = 'ONCE';
  4182. } else {
  4183. $hash->{Recurrence} = 'ON_';
  4184. $hash->{Recurrence} .= '0' if ($hash->{Recurrence_Sunday});
  4185. $hash->{Recurrence} .= '1' if ($hash->{Recurrence_Monday});
  4186. $hash->{Recurrence} .= '2' if ($hash->{Recurrence_Tuesday});
  4187. $hash->{Recurrence} .= '3' if ($hash->{Recurrence_Wednesday});
  4188. $hash->{Recurrence} .= '4' if ($hash->{Recurrence_Thursday});
  4189. $hash->{Recurrence} .= '5' if ($hash->{Recurrence_Friday});
  4190. $hash->{Recurrence} .= '6' if ($hash->{Recurrence_Saturday});
  4191. # Specials
  4192. $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}));
  4193. $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}));
  4194. $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}));
  4195. }
  4196. # If nothing is given, set 'ONCE'
  4197. if ($hash->{Recurrence} eq 'ON_') {
  4198. $hash->{Recurrence} = 'ONCE';
  4199. }
  4200. return 1;
  4201. }
  4202. ########################################################################################
  4203. #
  4204. # SONOS_RestoreOldPlaystate - Restores the old Position of a playing state
  4205. #
  4206. ########################################################################################
  4207. sub SONOS_RestoreOldPlaystate() {
  4208. SONOS_Log undef, 1, 'Restore-Thread gestartet. Warte auf Arbeit...';
  4209. my $runEndlessLoop = 1;
  4210. my $controlPoint = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, SubscriptionURL => '/fhemmodule', MaxWait => 20, LogLevel => $SONOS_Client_LogLevel, UsedOnlyIP => \@usedonlyIPs, IgnoreIP => \@ignoredIPs, ReusePort => $reusePort);
  4211. $SIG{'PIPE'} = 'IGNORE';
  4212. $SIG{'CHLD'} = 'IGNORE';
  4213. $SIG{'INT'} = sub {
  4214. $runEndlessLoop = 0;
  4215. };
  4216. while ($runEndlessLoop) {
  4217. select(undef, undef, undef, 0.2);
  4218. next if (!$SONOS_PlayerRestoreQueue->pending());
  4219. # Es ist was auf der Queue... versuchen zu verarbeiten...
  4220. my %old = %{$SONOS_PlayerRestoreQueue->peek()};
  4221. next if (!defined($old{RestoreTime}));
  4222. # Wenn die Zeit noch nicht reif ist, dann doch wieder übergehen...
  4223. # Dabei die Schleife wieder von vorne beginnen lassen, da noch andere dazwischengeschoben werden könnten.
  4224. # Eine Weile in die Zukunft, da das ermitteln der Proxies Zeit benötigt.
  4225. next if ($old{RestoreTime} > time() + 2);
  4226. # ...sonst das Ding von der Queue nehmen...
  4227. $SONOS_PlayerRestoreQueue->dequeue();
  4228. # Hier die ursprünglichen Proxies wiederherstellen/neu verbinden...
  4229. my $device = $controlPoint->_createDevice($old{location});
  4230. my $AVProxy;
  4231. my $GRProxy;
  4232. my $CCProxy;
  4233. for my $subdevice ($device->children) {
  4234. if ($subdevice->UDN =~ /.*_MR/i) {
  4235. $AVProxy = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1')->controlProxy();
  4236. $GRProxy = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')->controlProxy();
  4237. }
  4238. if ($subdevice->UDN =~ /.*_MS/i) {
  4239. $CCProxy = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')->controlProxy();
  4240. }
  4241. }
  4242. my $udn = $device->UDN.'_MR';
  4243. $udn =~ s/.*?:(.*)/$1/;
  4244. SONOS_Log $udn.'_MR', 3, 'Restorethread has found a job. Waiting for stop playing...';
  4245. # Ist das Ding fertig abgespielt?
  4246. my $result;
  4247. do {
  4248. select(undef, undef, undef, 0.7);
  4249. $result = $AVProxy->GetTransportInfo(0);
  4250. } while ($result->getValue('CurrentTransportState') ne 'STOPPED');
  4251. SONOS_Log $udn, 3, 'Restoring playerstate...';
  4252. SONOS_Log $udn, 5, 'StoredURI: "'.$old{CurrentURI}.'"';
  4253. # Die Liste als aktuelles Abspielstück einstellen, oder den Stream wieder anwerfen
  4254. if ($old{CurrentURI} =~ /^x-.*?-(.*?stream|mp3radio)/) {
  4255. SONOS_Log $udn, 4, 'Restore Stream...';
  4256. $AVProxy->SetAVTransportURI(0, $old{CurrentURI}, $old{CurrentURIMetaData});
  4257. } else {
  4258. SONOS_Log $udn, 4, 'Restore Track #'.$old{Track}.', RelTime: "'.$old{RelTime}.'"...';
  4259. my $queueMetadata = $CCProxy->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '');
  4260. $AVProxy->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), '');
  4261. $AVProxy->Seek(0, 'TRACK_NR', $old{Track});
  4262. $AVProxy->Seek(0, 'REL_TIME', $old{RelTime});
  4263. }
  4264. my $oldMute = $GRProxy->GetGroupMute(0)->getValue('CurrentMute');
  4265. $GRProxy->SetGroupMute(0, $old{Mute}) if (defined($old{Mute}) && ($old{Mute} != $oldMute));
  4266. my $oldVolume = $GRProxy->GetGroupVolume(0)->getValue('CurrentVolume');
  4267. $GRProxy->SetGroupVolume(0, $old{Volume}) if (defined($old{Volume}) && ($old{Volume} != $oldVolume));
  4268. if (($old{CurrentTransportState} eq 'PLAYING') || ($old{CurrentTransportState} eq 'TRANSITIONING')) {
  4269. $AVProxy->Play(0, 1);
  4270. } elsif ($old{CurrentTransportState} eq 'PAUSED_PLAYBACK') {
  4271. $AVProxy->Pause(0);
  4272. }
  4273. $SONOS_PlayerRestoreRunningUDN{$udn} = 0;
  4274. SONOS_Log $udn, 3, 'Playerstate restored!';
  4275. }
  4276. undef($controlPoint);
  4277. SONOS_Log undef, 1, 'Restore-Thread wurde beendet.';
  4278. $SONOS_Thread_PlayerRestore = -1;
  4279. }
  4280. ########################################################################################
  4281. #
  4282. # SONOS_PlayURITemp - Plays an URI temporary
  4283. #
  4284. # Parameter $udn = The udn of the SonosPlayer
  4285. # $destURLParam = URI, that has to be played
  4286. # $volumeParam = Volume for playing
  4287. #
  4288. ########################################################################################
  4289. sub SONOS_PlayURITemp($$$) {
  4290. my ($udn, $destURLParam, $volumeParam) = @_;
  4291. my %old;
  4292. $old{DestURIOriginal} = $destURLParam;
  4293. my ($songURI, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($old{DestURIOriginal}));
  4294. # Wenn auf diesem Player bereits eine temporäre Wiedergabe erfolgt, dann hier auf dessen Beendigung warten...
  4295. if (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) {
  4296. SONOS_Log $udn, 3, 'Temporary playing of "'.$old{DestURIOriginal}.'" must wait, because another playing is in work...';
  4297. while (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) {
  4298. select(undef, undef, undef, 0.2);
  4299. }
  4300. }
  4301. $SONOS_PlayerRestoreRunningUDN{$udn} = 1;
  4302. SONOS_Log $udn, 3, 'Start temporary playing of "'.$old{DestURIOriginal}.'"';
  4303. my $volume = $volumeParam;
  4304. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  4305. $old{UDN} = $udn;
  4306. my $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0);
  4307. $old{Track} = $result->getValue('Track');
  4308. $old{RelTime} = $result->getValue('RelTime');
  4309. $result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  4310. $old{CurrentURI} = $result->getValue('CurrentURI');
  4311. $old{CurrentURIMetaData} = $result->getValue('CurrentURIMetaData');
  4312. $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportInfo(0);
  4313. $old{CurrentTransportState} = $result->getValue('CurrentTransportState');
  4314. $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $songURI, $meta);
  4315. if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) {
  4316. $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0);
  4317. $old{Mute} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute');
  4318. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, 0) if $old{Mute};
  4319. $old{Volume} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume');
  4320. if (defined($volume)) {
  4321. if ($volume =~ m/^[+-]{1}/) {
  4322. $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume) if $volume;
  4323. } else {
  4324. $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume) if ($volume != $old{Volume});
  4325. }
  4326. }
  4327. }
  4328. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'PlayURITemp: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)));
  4329. SONOS_Log $udn, 4, 'All is started successfully. Retreive Positioninfo...';
  4330. $old{SleepTime} = 0;
  4331. eval {
  4332. $old{SleepTime} = SONOS_GetTimeSeconds($SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('TrackDuration'));
  4333. # 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...
  4334. if ($old{SleepTime} == 0) {
  4335. SONOS_Log $udn, 3, 'SleepTimer berechnet die Laufzeit des Titels selber, da keine Wartezeit uebermittelt wurde!';
  4336. eval {
  4337. use MP3::Info;
  4338. my $tag = get_mp3info($old{DestURIOriginal});
  4339. if ($tag) {
  4340. $old{SleepTime} = $tag->{SECS};
  4341. }
  4342. };
  4343. if ($@) {
  4344. SONOS_Log $udn, 2, 'Bei der MP3-Längenermittlung ist ein Fehler aufgetreten: '.$@;
  4345. }
  4346. }
  4347. $old{RestoreTime} = time() + $old{SleepTime} - 1;
  4348. SONOS_Log $udn, 3, 'Laufzeitermittlung abgeschlossen: '.$old{SleepTime}.'s, Restore-Zeit: '.GetTimeString($old{RestoreTime});
  4349. };
  4350. # Location mitsichern, damit die Proxies neu geholt werden können
  4351. my %revUDNs = reverse %SONOS_Locations;
  4352. $old{location} = $revUDNs{$udn};
  4353. # Restore-Daten an der richtigen Stelle auf die Queue legen, damit der Player-Restore-Thread sich darum kümmern kann
  4354. # Aber nur, wenn auch ein Restore erfolgen kann, weil eine Zeit existiert
  4355. if (defined($old{SleepTime}) && ($old{SleepTime} != 0)) {
  4356. my $i;
  4357. for ($i = $SONOS_PlayerRestoreQueue->pending() - 1; $i >= 0; $i--) {
  4358. my %tmpOld = %{$SONOS_PlayerRestoreQueue->peek($i)};
  4359. last if ($old{RestoreTime} > $tmpOld{RestoreTime});
  4360. }
  4361. $SONOS_PlayerRestoreQueue->insert($i + 1, \%old);
  4362. } else {
  4363. SONOS_Log $udn, 1, 'Da keine Endzeit ermittelt werden konnte, wird kein Restoring durchgeführt werden!';
  4364. $SONOS_PlayerRestoreRunningUDN{$udn} = 0;
  4365. }
  4366. }
  4367. }
  4368. ########################################################################################
  4369. #
  4370. # SONOS_GetTrackProvider - Retrieves a textual representation of the Provider of the given URI
  4371. #
  4372. # Parameter $songURI = The URI that has to be converted
  4373. #
  4374. ########################################################################################
  4375. sub SONOS_GetTrackProvider($;$) {
  4376. my ($songURI, $songTitle) = @_;
  4377. return ('', '', '') if (!defined($songURI) || ($songURI eq ''));
  4378. # Backslashe umwandeln
  4379. $songURI =~ s/\\/\//g;
  4380. # Gruppen- und LineIn-Wiedergaben bereits hier erkennen
  4381. if ($songURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) {
  4382. return ('Gruppenwiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '', '');
  4383. } elsif ($songURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) {
  4384. my $elem = 'LineIn';
  4385. $elem = $songTitle if (defined($songTitle) && $songTitle);
  4386. return ($elem.'-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/linein_round.png', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/linein_quadratic.jpg');
  4387. } elsif ($songURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) {
  4388. return ('Dock-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/dock_round.png', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/dock_quadratic.jpg');
  4389. } elsif ($songURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) {
  4390. return ('SPDIF-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/playbar_round.png', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/playbar_quadratic.jpg');
  4391. } elsif (($songURI =~ m/^http:(\/\/.*)/) || ($songURI =~ m/^aac:(\/\/.*)/) || ($songURI =~ m/x-rincon-mp3radio:\/\//)) {
  4392. return ('Radio', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/tunein_round.png', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/tunein_quadratic.jpg');
  4393. } elsif ($songURI =~ m/^\/\//) {
  4394. return ('Bibliothek', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/bibliothek_round.png', '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/bibliothek_quadratic.jpg');
  4395. }
  4396. my ($a, $b, $c) = eval {
  4397. my $musicServicesReading = SONOS_Client_Data_Retreive('undef', 'reading', 'MusicServicesList', undef);
  4398. my %musicServices = ();
  4399. %musicServices = %{eval($musicServicesReading)} if (defined($musicServicesReading));
  4400. if ($songURI =~ m/sid=(\d+)/i) {
  4401. my $sid = $1;
  4402. if (defined($musicServices{$sid})) {
  4403. my $result = $musicServices{$sid}{Name};
  4404. $result = '' if (!defined($result));
  4405. SONOS_Log undef, 4, 'TrackProvider for "'.$songURI.'" ~ SID='.$sid.' ~ Name: '.$result;
  4406. my $roundIcon = $musicServices{$sid}{IconRoundURL};
  4407. $roundIcon = '' if (!defined($roundIcon));
  4408. my $quadraticIcon = $musicServices{$sid}{IconQuadraticURL};
  4409. $quadraticIcon = '' if (!defined($quadraticIcon));
  4410. SONOS_Log undef, 4, 'Trackprovider-Icons for "'.$result.'": Round: '.$roundIcon.' ~ Quadratic: '.$quadraticIcon;
  4411. return ($result, $roundIcon, $quadraticIcon);
  4412. }
  4413. }
  4414. return ('', '', '');
  4415. };
  4416. if ($@) {
  4417. SONOS_Log undef, 2, 'Unable to identify TrackProvider for "'.$songURI.'". Revert to empty default! Errormessage: '.$@;
  4418. return ('', '', '');
  4419. } else {
  4420. return ($a, $b, $c);
  4421. }
  4422. }
  4423. ########################################################################################
  4424. #
  4425. # SONOS_ExpandURIForQueueing - Expands and corrects a given URI
  4426. #
  4427. # Parameter $songURI = The URI that has to be converted
  4428. #
  4429. ########################################################################################
  4430. sub SONOS_ExpandURIForQueueing($) {
  4431. my ($songURI) = @_;
  4432. # Backslashe umwandeln
  4433. $songURI =~ s/\\/\//g;
  4434. # SongURI erweitern/korrigieren
  4435. $songURI = 'x-file-cifs:'.$songURI if ($songURI =~ m/^\/\//);
  4436. $songURI = 'x-rincon-mp3radio:'.$1 if ($songURI =~ m/^http:(\/\/.*)/);
  4437. return $songURI;
  4438. }
  4439. ########################################################################################
  4440. #
  4441. # SONOS_GetURIFromQueueValue - Gets the URI from current Informations
  4442. #
  4443. # Parameter $songURI = The URI that has to be converted
  4444. #
  4445. ########################################################################################
  4446. sub SONOS_GetURIFromQueueValue($) {
  4447. my ($songURI) = @_;
  4448. # SongURI erweitern/korrigieren
  4449. $songURI = $1 if ($songURI =~ m/^x-file-cifs:(.*)/i);
  4450. $songURI = 'http:'.$1 if ($songURI =~ m/^x-rincon-mp3radio:(.*)/i);
  4451. $songURI = uri_unescape($songURI) if ($songURI =~ m/^x-sonos-spotify:/i);
  4452. return $songURI;
  4453. }
  4454. ########################################################################################
  4455. #
  4456. # SONOS_ExpandTimeString - Make sure, that the given TimeString is complete (like '0:04:12')
  4457. #
  4458. # Parameter $timeStr = The timeStr that has to be proofed
  4459. #
  4460. ########################################################################################
  4461. sub SONOS_ExpandTimeString($) {
  4462. my ($timeStr) = @_;
  4463. if ($timeStr !~ m/:/) {
  4464. return '0:00:'.$timeStr;
  4465. } elsif ($timeStr =~ m/^[^:]*:{1,1}[^:]*$/) {
  4466. return '0:'.$timeStr;
  4467. }
  4468. return $timeStr;
  4469. }
  4470. ########################################################################################
  4471. #
  4472. # SONOS_GetTimeSeconds - Converts a Time-String like '0:04:12' to seconds (e.g. 252)
  4473. #
  4474. # Parameter $timeStr = The timeStr that has to be converted
  4475. #
  4476. ########################################################################################
  4477. sub SONOS_GetTimeSeconds($) {
  4478. my ($timeStr) = @_;
  4479. return (int($1)*3600 + int($2)*60 + int($3)) if ($timeStr =~ m/(\d+):(\d+):(\d+)/);
  4480. return 0;
  4481. }
  4482. ########################################################################################
  4483. #
  4484. # SONOS_ConvertSecondsToTime - Converts seconds (e.g. 252) into a Time-String like '0:04:12'
  4485. #
  4486. # Parameter $seconds = The seconds that have to be converted
  4487. #
  4488. ########################################################################################
  4489. sub SONOS_ConvertSecondsToTime($) {
  4490. my ($seconds) = @_;
  4491. return sprintf('%01d:%02d:%02d', $seconds / 3600, ($seconds%3600) / 60, $seconds%60) if ($seconds > 0);
  4492. return '0:00:00';
  4493. }
  4494. ########################################################################################
  4495. #
  4496. # 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
  4497. #
  4498. # Parameter $proxyObject = The Proxy that has to be checked
  4499. #
  4500. ########################################################################################
  4501. sub SONOS_CheckProxyObject($$) {
  4502. my ($udn, $proxyObject) = @_;
  4503. if (defined($proxyObject)) {
  4504. SONOS_Log $udn, 4, 'ProxyObject exists: '.$proxyObject;
  4505. return 1;
  4506. } else {
  4507. SONOS_Log $udn, 3, 'ProxyObject does not exists';
  4508. # Das Aufräumen der ProxyObjects und das Erzeugen des Notify wurde absichtlich nicht hier reingeschrieben, da es besser im IsAlive-Checker aufgehoben ist.
  4509. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'CheckProxyObject-ERROR: SonosPlayer disappeared?');
  4510. return 0;
  4511. }
  4512. }
  4513. ########################################################################################
  4514. #
  4515. # SONOS_MakeSigHandlerReturnValue - Enqueue all necessary elements on upward-queue
  4516. #
  4517. # Parameter $returnValue = The value that has to be laid on the queue.
  4518. #
  4519. ########################################################################################
  4520. sub SONOS_MakeSigHandlerReturnValue($$$) {
  4521. my ($udn, $returnName, $returnValue) = @_;
  4522. #Antwort melden
  4523. SONOS_Client_Notifier('DoWorkAnswer:'.$udn.':'.$returnName.':'.$returnValue);
  4524. }
  4525. ########################################################################################
  4526. #
  4527. # SONOS_RestartControlPoint - Restarts the UPnP-ControlPoint
  4528. #
  4529. ########################################################################################
  4530. sub SONOS_RestartControlPoint() {
  4531. if (defined($SONOS_Controlpoint)) {
  4532. $SONOS_RestartControlPoint = 1;
  4533. $SONOS_Controlpoint->stopSearch($SONOS_Search);
  4534. $SONOS_Controlpoint->stopHandling();
  4535. SONOS_Log undef, 4, 'ControlPoint is successfully stopped for restarting!';
  4536. }
  4537. }
  4538. ########################################################################################
  4539. #
  4540. # SONOS_StopControlPoint - Stops all open Net-Handles and Search-Token of the UPnP Part
  4541. #
  4542. ########################################################################################
  4543. sub SONOS_StopControlPoint {
  4544. if (defined($SONOS_Controlpoint)) {
  4545. $SONOS_Controlpoint->stopSearch($SONOS_Search);
  4546. $SONOS_Controlpoint->stopHandling();
  4547. undef($SONOS_Controlpoint);
  4548. SONOS_Log undef, 4, 'ControlPoint is successfully stopped!';
  4549. }
  4550. }
  4551. ########################################################################################
  4552. #
  4553. # SONOS_GetTagData - Return the content of the given tag in the given string
  4554. #
  4555. # Parameter $tagName = The tag to be searched for
  4556. # $data = The string in which to search for
  4557. #
  4558. ########################################################################################
  4559. sub SONOS_GetTagData($$) {
  4560. my ($tagName, $data) = @_;
  4561. return $1 if ($data =~ m/<$tagName.*?>(.*?)<\/$tagName>/i);
  4562. return '';
  4563. }
  4564. ########################################################################################
  4565. #
  4566. # SONOS_AnswerMessage - Return 'Success' if param is true, 'Error' otherwise
  4567. #
  4568. # Parameter $var = The value to check
  4569. #
  4570. ########################################################################################
  4571. sub SONOS_AnswerMessage($) {
  4572. my ($var) = @_;
  4573. if ($var) {
  4574. return 'Success!';
  4575. } else {
  4576. return 'Error!';
  4577. }
  4578. }
  4579. ########################################################################################
  4580. #
  4581. # SONOS_UPnPAnswerMessage - Return 'Success' if param is true, a complete error-message of the UPnP-answer otherwise
  4582. #
  4583. # Parameter $var = The UPnP-answer to check
  4584. #
  4585. ########################################################################################
  4586. sub SONOS_UPnPAnswerMessage($) {
  4587. my ($var) = @_;
  4588. if ($var->isSuccessful) {
  4589. return 'Success!';
  4590. } else {
  4591. my $faultcode = '-';
  4592. my $faultstring = '-';
  4593. my $faultactor = '-';
  4594. my $faultdetail = '-';
  4595. $faultcode = $var->faultcode if ($var->faultcode);
  4596. $faultstring = $var->faultstring if ($var->faultstring);
  4597. $faultactor = $var->faultactor if ($var->faultactor);
  4598. $faultdetail = $var->faultdetail if ($var->faultdetail);
  4599. return 'Error! UPnP-Fault-Fields: Code: "'.$faultcode.'", String: "'.$faultstring.'", Actor: "'.$faultactor.'", Detail: "'.SONOS_Stringify($faultdetail).'"';
  4600. }
  4601. }
  4602. ########################################################################################
  4603. #
  4604. # SONOS_Dumper - Returns the 'Dumpered' Output of the given Datastructure-Reference
  4605. #
  4606. ########################################################################################
  4607. sub SONOS_Dumper($) {
  4608. my ($varRef) = @_;
  4609. $Data::Dumper::Indent = 0;
  4610. my $text = Dumper($varRef);
  4611. $Data::Dumper::Indent = 2;
  4612. return $text;
  4613. }
  4614. ########################################################################################
  4615. #
  4616. # SONOS_Stringify - Converts a given Value (Array, Hash, Scalar) to a readable string version
  4617. #
  4618. # Parameter $varRef = The value to convert to a readable version
  4619. #
  4620. ########################################################################################
  4621. sub SONOS_Stringify {
  4622. my ($varRef) = @_;
  4623. return 'undef' if (!defined($varRef));
  4624. my $reftype = reftype $varRef;
  4625. if (!defined($reftype) || ($reftype eq '')) {
  4626. if (looks_like_number($varRef)) {
  4627. return $varRef;
  4628. } else {
  4629. $varRef =~ s/'/\\'/g;
  4630. return "'".$varRef."'";
  4631. }
  4632. } elsif ($reftype eq 'HASH') {
  4633. my %var = %{$varRef};
  4634. my @result;
  4635. foreach my $key (keys %var) {
  4636. push(@result, $key.' => '.SONOS_Stringify($var{$key}));
  4637. }
  4638. return '{'.join(', ', @result).'}';
  4639. } elsif ($reftype eq 'ARRAY') {
  4640. my @var = @{$varRef};
  4641. my @result;
  4642. foreach my $value (@var) {
  4643. push(@result, SONOS_Stringify($value));
  4644. }
  4645. return '['.join(', ', @result).']';
  4646. } elsif ($reftype eq 'SCALAR') {
  4647. if (looks_like_number(${$varRef})) {
  4648. return ${$varRef};
  4649. } else {
  4650. ${$varRef} =~ s/'/\\'/g;
  4651. return "'".${$varRef}."'";
  4652. }
  4653. } else {
  4654. return 'Unsupported Type ('.$reftype.') of: '.$varRef;
  4655. }
  4656. }
  4657. ########################################################################################
  4658. #
  4659. # SONOS_UmlautConvert - Converts any umlaut (e.g. ä) to Ascii-conform writing (e.g. ae)
  4660. #
  4661. # Parameter $var = The value to convert
  4662. #
  4663. ########################################################################################
  4664. sub SONOS_UmlautConvert($) {
  4665. eval {
  4666. use utf8;
  4667. my ($var) = @_;
  4668. if ($var eq 'ä') {
  4669. return 'ae';
  4670. } elsif ($var eq 'ö') {
  4671. return 'oe';
  4672. } elsif ($var eq 'ü') {
  4673. return 'ue';
  4674. } elsif ($var eq 'Ä') {
  4675. return 'Ae';
  4676. } elsif ($var eq 'Ö') {
  4677. return 'Oe';
  4678. } elsif ($var eq 'Ü') {
  4679. return 'Ue';
  4680. } elsif ($var eq 'ß') {
  4681. return 'ss';
  4682. } else {
  4683. return '_';
  4684. }
  4685. }
  4686. }
  4687. ########################################################################################
  4688. #
  4689. # SONOS_ConvertUmlautToHtml - Converts any umlaut (e.g. ä) to Html-conform writing (e.g. &auml;)
  4690. #
  4691. # Parameter $var = The value to convert
  4692. #
  4693. ########################################################################################
  4694. sub SONOS_ConvertUmlautToHtml($) {
  4695. my ($var) = @_;
  4696. if ($var eq 'ä') {
  4697. return '&auml;';
  4698. } elsif ($var eq 'ö') {
  4699. return '&ouml;';
  4700. } elsif ($var eq 'ü') {
  4701. return '&uuml;';
  4702. } elsif ($var eq 'Ä') {
  4703. return '&Auml;';
  4704. } elsif ($var eq 'Ö') {
  4705. return '&Ouml;';
  4706. } elsif ($var eq 'Ü') {
  4707. return '&Uuml;';
  4708. } elsif ($var eq 'ß') {
  4709. return '&szlig;';
  4710. } else {
  4711. return $var;
  4712. }
  4713. }
  4714. ########################################################################################
  4715. #
  4716. # SONOS_Latin1ToUtf8 - Converts Latin1 coding to UTF8
  4717. #
  4718. # Parameter $var = The value to convert
  4719. #
  4720. # http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS
  4721. #
  4722. ########################################################################################
  4723. sub SONOS_Latin1ToUtf8($) {
  4724. my ($s)= @_;
  4725. $s =~ s/([\x80-\xFF])/chr(0xC0|ord($1)>>6).chr(0x80|ord($1)&0x3F)/eg;
  4726. return $s;
  4727. }
  4728. ########################################################################################
  4729. #
  4730. # SONOS_Utf8ToLatin1 - Converts UTF8 coding to Latin1
  4731. #
  4732. # Parameter $var = The value to convert
  4733. #
  4734. # http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS
  4735. #
  4736. ########################################################################################
  4737. sub SONOS_Utf8ToLatin1($) {
  4738. my ($s)= @_;
  4739. $s =~ s/([\xC2\xC3])([\x80-\xBF])/chr(ord($1)<<6&0xC0|ord($2)&0x3F)/eg;
  4740. return $s;
  4741. }
  4742. ########################################################################################
  4743. #
  4744. # SONOS_ConvertNumToWord - Converts the values "0, 1" to "off, on"
  4745. #
  4746. # Parameter $var = The value to convert
  4747. #
  4748. ########################################################################################
  4749. sub SONOS_ConvertNumToWord($) {
  4750. my ($var) = @_;
  4751. return 'off' if (!defined($var));
  4752. if (!looks_like_number($var)) {
  4753. return 'on' if (lc($var) ne 'off');
  4754. return 'off';
  4755. }
  4756. if ($var == 0) {
  4757. return 'off';
  4758. } else {
  4759. return 'on';
  4760. }
  4761. }
  4762. ########################################################################################
  4763. #
  4764. # SONOS_ConvertWordToNum - Converts the values "off, on" to "0, 1"
  4765. #
  4766. # Parameter $var = The value to convert
  4767. #
  4768. ########################################################################################
  4769. sub SONOS_ConvertWordToNum($) {
  4770. my ($var) = @_;
  4771. if (looks_like_number($var)) {
  4772. return 1 if ($var != 0);
  4773. return 0;
  4774. }
  4775. if (lc($var) eq 'off') {
  4776. return 0;
  4777. } else {
  4778. return 1;
  4779. }
  4780. }
  4781. ########################################################################################
  4782. #
  4783. # SONOS_ToggleNum - Convert the values "0, 1" to "1, 0"
  4784. #
  4785. # Parameter $var = The value to convert
  4786. #
  4787. ########################################################################################
  4788. sub SONOS_ToggleNum($) {
  4789. my ($var) = @_;
  4790. if ($var == 0) {
  4791. return 1;
  4792. } else {
  4793. return 0;
  4794. }
  4795. }
  4796. ########################################################################################
  4797. #
  4798. # SONOS_ToggleWord - Convert the values "off, on" to "on, off"
  4799. #
  4800. # Parameter $var = The value to convert
  4801. #
  4802. ########################################################################################
  4803. sub SONOS_ToggleWord($) {
  4804. my ($var) = @_;
  4805. if (lc($var) eq 'off') {
  4806. return 'on';
  4807. } else {
  4808. return 'off';
  4809. }
  4810. }
  4811. ########################################################################################
  4812. #
  4813. # SONOS_Discover_Callback - Discover-Callback,
  4814. # autocreate devices if not already present
  4815. #
  4816. # Parameter $search =
  4817. # $device =
  4818. # $action =
  4819. #
  4820. ########################################################################################
  4821. sub SONOS_Discover_Callback($$$) {
  4822. my ($search, $device, $action) = @_;
  4823. # Sicherheitsabfrage, da offensichtlich manchmal falsche Elemente durchkommen...
  4824. if ($device->deviceType() ne 'urn:schemas-upnp-org:device:ZonePlayer:1') {
  4825. SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received!';
  4826. return;
  4827. }
  4828. if ($action eq 'deviceAdded') {
  4829. my $descriptionDocument;
  4830. eval {
  4831. $descriptionDocument = $device->descriptionDocument();
  4832. };
  4833. if ($@) {
  4834. # Das Descriptiondocument konnte nicht abgefragt werden
  4835. SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received! Detected while trying to download the Description-Document from Player.';
  4836. return;
  4837. }
  4838. # Wenn kein Description-Dokument geliefert wurde...
  4839. if (!defined($descriptionDocument) || ($descriptionDocument eq '')) {
  4840. SONOS_Log undef, 2, "Discover-Event: Description-Document is empty. Aborting this deviceadding-process.";
  4841. return;
  4842. }
  4843. # Alles OK, es kann weitergehen
  4844. SONOS_Log undef, 5, "Discover-Event: Description-Document: $descriptionDocument";
  4845. $SONOS_Client_SendQueue_Suspend = 1;
  4846. # Variablen initialisieren
  4847. my $roomName = '';
  4848. my $saveRoomName = '';
  4849. my $modelNumber = '';
  4850. my $displayVersion = '';
  4851. my $serialNum = '';
  4852. my $iconURI = '';
  4853. # Um einen XML-Parser zu vermeiden, werden hier reguläre Ausdrücke für die Ermittlung der Werte eingesetzt...
  4854. # RoomName ermitteln
  4855. $roomName = decode_entities($1) if ($descriptionDocument =~ m/<roomName>(.*?)<\/roomName>/im);
  4856. $saveRoomName = decode('UTF-8', $roomName);
  4857. eval {
  4858. use utf8;
  4859. $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  4860. };
  4861. $saveRoomName =~ s/[^a-zA-Z0-9_ ]//g;
  4862. $saveRoomName = SONOS_Trim($saveRoomName);
  4863. $saveRoomName =~ s/ /_/g;
  4864. my $groupName = $saveRoomName;
  4865. # Modelnumber ermitteln
  4866. $modelNumber = decode_entities($1) if ($descriptionDocument =~ m/<modelNumber>(.*?)<\/modelNumber>/im);
  4867. # DisplayVersion ermitteln
  4868. $displayVersion = decode_entities($1) if ($descriptionDocument =~ m/<displayVersion>(.*?)<\/displayVersion>/im);
  4869. # SerialNum ermitteln
  4870. $serialNum = decode_entities($1) if ($descriptionDocument =~ m/<serialNum>(.*?)<\/serialNum>/im);
  4871. # Icon-URI ermitteln
  4872. $iconURI = decode_entities($1) if ($descriptionDocument =~ m/<iconList>.*?<icon>.*?<id>0<\/id>.*?<url>(.*?)<\/url>.*?<\/icon>.*?<\/iconList>/sim);
  4873. # Kompletten Pfad zum Download des ZonePlayer-Bildchens zusammenbauen
  4874. my $iconOrigPath = $device->location();
  4875. $iconOrigPath =~ s/(http:\/\/.*?)\/.*/$1$iconURI/i;
  4876. # Zieldateiname für das ZonePlayer-Bildchen zusammenbauen
  4877. my $iconPath = $iconURI;
  4878. $iconPath =~ s/.*\/(.*)/icoSONOSPLAYER_$1/i;
  4879. my $udnShort = $device->UDN;
  4880. $udnShort =~ s/.*?://i;
  4881. my $udn = $udnShort.'_MR';
  4882. $SONOS_Locations{$device->location()} = $udn;
  4883. my $name = $SONOS_Client_Data{SonosDeviceName}."_".$saveRoomName;
  4884. # Erkannte Werte ausgeben...
  4885. SONOS_Log undef, 4, "RoomName: '$roomName', SaveRoomName: '$saveRoomName', ModelNumber: '$modelNumber', DisplayVersion: '$displayVersion', SerialNum: '$serialNum', IconURI: '$iconURI', IconOrigPath: '$iconOrigPath', IconPath: '$iconPath'";
  4886. SONOS_Log undef, 2, "Discover Sonosplayer '$roomName' ($modelNumber) Software Revision $displayVersion with ID '$udn'";
  4887. # Device sichern...
  4888. $SONOS_UPnPDevice{$udn} = $device;
  4889. # ServiceProxies für spätere Aufrufe merken
  4890. my $alarmService = $device->getService('urn:schemas-upnp-org:service:AlarmClock:1');
  4891. $SONOS_AlarmClockControlProxy{$udn} = $alarmService->controlProxy if ($alarmService);
  4892. my $audioInService = $device->getService('urn:schemas-upnp-org:service:AudioIn:1');
  4893. $SONOS_AudioInProxy{$udn} = $audioInService->controlProxy if ($audioInService);
  4894. my $devicePropertiesService = $device->getService('urn:schemas-upnp-org:service:DeviceProperties:1');
  4895. $SONOS_DevicePropertiesProxy{$udn} = $devicePropertiesService->controlProxy if ($devicePropertiesService);
  4896. #$SONOS_GroupManagementProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:GroupManagement:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:GroupManagement:1'));
  4897. my $musicServicesService = $device->getService('urn:schemas-upnp-org:service:MusicServices:1');
  4898. $SONOS_MusicServicesProxy{$udn} = $musicServicesService->controlProxy if ($musicServicesService);
  4899. my $zoneGroupTopologyService = $device->getService('urn:schemas-upnp-org:service:ZoneGroupTopology:1');
  4900. $SONOS_ZoneGroupTopologyProxy{$udn} = $zoneGroupTopologyService->controlProxy if ($zoneGroupTopologyService);
  4901. # Bei einem Dock gibt es AVTransport nur am Hauptdevice, deshalb mal schauen, ob wir es hier bekommen können
  4902. my $transportService = $device->getService('urn:schemas-upnp-org:service:AVTransport:1');
  4903. $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService);
  4904. my $renderingService;
  4905. my $groupRenderingService;
  4906. my $contentDirectoryService;
  4907. # Hier die Subdevices durchgehen...
  4908. for my $subdevice ($device->children) {
  4909. SONOS_Log undef, 4, 'SubDevice found: '.$subdevice->UDN;
  4910. if ($subdevice->UDN =~ /.*_MR/i) {
  4911. # Wir haben hier das Media-Renderer Subdevice
  4912. $transportService = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1');
  4913. $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService);
  4914. if ($modelNumber ne 'Sub') {
  4915. $renderingService = $subdevice->getService('urn:schemas-upnp-org:service:RenderingControl:1');
  4916. $SONOS_RenderingControlProxy{$udn} = $renderingService->controlProxy if ($renderingService);
  4917. }
  4918. $groupRenderingService = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1');
  4919. $SONOS_GroupRenderingControlProxy{$udn} = $groupRenderingService->controlProxy if ($groupRenderingService);
  4920. }
  4921. if ($subdevice->UDN =~ /.*_MS/i) {
  4922. # Wir haben hier das Media-Server Subdevice
  4923. $contentDirectoryService = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1');
  4924. $SONOS_ContentDirectoryControlProxy{$udn} = $contentDirectoryService->controlProxy if ($contentDirectoryService);
  4925. }
  4926. }
  4927. SONOS_Log undef, 4, 'ControlProxies wurden gesichert';
  4928. # ZoneTopology laden, um die Benennung der Fhem-Devices besser an die Realität anpassen zu können
  4929. my ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState) = SONOS_AnalyzeZoneGroupTopology($udn, $udnShort);
  4930. my ($slavePlayerNamesRef, $notBondedSlavePlayerNamesRef) = SONOS_AnalyzeTopologyForSlavePlayer($udnShort, $zoneGroupState);
  4931. my @slavePlayerNames = @{$slavePlayerNamesRef};
  4932. my @slavePlayerNotBondedNames = @{$notBondedSlavePlayerNamesRef};
  4933. # Wenn der aktuelle Player der Master ist, dann kein Kürzel anhängen,
  4934. # damit gibt es immer einen Player, der den Raumnamen trägt, und die anderen enthalten Kürzel
  4935. if ($master) {
  4936. $topoType = '';
  4937. }
  4938. # Raumnamen erweitern
  4939. $name .= $topoType;
  4940. $saveRoomName .= $topoType;
  4941. # Volume laden um diese im Reading ablegen zu können
  4942. my $currentVolume = 0;
  4943. my $balance = 0;
  4944. if (!$isZoneBridge) {
  4945. if ($SONOS_RenderingControlProxy{$udn}) {
  4946. eval {
  4947. $currentVolume = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume');
  4948. # Balance ermitteln
  4949. my $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume');
  4950. my $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume');
  4951. $balance = (-$volumeLeft) + $volumeRight;
  4952. SONOS_Log undef, 4, 'Retrieve Current Volumelevels. Master: "'.$currentVolume.'", Balance: "'.$balance.'"';
  4953. };
  4954. if ($@) {
  4955. $currentVolume = 0;
  4956. $balance = 0;
  4957. SONOS_Log undef, 4, 'Couldn\'t retrieve Current Volumelevels: '. $@;
  4958. }
  4959. } else {
  4960. SONOS_Log undef, 4, 'Couldn\'t get any Volume Information due to missing RenderingControlProxy';
  4961. }
  4962. }
  4963. # Load official icon from zoneplayer and copy it to local place for FHEM-use
  4964. SONOS_Client_Notifier('getstore(\''.$iconOrigPath.'\', $attr{global}{modpath}.\'/www/images/default/'.$iconPath."');\n");
  4965. # Icons neu einlesen lassen
  4966. SONOS_Client_Notifier('SONOS_RefreshIconsInFHEMWEB(\'/www/images/default/'.$iconPath.'\');');
  4967. # Transport Informations to FHEM
  4968. # Check if this device is already defined...
  4969. if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerUDNs}})) {
  4970. push @{$SONOS_Client_Data{PlayerUDNs}}, $udn;
  4971. # Wenn der Name schon mal verwendet wurde, dann solange ein Kürzel anhängen, bis ein freier Name gefunden wurde...
  4972. while (SONOS_isInList($name, @{$SONOS_Client_Data{PlayerNames}})) {
  4973. $name .= '_X';
  4974. $saveRoomName .= '_X';
  4975. SONOS_Log undef, 2, "New Fhem-Name neccessary for '$roomName' -> '$name', ID '$udn'";
  4976. }
  4977. push @{$SONOS_Client_Data{PlayerNames}}, $name;
  4978. my %elemValues = ();
  4979. $SONOS_Client_Data{Buffer}->{$udn} = shared_clone(\%elemValues);
  4980. # Define SonosPlayer-Device...
  4981. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER', undef, $udn, undef, $name, undef, undef, undef, undef, undef)) {
  4982. SONOS_Client_Notifier($elem);
  4983. }
  4984. # ...and his attributes
  4985. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Attributes', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge)) {
  4986. SONOS_Client_Notifier($elem);
  4987. }
  4988. # Setting Internal-Data
  4989. if (!$isZoneBridge) {
  4990. SONOS_Client_Data_Refresh('', $udn, 'getAlarms', 1);
  4991. SONOS_Client_Data_Refresh('', $udn, 'minVolume', 0);
  4992. }
  4993. # Define ReadingsGroup
  4994. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_ReadingsGroup', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, undef, undef, $groupName, undef, $isZoneBridge)) {
  4995. SONOS_Client_Notifier($elem);
  4996. }
  4997. # Define ReadingsGroup-Listen
  4998. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_ReadingsGroup_Listen', undef, undef, $master, $name, undef, undef, undef, undef, $isZoneBridge)) {
  4999. SONOS_Client_Notifier($elem);
  5000. }
  5001. # Define RemoteControl
  5002. for my $elem (SONOS_GetDefineStringlist('SONOSPLAYER_Remotecontrol', $SONOS_Client_Data{SonosDeviceName}, undef, $master, $name, undef, undef, $groupName, undef, $isZoneBridge)) {
  5003. SONOS_Client_Notifier($elem);
  5004. }
  5005. # Name sichern...
  5006. SONOS_Client_Data_Refresh('', $udn, 'NAME', $name);
  5007. SONOS_Client_Data_Refresh('', 'udn', $name, $udn);
  5008. SONOS_Log undef, 1, "Successfully autocreated SonosPlayer '$saveRoomName' ($modelNumber) as '$name' with Software Revision $displayVersion and ID '$udn'";
  5009. } else {
  5010. # Wenn das Device schon existiert, dann den dort verwendeten Namen holen
  5011. $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5012. SONOS_Client_Data_Refresh('', 'udn', $name, $udn);
  5013. SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' ($modelNumber) with ID '$udn' is already defined (as '$name') and will only be updated";
  5014. }
  5015. # Wenn der Player noch nicht auf der "Aktiv"-Liste steht, dann draufpacken...
  5016. push @{$SONOS_Client_Data{PlayerAlive}}, $udn if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerAlive}}));
  5017. # Readings aktualisieren
  5018. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  5019. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'presence', 'appeared');
  5020. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume);
  5021. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Balance', $balance);
  5022. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomName', $roomName);
  5023. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  5024. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName);
  5025. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'playerType', $modelNumber);
  5026. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume);
  5027. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'location', $device->location);
  5028. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'softwareRevision', $displayVersion);
  5029. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'serialNum', $serialNum);
  5030. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'fieldType', $fieldType);
  5031. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsBonded', (($fieldType eq '') || ($fieldType eq 'LF') || ($fieldType eq 'LF_RF')) ? '0' : '1');
  5032. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsMaster', $master ? '1' : '0');
  5033. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'MasterPlayer', $masterPlayerName);
  5034. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayer', SONOS_Dumper(\@slavePlayerNames));
  5035. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBonded', SONOS_Dumper(\@slavePlayerNotBondedNames));
  5036. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  5037. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBondedList', (scalar(@slavePlayerNotBondedNames) ? '-|' : '').join('|', @slavePlayerNotBondedNames));
  5038. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBondedListAlias', (scalar(@slavePlayerNotBondedNames) ? 'Auswahl|' : '').join('|', map { $_ = SONOS_Client_Data_Retreive($_, 'reading', 'roomName', $_); $_ } @slavePlayerNotBondedNames));
  5039. }
  5040. # Abspielreadings vorab ermitteln, um darauf prüfen zu können...
  5041. if (!$isZoneBridge) {
  5042. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5043. eval {
  5044. my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportInfo(0);
  5045. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'transportState', $result->getValue('CurrentTransportState'));
  5046. $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0);
  5047. my $tmp = $result->getValue('TrackURI');
  5048. $tmp =~ s/&apos;/'/gi;
  5049. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackURI', $tmp);
  5050. my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($result->getValue('TrackURI'));
  5051. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProvider', $trackProvider);
  5052. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProviderIconRoundURL', $trackProviderRoundURL);
  5053. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProviderIconQuadraticURL', $trackProviderQuadraticURL);
  5054. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackDuration', $result->getValue('TrackDuration'));
  5055. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackDurationSec', SONOS_GetTimeSeconds($result->getValue('TrackDuration')));
  5056. my $modus = 'ReadingsBulkUpdate'.((SONOS_Client_Data_Retreive($udn, 'reading', 'currentStreamAudio', 0)) ? 'IfChanged' : '');
  5057. SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPosition', $result->getValue('RelTime'));
  5058. SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($result->getValue('RelTime')));
  5059. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrack', $result->getValue('Track'));
  5060. $result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
  5061. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'numberOfTracks', $result->getValue('NrTracks'));
  5062. my $stream = ($result->getValue('CurrentURI') =~ m/^x-(sonosapi|rincon)-(stream|mp3radio):.*?/);
  5063. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentStreamAudio', $stream);
  5064. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentNormalAudio', !$stream);
  5065. };
  5066. if ($@) {
  5067. SONOS_Log undef, 1, 'Couldn\'t retrieve Current Transportsettings during Discovery: '. $@;
  5068. }
  5069. }
  5070. }
  5071. SONOS_Client_Data_Refresh('', $udn, 'LastSubscriptionsRenew', SONOS_TimeNow());
  5072. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  5073. SONOS_Client_Notifier('CommandAttrWithUDN:'.$udn.':model Sonos_'.$modelNumber);
  5074. $SONOS_Client_SendQueue_Suspend = 0;
  5075. SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' is now updated";
  5076. # AVTransport-Subscription
  5077. if (!$isZoneBridge) {
  5078. if ($transportService) {
  5079. $SONOS_TransportSubscriptions{$udn} = $transportService->subscribe(\&SONOS_TransportCallback);
  5080. if (defined($SONOS_TransportSubscriptions{$udn})) {
  5081. SONOS_Log undef, 2, 'Service-subscribing successful with SID='.$SONOS_TransportSubscriptions{$udn}->SID;
  5082. } else {
  5083. SONOS_Log undef, 1, 'Service-subscribing NOT successful';
  5084. }
  5085. } else {
  5086. undef($SONOS_TransportSubscriptions{$udn});
  5087. SONOS_Log undef, 1, 'Service-subscribing not possible due to missing TransportService';
  5088. }
  5089. }
  5090. # Rendering-Subscription, wenn eine untere oder obere Lautstärkegrenze angegeben wurde, und Lautstärke überhaupt geht
  5091. if ($renderingService && (SONOS_Client_Data_Retreive($udn, 'attr', 'minVolume', -1) != -1
  5092. || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolume', -1) != -1
  5093. || SONOS_Client_Data_Retreive($udn, 'attr', 'minVolumeHeadphone', -1) != -1
  5094. || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolumeHeadphone', -1) != -1)) {
  5095. eval {
  5096. $SONOS_RenderingSubscriptions{$udn} = $renderingService->subscribe(\&SONOS_RenderingCallback);
  5097. };
  5098. $SONOS_ButtonPressQueue{$udn} = Thread::Queue->new();
  5099. if (defined($SONOS_RenderingSubscriptions{$udn})) {
  5100. SONOS_Log undef, 2, 'Rendering-Service-subscribing successful with SID='.$SONOS_RenderingSubscriptions{$udn}->SID;
  5101. } else {
  5102. SONOS_Log undef, 1, 'Rendering-Service-subscribing NOT successful: '.$@;
  5103. }
  5104. } else {
  5105. undef($SONOS_RenderingSubscriptions{$udn});
  5106. }
  5107. # GroupRendering-Subscription
  5108. 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 )) {
  5109. $SONOS_GroupRenderingSubscriptions{$udn} = $groupRenderingService->subscribe(\&SONOS_GroupRenderingCallback);
  5110. if (defined($SONOS_GroupRenderingSubscriptions{$udn})) {
  5111. SONOS_Log undef, 2, 'GroupRendering-Service-subscribing successful with SID='.$SONOS_GroupRenderingSubscriptions{$udn}->SID;
  5112. } else {
  5113. SONOS_Log undef, 1, 'GroupRendering-Service-subscribing NOT successful';
  5114. }
  5115. } else {
  5116. undef($SONOS_GroupRenderingSubscriptions{$udn});
  5117. }
  5118. # ContentDirectory-Subscription
  5119. if ($contentDirectoryService) {
  5120. eval {
  5121. $SONOS_ContentDirectorySubscriptions{$udn} = $contentDirectoryService->subscribe(\&SONOS_ContentDirectoryCallback);
  5122. if (defined($SONOS_ContentDirectorySubscriptions{$udn})) {
  5123. SONOS_Log undef, 2, 'ContentDirectory-Service-subscribing successful with SID='.$SONOS_ContentDirectorySubscriptions{$udn}->SID;
  5124. } else {
  5125. SONOS_Log undef, 1, 'ContentDirectory-Service-subscribing NOT successful';
  5126. }
  5127. };
  5128. if ($@) {
  5129. SONOS_Log undef, 1, 'ContentDirectory-Service-subscribing NOT successful: '.$@;
  5130. }
  5131. } else {
  5132. undef($SONOS_ContentDirectorySubscriptions{$udn});
  5133. }
  5134. # Alarm-Subscription
  5135. if ($alarmService && (SONOS_Client_Data_Retreive($udn, 'attr', 'getAlarms', 0) != 0)) {
  5136. eval {
  5137. $SONOS_AlarmSubscriptions{$udn} = $alarmService->subscribe(\&SONOS_AlarmCallback);
  5138. if (defined($SONOS_AlarmSubscriptions{$udn})) {
  5139. SONOS_Log undef, 2, 'Alarm-Service-subscribing successful with SID='.$SONOS_AlarmSubscriptions{$udn}->SID;
  5140. } else {
  5141. SONOS_Log undef, 1, 'Alarm-Service-subscribing NOT successful';
  5142. }
  5143. };
  5144. if ($@) {
  5145. SONOS_Log undef, 1, 'Alarm-Service-Service-subscribing NOT successful: '.$@;
  5146. }
  5147. } else {
  5148. undef($SONOS_AlarmSubscriptions{$udn});
  5149. }
  5150. # ZoneGroupTopology-Subscription
  5151. if ($zoneGroupTopologyService) {
  5152. eval {
  5153. $SONOS_ZoneGroupTopologySubscriptions{$udn} = $zoneGroupTopologyService->subscribe(\&SONOS_ZoneGroupTopologyCallback);
  5154. if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn})) {
  5155. SONOS_Log undef, 2, 'ZoneGroupTopology-Service-subscribing successful with SID='.$SONOS_ZoneGroupTopologySubscriptions{$udn}->SID;
  5156. } else {
  5157. SONOS_Log undef, 1, 'ZoneGroupTopology-Service-subscribing NOT successful';
  5158. }
  5159. };
  5160. if ($@) {
  5161. SONOS_Log undef, 1, 'ZoneGroupTopology-Service-subscribing NOT successful: '.$@;
  5162. }
  5163. } else {
  5164. undef($SONOS_ZoneGroupTopologySubscriptions{$udn});
  5165. }
  5166. # DeviceProperties-Subscription
  5167. if ($devicePropertiesService) {
  5168. eval {
  5169. $SONOS_DevicePropertiesSubscriptions{$udn} = $devicePropertiesService->subscribe(\&SONOS_DevicePropertiesCallback);
  5170. if (defined($SONOS_DevicePropertiesSubscriptions{$udn})) {
  5171. SONOS_Log undef, 2, 'DeviceProperties-Service-subscribing successful with SID='.$SONOS_DevicePropertiesSubscriptions{$udn}->SID;
  5172. } else {
  5173. SONOS_Log undef, 1, 'DeviceProperties-Service-subscribing NOT successful';
  5174. }
  5175. };
  5176. if ($@) {
  5177. SONOS_Log undef, 1, 'DeviceProperties-Service-subscribing NOT successful: '.$@;
  5178. }
  5179. } else {
  5180. undef($SONOS_DevicePropertiesSubscriptions{$udn});
  5181. }
  5182. # AudioIn-Subscription
  5183. if ($audioInService) {
  5184. eval {
  5185. $SONOS_AudioInSubscriptions{$udn} = $audioInService->subscribe(\&SONOS_AudioInCallback);
  5186. if (defined($SONOS_AudioInSubscriptions{$udn})) {
  5187. SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID='.$SONOS_AudioInSubscriptions{$udn}->SID;
  5188. } else {
  5189. SONOS_Log undef, 1, 'AudioIn-Service-subscribing NOT successful';
  5190. delete($SONOS_AudioInSubscriptions{$udn});
  5191. }
  5192. };
  5193. if ($@) {
  5194. SONOS_Log undef, 1, 'AudioIn-Service-Service-subscribing NOT successful: '.$@;
  5195. }
  5196. } else {
  5197. undef($SONOS_AudioInSubscriptions{$udn});
  5198. }
  5199. # MusicServices-Subscription
  5200. if ($musicServicesService) {
  5201. eval {
  5202. $SONOS_MusicServicesSubscriptions{$udn} = $musicServicesService->subscribe(\&SONOS_MusicServicesCallback);
  5203. if (defined($SONOS_MusicServicesSubscriptions{$udn})) {
  5204. SONOS_Log undef, 2, 'MusicServices-Service-subscribing successful with SID='.$SONOS_MusicServicesSubscriptions{$udn}->SID;
  5205. } else {
  5206. SONOS_Log undef, 1, 'MusicServices-Service-subscribing NOT successful';
  5207. delete($SONOS_MusicServicesSubscriptions{$udn});
  5208. }
  5209. };
  5210. if ($@) {
  5211. SONOS_Log undef, 1, 'MusicServices-Service-Service-subscribing NOT successful: '.$@;
  5212. }
  5213. } else {
  5214. undef($SONOS_MusicServicesSubscriptions{$udn});
  5215. }
  5216. SONOS_Log undef, 3, 'Discover: End of discover-event for "'.$roomName.'".';
  5217. } elsif ($action eq 'deviceRemoved') {
  5218. my $udn = $device->UDN;
  5219. $udn =~ s/.*?://i;
  5220. $udn .= '_MR';
  5221. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
  5222. SONOS_Log undef, 2, "Device '$udn' removed. Do nothing special here, cause all is done in another way...";
  5223. }
  5224. return 0;
  5225. }
  5226. ########################################################################################
  5227. #
  5228. # SONOS_GetDefineStringlist - Generates a list of define- or attr-commands acoording to the given desired-device
  5229. #
  5230. ########################################################################################
  5231. sub SONOS_GetDefineStringlist($$$$$$$$$$) {
  5232. my ($devicetype, $sonosDeviceName, $udn, $master, $name, $roomName, $aliasSuffix, $groupName, $iconPath, $isZoneBridge) = @_;
  5233. my @defs = ();
  5234. if (lc($devicetype) eq 'sonosplayer') {
  5235. push(@defs, 'CommandDefine:'.$name.' SONOSPLAYER '.$udn);
  5236. } elsif (lc($devicetype) eq 'sonosplayer_attributes') {
  5237. push(@defs, 'CommandAttr:'.$name.' room '.$sonosDeviceName);
  5238. push(@defs, 'CommandAttr:'.$name.' alias '.$roomName.$aliasSuffix);
  5239. push(@defs, 'CommandAttr:'.$name.' group '.$groupName);
  5240. push(@defs, 'CommandAttr:'.$name.' icon '.$iconPath);
  5241. push(@defs, 'CommandAttr:'.$name.' sortby 1');
  5242. if (!$isZoneBridge) {
  5243. if (!SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  5244. 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; } }, Queue:LastActionResult.*?GetQueueWithCovers.* { if (ReadingsVal($name, "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, currentTrackPosition:LastActionResult.*?GetCurrentTrackPosition.* { if (ReadingsVal($name, "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }');
  5245. }
  5246. 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>');
  5247. push(@defs, 'CommandAttr:'.$name.' generateInfoSummarize2 <TransportState/><InfoSummarize1 prefix=" => "/>');
  5248. 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"/>');
  5249. push(@defs, 'CommandAttr:'.$name.' generateVolumeSlider 1');
  5250. push(@defs, 'CommandAttr:'.$name.' getAlarms 1');
  5251. push(@defs, 'CommandAttr:'.$name.' minVolume 0');
  5252. #push(@defs, 'CommandAttr:'.$name.' stateVariable Presence');
  5253. push(@defs, 'CommandAttr:'.$name.' stateFormat presence ~ currentTrackPositionSimulatedPercent% (currentTrackPositionSimulated / currentTrackDuration)');
  5254. push(@defs, 'CommandAttr:'.$name.' getTitleInfoFromMaster 1');
  5255. push(@defs, 'CommandAttr:'.$name.' simulateCurrentTrackPosition 1');
  5256. push(@defs, 'CommandAttr:'.$name.' webCmd Volume');
  5257. #push(@defs, 'CommandAttr:'.$name.' webCmd Play:Pause:Previous:Next:VolumeD:VolumeU:MuteT');
  5258. } else {
  5259. push(@defs, 'CommandAttr:'.$name.' stateFormat presence');
  5260. }
  5261. } elsif (lc($devicetype) eq 'sonosplayer_readingsgroup') {
  5262. if (!$isZoneBridge) {
  5263. if ($master) {
  5264. # push(@defs, 'CommandDefine:'.$name.'RG readingsGroup '.$name.':<{SONOS_getCoverTitleRG($DEVICE)}@infoSummarize2>');
  5265. # push(@defs, 'CommandAttr:'.$name.'RG room '.$sonosDeviceName);
  5266. # push(@defs, 'CommandAttr:'.$name.'RG group '.$groupName);
  5267. # push(@defs, 'CommandAttr:'.$name.'RG sortby 2');
  5268. # push(@defs, 'CommandAttr:'.$name.'RG noheading 1');
  5269. # push(@defs, 'CommandAttr:'.$name.'RG nonames 1');
  5270. #push(@defs, 'CommandDefine:'.$name.'RG2 readingsGroup '.$name.':infoSummarize2@{SONOSPLAYER_GetMasterPlayerName($DEVICE)}');
  5271. #push(@defs, 'CommandAttr:'.$name.'RG2 valueFormat {" "}');
  5272. #push(@defs, 'CommandAttr:'.$name.'RG2 valuePrefix {SONOS_getCoverTitleRG(SONOSPLAYER_GetMasterPlayerName($DEVICE))}');
  5273. #push(@defs, 'CommandAttr:'.$name.'RG2 room '.$SONOS_Client_Data{SonosDeviceName});
  5274. #push(@defs, 'CommandAttr:'.$name.'RG2 group '.$groupName);
  5275. #push(@defs, 'CommandAttr:'.$name.'RG2 sortby 4');
  5276. #push(@defs, 'CommandAttr:'.$name.'RG2 noheading 1');
  5277. #push(@defs, 'CommandAttr:'.$name.'RG2 nonames 1');
  5278. #push(@defs, 'CommandAttr:'.$name.'RG2 notime 1');
  5279. }
  5280. }
  5281. } elsif (lc($devicetype) eq 'sonosplayer_readingsgroup_listen') {
  5282. if (!$isZoneBridge) {
  5283. if ($master) {
  5284. push(@defs, 'CommandDefine:'.$name.'RG_Favourites readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Favourites",1)}@Favourites>');
  5285. push(@defs, 'CommandDefine:'.$name.'RG_Radios readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Radios",1)}@Radios>');
  5286. push(@defs, 'CommandDefine:'.$name.'RG_Playlists readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Playlists")}@Playlists>');
  5287. push(@defs, 'CommandDefine:'.$name.'RG_Queue readingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Queue")}@Queue>');
  5288. }
  5289. }
  5290. } elsif (lc($devicetype) eq 'sonosplayer_remotecontrol') {
  5291. if (!$isZoneBridge) {
  5292. if ($master) {
  5293. #push(@defs, 'CommandDefine:'.$name.'RC remotecontrol');
  5294. #push(@defs, 'CommandAttr:'.$name.'RC room hidden');
  5295. #push(@defs, 'CommandAttr:'.$name.'RC group '.$sonosDeviceName);
  5296. #push(@defs, 'CommandAttr:'.$name.'RC rc_iconpath icons/remotecontrol');
  5297. #push(@defs, 'CommandAttr:'.$name.'RC rc_iconprefix black_btn_');
  5298. #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');
  5299. #push(@defs, 'CommandDefine:'.$name.'RC_Notify notify '.$name.'RC set '.$name.' $EVENT');
  5300. #push(@defs, 'CommandDefine:'.$name.'RC_Weblink weblink htmlCode {fhem("get '.$name.'RC htmlcode", 1)}');
  5301. #push(@defs, 'CommandAttr:'.$name.'RC_Weblink room '.$sonosDeviceName);
  5302. #push(@defs, 'CommandAttr:'.$name.'RC_Weblink group '.$groupName);
  5303. #push(@defs, 'CommandAttr:'.$name.'RC_Weblink sortby 3');
  5304. }
  5305. }
  5306. }
  5307. return @defs;
  5308. }
  5309. ########################################################################################
  5310. #
  5311. # SONOS_AnalyzeZoneGroupTopology - Analyzes the current Zoneplayertopology for better naming of the components
  5312. #
  5313. ########################################################################################
  5314. sub SONOS_AnalyzeZoneGroupTopology($$) {
  5315. my ($udn, $udnShort) = @_;
  5316. # ZoneTopology laden, um die Benennung der Fhem-Devices besser an die Realität anpassen zu können
  5317. my $topoType = '';
  5318. my $fieldType = '';
  5319. my $master = 1;
  5320. my $masterPlayerName;
  5321. my $isZoneBridge = 0;
  5322. my $zoneGroupState = '';
  5323. if ($SONOS_ZoneGroupTopologyProxy{$udn}) {
  5324. $zoneGroupState = $SONOS_ZoneGroupTopologyProxy{$udn}->GetZoneGroupState()->getValue('ZoneGroupState');
  5325. SONOS_Log undef, 5, 'ZoneGroupState: '.$zoneGroupState;
  5326. if ($zoneGroupState =~ m/.*(<ZoneGroup Coordinator="(RINCON_[0-9a-f]+)".*?>).*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) {
  5327. my $coordinator = $2;
  5328. my $member = $3;
  5329. $masterPlayerName = SONOS_Client_Data_Retreive($coordinator.'_MR', 'def', 'NAME', $coordinator.'_MR');
  5330. # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten?
  5331. if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) {
  5332. $topoType = '_'.$1;
  5333. }
  5334. # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten?
  5335. if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) {
  5336. $topoType = '_'.$1;
  5337. $topoType =~ s/,/_/g;
  5338. }
  5339. SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType;
  5340. $fieldType = substr($topoType, 1) if ($topoType);
  5341. my $invisible = 0;
  5342. $invisible = 1 if ($member =~ m/Invisible="1"/i);
  5343. $isZoneBridge = 1 if ($member =~ m/IsZoneBridge="1"/i);
  5344. $master = !$invisible || $isZoneBridge;
  5345. }
  5346. }
  5347. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  5348. my $aliasSuffix = '';
  5349. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  5350. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  5351. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  5352. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  5353. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  5354. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  5355. return ($isZoneBridge, $topoType, $fieldType, $master, $masterPlayerName, $aliasSuffix, $zoneGroupState);
  5356. }
  5357. ########################################################################################
  5358. #
  5359. # SONOS_IsAlive - Checks if the given Device is alive or not and triggers the proper event if status changed
  5360. #
  5361. # Parameter $udn = UDN of the Device in short-form (e.g. RINCON_000E5828D0F401400_MR)
  5362. #
  5363. ########################################################################################
  5364. sub SONOS_IsAlive($) {
  5365. my ($udn) = @_;
  5366. SONOS_Log $udn, 4, "IsAlive-Event UDN=$udn";
  5367. my $result = 1;
  5368. my $doDeleteProxyObjects = 0;
  5369. $SONOS_Client_SendQueue_Suspend = 1;
  5370. my $location = SONOS_Client_Data_Retreive($udn, 'reading', 'location', '');
  5371. if ($location) {
  5372. SONOS_Log $udn, 5, "Location: $location";
  5373. my ($host, $port) = ($1, $2) if ($location =~ m/http:\/\/(.*?):(.*?)\//);
  5374. my $pingType = $SONOS_Client_Data{pingType};
  5375. return 1 if (lc($pingType) eq 'none');
  5376. if (SONOS_isInList($pingType, @SONOS_PINGTYPELIST)) {
  5377. SONOS_Log $udn, 5, "PingType: $pingType";
  5378. } else {
  5379. SONOS_Log $udn, 1, "Wrong pingType given for '$udn': '$pingType'. Choose one of '".join(', ', @SONOS_PINGTYPELIST)."'";
  5380. $pingType = $SONOS_DEFAULTPINGTYPE;
  5381. }
  5382. my $ping = Net::Ping->new($pingType, 1);
  5383. $ping->source_verify(0); # Es ist egal, von welcher Schnittstelle des Zielsystems die Antwort kommt
  5384. $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
  5385. if ($ping->ping($host)) {
  5386. # Alive
  5387. SONOS_Log $udn, 4, "$host is alive";
  5388. $result = 1;
  5389. # IsAlive-Negativ-Counter zurücksetzen
  5390. $SONOS_Thread_IsAlive_Counter{$host} = 0;
  5391. } else {
  5392. # Not Alive
  5393. $SONOS_Thread_IsAlive_Counter{$host}++;
  5394. if ($SONOS_Thread_IsAlive_Counter{$host} > $SONOS_Thread_IsAlive_Counter_MaxMerci) {
  5395. SONOS_Log $udn, 3, "$host is REALLY NOT alive (out of merci maxlevel '".$SONOS_Thread_IsAlive_Counter_MaxMerci.'\')';
  5396. $result = 0;
  5397. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
  5398. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'state', 'disappeared');
  5399. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'transportState', 'STOPPED');
  5400. $doDeleteProxyObjects = 1;
  5401. } else {
  5402. SONOS_Log $udn, 3, "$host is NOT alive, but in merci level ".$SONOS_Thread_IsAlive_Counter{$host}.'/'.$SONOS_Thread_IsAlive_Counter_MaxMerci.'.';
  5403. }
  5404. }
  5405. $ping->close();
  5406. }
  5407. $SONOS_Client_SendQueue_Suspend = 0;
  5408. # Jetzt, wo das Reading dazu auch gesetzt wurde, hier ausführen
  5409. if ($doDeleteProxyObjects) {
  5410. my %data;
  5411. $data{WorkType} = 'deleteProxyObjects';
  5412. $data{UDN} = $udn;
  5413. my @params = ();
  5414. $data{Params} = \@params;
  5415. $SONOS_Client_ReceiveQueue->enqueue(\%data);
  5416. }
  5417. return $result;
  5418. }
  5419. ########################################################################################
  5420. #
  5421. # SONOS_DeleteProxyObjects - Deletes all references to the proxy objects of the given zoneplayer
  5422. #
  5423. # Parameter $name = The name of zoneplayerdevice
  5424. #
  5425. ########################################################################################
  5426. sub SONOS_DeleteProxyObjects($) {
  5427. my ($udn) = @_;
  5428. SONOS_Log $udn, 4, "Delete ProxyObjects and SubscriptionObjects for '$udn'";
  5429. delete $SONOS_AVTransportControlProxy{$udn};
  5430. delete $SONOS_RenderingControlProxy{$udn};
  5431. delete $SONOS_ContentDirectoryControlProxy{$udn};
  5432. delete $SONOS_AlarmClockControlProxy{$udn};
  5433. delete $SONOS_AudioInProxy{$udn};
  5434. delete $SONOS_DevicePropertiesProxy{$udn};
  5435. delete $SONOS_GroupManagementProxy{$udn};
  5436. delete $SONOS_MusicServicesProxy{$udn};
  5437. delete $SONOS_ZoneGroupTopologyProxy{$udn};
  5438. delete $SONOS_TransportSubscriptions{$udn};
  5439. delete $SONOS_RenderingSubscriptions{$udn};
  5440. delete $SONOS_GroupRenderingSubscriptions{$udn};
  5441. delete $SONOS_ContentDirectorySubscriptions{$udn};
  5442. delete $SONOS_AlarmSubscriptions{$udn};
  5443. delete $SONOS_ZoneGroupTopologySubscriptions{$udn};
  5444. delete $SONOS_DevicePropertiesSubscriptions{$udn};
  5445. delete $SONOS_AudioInSubscriptions{$udn};
  5446. delete $SONOS_MusicServicesSubscriptions{$udn};
  5447. # Am Ende noch das Device entfernen...
  5448. delete $SONOS_UPnPDevice{$udn};
  5449. SONOS_Log $udn, 4, "Delete of ProxyObjects and SubscriptionObjects DONE for '$udn'";
  5450. }
  5451. ########################################################################################
  5452. #
  5453. # SONOS_GetReadingsToCurrentHash - Get all neccessary readings from named device
  5454. #
  5455. # Parameter $name = The name of the player-device
  5456. #
  5457. ########################################################################################
  5458. sub SONOS_GetReadingsToCurrentHash($$) {
  5459. my ($name, $emptyCurrent) = @_;
  5460. my %current;
  5461. if ($emptyCurrent) {
  5462. # Empty Values for Current Track Readings
  5463. $current{TransportState} = 'ERROR';
  5464. $current{Shuffle} = 0;
  5465. $current{Repeat} = 0;
  5466. $current{RepeatOne} = 0;
  5467. $current{CrossfadeMode} = 0;
  5468. $current{NumberOfTracks} = '';
  5469. $current{Track} = '';
  5470. $current{TrackURI} = '';
  5471. $current{TrackHandle} = '';
  5472. $current{TrackDuration} = '';
  5473. $current{TrackDurationSec} = '';
  5474. $current{TrackPosition} = '';
  5475. $current{TrackProvider} = '';
  5476. $current{TrackProviderIconQuadraticURL} = '';
  5477. $current{TrackProviderIconRoundURL} = '';
  5478. $current{TrackMetaData} = '';
  5479. $current{AlbumArtURI} = '';
  5480. $current{AlbumArtURL} = '';
  5481. $current{Title} = '';
  5482. $current{Artist} = '';
  5483. $current{Album} = '';
  5484. $current{Source} = '';
  5485. $current{OriginalTrackNumber} = '';
  5486. $current{AlbumArtist} = '';
  5487. $current{Sender} = '';
  5488. $current{SenderCurrent} = '';
  5489. $current{SenderInfo} = '';
  5490. $current{nextTrackDuration} = '';
  5491. $current{nextTrackDurationSec} = '';
  5492. $current{nextTrackURI} = '';
  5493. $current{nextTrackHandle} = '';
  5494. $current{nextAlbumArtURI} = '';
  5495. $current{nextAlbumArtURL} = '';
  5496. $current{nextTitle} = '';
  5497. $current{nextArtist} = '';
  5498. $current{nextAlbum} = '';
  5499. $current{nextAlbumArtist} = '';
  5500. $current{nextOriginalTrackNumber} = '';
  5501. $current{InfoSummarize1} = '';
  5502. $current{InfoSummarize2} = '';
  5503. $current{InfoSummarize3} = '';
  5504. $current{InfoSummarize4} = '';
  5505. $current{StreamAudio} = 0;
  5506. $current{NormalAudio} = 0;
  5507. } else {
  5508. # Insert normal Current Track Readings
  5509. $current{TransportState} = ReadingsVal($name, 'transportState', 'ERROR');
  5510. $current{Shuffle} = ReadingsVal($name, 'Shuffle', 0);
  5511. $current{Repeat} = ReadingsVal($name, 'Repeat', 0);
  5512. $current{RepeatOne} = ReadingsVal($name, 'RepeatOne', 0);
  5513. $current{CrossfadeMode} = ReadingsVal($name, 'CrossfadeMode', 0);
  5514. $current{NumberOfTracks} = ReadingsVal($name, 'numberOfTracks', '');
  5515. $current{Track} = ReadingsVal($name, 'currentTrack', '');
  5516. $current{TrackURI} = ReadingsVal($name, 'currentTrackURI', '');
  5517. $current{TrackHandle} = ReadingsVal($name, 'currentTrackHandle', '');
  5518. $current{EnqueuedTransportURI} = ReadingsVal($name, 'currentEnqueuedTransportURI', '');
  5519. $current{EnqueuedTransportHandle} = ReadingsVal($name, 'currentEnqueuedTransportHandle', '');
  5520. $current{TrackDuration} = ReadingsVal($name, 'currentTrackDuration', '');
  5521. $current{TrackDurationSec} = ReadingsVal($name, 'currentTrackDurationSec', '');
  5522. $current{TrackPosition} = ReadingsVal($name, 'currentTrackPosition', '');
  5523. $current{TrackPosition} = ReadingsVal($name, 'currentTrackPositionSec', '');
  5524. $current{TrackProvider} = ReadingsVal($name, 'currentTrackProvider', '');
  5525. $current{TrackProviderIconQuadraticURL} = ReadingsVal($name, 'currentTrackProviderIconQuadraticURL', '');
  5526. $current{TrackProviderIconRoundURL} = ReadingsVal($name, 'currentTrackProviderIconRoundURL', '');
  5527. #$current{TrackMetaData} = '';
  5528. $current{AlbumArtURI} = ReadingsVal($name, 'currentAlbumArtURI', '');
  5529. $current{AlbumArtURL} = ReadingsVal($name, 'currentAlbumArtURL', '');
  5530. $current{Title} = ReadingsVal($name, 'currentTitle', '');
  5531. $current{Artist} = ReadingsVal($name, 'currentArtist', '');
  5532. $current{Album} = ReadingsVal($name, 'currentAlbum', '');
  5533. $current{Source} = ReadingsVal($name, 'currentSource', '');
  5534. $current{OriginalTrackNumber} = ReadingsVal($name, 'currentOriginalTrackNumber', '');
  5535. $current{AlbumArtist} = ReadingsVal($name, 'currentAlbumArtist', '');
  5536. $current{Sender} = ReadingsVal($name, 'currentSender', '');
  5537. $current{SenderCurrent} = ReadingsVal($name, 'currentSenderCurrent', '');
  5538. $current{SenderInfo} = ReadingsVal($name, 'currentSenderInfo', '');
  5539. $current{nextTrackDuration} = ReadingsVal($name, 'nextTrackDuration', '');
  5540. $current{nextTrackDurationSec} = ReadingsVal($name, 'nextTrackDurationSec', '');
  5541. $current{nextTrackURI} = ReadingsVal($name, 'nextTrackURI', '');
  5542. $current{nextTrackHandle} = ReadingsVal($name, 'nextTrackHandle', '');
  5543. $current{nextTrackProvider} = ReadingsVal($name, 'nextTrackProvider', '');
  5544. $current{nextTrackProviderIconQuadraticURL} = ReadingsVal($name, 'nextTrackProviderIconQuadraticURL', '');
  5545. $current{nextTrackProviderIconRoundURL} = ReadingsVal($name, 'nextTrackProviderIconRoundURL', '');
  5546. $current{nextAlbumArtURI} = ReadingsVal($name, 'nextAlbumArtURI', '');
  5547. $current{nextAlbumArtURL} = ReadingsVal($name, 'nextAlbumArtURL', '');
  5548. $current{nextTitle} = ReadingsVal($name, 'nextTitle', '');
  5549. $current{nextArtist} = ReadingsVal($name, 'nextArtist', '');
  5550. $current{nextAlbum} = ReadingsVal($name, 'nextAlbum', '');
  5551. $current{nextAlbumArtist} = ReadingsVal($name, 'nextAlbumArtist', '');
  5552. $current{nextOriginalTrackNumber} = ReadingsVal($name, 'nextOriginalTrackNumber', '');
  5553. $current{InfoSummarize1} = ReadingsVal($name, 'infoSummarize1', '');
  5554. $current{InfoSummarize2} = ReadingsVal($name, 'infoSummarize2', '');
  5555. $current{InfoSummarize3} = ReadingsVal($name, 'infoSummarize3', '');
  5556. $current{InfoSummarize4} = ReadingsVal($name, 'infoSummarize4', '');
  5557. $current{StreamAudio} = ReadingsVal($name, 'currentStreamAudio', 0);
  5558. $current{NormalAudio} = ReadingsVal($name, 'currentNormalAudio', 0);
  5559. }
  5560. # Insert Variables scanned during Device Detection or other events (for simple Replacing-Option of InfoSummarize)
  5561. $current{Volume} = ReadingsVal($name, 'Volume', 0);
  5562. $current{Mute} = ReadingsVal($name, 'Mute', 0);
  5563. $current{OutputFixed} = ReadingsVal($name, 'OutputFixed', 0);
  5564. $current{Balance} = ReadingsVal($name, 'Balance', 0);
  5565. $current{HeadphoneConnected} = ReadingsVal($name, 'HeadphoneConnected', 0);
  5566. $current{SleepTimer} = ReadingsVal($name, 'SleepTimer', '');
  5567. $current{AlarmRunning} = ReadingsVal($name, 'AlarmRunning', '');
  5568. $current{AlarmRunningID} = ReadingsVal($name, 'AlarmRunningID', '');
  5569. $current{DirectControlClientID} = ReadingsVal($name, 'DirectControlClientID', '');
  5570. $current{DirectControlIsSuspended} = ReadingsVal($name, 'DirectControlIsSuspended', '');
  5571. $current{DirectControlAccountID} = ReadingsVal($name, 'DirectControlAccountID', '');
  5572. $current{Presence} = ReadingsVal($name, 'presence', '');
  5573. $current{RoomName} = ReadingsVal($name, 'roomName', '');
  5574. $current{RoomNameAlias} = ReadingsVal($name, 'roomNameAlias', '');
  5575. $current{SaveRoomName} = ReadingsVal($name, 'saveRoomName', '');
  5576. $current{PlayerType} = ReadingsVal($name, 'playerType', '');
  5577. $current{Location} = ReadingsVal($name, 'location', '');
  5578. $current{SoftwareRevision} = ReadingsVal($name, 'softwareRevision', '');
  5579. $current{SerialNum} = ReadingsVal($name, 'serialNum', '');
  5580. $current{ZoneGroupID} = ReadingsVal($name, 'ZoneGroupID', '');
  5581. $current{ZoneGroupName} = ReadingsVal($name, 'ZoneGroupName', '');
  5582. $current{ZonePlayerUUIDsInGroup} = ReadingsVal($name, 'ZonePlayerUUIDsInGroup', '');
  5583. return %current;
  5584. }
  5585. ########################################################################################
  5586. #
  5587. # SONOS_TransportCallback - Transport-Callback,
  5588. #
  5589. # Parameter $service = Service-Representing Object
  5590. # $properties = Properties, that have been changed in this event
  5591. #
  5592. ########################################################################################
  5593. sub SONOS_TransportCallback($$) {
  5594. my ($service, %properties) = @_;
  5595. my $udn = $SONOS_Locations{$service->base};
  5596. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5597. if (!$udn) {
  5598. SONOS_Log undef, 1, 'Transport-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  5599. return;
  5600. }
  5601. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5602. # If the Device is disabled, return here...
  5603. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5604. SONOS_Log $udn, 4, "Transport-Event: device '$name' disabled. No Events/Data will be processed!";
  5605. return;
  5606. }
  5607. SONOS_Log $udn, 3, 'Event: Received Transport-Event for Zone "'.$name.'".';
  5608. # Check if the correct ServiceType
  5609. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AVTransport:1') {
  5610. SONOS_Log $udn, 1, 'Transport-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5611. return;
  5612. }
  5613. # Check if the Variable called LastChange exists
  5614. if (not defined($properties{LastChange})) {
  5615. SONOS_Log $udn, 1, 'Transport-Event receive error: Property \'LastChange\' does not exists!';
  5616. return;
  5617. }
  5618. SONOS_Log $udn, 4, "Transport-Event: All correct with this service-call till now. UDN='uuid:$udn'";
  5619. $SONOS_Client_SendQueue_Suspend = 1;
  5620. my $affectSleeptimer = 0;
  5621. # Determine the base URLs for downloading things from player
  5622. my $groundURL = ($1) if ($service->base =~ m/(http:\/\/.*?:\d+)/i);
  5623. SONOS_Log $udn, 4, "Transport-Event: GroundURL: $groundURL";
  5624. # Variablen initialisieren
  5625. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':1');
  5626. # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden...
  5627. $properties{LastChangeDecoded} = decode_entities($properties{LastChange});
  5628. $properties{LastChangeDecoded} =~ s/[\r\n]//isg; # Komischerweise können hier unmaskierte Newlines auftauchen... wegmachen
  5629. # Verarbeitung starten
  5630. SONOS_Log $udn, 4, 'Transport-Event: LastChange: '.$properties{LastChangeDecoded};
  5631. # Alte Bookmarks aktualisieren, gespeicherte Trackposition bei Bedarf anspringen...
  5632. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  5633. { # Start local area...
  5634. my $bufferedURI = '';
  5635. $bufferedURI = SONOS_GetURIFromQueueValue($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackURI val="(.*?)"\/>/i);
  5636. $bufferedURI =~ s/&apos;/'/gi;
  5637. my $bufferedTrackDuration = 0;
  5638. $bufferedTrackDuration = SONOS_GetTimeSeconds(decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackDuration val="(.*?)"\/>/i);
  5639. my $bufferedTrackPosition = 0;
  5640. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5641. $bufferedTrackPosition = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
  5642. if ($bufferedTrackPosition !~ /\d+:\d+:\d+/i) { # e.g. NOT_IMPLEMENTED
  5643. $bufferedTrackPosition = '0:00:00';
  5644. }
  5645. $bufferedTrackPosition = SONOS_GetTimeSeconds($bufferedTrackPosition);
  5646. }
  5647. if (($SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} ne $bufferedURI) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5648. my $timestamp = scalar(gettimeofday());
  5649. foreach my $gKey (SONOS_getBookmarkGroupKeys('Title', $udn)) {
  5650. if (defined($SONOS_BookmarkTitleHash{$gKey}{$bufferedURI}) && SONOS_getBookmarkTitleIsRelevant($gKey, $timestamp, $bufferedURI, $bufferedTrackPosition, $bufferedTrackDuration)) {
  5651. my $newTrackposition = $SONOS_BookmarkTitleHash{$gKey}{$bufferedURI}{TrackPosition};
  5652. my $result = $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', SONOS_ConvertSecondsToTime($newTrackposition));
  5653. 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});
  5654. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'JumpToTrackPosition "'.SONOS_ConvertSecondsToTime($newTrackposition).'": '.SONOS_UPnPAnswerMessage($result));
  5655. last;
  5656. }
  5657. }
  5658. }
  5659. }
  5660. # Bulkupdate hier starten...
  5661. #SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  5662. # Check, if this is a SleepTimer-Event
  5663. my $sleepTimerVersion = $1 if ($properties{LastChangeDecoded} =~ m/<r:SleepTimerGeneration val="(.*?)"\/>/i);
  5664. if (defined($sleepTimerVersion) && $sleepTimerVersion ne SONOS_Client_Data_Retreive($udn, 'reading', 'SleepTimerVersion', '')) {
  5665. # Variablen neu initialisieren, und die Original-Werte wieder mit reinholen
  5666. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0');
  5667. # Neuer SleepTimer da!
  5668. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5669. my $result = $SONOS_AVTransportControlProxy{$udn}->GetRemainingSleepTimerDuration();
  5670. my $currentValue = $result->getValue('RemainingSleepTimerDuration');
  5671. # Wenn der Timer abgelaufen ist, wird nur ein Leerstring übergeben. Diesen durch das Wort off ersetzen.
  5672. $currentValue = 'off' if (!defined($currentValue) || ($currentValue eq ''));
  5673. SONOS_Client_Notifier('SetCurrent:SleepTimer:'.$currentValue);
  5674. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SleepTimerVersion', ($result->getValue('CurrentSleepTimerGeneration') ? $result->getValue('CurrentSleepTimerGeneration') : ''));
  5675. }
  5676. }
  5677. # Um einen XML-Parser zu vermeiden, werden hier einige reguläre Ausdrücke für die Ermittlung der Werte eingesetzt...
  5678. # Transportstate ermitteln
  5679. if ($properties{LastChangeDecoded} =~ m/<TransportState val="(.*?)"\/>/i) {
  5680. my $currentValue = decode_entities($1);
  5681. # Wenn der TransportState den neuen Wert 'Transitioning' hat, dann diesen auf Playing umsetzen, da das hier ausreicht.
  5682. $currentValue = 'PLAYING' if ($currentValue =~ m/TRANSITIONING/i);
  5683. SONOS_Client_Notifier('SetCurrent:TransportState:'.$currentValue);
  5684. $affectSleeptimer = 1 if (($currentValue ne SONOS_Client_Data_Retreive($udn, 'reading', 'currentTransportState', '')) && ($currentValue ne 'PLAYING'));
  5685. $SONOS_BookmarkSpeicher{OldTransportstate}{$udn} = $currentValue;
  5686. }
  5687. #Wird hier gerade eine DirectPlay-Wiedergabe durchgeführt?
  5688. SONOS_Client_Notifier('SetCurrent:DirectControlClientID:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:DirectControlClientID val="(.*?)"\/>/i);
  5689. SONOS_Client_Notifier('SetCurrent:DirectControlIsSuspended:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:DirectControlIsSuspended val="(.*?)"\/>/i);
  5690. SONOS_Client_Notifier('SetCurrent:DirectControlAccountID:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:DirectControlAccountID val="(.*?)"\/>/i);
  5691. # Wird hier gerade eine Alarm-Abspielung durchgeführt (oder beendet)?
  5692. SONOS_Client_Notifier('SetCurrent:AlarmRunning:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:AlarmRunning val="(.*?)"\/>/i);
  5693. # Wenn ein Alarm läuft, dann zusätzliche Informationen besorgen, ansonsten das entsprechende Reading leeren
  5694. if (defined($1) && $1 eq '1') {
  5695. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5696. my $alarmID = $SONOS_AVTransportControlProxy{$udn}->GetRunningAlarmProperties(0)->getValue('AlarmID');
  5697. SONOS_Client_Notifier('SetCurrent:AlarmRunningID:'.$alarmID);
  5698. }
  5699. } elsif (defined($1) && $1 eq '0') {
  5700. SONOS_Client_Notifier('SetCurrent:AlarmRunningID:');
  5701. }
  5702. my $isStream = 0;
  5703. my $oldMaster = !SONOS_Client_Data_Retreive($udn, 'reading', 'IsMaster', 0);
  5704. # Das nächste nur machen, wenn dieses Event die Track-Informationen auch enthält
  5705. if ($properties{LastChangeDecoded} =~ m/<(AVTransportURI|TransportState) val=".*?"\/>/i) {
  5706. # PlayMode ermitteln
  5707. my $currentPlayMode = 'NORMAL';
  5708. $currentPlayMode = $1 if ($properties{LastChangeDecoded} =~ m/<CurrentPlayMode.*?val="(.*?)".*?\/>/i);
  5709. my ($shuffle, $repeat, $repeatOne) = SONOS_GetShuffleRepeatStates($currentPlayMode);
  5710. SONOS_Client_Notifier('SetCurrent:Shuffle:1') if ($shuffle);
  5711. SONOS_Client_Notifier('SetCurrent:Repeat:1') if ($repeat);
  5712. SONOS_Client_Notifier('SetCurrent:RepeatOne:1') if ($repeatOne);
  5713. # CrossfadeMode ermitteln
  5714. SONOS_Client_Notifier('SetCurrent:CrossfadeMode:'.$1) if ($properties{LastChangeDecoded} =~ m/<CurrentCrossfadeMode.*?val="(\d+)".*?\/>/i);
  5715. # Anzahl Tracknumber ermitteln
  5716. SONOS_Client_Notifier('SetCurrent:NumberOfTracks:'.decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<NumberOfTracks val="(.*?)"\/>/i);
  5717. # Current Tracknumber ermitteln
  5718. if ($properties{LastChangeDecoded} =~ m/<CurrentTrack val="(.*?)"\/>/i) {
  5719. SONOS_Client_Notifier('SetCurrent:Track:'.decode_entities($1));
  5720. # Für die Bookmarkverwaltung ablegen
  5721. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = decode_entities($1);
  5722. }
  5723. # Current TrackURI ermitteln
  5724. my $currentTrackURI = SONOS_GetURIFromQueueValue($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackURI val="(.*?)"\/>/i);
  5725. $currentTrackURI =~ s/&apos;/'/gi;
  5726. SONOS_Client_Notifier('SetCurrent:TrackURI:'.$currentTrackURI);
  5727. # Für die Bookmarkverwaltung ablegen
  5728. $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} = $currentTrackURI;
  5729. my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<r:EnqueuedTransportURIMetaData val="(.*?)"\/>/i);
  5730. # Wenn es ein Spotify-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
  5731. if ($currentTrackURI =~ m/^x-sonos-spotify:/i) {
  5732. SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Spotify:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i);
  5733. }
  5734. # Wenn es ein Napster/Rhapsody-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
  5735. if ($currentTrackURI =~ m/^npsdy:/i) {
  5736. SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Napster:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i);
  5737. }
  5738. # (Wenn möglich) Aktuell abgespielten Favoriten ermitteln...
  5739. my $enqueuedTransportURI = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<r:EnqueuedTransportURI val="(.*?)"\/>/i);
  5740. $enqueuedTransportURI = "" if (!defined($enqueuedTransportURI));
  5741. SONOS_Client_Notifier('SetCurrent:EnqueuedTransportURI:'.decode_entities($enqueuedTransportURI));
  5742. SONOS_Client_Notifier('SetCurrent:EnqueuedTransportHandle:'.decode_entities($enqueuedTransportURI).'|'.$enqueuedTransportMetaData);
  5743. if ($enqueuedTransportMetaData =~ m/<dc:title>(.*?)<\/dc:title>/) {
  5744. SONOS_Log $udn, 5, 'UTF8-Decode-Title1: '.$1;
  5745. my $text = $1;
  5746. eval {
  5747. $text = Encode::decode('UTF-8', $text, Encode::FB_CROAK);
  5748. };
  5749. eval {
  5750. SONOS_Log $udn, 5, 'UTF8-Decode-Title2: '.$text;
  5751. $text = Encode::decode('UTF-8', $text, Encode::FB_CROAK);
  5752. };
  5753. if ($@) {
  5754. SONOS_Log $udn, 5, 'UTF8-Decode: '.$@;
  5755. }
  5756. SONOS_Log $udn, 5, 'UTF8-Decode-Title3: '.$text;
  5757. SONOS_Client_Notifier('SetCurrent:Source:'.Encode::encode('UTF-8', $text));
  5758. }
  5759. # Current Trackdauer ermitteln
  5760. if ($properties{LastChangeDecoded} =~ m/<CurrentTrackDuration val="(.*?)"\/>/i) {
  5761. SONOS_Client_Notifier('SetCurrent:TrackDuration:'.decode_entities($1));
  5762. SONOS_Client_Notifier('SetCurrent:TrackDurationSec:'.SONOS_GetTimeSeconds(decode_entities($1)));
  5763. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = SONOS_GetTimeSeconds(decode_entities($1));
  5764. }
  5765. # Current Track Metadaten ermitteln
  5766. my $currentTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackMetaData val="(.*?)"\/>/is);
  5767. SONOS_Log $udn, 4, 'Transport-Event: CurrentTrackMetaData: '.$currentTrackMetaData;
  5768. SONOS_Client_Notifier('SetCurrent:TrackHandle:'.$currentTrackURI.'|'.$currentTrackMetaData);
  5769. # Cover herunterladen (Infos dazu in den Track Metadaten)
  5770. my $tempURIground = decode_entities($currentTrackMetaData);
  5771. #$tempURIground =~ s/%25/%/ig;
  5772. my $tempURI = '';
  5773. $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  5774. # Wenn in der URI bereits ein kompletter Pfad drinsteht, dann diese Basis verwenden (passiert bei Wiedergabe vom iPad z.B.)
  5775. if ($tempURI =~ m/^(http(s|):\/\/.*?\/)(.*)/) {
  5776. $groundURL = $1;
  5777. $tempURI = $3;
  5778. }
  5779. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.SONOS_URI_Escape($tempURI).':'.SONOS_URI_Escape($groundURL));
  5780. # Auch hier den XML-Parser verhindern, und alles per regulärem Ausdruck ermitteln...
  5781. if ($currentTrackMetaData =~ m/<dc:title>x-(sonosapi|rincon)-(stream|mp3radio):.*?<\/dc:title>/) {
  5782. # Wenn es ein Stream ist, dann muss da was anderes erkannt werden
  5783. SONOS_Log $udn, 4, "Transport-Event: Stream erkannt!";
  5784. SONOS_Client_Notifier('SetCurrent:StreamAudio:1');
  5785. $isStream = 1;
  5786. # Sender ermitteln (per SOAP-Request an den SonosPlayer)
  5787. if ($service->controlProxy()->GetMediaInfo(0)->getValue('CurrentURIMetaData') =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5788. SONOS_Client_Notifier('SetCurrent:Sender:'.$1);
  5789. my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($currentTrackURI, $1);
  5790. SONOS_Log undef, 4, 'Trackprovider for Sender "'.$trackProvider.'" ~ RoundIcon: '.$trackProviderRoundURL.' ~ QuadraticIcon: '.$trackProviderQuadraticURL;
  5791. SONOS_Client_Notifier('SetCurrent:TrackProvider:'.$trackProvider);
  5792. SONOS_Client_Notifier('SetCurrent:TrackProviderIconRoundURL:'.$trackProviderRoundURL);
  5793. SONOS_Client_Notifier('SetCurrent:TrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
  5794. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1;
  5795. }
  5796. # Sender-Läuft ermitteln
  5797. SONOS_Client_Notifier('SetCurrent:SenderCurrent:'.$1) if ($currentTrackMetaData =~ m/<r:radioShowMd>(.*?),p\d{6}<\/r:radioShowMd>/i);
  5798. # Sendungs-Informationen ermitteln
  5799. my $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:streamContent>(.*?)<\/r:streamContent>/i);
  5800. $currentValue = '' if (!defined($currentValue));
  5801. # Wenn hier eine Buffering- oder Connecting-Konstante zurückkommt, dann durch vernünftigen Text ersetzen
  5802. $currentValue = 'Verbindung herstellen...' if ($currentValue eq 'ZPSTR_CONNECTING');
  5803. $currentValue = 'Wird gestartet...' if ($currentValue eq 'ZPSTR_BUFFERING');
  5804. # Wenn hier RTL.it seine Infos liefert, diese zurechtschnippeln...
  5805. $currentValue = '' if ($currentValue eq '<songInfo />');
  5806. if ($currentValue =~ m/<class>Music<\/class>.*?<mus_art_name>(.*?)<\/mus_art_name>/i) {
  5807. $currentValue = $1;
  5808. $currentValue =~ s/\[e\]amp\[p\]/&/ig;
  5809. }
  5810. SONOS_Client_Notifier('SetCurrent:SenderInfo:'.encode_entities($currentValue));
  5811. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = 0;
  5812. } else {
  5813. SONOS_Log $udn, 4, "Transport-Event: Normal erkannt!";
  5814. SONOS_Client_Notifier('SetCurrent:NormalAudio:1');
  5815. my $currentArtist = '';
  5816. my $currentTitle = '';
  5817. if ($currentTrackURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) {
  5818. # Gruppenwiedergabe feststellen, und dann andere Informationen anzeigen
  5819. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5820. SONOS_Client_Notifier('SetCurrent:Title:Gruppenwiedergabe');
  5821. SONOS_Client_Notifier('SetCurrent:Artist:');
  5822. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = 'Gruppenwiedergabe von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5823. } elsif ($currentTrackURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) {
  5824. # LineIn-Wiedergabe feststellen, und dann andere Informationen anzeigen
  5825. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5826. # Fallback
  5827. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5828. if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5829. SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_replaceSpecialStringCharacters(decode_entities($1)));
  5830. $currentTitle = $1;
  5831. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1.' von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5832. }
  5833. SONOS_Client_Notifier('SetCurrent:Artist:');
  5834. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.SONOS_URI_Escape('/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/input_default.jpg').':');
  5835. } elsif ($currentTrackURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) {
  5836. # Dock-Wiedergabe feststellen, und dann andere Informationen anzeigen
  5837. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentAlbum', SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)));
  5838. my $tmpTitle = SONOS_replaceSpecialStringCharacters(decode_entities($1)) if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i);
  5839. $tmpTitle = '' if (!defined($tmpTitle));
  5840. SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentTitle', $tmpTitle));
  5841. $currentTitle = $tmpTitle;
  5842. SONOS_Client_Notifier('SetCurrent:Artist:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentArtist', ''));
  5843. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentTitle', $tmpTitle);
  5844. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.SONOS_URI_Escape('/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/input_dock.jpg').':');
  5845. } elsif ($currentTrackURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) {
  5846. # LineIn-Wiedergabe der Playbar feststellen, und dann andere Informationen anzeigen
  5847. SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1));
  5848. SONOS_Client_Notifier('SetCurrent:Title:SPDIF-Wiedergabe');
  5849. SONOS_Client_Notifier('SetCurrent:Artist:');
  5850. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = 'SPDIF-Wiedergabe von '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
  5851. SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.SONOS_URI_Escape('/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/cover/input_tv.jpg').':');
  5852. } else {
  5853. # Titel ermitteln
  5854. if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i) {
  5855. SONOS_Client_Notifier('SetCurrent:Title:'.$1);
  5856. $currentTitle = $1;
  5857. }
  5858. # Interpret ermitteln
  5859. if ($currentTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i) {
  5860. $currentArtist = decode_entities($1);
  5861. SONOS_Client_Notifier('SetCurrent:Artist:'.encode_entities($currentArtist));
  5862. }
  5863. # Album ermitteln
  5864. SONOS_Client_Notifier('SetCurrent:Album:'.$1) if ($currentTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  5865. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = '('.$currentArtist.') '.$currentTitle;
  5866. }
  5867. my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($currentTrackURI, $currentTitle);
  5868. SONOS_Log undef, 4, 'Trackprovider "'.$trackProvider.'" ~ RoundIcon: '.$trackProviderRoundURL.' ~ QuadraticIcon: '.$trackProviderQuadraticURL;
  5869. SONOS_Client_Notifier('SetCurrent:TrackProvider:'.$trackProvider);
  5870. SONOS_Client_Notifier('SetCurrent:TrackProviderIconRoundURL:'.$trackProviderRoundURL);
  5871. SONOS_Client_Notifier('SetCurrent:TrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
  5872. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1;
  5873. # Original Tracknumber ermitteln
  5874. SONOS_Client_Notifier('SetCurrent:OriginalTrackNumber:'.decode_entities($1)) if ($currentTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  5875. # Album Artist ermitteln
  5876. my $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  5877. $currentValue = $currentArtist if (!defined($currentValue) || ($currentValue eq ''));
  5878. SONOS_Client_Notifier('SetCurrent:AlbumArtist:'.encode_entities($currentValue));
  5879. }
  5880. # Next Track Metadaten ermitteln
  5881. my $nextTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<r:NextTrackMetaData val="(.*?)"\/>/i);
  5882. SONOS_Log $udn, 4, 'Transport-Event: NextTrackMetaData: '.$nextTrackMetaData;
  5883. SONOS_Client_Notifier('SetCurrent:nextTrackDuration:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<res.*?duration="(.*?)".*?>/i);
  5884. SONOS_Client_Notifier('SetCurrent:nextTrackDurationSec:'.SONOS_GetTimeSeconds(decode_entities($1))) if ($nextTrackMetaData =~ m/<res.*?duration="(.*?)".*?>/i);
  5885. if ($properties{LastChangeDecoded} =~ m/<r:NextTrackURI val="(.*?)"\/>/i) {
  5886. my $tmp = SONOS_GetURIFromQueueValue($1);
  5887. $tmp =~ s/&apos;/'/gi;
  5888. SONOS_Client_Notifier('SetCurrent:nextTrackURI:'.$tmp);
  5889. SONOS_Client_Notifier('SetCurrent:nextTrackHandle:'.$tmp.'|'.$nextTrackMetaData);
  5890. my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($tmp);
  5891. SONOS_Log undef, 4, 'NextTrackprovider "'.$trackProvider.'" ~ RoundIcon: '.$trackProviderRoundURL.' ~ QuadraticIcon: '.$trackProviderQuadraticURL;
  5892. SONOS_Client_Notifier('SetCurrent:nextTrackProvider:'.$trackProvider);
  5893. SONOS_Client_Notifier('SetCurrent:nextTrackProviderIconRoundURL:'.$trackProviderRoundURL);
  5894. SONOS_Client_Notifier('SetCurrent:nextTrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
  5895. }
  5896. $tempURIground = decode_entities($nextTrackMetaData);
  5897. #$tempURIground =~ s/%25/%/ig;
  5898. $tempURI = '';
  5899. $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i);
  5900. SONOS_Client_Notifier('ProcessCover:'.$udn.':1:'.SONOS_URI_Escape($tempURI).':'.SONOS_URI_Escape($groundURL));
  5901. SONOS_Client_Notifier('SetCurrent:nextTitle:'.$1) if ($nextTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i);
  5902. SONOS_Client_Notifier('SetCurrent:nextArtist:'.$1) if ($nextTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i);
  5903. SONOS_Client_Notifier('SetCurrent:nextAlbum:'.$1) if ($nextTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i);
  5904. SONOS_Client_Notifier('SetCurrent:nextAlbumArtist:'.$1) if ($nextTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i);
  5905. SONOS_Client_Notifier('SetCurrent:nextOriginalTrackNumber:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i);
  5906. } else {
  5907. SONOS_Log undef, 4, 'No trackinformations found in data: '.$properties{LastChangeDecoded};
  5908. }
  5909. # Current Trackposition ermitteln (durch Abfrage beim Player, bzw. bei Streams statisch)
  5910. if ($isStream) {
  5911. SONOS_Client_Notifier('SetCurrent:TrackPosition:0:00:00');
  5912. SONOS_Client_Notifier('SetCurrent:TrackPositionSec:0');
  5913. } else {
  5914. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5915. my $trackPosition = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
  5916. if ($trackPosition !~ /\d+:\d+:\d+/i) { # e.g. NOT_IMPLEMENTED
  5917. $trackPosition = '0:00:00';
  5918. }
  5919. SONOS_Client_Notifier('SetCurrent:TrackPosition:'.$trackPosition);
  5920. SONOS_Client_Notifier('SetCurrent:TrackPositionSec:'.SONOS_GetTimeSeconds($trackPosition));
  5921. $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn} = SONOS_GetTimeSeconds($trackPosition);
  5922. $SONOS_BookmarkSpeicher{OldTimestamp}{$udn} = scalar(gettimeofday()) - SONOS_GetTimeSeconds($trackPosition);
  5923. }
  5924. }
  5925. # Neue Bookmarks aktualisieren
  5926. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  5927. # Trigger/Transfer the whole bunch and generate InfoSummarize
  5928. SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn);
  5929. # (Etwaige) Neue ZonenTopologie ermitteln und propagieren...
  5930. if (SONOS_CheckProxyObject($udn, $SONOS_ZoneGroupTopologyProxy{$udn})) {
  5931. my $zoneGroupState = $SONOS_ZoneGroupTopologyProxy{$udn}->GetZoneGroupState()->getValue('ZoneGroupState');
  5932. my ($masterPlayerUDN, $masterSlavePlayer) = SONOS_AnalyzeTopologyForFindingMastersSlaves($udnShort, $zoneGroupState);
  5933. my $masterPlayer = SONOS_Client_Data_Retreive($masterPlayerUDN, 'def', 'NAME', '~~~DELETE~~~');
  5934. # Wenn der MasterPlayer unbekannt ist, dann hier überspringen...
  5935. if ($masterPlayer ne '~~~DELETE~~~') {
  5936. SONOS_AnalyzeTopologyForMasterPlayer($zoneGroupState);
  5937. SONOS_Log undef, 5, 'Player: '.$name.' ~ Master: '.$masterPlayer.' ~ Slaves: '.SONOS_Client_Data_Retreive($masterPlayerUDN, 'reading', 'SlavePlayer', '');
  5938. # Wenn alle Player als Sonos-Devices vorhanden und damit bekannt sind...
  5939. if (SONOS_Client_Data_Retreive($masterPlayerUDN, 'reading', 'SlavePlayer', '') !~ /~~~DELETE~~~/) {
  5940. # Masterplayer zum Propagieren beauftragen, wenn es einen echten Master gibt...
  5941. if ($masterPlayer ne $name) {
  5942. # Wir sind gerade der Slaveplayer, dann auch nur uns aktualisieren lassen...
  5943. SONOS_Client_Notifier('PropagateTitleInformationsToSlave:'.$masterPlayer.':'.$name);
  5944. } else {
  5945. # Wir sind gerade der Masterplayer, dann alle Informationen an alle Slaveplayer weiterreichen
  5946. SONOS_Client_Notifier('PropagateTitleInformationsToSlaves:'.$name);
  5947. }
  5948. }
  5949. }
  5950. }
  5951. # Wenn der SleepTimer nach einer Aktion gelöscht werden soll...
  5952. if ($affectSleeptimer && SONOS_Client_Data_Retreive($udn, 'attr', 'stopSleeptimerInAction', 0) && !SONOS_Client_Data_Retreive($udn, 'attr', 'saveSleeptimerInAction', 0)) {
  5953. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  5954. $SONOS_AVTransportControlProxy{$udn}->ConfigureSleepTimer(0, '');
  5955. }
  5956. }
  5957. $SONOS_Client_SendQueue_Suspend = 0;
  5958. SONOS_Log $udn, 3, 'Event: End of Transport-Event for Zone "'.$name.'".';
  5959. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  5960. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  5961. SONOS_Log $udn, 1, "Transport-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  5962. SONOS_RestartControlPoint();
  5963. }
  5964. return 0;
  5965. }
  5966. ########################################################################################
  5967. #
  5968. # SONOS_RenderingCallback - Rendering-Callback,
  5969. #
  5970. # Parameter $service = Service-Representing Object
  5971. # $properties = Properties, that have been changed in this event
  5972. #
  5973. ########################################################################################
  5974. sub SONOS_RenderingCallback($$) {
  5975. my ($service, %properties) = @_;
  5976. my $udn = $SONOS_Locations{$service->base};
  5977. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  5978. if (!$udn) {
  5979. SONOS_Log undef, 1, 'Rendering-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  5980. return;
  5981. }
  5982. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  5983. # If the Device is disabled, return here...
  5984. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  5985. SONOS_Log $udn, 3, "Rendering-Event: device '$name' disabled. No Events/Data will be processed!";
  5986. return;
  5987. }
  5988. SONOS_Log $udn, 3, 'Event: Received Rendering-Event for Zone "'.$name.'".';
  5989. $SONOS_Client_SendQueue_Suspend = 1;
  5990. # Check if the correct ServiceType
  5991. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:RenderingControl:1') {
  5992. SONOS_Log $udn, 1, 'Rendering-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  5993. return;
  5994. }
  5995. # Check if the Variable called LastChange exists
  5996. if (not defined($properties{LastChange})) {
  5997. SONOS_Log $udn, 1, 'Rendering-Event receive error: Property \'LastChange\' does not exists!';
  5998. return;
  5999. }
  6000. SONOS_Log $udn, 4, "Rendering-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6001. # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden...
  6002. $properties{LastChangeDecoded} = decode_entities($properties{LastChange});
  6003. SONOS_Log $udn, 4, 'Rendering-Event: LastChange: '.$properties{LastChangeDecoded};
  6004. my $generateVolumeEvent = SONOS_Client_Data_Retreive($udn, 'attr', 'generateVolumeEvent', 0);
  6005. # Mute?
  6006. my $mute = SONOS_Client_Data_Retreive($udn, 'reading', 'Mute', 0);
  6007. if ($properties{LastChangeDecoded} =~ m/<Mute.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  6008. SONOS_AddToButtonQueue($udn, 'M') if ($1 ne $mute);
  6009. $mute = $1;
  6010. if ($generateVolumeEvent) {
  6011. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Mute', $mute);
  6012. } else {
  6013. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Mute', $mute);
  6014. }
  6015. }
  6016. # Headphone?
  6017. my $headphoneConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'HeadphoneConnected', 0);
  6018. if ($properties{LastChangeDecoded} =~ m/<HeadphoneConnected.*?val="(\d+)".*?\/>/i) {
  6019. SONOS_AddToButtonQueue($udn, 'H') if ($1 ne $headphoneConnected);
  6020. $headphoneConnected = $1;
  6021. if ($generateVolumeEvent) {
  6022. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'HeadphoneConnected', $headphoneConnected);
  6023. } else {
  6024. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'HeadphoneConnected', $headphoneConnected);
  6025. }
  6026. }
  6027. # Balance ermitteln
  6028. my $balance = SONOS_Client_Data_Retreive($udn, 'reading', 'Balance', 0);
  6029. if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="LF".*?val="(\d+)".*?\/>/i) {
  6030. my $volumeLeft = $1;
  6031. my $volumeRight = $1 if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="RF".*?val="(\d+)".*?\/>/i);
  6032. $balance = (-$volumeLeft) + $volumeRight if ($volumeLeft && $volumeRight);
  6033. if ($generateVolumeEvent) {
  6034. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Balance', $balance);
  6035. } else {
  6036. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Balance', $balance);
  6037. }
  6038. }
  6039. # Volume ermitteln
  6040. my $currentVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'Volume', 0);
  6041. if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  6042. SONOS_AddToButtonQueue($udn, 'U') if ($1 > $currentVolume);
  6043. SONOS_AddToButtonQueue($udn, 'D') if ($1 < $currentVolume);
  6044. $currentVolume = $1 ;
  6045. if ($generateVolumeEvent) {
  6046. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Volume', $currentVolume);
  6047. } else {
  6048. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Volume', $currentVolume);
  6049. }
  6050. }
  6051. # Loudness?
  6052. my $loudness = SONOS_Client_Data_Retreive($udn, 'reading', 'Loudness', 0);
  6053. if ($properties{LastChangeDecoded} =~ m/<Loudness.*?channel="Master".*?val="(\d+)".*?\/>/i) {
  6054. $loudness = $1;
  6055. if ($generateVolumeEvent) {
  6056. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Loudness', $loudness);
  6057. } else {
  6058. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Loudness', $loudness);
  6059. }
  6060. }
  6061. # Bass?
  6062. my $bass = SONOS_Client_Data_Retreive($udn, 'reading', 'Bass', 0);
  6063. if ($properties{LastChangeDecoded} =~ m/<Bass.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6064. $bass = $1;
  6065. if ($generateVolumeEvent) {
  6066. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Bass', $bass);
  6067. } else {
  6068. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Bass', $bass);
  6069. }
  6070. }
  6071. # Treble?
  6072. my $treble = SONOS_Client_Data_Retreive($udn, 'reading', 'Treble', 0);
  6073. if ($properties{LastChangeDecoded} =~ m/<Treble.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6074. $treble = $1;
  6075. if ($generateVolumeEvent) {
  6076. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Treble', $treble);
  6077. } else {
  6078. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Treble', $treble);
  6079. }
  6080. }
  6081. # TruePlay?
  6082. my $trueplay = SONOS_Client_Data_Retreive($udn, 'reading', 'TruePlay', 0);
  6083. if ($properties{LastChangeDecoded} =~ m/<SonarEnabled.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6084. $trueplay = $1;
  6085. if ($generateVolumeEvent) {
  6086. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'TruePlay', $trueplay);
  6087. } else {
  6088. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'TruePlay', $trueplay);
  6089. }
  6090. }
  6091. # SurroundEnable?
  6092. my $surroundEnable = SONOS_Client_Data_Retreive($udn, 'reading', 'SurroundEnable', 0);
  6093. if ($properties{LastChangeDecoded} =~ m/<SurroundEnable.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6094. $surroundEnable = $1;
  6095. if ($generateVolumeEvent) {
  6096. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SurroundEnable', $surroundEnable);
  6097. } else {
  6098. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SurroundEnable', $surroundEnable);
  6099. }
  6100. }
  6101. # SurroundLevel?
  6102. my $surroundLevel = SONOS_Client_Data_Retreive($udn, 'reading', 'SurroundLevel', 0);
  6103. if ($properties{LastChangeDecoded} =~ m/<SurroundLevel.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6104. $surroundLevel = $1;
  6105. if ($generateVolumeEvent) {
  6106. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SurroundLevel', $surroundLevel);
  6107. } else {
  6108. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SurroundLevel', $surroundLevel);
  6109. }
  6110. }
  6111. # SubEnable?
  6112. my $subEnable = SONOS_Client_Data_Retreive($udn, 'reading', 'SubEnable', 0);
  6113. if ($properties{LastChangeDecoded} =~ m/<SubEnable.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6114. $subEnable = $1;
  6115. if ($generateVolumeEvent) {
  6116. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubEnable', $subEnable);
  6117. } else {
  6118. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubEnable', $subEnable);
  6119. }
  6120. }
  6121. # SubGain?
  6122. my $subGain = SONOS_Client_Data_Retreive($udn, 'reading', 'SubGain', 0);
  6123. if ($properties{LastChangeDecoded} =~ m/<SubGain.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6124. $subGain = $1;
  6125. if ($generateVolumeEvent) {
  6126. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubGain', $subGain);
  6127. } else {
  6128. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubGain', $subGain);
  6129. }
  6130. }
  6131. # SubPolarity?
  6132. my $subPolarity = SONOS_Client_Data_Retreive($udn, 'reading', 'SubPolarity', 0);
  6133. if ($properties{LastChangeDecoded} =~ m/<SubPolarity.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6134. $subPolarity = $1;
  6135. if ($generateVolumeEvent) {
  6136. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SubPolarity', $subPolarity);
  6137. } else {
  6138. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'SubPolarity', $subPolarity);
  6139. }
  6140. }
  6141. # AudioDelay?
  6142. my $audioDelay = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelay', 0);
  6143. if ($properties{LastChangeDecoded} =~ m/<AudioDelay.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6144. $audioDelay = $1;
  6145. if ($generateVolumeEvent) {
  6146. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelay', $audioDelay);
  6147. } else {
  6148. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelay', $audioDelay);
  6149. }
  6150. }
  6151. # AudioDelayLeftRear?
  6152. my $audioDelayLeftRear = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelayLeftRear', 0);
  6153. if ($properties{LastChangeDecoded} =~ m/<AudioDelayLeftRear.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6154. $audioDelayLeftRear = $1;
  6155. if ($generateVolumeEvent) {
  6156. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelayLeftRear', $audioDelayLeftRear);
  6157. } else {
  6158. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelayLeftRear', $audioDelayLeftRear);
  6159. }
  6160. }
  6161. # AudioDelayRightRear?
  6162. my $audioDelayRightRear = SONOS_Client_Data_Retreive($udn, 'reading', 'AudioDelayRightRear', 0);
  6163. if ($properties{LastChangeDecoded} =~ m/<AudioDelayRightRear.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6164. $audioDelayRightRear = $1;
  6165. if ($generateVolumeEvent) {
  6166. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'AudioDelayRightRear', $audioDelayRightRear);
  6167. } else {
  6168. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'AudioDelayRightRear', $audioDelayRightRear);
  6169. }
  6170. }
  6171. # NightMode?
  6172. my $nightMode = SONOS_Client_Data_Retreive($udn, 'reading', 'NightMode', 0);
  6173. if ($properties{LastChangeDecoded} =~ m/<NightMode.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6174. $nightMode = $1;
  6175. if ($generateVolumeEvent) {
  6176. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'NightMode', $nightMode);
  6177. } else {
  6178. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'NightMode', $nightMode);
  6179. }
  6180. }
  6181. # DialogLevel?
  6182. my $dialogLevel = SONOS_Client_Data_Retreive($udn, 'reading', 'DialogLevel', 0);
  6183. if ($properties{LastChangeDecoded} =~ m/<DialogLevel.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6184. $dialogLevel = $1;
  6185. if ($generateVolumeEvent) {
  6186. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'DialogLevel', $dialogLevel);
  6187. } else {
  6188. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'DialogLevel', $dialogLevel);
  6189. }
  6190. }
  6191. # OutputFixed?
  6192. my $outputFixed = SONOS_Client_Data_Retreive($udn, 'reading', 'OutputFixed', 0);
  6193. if ($properties{LastChangeDecoded} =~ m/<OutputFixed.*?val="([-]{0,1}\d+)".*?\/>/i) {
  6194. $outputFixed = $1;
  6195. if ($generateVolumeEvent) {
  6196. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'OutputFixed', $outputFixed);
  6197. } else {
  6198. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'OutputFixed', $outputFixed);
  6199. }
  6200. }
  6201. SONOS_Log $udn, 4, "Rendering-Event: Current Values for '$name' ~ Volume: $currentVolume, HeadphoneConnected: $headphoneConnected, Bass: $bass, Treble: $treble, Balance: $balance, Loudness: $loudness, Mute: $mute";
  6202. # Ensures the defined volume-borders
  6203. if (SONOS_EnsureMinMaxVolumes($udn)) {
  6204. # Variablen initialisieren
  6205. SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0');
  6206. SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn);
  6207. }
  6208. # ButtonQueue prüfen
  6209. SONOS_CheckButtonQueue($udn);
  6210. $SONOS_Client_SendQueue_Suspend = 0;
  6211. SONOS_Log $udn, 3, 'Event: End of Rendering-Event for Zone "'.$name.'".';
  6212. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6213. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6214. SONOS_Log $udn, 1, "Rendering-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6215. SONOS_RestartControlPoint();
  6216. }
  6217. return 0;
  6218. }
  6219. ########################################################################################
  6220. #
  6221. # SONOS_EnsureMinMaxVolumes - Ensures the defined volume-borders
  6222. #
  6223. ########################################################################################
  6224. sub SONOS_EnsureMinMaxVolumes($) {
  6225. my ($udn) = @_;
  6226. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6227. my $currentVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'Volume', 0);
  6228. my $headphoneConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'HeadphoneConnected', 0);
  6229. my $mute = SONOS_Client_Data_Retreive($udn, 'reading', 'Mute', 0);
  6230. # Grenzen passend zum verwendeten Tonausgang ermitteln
  6231. # Untere Grenze ermitteln
  6232. my $key = 'minVolume'.($headphoneConnected ? 'Headphone' : '');
  6233. my $minVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 0);
  6234. # Obere Grenze ermitteln
  6235. $key = 'maxVolume'.($headphoneConnected ? 'Headphone' : '');
  6236. my $maxVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 100);
  6237. SONOS_Log $udn, 4, "Rendering-Event: Current Borders for '$name' ~ minVolume: $minVolume, maxVolume: $maxVolume";
  6238. # Fehlerhafte Attributangaben?
  6239. if ($minVolume > $maxVolume) {
  6240. SONOS_Log $udn, 0, 'Min-/MaxVolume check Error: MinVolume('.$minVolume.') > MaxVolume('.$maxVolume.'), using Headphones: '.$headphoneConnected.'!';
  6241. return;
  6242. }
  6243. # Prüfungen und Aktualisierungen durchführen
  6244. if (!$mute && ($minVolume > $currentVolume)) {
  6245. # Grenzen prüfen: Zu Leise
  6246. SONOS_Log $udn, 4, 'Volume to Low. Correct it to "'.$minVolume.'"';
  6247. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $minVolume);
  6248. } elsif (!$mute && ($currentVolume > $maxVolume)) {
  6249. # Grenzen prüfen: Zu Laut
  6250. SONOS_Log $udn, 4, 'Volume to High. Correct it to "'.$maxVolume.'"';
  6251. $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $maxVolume);
  6252. } else {
  6253. return 0;
  6254. }
  6255. return 1;
  6256. }
  6257. ########################################################################################
  6258. #
  6259. # SONOS_GroupRenderingCallback - GroupRendering-Callback,
  6260. #
  6261. # Parameter $service = Service-Representing Object
  6262. # $properties = Properties, that have been changed in this event
  6263. #
  6264. ########################################################################################
  6265. sub SONOS_GroupRenderingCallback($$) {
  6266. my ($service, %properties) = @_;
  6267. my $udn = $SONOS_Locations{$service->base};
  6268. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6269. if (!$udn) {
  6270. SONOS_Log undef, 1, 'GroupRendering-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  6271. return;
  6272. }
  6273. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6274. # If the Device is disabled, return here...
  6275. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6276. SONOS_Log $udn, 3, "GroupRendering-Event: device '$name' disabled. No Events/Data will be processed!";
  6277. return;
  6278. }
  6279. SONOS_Log $udn, 3, 'Event: Received GroupRendering-Event for Zone "'.$name.'".';
  6280. $SONOS_Client_SendQueue_Suspend = 1;
  6281. # Check if the correct ServiceType
  6282. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:GroupRenderingControl:1') {
  6283. SONOS_Log $udn, 1, 'GroupRendering-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6284. return;
  6285. }
  6286. SONOS_Log $udn, 4, "GroupRendering-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6287. my $generateVolumeEvent = SONOS_Client_Data_Retreive($udn, 'attr', 'generateVolumeEvent', 0);
  6288. # GroupVolume...
  6289. my $groupVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'GroupVolume', '~~');
  6290. if (defined($properties{GroupVolume}) && ($properties{GroupVolume} ne $groupVolume)) {
  6291. if ($generateVolumeEvent) {
  6292. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'GroupVolume', $properties{GroupVolume});
  6293. } else {
  6294. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'GroupVolume', $properties{GroupVolume});
  6295. }
  6296. }
  6297. # GroupMute...
  6298. my $groupMute = SONOS_Client_Data_Retreive($udn, 'reading', 'GroupMute', '~~');
  6299. if (defined($properties{GroupMute}) && ($properties{GroupMute} ne $groupMute)) {
  6300. if ($generateVolumeEvent) {
  6301. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'GroupMute', $properties{GroupMute});
  6302. } else {
  6303. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'GroupMute', $properties{GroupMute});
  6304. }
  6305. }
  6306. $SONOS_Client_SendQueue_Suspend = 0;
  6307. SONOS_Log $udn, 3, 'Event: End of GroupRendering-Event for Zone "'.$name.'".';
  6308. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6309. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6310. SONOS_Log $udn, 1, "GroupRendering-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6311. SONOS_RestartControlPoint();
  6312. }
  6313. return 0;
  6314. }
  6315. ########################################################################################
  6316. #
  6317. # SONOS_ContentDirectoryCallback - ContentDirectory-Callback,
  6318. #
  6319. # Parameter $service = Service-Representing Object
  6320. # $properties = Properties, that have been changed in this event
  6321. #
  6322. ########################################################################################
  6323. sub SONOS_ContentDirectoryCallback($$) {
  6324. my ($service, %properties) = @_;
  6325. my $udn = $SONOS_Locations{$service->base};
  6326. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6327. if (!$udn) {
  6328. SONOS_Log undef, 1, 'ContentDirectory-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!';
  6329. return;
  6330. }
  6331. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6332. # If the Device is disabled, return here...
  6333. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6334. SONOS_Log $udn, 3, "ContentDirectory-Event: device '$name' disabled. No Events/Data will be processed!";
  6335. return;
  6336. }
  6337. SONOS_Log $udn, 3, 'Event: Received ContentDirectory-Event for Zone "'.$name.'".';
  6338. $SONOS_Client_SendQueue_Suspend = 1;
  6339. # Check if the correct ServiceType
  6340. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:ContentDirectory:1') {
  6341. SONOS_Log $udn, 1, 'ContentDirectory-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6342. return;
  6343. }
  6344. SONOS_Log $udn, 4, "ContentDirectory-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6345. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  6346. #FavoritesUpdateID...
  6347. if (defined($properties{FavoritesUpdateID})) {
  6348. my $containerUpdateIDs = '';
  6349. $containerUpdateIDs = $properties{ContainerUpdateIDs} if ($properties{ContainerUpdateIDs});
  6350. my $favouritesUpdateID = $1 if ($containerUpdateIDs =~ m/FV:2,\d+?/i);
  6351. my $radiosUpdateID = $1 if ($containerUpdateIDs =~ m/R:0,\d+?/i);
  6352. # Wenn beide nicht geliefert wurden, dann beide setzen...
  6353. $containerUpdateIDs = '' if (!defined($favouritesUpdateID) && !defined($radiosUpdateID));
  6354. if (defined($favouritesUpdateID) || ($containerUpdateIDs eq '')) {
  6355. # Wenn eine neue Favoritenversion vorliegt, und eine automatische Aktualisierung gewünscht ist...
  6356. if (($properties{FavoritesUpdateID} ne SONOS_Client_Data_Retreive($udn, 'reading', 'FavouritesVersion', '~~'))
  6357. && SONOS_Client_Data_Retreive('undef', 'attr', 'getFavouritesListAtNewVersion', 0)
  6358. && SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  6359. SONOS_GetBrowseStructuredResult($udn, 'FV:2', 0, 'getFavouritesWithCovers');
  6360. }
  6361. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'FavouritesVersion', $properties{FavoritesUpdateID});
  6362. }
  6363. if (defined($radiosUpdateID) || ($containerUpdateIDs eq '')) {
  6364. # Wenn eine neue Favoritenversion vorliegt, und eine automatische Aktualisierung gewünscht ist...
  6365. if (($properties{FavoritesUpdateID} ne SONOS_Client_Data_Retreive($udn, 'reading', 'RadiosVersion', '~~'))
  6366. && SONOS_Client_Data_Retreive('undef', 'attr', 'getRadiosListAtNewVersion', 0)
  6367. && SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  6368. SONOS_GetBrowseStructuredResult($udn, 'R:0/0', 0, 'getRadiosWithCovers');
  6369. }
  6370. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'RadiosVersion', $properties{FavoritesUpdateID});
  6371. }
  6372. }
  6373. #SavedQueuesUpdateID...
  6374. my $savedQueuesUpdateID = SONOS_Client_Data_Retreive($udn, 'reading', 'PlaylistsVersion', '~~');
  6375. if (defined($properties{SavedQueuesUpdateID}) && ($properties{SavedQueuesUpdateID} ne $savedQueuesUpdateID)) {
  6376. # Wenn eine neue Playlistversion vorliegt, und eine automatische Aktualisierung gewünscht ist...
  6377. if (($properties{SavedQueuesUpdateID} ne SONOS_Client_Data_Retreive($udn, 'reading', 'PlaylistsVersion', '~~'))
  6378. && SONOS_Client_Data_Retreive('undef', 'attr', 'getPlaylistsListAtNewVersion', 0)
  6379. && SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  6380. SONOS_GetBrowseStructuredResult($udn, 'SQ:', 0, 'getPlaylistsWithCovers', 0, 1);
  6381. }
  6382. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'PlaylistsVersion', $properties{SavedQueuesUpdateID});
  6383. }
  6384. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  6385. #QueueUpdateID...
  6386. if (defined($properties{ContainerUpdateIDs})) {
  6387. my $oldVersion = SONOS_Client_Data_Retreive($udn, 'reading', 'QueueVersion', '~~');
  6388. my $newVersion = '';
  6389. $newVersion = $1 if ($properties{ContainerUpdateIDs} =~ m/Q:0,(\d+)/i);
  6390. SONOS_Log $udn, 3, 'ContainerUpdateIDs: '.$properties{ContainerUpdateIDs};
  6391. if ($newVersion && ($oldVersion ne $newVersion)) {
  6392. # Wenn eine neue Queueversion vorliegt, und eine automatische Aktualisierung gewünscht ist...
  6393. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getQueueListAtNewVersion', 0)
  6394. && SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  6395. SONOS_GetQueueStructuredResult($udn, 0, 'getQueueWithCovers');
  6396. }
  6397. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'QueueVersion', $newVersion);
  6398. # Für die Queue-Bookmarkverarbeitung den Queue-Hash neu berechnen und u.U. auf anderen Titel springen...
  6399. SONOS_CalculateQueueHash($udn);
  6400. }
  6401. }
  6402. #ShareIndexInProgress
  6403. my $shareIndexInProgress = SONOS_Client_Data_Retreive('undef', 'reading', 'ShareIndexInProgress', '~~');
  6404. if (defined($properties{ShareIndexInProgress}) && ($properties{ShareIndexInProgress} ne $shareIndexInProgress)) {
  6405. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', 'undef', 'ShareIndexInProgress', $properties{ShareIndexInProgress});
  6406. }
  6407. $SONOS_Client_SendQueue_Suspend = 0;
  6408. SONOS_Log $udn, 3, 'Event: End of ContentDirectory-Event for Zone "'.$name.'".';
  6409. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6410. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6411. SONOS_Log $udn, 1, "ContentDirectory-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6412. SONOS_RestartControlPoint();
  6413. }
  6414. return 0;
  6415. }
  6416. ########################################################################################
  6417. #
  6418. # SONOS_SaveBookmarkValues - Saves the current queue-values for Bookmarks
  6419. #
  6420. ########################################################################################
  6421. sub SONOS_SaveBookmarkValues(;$$) {
  6422. my ($gKey, $type) = @_;
  6423. my $pathname = SONOS_Client_Data_Retreive('undef', 'attr', 'bookmarkSaveDir', '.');
  6424. SONOS_Log undef, 4, 'Calling SONOS_SaveBookmarkValues("'.(defined($gKey) ? $gKey : 'undef').'", "'.(defined($type) ? $type : 'undef').'") ~ SaveDir: "'.$pathname.'"';
  6425. my @types = ();
  6426. if (defined($type) && ($type ne '')) {
  6427. push(@types, $type);
  6428. } else {
  6429. @types = qw(Queue Title);
  6430. }
  6431. foreach my $type (@types) {
  6432. my @groups = ();
  6433. if (defined($gKey) && ($gKey ne '')) {
  6434. push(@groups, $gKey);
  6435. } else {
  6436. my $hashList = \%SONOS_BookmarkTitleHash;
  6437. $hashList = \%SONOS_BookmarkQueueHash if (lc($type) eq 'queue');
  6438. foreach my $group (keys %{$hashList}) {
  6439. push(@groups, $group);
  6440. }
  6441. }
  6442. foreach my $group (@groups) {
  6443. # ReadOnly-Gruppen niemals speichern
  6444. next if ((lc($type) eq 'queue') && defined($SONOS_BookmarkQueueDefinition{$group}{ReadOnly}) && $SONOS_BookmarkQueueDefinition{$group}{ReadOnly});
  6445. next if ((lc($type) ne 'queue') && defined($SONOS_BookmarkTitleDefinition{$group}{ReadOnly}) && $SONOS_BookmarkTitleDefinition{$group}{ReadOnly});
  6446. my $filename;
  6447. $filename = $pathname.'/SONOS_BookmarksPlaylists_'.$group.'.save' if (lc($type) eq 'queue');
  6448. $filename = $pathname.'/SONOS_BookmarksTitles_'.$group.'.save' if (lc($type) ne 'queue');
  6449. my $data;
  6450. $data = $SONOS_BookmarkQueueHash{$group} if (lc($type) eq 'queue');
  6451. $data = $SONOS_BookmarkTitleHash{$group} if (lc($type) ne 'queue');
  6452. if (defined($data)) {
  6453. eval {
  6454. open FILE, '>'.$filename;
  6455. binmode(FILE, ':encoding(utf-8)');
  6456. print FILE SONOS_Dumper($data);
  6457. close FILE;
  6458. SONOS_Log undef, 3, 'Successfully saved '.$type.'-Bookmarks of group "'.$group.'" to file "'.$filename.'"!';
  6459. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'SaveBookmarks: Success!');
  6460. };
  6461. if ($@) {
  6462. SONOS_Log undef, 2, 'Error during saving '.$type.'-Bookmarks of group "'.$group.'" to file "'.$filename.'": '.$@;
  6463. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'SaveBookmarks: Error! '.$@);
  6464. }
  6465. }
  6466. }
  6467. }
  6468. }
  6469. ########################################################################################
  6470. #
  6471. # SONOS_LoadBookmarkValues - Loads the current queue-values for Bookmarks
  6472. #
  6473. ########################################################################################
  6474. sub SONOS_LoadBookmarkValues(;$$) {
  6475. my ($gKey, $type) = @_;
  6476. my $pathname = SONOS_Client_Data_Retreive('undef', 'attr', 'bookmarkSaveDir', '.');
  6477. SONOS_Log undef, 4, 'Calling SONOS_LoadBookmarkValues("'.(defined($gKey) ? $gKey : 'undef').'", "'.(defined($type) ? $type : 'undef').'") ~ SaveDir: "'.$pathname.'"';
  6478. my @types = ();
  6479. if (defined($type) && ($type ne '')) {
  6480. push(@types, $type);
  6481. } else {
  6482. @types = qw(Queue Title);
  6483. }
  6484. foreach my $type (@types) {
  6485. my @groups = ();
  6486. if (defined($gKey) && ($gKey ne '')) {
  6487. push(@groups, $gKey);
  6488. } else {
  6489. my $hashList = \%SONOS_BookmarkTitleDefinition;
  6490. $hashList = \%SONOS_BookmarkQueueDefinition if (lc($type) eq 'queue');
  6491. foreach my $group (keys %{$hashList}) {
  6492. push(@groups, $group);
  6493. }
  6494. }
  6495. foreach my $group (@groups) {
  6496. my $filename;
  6497. $filename = $pathname.'/SONOS_BookmarksPlaylists_'.$group.'.save' if (lc($type) eq 'queue');
  6498. $filename = $pathname.'/SONOS_BookmarksTitles_'.$group.'.save' if (lc($type) ne 'queue');
  6499. eval {
  6500. if (open FILE, '<'.$filename) {
  6501. binmode(FILE, ':encoding(utf-8)');
  6502. my $fileInhalt = '';
  6503. while (<FILE>) {
  6504. $fileInhalt .= $_;
  6505. }
  6506. close FILE;
  6507. $SONOS_BookmarkQueueHash{$group} = eval($fileInhalt) if (lc($type) eq 'queue');
  6508. $SONOS_BookmarkTitleHash{$group} = eval($fileInhalt) if (lc($type) ne 'queue');
  6509. SONOS_Log undef, 3, 'Successfully loaded '.$type.'-Bookmarks of group "'.$group.'" from file "'.$filename.'"!';
  6510. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'LoadBookmarks: Group "'.$group.'" Success!');
  6511. }
  6512. };
  6513. if ($@) {
  6514. SONOS_Log undef, 2, 'Error during loading '.$type.'-Bookmarks of group "'.$group.'" from file "'.$filename.'": '.$@;
  6515. SONOS_MakeSigHandlerReturnValue('undef', 'LastActionResult', 'LoadBookmarks: Group "'.$group.'" Error! '.$@);
  6516. }
  6517. }
  6518. }
  6519. }
  6520. ########################################################################################
  6521. #
  6522. # SONOS_CalculateQueueHash - Calculates the Hash over all Queue members and jumps to the saved position
  6523. #
  6524. ########################################################################################
  6525. sub SONOS_CalculateQueueHash($) {
  6526. my ($udn) = @_;
  6527. SONOS_RefreshCurrentBookmarkQueueValues($udn);
  6528. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  6529. my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', 0, 0, '');
  6530. my $tmp = $result->getValue('Result');
  6531. my $numberReturned = $result->getValue('NumberReturned');
  6532. my $totalMatches = $result->getValue('TotalMatches');
  6533. while ($numberReturned < $totalMatches) {
  6534. $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $numberReturned, 0, '');
  6535. $tmp .= $result->getValue('Result');
  6536. $numberReturned += $result->getValue('NumberReturned');
  6537. $totalMatches = $result->getValue('TotalMatches');
  6538. }
  6539. my $hashIn = $totalMatches.':';
  6540. while ($tmp =~ m/<item id="(.*?)".*?>(.*?)<\/item>/ig) {
  6541. my $item = $2;
  6542. my $uri = $1 if ($item =~ m/<res.*?>(.*?)<\/res>/i);
  6543. $uri =~ s/&apos;/'/gi;
  6544. $hashIn .= $uri.':';
  6545. }
  6546. # Neuen Hashwert berechnen
  6547. my $newHash = md5_hex($hashIn);
  6548. # Werte aktualisieren
  6549. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'QueueHash', $newHash);
  6550. # Aktuellen Track ermitteln...
  6551. my $newTrack = 0;
  6552. if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  6553. $newTrack = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track');
  6554. }
  6555. # Soll was getan werden?
  6556. foreach my $gKey (SONOS_getBookmarkGroupKeys('Queue', $udn)) {
  6557. if (defined($SONOS_BookmarkQueueHash{$gKey}{$newHash}) && SONOS_getBookmarkQueueIsRelevant($gKey, $newHash, scalar(gettimeofday()), $totalMatches)) {
  6558. $newTrack = $SONOS_BookmarkQueueHash{$gKey}{$newHash}{Track};
  6559. # Hier muss jetzt die gespeicherte Position angesprungen werden...
  6560. if (($SONOS_BookmarkSpeicher{OldTracks}{$udn} != $newTrack) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
  6561. my $result = $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', $newTrack);
  6562. 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});
  6563. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'JumpToTrack #'.$newTrack.': '.SONOS_UPnPAnswerMessage($result));
  6564. last; # Nur den ersten gültigen Eintrag suchen/ausführen...
  6565. }
  6566. }
  6567. }
  6568. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = $newTrack;
  6569. $SONOS_BookmarkSpeicher{NumTracks}{$udn} = $totalMatches;
  6570. }
  6571. }
  6572. ########################################################################################
  6573. #
  6574. # SONOS_RefreshCurrentBookmarkQueueValues - Saves the current queue-values for Bookmarks
  6575. #
  6576. ########################################################################################
  6577. sub SONOS_RefreshCurrentBookmarkQueueValues($) {
  6578. my ($udn) = @_;
  6579. # Aktuelle Werte im Speicher sicherstellen...
  6580. $SONOS_BookmarkSpeicher{OldTracks}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTracks}{$udn}));
  6581. $SONOS_BookmarkSpeicher{NumTracks}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{NumTracks}{$udn}));
  6582. $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} = '' if (!defined($SONOS_BookmarkSpeicher{OldTrackURIs}{$udn}));
  6583. $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTrackPositions}{$udn}));
  6584. $SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = 0 if (!defined($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn}));
  6585. $SONOS_BookmarkSpeicher{OldTransportstate}{$udn} = 'STOPPED' if (!defined($SONOS_BookmarkSpeicher{OldTransportstate}{$udn}));
  6586. $SONOS_BookmarkSpeicher{OldTimestamp}{$udn} = scalar(gettimeofday()) if (!defined($SONOS_BookmarkSpeicher{OldTimestamp}{$udn}));
  6587. $SONOS_BookmarkSpeicher{OldTitles}{$udn} = '' if (!defined($SONOS_BookmarkSpeicher{OldTitles}{$udn}));
  6588. # Große Logausgabe fürs debugging...
  6589. SONOS_Log $udn, 5, '___________________________________________________________________________';
  6590. SONOS_Log $udn, 5, 'OldTracks: '.$SONOS_BookmarkSpeicher{OldTracks}{$udn};
  6591. SONOS_Log $udn, 5, 'NumTracks: '.$SONOS_BookmarkSpeicher{NumTracks}{$udn};
  6592. SONOS_Log $udn, 5, 'OldTrackURIs: '.$SONOS_BookmarkSpeicher{OldTrackURIs}{$udn};
  6593. SONOS_Log $udn, 5, 'OldTrackPositions: '.$SONOS_BookmarkSpeicher{OldTrackPositions}{$udn};
  6594. SONOS_Log $udn, 5, 'OldTrackDurations: '.$SONOS_BookmarkSpeicher{OldTrackDurations}{$udn};
  6595. SONOS_Log $udn, 5, 'OldTransportstate: '.$SONOS_BookmarkSpeicher{OldTransportstate}{$udn};
  6596. SONOS_Log $udn, 5, 'OldTimestamp: '.$SONOS_BookmarkSpeicher{OldTimestamp}{$udn};
  6597. SONOS_Log $udn, 5, 'OldTitle: '.$SONOS_BookmarkSpeicher{OldTitles}{$udn};
  6598. # Gemeinsamer Zeitstempel...
  6599. my $timestamp = scalar(gettimeofday());
  6600. # Aktuelle Werte für Title sichern...
  6601. my $trackURI = $SONOS_BookmarkSpeicher{OldTrackURIs}{$udn};
  6602. if ($trackURI) {
  6603. foreach my $gKey (SONOS_getBookmarkGroupKeys('Title', $udn)) {
  6604. next if ($SONOS_BookmarkTitleDefinition{$gKey}{Chapter});
  6605. # Passt der Titel zum RegEx-Filter?
  6606. if ($trackURI !~ m/$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}/) {
  6607. SONOS_Log $udn, 5, 'Skipped Title because of no match to m/'.$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}.'/';
  6608. delete($SONOS_BookmarkTitleHash{$gKey}{$trackURI});
  6609. next;
  6610. }
  6611. # Config-Parameter ausgeben...
  6612. SONOS_Log $udn, 5, 'Match Title! Defined group "'.$gKey.'" ~ RemainingLength: '.$SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength}.' ~ MinTitleLength: '.$SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength};
  6613. # U.u. eine Trackposition berechnen...
  6614. my $trackPosition = $SONOS_BookmarkSpeicher{OldTrackPositions}{$udn};
  6615. $trackPosition = ($timestamp - $SONOS_BookmarkSpeicher{OldTimestamp}{$udn}) if ($SONOS_BookmarkSpeicher{OldTransportstate}{$udn} eq 'PLAYING');
  6616. SONOS_Log $udn, 5, 'Used TrackPosition: '.SONOS_ConvertSecondsToTime($trackPosition);
  6617. # Wenn der Titel bereits im Bereich der RemainingTime ist oder die Mindestgröße unterschreitet, dann aus den Bookmarks löschen
  6618. if (($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} - $trackPosition <= $SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength})
  6619. || ($SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} < $SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength})) {
  6620. delete($SONOS_BookmarkTitleHash{$gKey}{$trackURI});
  6621. next;
  6622. }
  6623. # Sonst die Werte aktualisieren/hinzufügen...
  6624. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{TrackPosition} = $trackPosition;
  6625. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastAccess} = $timestamp;
  6626. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastPlayer} = $udn;
  6627. $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{Title} = $SONOS_BookmarkSpeicher{OldTitles}{$udn}.' - Position 1';
  6628. }
  6629. }
  6630. # Aktuelle Werte für Queue sichern...
  6631. my $listHash = SONOS_Client_Data_Retreive($udn, 'reading', 'QueueHash', '');
  6632. if ($listHash) {
  6633. foreach my $gKey (SONOS_getBookmarkGroupKeys('Queue', $udn)) {
  6634. if (SONOS_getBookmarkQueueIsRelevant($gKey, $listHash, undef, $SONOS_BookmarkSpeicher{NumTracks}{$udn})) {
  6635. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{Track} = $SONOS_BookmarkSpeicher{OldTracks}{$udn};
  6636. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastAccess} = $timestamp;
  6637. $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastPlayer} = $udn;
  6638. $SONOS_BookmarkQueueHash{$gKey}{$trackURI}{Title} = $SONOS_BookmarkSpeicher{OldTitles}{$udn};
  6639. }
  6640. }
  6641. }
  6642. SONOS_Log $udn, 5, '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~';
  6643. }
  6644. ########################################################################################
  6645. #
  6646. # SONOS_getBookmarkQueueRelevant - Decide wether or not this Bookmark is relevant
  6647. #
  6648. ########################################################################################
  6649. sub SONOS_getBookmarkQueueIsRelevant($$$$) {
  6650. my ($gKey, $listHash, $timestamp, $listLength) = @_;
  6651. return ((!defined($timestamp) || ($SONOS_BookmarkQueueDefinition{$gKey}{MaxAge} >= ($timestamp - $SONOS_BookmarkQueueHash{$gKey}{$listHash}{LastAccess})))
  6652. && ($SONOS_BookmarkQueueDefinition{$gKey}{MinListLength} <= $listLength)
  6653. && ($SONOS_BookmarkQueueDefinition{$gKey}{MaxListLength} >= $listLength));
  6654. }
  6655. ########################################################################################
  6656. #
  6657. # SONOS_getBookmarkTitleRelevant - Decide wether or not this Bookmark is relevant
  6658. #
  6659. ########################################################################################
  6660. sub SONOS_getBookmarkTitleIsRelevant($$$$$) {
  6661. my ($gKey, $timestamp, $trackURI, $titlePosition, $titleLength) = @_;
  6662. return 0 if ($SONOS_BookmarkTitleDefinition{$gKey}{Chapter});
  6663. return (($trackURI =~ m/$SONOS_BookmarkTitleDefinition{$gKey}{TrackURIRegEx}/)
  6664. && (!defined($timestamp) || ($SONOS_BookmarkTitleDefinition{$gKey}{MaxAge} >= ($timestamp - $SONOS_BookmarkTitleHash{$gKey}{$trackURI}{LastAccess})))
  6665. && ($SONOS_BookmarkTitleDefinition{$gKey}{MinTitleLength} <= $titleLength)
  6666. && ($SONOS_BookmarkTitleDefinition{$gKey}{RemainingLength} <= ($titleLength - $titlePosition)));
  6667. }
  6668. ########################################################################################
  6669. #
  6670. # SONOS_AddToButtonQueue - Adds the given Event-Name to the ButtonQueue
  6671. #
  6672. ########################################################################################
  6673. sub SONOS_AddToButtonQueue($$) {
  6674. my ($udn, $event) = @_;
  6675. my $data = {Action => uc($event), Time => time()};
  6676. $SONOS_ButtonPressQueue{$udn}->enqueue($data);
  6677. }
  6678. ########################################################################################
  6679. #
  6680. # SONOS_CheckButtonQueue - Checks ButtonQueue and triggers events if neccessary
  6681. #
  6682. ########################################################################################
  6683. sub SONOS_CheckButtonQueue($) {
  6684. my ($udn) = @_;
  6685. my $eventDefinitions = SONOS_Client_Data_Retreive($udn, 'attr', 'buttonEvents', '');
  6686. # Wenn keine Events definiert wurden, dann Queue einfach leeren und zurückkehren...
  6687. # Das beschleunigt die Verarbeitung, da im allgemeinen keine (oder eher wenig) Events definiert werden.
  6688. if (!$eventDefinitions) {
  6689. $SONOS_ButtonPressQueue{$udn}->dequeue_nb(10); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen...
  6690. return;
  6691. }
  6692. my $maxElems = 0;
  6693. while ($eventDefinitions =~ m/(\d+):([MHUD]+)/g) {
  6694. $maxElems = SONOS_Max($maxElems, length($2));
  6695. # Sind überhaupt ausreichend Events in der Queue, das dieses ButtonEvent ausgefüllt sein könnte?
  6696. my $ok = $SONOS_ButtonPressQueue{$udn}->pending() >= length($2);
  6697. # Prüfen, ob alle Events in der Queue der Reihenfolge des ButtonEvents entsprechen
  6698. if ($ok) {
  6699. for (my $i = 0; $i < length($2); $i++) {
  6700. if ($SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2) + $i)->{Action} ne substr($2, $i, 1)) {
  6701. $ok = 0;
  6702. }
  6703. }
  6704. }
  6705. # Wenn die Kette stimmt, dann hier prüfen, ob die Maximalzeit eingehalten wurde, und dann u.U. das Event werfen...
  6706. if ($ok) {
  6707. if (time() - $SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2))->{Time} <= $1) {
  6708. # Event here...
  6709. SONOS_Log $udn, 3, 'Generating ButtonEvent for Zone "'.$udn.'": '.$2.'.';
  6710. SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'ButtonEvent', $2);
  6711. }
  6712. }
  6713. }
  6714. # Einträge, die "zu viele Elemente" her sind, wieder entfernen, da diese sowieso keine Berücksichtigung mehr finden werden
  6715. if ($SONOS_ButtonPressQueue{$udn}->pending() > $maxElems) {
  6716. $SONOS_ButtonPressQueue{$udn}->extract(0, $SONOS_ButtonPressQueue{$udn}->pending() - $maxElems); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen...
  6717. }
  6718. }
  6719. ########################################################################################
  6720. #
  6721. # SONOS_AlarmCallback - Alarm-Callback,
  6722. #
  6723. # Parameter $service = Service-Representing Object
  6724. # $properties = Properties, that have been changed in this event
  6725. #
  6726. ########################################################################################
  6727. sub SONOS_AlarmCallback($$) {
  6728. my ($service, %properties) = @_;
  6729. my $udn = $SONOS_Locations{$service->base};
  6730. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6731. if (!$udn) {
  6732. SONOS_Log undef, 1, 'Alarm-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  6733. return;
  6734. }
  6735. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6736. # If the Device is disabled, return here...
  6737. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6738. SONOS_Log $udn, 3, "Alarm-Event: device '$name' disabled. No Events/Data will be processed!";
  6739. return;
  6740. }
  6741. SONOS_Log $udn, 3, 'Event: Received Alarm-Event for Zone "'.$name.'".';
  6742. $SONOS_Client_SendQueue_Suspend = 1;
  6743. # Check if the correct ServiceType
  6744. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AlarmClock:1') {
  6745. SONOS_Log $udn, 1, 'Alarm-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6746. return;
  6747. }
  6748. # Check if the Variable called AlarmListVersion or DailyIndexRefreshTime exists
  6749. if (!defined($properties{AlarmListVersion}) && !defined($properties{DailyIndexRefreshTime})) {
  6750. return;
  6751. }
  6752. SONOS_Log $udn, 4, "Alarm-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6753. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  6754. # If a new AlarmListVersion is available
  6755. my $alarmListVersion = SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListVersion', '~~');
  6756. if (defined($properties{AlarmListVersion}) && ($properties{AlarmListVersion} ne $alarmListVersion)) {
  6757. SONOS_Log $udn, 4, 'Set new Alarm-Data';
  6758. # Retrieve new AlarmList
  6759. my $result = $SONOS_AlarmClockControlProxy{$udn}->ListAlarms();
  6760. my $currentAlarmList = $result->getValue('CurrentAlarmList');
  6761. my %alarms = ();
  6762. my @alarmIDs = ();
  6763. while ($currentAlarmList =~ m/<Alarm (.*?)\/>/gi) {
  6764. my $alarm = $1;
  6765. # Nur die Alarme, die auch für diesen Raum gelten, reinholen...
  6766. if ($alarm =~ m/RoomUUID="$udnShort"/i) {
  6767. my $id = $1 if ($alarm =~ m/ID="(\d+)"/i);
  6768. SONOS_Log $udn, 5, 'Alarm-Event: Alarm: '.SONOS_Stringify($alarm);
  6769. push @alarmIDs, $id;
  6770. $alarms{$id}{StartTime} = $1 if ($alarm =~ m/StartTime="(.*?)"/i);
  6771. $alarms{$id}{Duration} = $1 if ($alarm =~ m/Duration="(.*?)"/i);
  6772. $alarms{$id}{Recurrence_Once} = 0;
  6773. $alarms{$id}{Recurrence_Monday} = 0;
  6774. $alarms{$id}{Recurrence_Tuesday} = 0;
  6775. $alarms{$id}{Recurrence_Wednesday} = 0;
  6776. $alarms{$id}{Recurrence_Thursday} = 0;
  6777. $alarms{$id}{Recurrence_Friday} = 0;
  6778. $alarms{$id}{Recurrence_Saturday} = 0;
  6779. $alarms{$id}{Recurrence_Sunday} = 0;
  6780. $alarms{$id}{Enabled} = $1 if ($alarm =~ m/Enabled="(.*?)"/i);
  6781. $alarms{$id}{RoomUUID} = $1 if ($alarm =~ m/RoomUUID="(.*?)"/i);
  6782. $alarms{$id}{ProgramURI} = decode_entities($1) if ($alarm =~ m/ProgramURI="(.*?)"/i);
  6783. $alarms{$id}{ProgramMetaData} = decode_entities($1) if ($alarm =~ m/ProgramMetaData="(.*?)"/i);
  6784. $alarms{$id}{Shuffle} = 0;
  6785. $alarms{$id}{Repeat} = 0;
  6786. $alarms{$id}{Volume} = $1 if ($alarm =~ m/Volume="(.*?)"/i);
  6787. $alarms{$id}{IncludeLinkedZones} = $1 if ($alarm =~ m/IncludeLinkedZones="(.*?)"/i);
  6788. # PlayMode ermitteln...
  6789. my $currentPlayMode = 'NORMAL';
  6790. $currentPlayMode = $1 if ($alarm =~ m/PlayMode="(.*?)"/i);
  6791. $alarms{$id}{Shuffle} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'SHUFFLE_NOREPEAT');
  6792. $alarms{$id}{Repeat} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'REPEAT_ALL');
  6793. # Recurrence ermitteln...
  6794. my $currentRecurrence = $1 if ($alarm =~ m/Recurrence="(.*?)"/i);
  6795. $alarms{$id}{Recurrence_Once} = 1 if ($currentRecurrence eq 'ONCE');
  6796. $alarms{$id}{Recurrence_Sunday} = 1 if (($currentRecurrence =~ m/^ON_\d*?0/i) || ($currentRecurrence =~ m/^WEEKENDS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6797. $alarms{$id}{Recurrence_Monday} = 1 if (($currentRecurrence =~ m/^ON_\d*?1/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6798. $alarms{$id}{Recurrence_Tuesday} = 1 if (($currentRecurrence =~ m/^ON_\d*?2/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6799. $alarms{$id}{Recurrence_Wednesday} = 1 if (($currentRecurrence =~ m/^ON_\d*?3/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6800. $alarms{$id}{Recurrence_Thursday} = 1 if (($currentRecurrence =~ m/^ON_\d*?4/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6801. $alarms{$id}{Recurrence_Friday} = 1 if (($currentRecurrence =~ m/^ON_\d*?5/i) || ($currentRecurrence =~ m/^WEEKDAYS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6802. $alarms{$id}{Recurrence_Saturday} = 1 if (($currentRecurrence =~ m/^ON_\d*?6/i) || ($currentRecurrence =~ m/^WEEKENDS/i) || ($currentRecurrence =~ m/^DAILY/i));
  6803. SONOS_Log $udn, 5, 'Alarm-Event: Alarm-Decoded: '.SONOS_Stringify(\%alarms);
  6804. }
  6805. }
  6806. # Sets the approbriate Readings-Value
  6807. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmList', SONOS_Dumper(\%alarms));
  6808. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListIDs', join(',', @alarmIDs));
  6809. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListVersion', $result->getValue('CurrentAlarmListVersion'));
  6810. }
  6811. if (defined($properties{DailyIndexRefreshTime})) {
  6812. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'DailyIndexRefreshTime', $properties{DailyIndexRefreshTime});
  6813. }
  6814. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  6815. $SONOS_Client_SendQueue_Suspend = 0;
  6816. SONOS_Log $udn, 3, 'Event: End of Alarm-Event for Zone "'.$name.'".';
  6817. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6818. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6819. SONOS_Log $udn, 1, "Alarm-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6820. SONOS_RestartControlPoint();
  6821. }
  6822. return 0;
  6823. }
  6824. ########################################################################################
  6825. #
  6826. # SONOS_ZoneGroupTopologyCallback - ZoneGroupTopology-Callback,
  6827. #
  6828. # Parameter $service = Service-Representing Object
  6829. # $properties = Properties, that have been changed in this event
  6830. #
  6831. ########################################################################################
  6832. sub SONOS_ZoneGroupTopologyCallback($$) {
  6833. my ($service, %properties) = @_;
  6834. my $udn = $SONOS_Locations{$service->base};
  6835. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  6836. if (!$udn) {
  6837. SONOS_Log undef, 1, 'ZoneGroupTopology-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  6838. return;
  6839. }
  6840. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  6841. # If the Device is disabled, return here...
  6842. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  6843. SONOS_Log $udn, 3, "ZoneGroupTopology-Event: device '$name' disabled. No Events/Data will be processed!";
  6844. return;
  6845. }
  6846. SONOS_Log $udn, 3, 'Event: Received ZoneGroupTopology-Event for Zone "'.$name.'".';
  6847. $SONOS_Client_SendQueue_Suspend = 1;
  6848. # Check if the correct ServiceType
  6849. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:ZoneGroupTopology:1') {
  6850. SONOS_Log $udn, 1, 'ZoneGroupTopology-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  6851. return;
  6852. }
  6853. SONOS_Log $udn, 4, "ZoneGroupTopology-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  6854. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  6855. # ZoneGroupState: Gesamtkonstellation
  6856. my $zoneGroupState = '';
  6857. if ($properties{ZoneGroupState}) {
  6858. $zoneGroupState = decode_entities($1) if ($properties{ZoneGroupState} =~ m/(.*)/);
  6859. SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', 'undef', 'ZoneGroupState', $zoneGroupState);
  6860. }
  6861. # ZonePlayerUUIDsInGroup: Welche Player befinden sich alle in der gleichen Gruppe wie ich?
  6862. my $zonePlayerUUIDsInGroup = SONOS_Client_Data_Retreive($udn, 'reading', 'ZonePlayerUUIDsInGroup', '');
  6863. if ($properties{ZonePlayerUUIDsInGroup}) {
  6864. $zonePlayerUUIDsInGroup = $properties{ZonePlayerUUIDsInGroup};
  6865. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZonePlayerUUIDsInGroup', $zonePlayerUUIDsInGroup);
  6866. }
  6867. # ZoneGroupID: Welcher Gruppe gehöre ich aktuell an, und hat sich meine Aufgabe innerhalb der Gruppe verändert?
  6868. my $zoneGroupID = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupID', '');
  6869. my $fieldType = SONOS_Client_Data_Retreive($udn, 'reading', 'fieldType', '');
  6870. if ($zoneGroupState =~ m/.*(<ZoneGroup Coordinator="(RINCON_[0-9a-f]+)".*?>).*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) {
  6871. $zoneGroupID = $2;
  6872. my $member = $3;
  6873. my $master = ($zoneGroupID eq $udnShort);
  6874. my $masterPlayerName = SONOS_Client_Data_Retreive($zoneGroupID.'_MR', 'def', 'NAME', $zoneGroupID.'_MR');
  6875. my ($slavePlayerNamesRef, $notBondedSlavePlayerNamesRef) = SONOS_AnalyzeTopologyForSlavePlayer($udnShort, $zoneGroupState);
  6876. my @slavePlayerNames = @{$slavePlayerNamesRef};
  6877. my @slavePlayerNotBondedNames = @{$notBondedSlavePlayerNamesRef};
  6878. $zoneGroupID .= ':__' if ($zoneGroupID !~ m/:/);
  6879. my $topoType = '';
  6880. # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten?
  6881. if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) {
  6882. $topoType = '_'.$1;
  6883. }
  6884. # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten?
  6885. if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) {
  6886. $topoType = '_'.$1;
  6887. $topoType =~ s/,/_/g;
  6888. }
  6889. SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType;
  6890. if ($topoType ne '') {
  6891. $fieldType = substr($topoType, 1);
  6892. } else {
  6893. $fieldType = '';
  6894. }
  6895. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  6896. my $aliasSuffix = '';
  6897. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  6898. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  6899. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  6900. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  6901. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  6902. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  6903. my $roomName = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', '');
  6904. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  6905. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZoneGroupID', $zoneGroupID);
  6906. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'fieldType', $fieldType);
  6907. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsBonded', (($fieldType eq '') || ($fieldType eq 'LF') || ($fieldType eq 'LF_RF')) ? '0' : '1');
  6908. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'IsMaster', $master ? '1' : '0');
  6909. }
  6910. # ZoneGroupName: Welchen Namen hat die aktuelle Gruppe?
  6911. my $zoneGroupName = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupName', '');
  6912. if ($properties{ZoneGroupName}) {
  6913. $zoneGroupName = $properties{ZoneGroupName};
  6914. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZoneGroupName', $zoneGroupName);
  6915. }
  6916. SONOS_AnalyzeTopologyForMasterPlayer($zoneGroupState, $udn);
  6917. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  6918. $SONOS_Client_SendQueue_Suspend = 0;
  6919. SONOS_Log $udn, 3, 'Event: End of ZoneGroupTopology-Event for Zone "'.$name.'".';
  6920. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  6921. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  6922. SONOS_Log $udn, 1, "ZoneGroupTopology-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  6923. SONOS_RestartControlPoint();
  6924. }
  6925. return 0;
  6926. }
  6927. ########################################################################################
  6928. #
  6929. # SONOS_AnalyzeTopologyForSlavePlayer - Topology analysieren, um die Slaveplayer zu
  6930. # einem Masterplayer zu ermitteln
  6931. #
  6932. ########################################################################################
  6933. sub SONOS_AnalyzeTopologyForSlavePlayer($$) {
  6934. my ($masterUDNShort, $zoneGroupState) = @_;
  6935. my @slavePlayer = ();
  6936. my @notBonded = ();
  6937. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  6938. next if ($1 ne $masterUDNShort);
  6939. my $member = $2;
  6940. while ($member =~ m/<ZoneGroupMember(.*?UUID="(.*?)".*?)\/>/gi) {
  6941. next if ($2 eq $masterUDNShort); # Den Master selbst nicht in die Slaveliste reinpacken...
  6942. my $memberUUID = $2;
  6943. # Wenn der Player alleine ist, bzw. der Konstellationsmaster...
  6944. if (!SONOS_Client_Data_Retreive($memberUUID.'_MR', 'reading', 'IsBonded', '')) {
  6945. push(@notBonded, SONOS_Client_Data_Retreive($memberUUID.'_MR', 'def', 'NAME', $memberUUID.'_MR'));
  6946. }
  6947. push @slavePlayer, SONOS_Client_Data_Retreive($memberUUID.'_MR', 'def', 'NAME', $memberUUID.'_MR');
  6948. }
  6949. }
  6950. @slavePlayer = sort @slavePlayer;
  6951. @notBonded = sort @notBonded;
  6952. return (\@slavePlayer, \@notBonded);
  6953. }
  6954. ########################################################################################
  6955. #
  6956. # SONOS_AnalyzeTopologyForFindingMaster - Topology analysieren, um den Master zu einem Player zu ermitteln
  6957. #
  6958. ########################################################################################
  6959. sub SONOS_AnalyzeTopologyForFindingMaster($$) {
  6960. my ($udnShort, $zoneGroupState) = @_;
  6961. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  6962. my $coordinator = $1;
  6963. my $zoneGroup = $2;
  6964. while ($zoneGroup =~ m/<ZoneGroupMember(.*?UUID="(.*?)".*?)\/>/gi) {
  6965. return $coordinator.'_MR' if ($udnShort eq $2);
  6966. }
  6967. }
  6968. return '';
  6969. }
  6970. ########################################################################################
  6971. #
  6972. # SONOS_AnalyzeTopologyForFindingMastersSlaves - Topology analysieren, um die Slaves des Masters zu einem Player zu ermitteln
  6973. #
  6974. ########################################################################################
  6975. sub SONOS_AnalyzeTopologyForFindingMastersSlaves($$) {
  6976. my ($udnShort, $zoneGroupState) = @_;
  6977. my $masterCoordinator = SONOS_AnalyzeTopologyForFindingMaster($udnShort, $zoneGroupState);
  6978. my @masterSlaves = ();
  6979. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  6980. my $coordinator = $1.'_MR';
  6981. my $zoneGroup = $2;
  6982. if ($masterCoordinator eq $coordinator) {
  6983. while ($zoneGroup =~ m/<ZoneGroupMember(.*?UUID="(.*?)".*?)\/>/gi) {
  6984. push(@masterSlaves, $2.'_MR') if ($masterCoordinator ne $2.'_MR');
  6985. }
  6986. }
  6987. }
  6988. return ($masterCoordinator, \@masterSlaves);
  6989. }
  6990. ########################################################################################
  6991. #
  6992. # SONOS_AnalyzeTopologyForMasterPlayer - Topology analysieren, um das Reading "MasterPlayer"
  6993. # sowie die Readings "MasterPlayerPlaying" und
  6994. # "MasterPlayerNotPlaying" zu setzen.
  6995. #
  6996. ########################################################################################
  6997. sub SONOS_AnalyzeTopologyForMasterPlayer($;$) {
  6998. my ($zoneGroupState, $udnCallingPlayer) = @_;
  6999. $udnCallingPlayer = '' if (!defined($udnCallingPlayer));
  7000. return if (!defined($zoneGroupState) || ($zoneGroupState eq ''));
  7001. my @allplayer = ();
  7002. my @playing = ();
  7003. my @notplaying = ();
  7004. my %masterPlayer;
  7005. my %notBonded;
  7006. my %slavePlayer;
  7007. while ($zoneGroupState =~ m/<ZoneGroup.*?Coordinator="(.*?)".*?>(.*?)<\/ZoneGroup>/gi) {
  7008. my $udn = $1.'_MR';
  7009. my $zoneGroup = $2;
  7010. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', '~~~DELETE~~~');
  7011. # Keine Bridge o.ä. verwenden...
  7012. next if ($2 =~ m/IsZoneBridge="1"/i);
  7013. $masterPlayer{$udn} = $udn;
  7014. if ($name ne '~~~DELETE~~~') {
  7015. push(@allplayer, $name) if (!SONOS_isInList($name, @allplayer));
  7016. my $transportState = SONOS_Client_Data_Retreive($udn, 'reading', 'TransportState', '-');
  7017. if ($transportState eq 'PLAYING') {
  7018. push(@playing, $name);
  7019. } else {
  7020. push(@notplaying, $name);
  7021. }
  7022. while ($zoneGroup =~ m/<ZoneGroupMember(.*?UUID="(.*?)".*?)\/>/gi) {
  7023. my $member = $1;
  7024. my $memberUUID = $2.'_MR';
  7025. my $memberName = SONOS_Client_Data_Retreive($memberUUID, 'def', 'NAME', '~~~DELETE~~~');
  7026. push(@allplayer, $memberName) if (!SONOS_isInList($memberName, @allplayer));
  7027. push(@{$slavePlayer{$udn}}, $memberName) if ((!SONOS_isInList($memberName, $slavePlayer{$udn})) && ($memberUUID ne $udn));
  7028. $masterPlayer{$memberUUID} = $udn;
  7029. # Wenn der Player alleine ist, bzw. der Konstellationsmaster...
  7030. if (!SONOS_Client_Data_Retreive($memberUUID, 'reading', 'IsBonded', 0)) {
  7031. push(@{$notBonded{$udn}}, $memberName) if ((!SONOS_isInList($memberName, $notBonded{$udn})) && ($memberUUID ne $udn));
  7032. push(@{$notBonded{x}}, $memberName) if (!SONOS_isInList($memberName, $notBonded{x}));
  7033. }
  7034. }
  7035. }
  7036. }
  7037. # Die Listen normalisieren
  7038. @allplayer = sort @allplayer;
  7039. @playing = sort @playing;
  7040. @notplaying = sort @notplaying;
  7041. SONOS_Client_Notifier('ReadingsBeginUpdate:undef');
  7042. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'AllPlayer', SONOS_Dumper(\@allplayer));
  7043. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'AllPlayerCount', scalar(@allplayer));
  7044. my @list = ();
  7045. @list = sort @{$notBonded{x}} if (defined($notBonded{x}));
  7046. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'AllPlayerNotBonded', SONOS_Dumper(\@list));
  7047. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'AllPlayerNotBondedCount', scalar(@list));
  7048. # LineInPlayer-Listen aktualisieren...
  7049. my @lineInPlayer = grep { SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $_, $_), 'reading', 'LineInConnected', 0) } @allplayer;
  7050. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayer', SONOS_Dumper(\@lineInPlayer));
  7051. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  7052. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayerList', (scalar(@lineInPlayer) ? '-|' : '').join('|', @lineInPlayer));
  7053. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayerListAlias', (scalar(@lineInPlayer) ? 'Auswahl|' : '').join('|', map { my $udn = SONOS_Client_Data_Retreive('udn', 'udn', $_, $_); $_ = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', $udn).' ~ '.SONOS_Client_Data_Retreive($udn, 'reading', 'LineInName', $udn); $_ } @lineInPlayer));
  7054. }
  7055. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerPlaying', SONOS_Dumper(\@playing));
  7056. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerPlayingCount', scalar(@playing));
  7057. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerNotPlaying', SONOS_Dumper(\@notplaying));
  7058. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerNotPlayingCount', scalar(@notplaying));
  7059. push(@playing, @notplaying);
  7060. @playing = sort @playing;
  7061. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayer', SONOS_Dumper(\@playing));
  7062. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MasterPlayerCount', scalar(@playing));
  7063. SONOS_Client_Notifier('ReadingsEndUpdate:undef');
  7064. # SlavePlayerNotBonded (AvailablePlayerList) für jeden (bereits bekannten!) Sonos-Player neu ermitteln...
  7065. foreach my $udn (@{$SONOS_Client_Data{PlayerUDNs}}) {
  7066. my $elem = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', '');
  7067. next if ($elem eq '');
  7068. # Sicherstellen, dass immer ein Masterplayer eingetragen ist...
  7069. $masterPlayer{$udn} = $udn if (!defined($masterPlayer{$udn}));
  7070. my @notBondedPlayer = ();
  7071. if (defined($notBonded{$udn})) {
  7072. @notBondedPlayer = sort @{$notBonded{$udn}};
  7073. }
  7074. my @availablePlayer;
  7075. if (defined($notBonded{x})) {
  7076. @availablePlayer = sort @{$notBonded{x}};
  7077. }
  7078. my @slavePlayer;
  7079. if (defined($slavePlayer{$udn})) {
  7080. @slavePlayer = sort @{$slavePlayer{$udn}};
  7081. }
  7082. my $pos = SONOS_posInList($elem, @availablePlayer);
  7083. splice(@availablePlayer, $pos, 1) if (($pos >= 0) && ($pos < scalar(@availablePlayer)));
  7084. # Die Slaveplayer entfernen...
  7085. foreach my $slaveElem (@slavePlayer) {
  7086. $pos = SONOS_posInList($slaveElem, @availablePlayer);
  7087. splice(@availablePlayer, $pos, 1) if (($pos >= 0) && ($pos < scalar(@availablePlayer)));
  7088. }
  7089. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn) if (($udnCallingPlayer eq '') || ($udnCallingPlayer ne $udn));
  7090. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'MasterPlayer', SONOS_Client_Data_Retreive($masterPlayer{$udn}, 'def', 'NAME', $udn));
  7091. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AvailablePlayer', SONOS_Dumper(\@availablePlayer));
  7092. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  7093. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AvailablePlayerList', (scalar(@availablePlayer) ? '-|' : '').join('|', @availablePlayer));
  7094. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AvailablePlayerListAlias', (scalar(@availablePlayer) ? 'Auswahl|' : '').join('|', map { $_ = SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $_, $_), 'reading', 'roomName', $_); $_ } @availablePlayer));
  7095. }
  7096. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayer', SONOS_Dumper(\@slavePlayer));
  7097. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  7098. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerList', (scalar(@slavePlayer) ? '-|' : '').join('|', @slavePlayer));
  7099. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerListAlias', (scalar(@slavePlayer) ? 'Auswahl|' : '').join('|', map { $_ = SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $_, $_), 'reading', 'roomName', $_); $_ } @slavePlayer));
  7100. }
  7101. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBonded', SONOS_Dumper(\@notBondedPlayer));
  7102. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  7103. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBondedList', (scalar(@notBondedPlayer) ? '-|' : '').join('|', @notBondedPlayer));
  7104. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'SlavePlayerNotBondedListAlias', (scalar(@notBondedPlayer) ? 'Auswahl|' : '').join('|', map { $_ = SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $_, $_), 'reading', 'roomName', $_); $_ } @notBondedPlayer));
  7105. }
  7106. my $zoneGroupNameDetails = '';
  7107. if ($masterPlayer{$udn} ne $udn) {
  7108. $zoneGroupNameDetails = SONOS_Client_Data_Retreive($masterPlayer{$udn}, 'reading', 'roomName', 'k.A.');
  7109. }
  7110. foreach my $slave (@notBondedPlayer) {
  7111. $zoneGroupNameDetails .= ' + '.SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $slave, $slave), 'reading', 'roomName', $slave),
  7112. }
  7113. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ZoneGroupNameDetails', $zoneGroupNameDetails);
  7114. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn) if (($udnCallingPlayer eq '') || ($udnCallingPlayer ne $udn));
  7115. }
  7116. }
  7117. ########################################################################################
  7118. #
  7119. # SONOS_DevicePropertiesCallback - DeviceProperties-Callback,
  7120. #
  7121. # Parameter $service = Service-Representing Object
  7122. # $properties = Properties, that have been changed in this event
  7123. #
  7124. ########################################################################################
  7125. sub SONOS_DevicePropertiesCallback($$) {
  7126. my ($service, %properties) = @_;
  7127. my $udn = $SONOS_Locations{$service->base};
  7128. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  7129. if (!$udn) {
  7130. SONOS_Log undef, 1, 'DeviceProperties-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  7131. return;
  7132. }
  7133. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  7134. # If the Device is disabled, return here...
  7135. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  7136. SONOS_Log $udn, 3, "DeviceProperties-Event: device '$name' disabled. No Events/Data will be processed!";
  7137. return;
  7138. }
  7139. SONOS_Log $udn, 3, 'Event: Received DeviceProperties-Event for Zone "'.$name.'".';
  7140. $SONOS_Client_SendQueue_Suspend = 1;
  7141. # Check if the correct ServiceType
  7142. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:DeviceProperties:1') {
  7143. SONOS_Log $udn, 1, 'DeviceProperties-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  7144. return;
  7145. }
  7146. SONOS_Log $udn, 4, "DeviceProperties-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  7147. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  7148. # Raumname wurde angepasst?
  7149. my $roomName = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', '');
  7150. if (defined($properties{ZoneName}) && $properties{ZoneName} ne '') {
  7151. $roomName = $properties{ZoneName};
  7152. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomName', $roomName);
  7153. my $saveRoomName = decode('UTF-8', $roomName);
  7154. eval {
  7155. use utf8;
  7156. $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden...
  7157. };
  7158. $saveRoomName =~ s/[^a-zA-Z0-9_ ]//g;
  7159. $saveRoomName = SONOS_Trim($saveRoomName);
  7160. $saveRoomName =~ s/ /_/g;
  7161. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName);
  7162. my $topoType = '_'.SONOS_Client_Data_Retreive($udn, 'reading', 'fieldType', '');
  7163. # Für den Aliasnamen schöne Bezeichnungen ermitteln...
  7164. my $aliasSuffix = '';
  7165. $aliasSuffix = ' - Hinten Links' if ($topoType eq '_LR');
  7166. $aliasSuffix = ' - Hinten Rechts' if ($topoType eq '_RR');
  7167. $aliasSuffix = ' - Links' if ($topoType eq '_LF');
  7168. $aliasSuffix = ' - Rechts' if ($topoType eq '_RF');
  7169. $aliasSuffix = ' - Subwoofer' if ($topoType eq '_SW');
  7170. $aliasSuffix = ' - Mitte' if ($topoType eq '_LF_RF');
  7171. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomNameAlias', $roomName.$aliasSuffix);
  7172. }
  7173. # Icon wurde angepasst?
  7174. my $roomIcon = SONOS_Client_Data_Retreive($udn, 'reading', 'roomIcon', '');
  7175. if (defined($properties{Icon}) && $properties{Icon} ne '') {
  7176. $properties{Icon} =~ s/.*?:(.*)/$1/i;
  7177. $roomIcon = $properties{Icon};
  7178. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomIcon', $roomIcon);
  7179. }
  7180. # ButtonState wurde angepasst?
  7181. my $buttonState = SONOS_Client_Data_Retreive($udn, 'reading', 'ButtonState', '');
  7182. if (defined($properties{ButtonState}) && $properties{ButtonState} ne '') {
  7183. $buttonState = $properties{ButtonState};
  7184. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ButtonState', $buttonState);
  7185. }
  7186. # ButtonLockState wurde angepasst?
  7187. my $buttonLockState = SONOS_Client_Data_Retreive($udn, 'reading', 'ButtonLockState', '');
  7188. if (defined($properties{ButtonLockState}) && $properties{ButtonLockState} ne '') {
  7189. $buttonLockState = $properties{ButtonLockState};
  7190. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'ButtonLockState', $buttonLockState);
  7191. }
  7192. # WifiEnabled wurde angepasst?
  7193. my $wifiEnabled = SONOS_Client_Data_Retreive($udn, 'reading', 'WifiEnabled', '');
  7194. if (defined($properties{WifiEnabled}) && $properties{WifiEnabled} ne '') {
  7195. $wifiEnabled = $properties{WifiEnabled};
  7196. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'WifiEnabled', $wifiEnabled);
  7197. }
  7198. # WirelessMode wurde angepasst?
  7199. my $wirelessMode = SONOS_Client_Data_Retreive($udn, 'reading', 'WirelessMode', '');
  7200. if (defined($properties{WirelessMode}) && $properties{WirelessMode} ne '') {
  7201. $wirelessMode = $properties{WirelessMode};
  7202. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'WirelessMode', $wirelessMode);
  7203. }
  7204. # Orientation wurde angepasst?
  7205. my $orientation = SONOS_Client_Data_Retreive($udn, 'reading', 'Orientation', '');
  7206. if (defined($properties{Orientation}) && $properties{Orientation} ne '') {
  7207. $orientation = $properties{Orientation};
  7208. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Orientation', $orientation);
  7209. }
  7210. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  7211. $SONOS_Client_SendQueue_Suspend = 0;
  7212. SONOS_Log $udn, 3, 'Event: End of DeviceProperties-Event for Zone "'.$name.'".';
  7213. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  7214. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  7215. SONOS_Log $udn, 1, "DeviceProperties-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  7216. SONOS_RestartControlPoint();
  7217. }
  7218. return 0;
  7219. }
  7220. ########################################################################################
  7221. #
  7222. # SONOS_AudioInCallback - AudioIn-Callback,
  7223. #
  7224. # Parameter $service = Service-Representing Object
  7225. # $properties = Properties, that have been changed in this event
  7226. #
  7227. ########################################################################################
  7228. sub SONOS_AudioInCallback($$) {
  7229. my ($service, %properties) = @_;
  7230. my $udn = $SONOS_Locations{$service->base};
  7231. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  7232. if (!$udn) {
  7233. SONOS_Log undef, 1, 'AudioIn-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  7234. return;
  7235. }
  7236. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  7237. # If the Device is disabled, return here...
  7238. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  7239. SONOS_Log $udn, 3, "AudioIn-Event: device '$name' disabled. No Events/Data will be processed!";
  7240. return;
  7241. }
  7242. SONOS_Log $udn, 3, 'Event: Received AudioIn-Event for Zone "'.$name.'".';
  7243. $SONOS_Client_SendQueue_Suspend = 1;
  7244. # Check if the correct ServiceType
  7245. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AudioIn:1') {
  7246. SONOS_Log $udn, 1, 'AudioIn-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  7247. return;
  7248. }
  7249. SONOS_Log $udn, 4, "AudioIn-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  7250. SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
  7251. # LineInConnected wurde angepasst?
  7252. my $lineInConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'LineInConnected', '');
  7253. if (defined($properties{LineInConnected}) && $properties{LineInConnected} ne '') {
  7254. $lineInConnected = $properties{LineInConnected};
  7255. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'LineInConnected', $lineInConnected);
  7256. }
  7257. # LineInName wurde angepasst?
  7258. my $lineInName = SONOS_Client_Data_Retreive($udn, 'reading', 'LineInName', '');
  7259. if (defined($properties{AudioInputName}) && $properties{AudioInputName} ne '') {
  7260. $lineInName = $properties{AudioInputName};
  7261. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'LineInName', $lineInName);
  7262. }
  7263. # LineInIcon wurde angepasst?
  7264. my $lineInIcon = SONOS_Client_Data_Retreive($udn, 'reading', 'LineInIcon', '');
  7265. if (defined($properties{Icon}) && $properties{Icon} ne '') {
  7266. $lineInIcon = $properties{Icon};
  7267. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'LineInIcon', $lineInIcon);
  7268. }
  7269. SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
  7270. SONOS_Client_Notifier('ReadingsBeginUpdate:undef');
  7271. # LineInPlayer-Listen aktualisieren...
  7272. my @lineInPlayer = grep { SONOS_Client_Data_Retreive(SONOS_Client_Data_Retreive('udn', 'udn', $_, $_), 'reading', 'LineInConnected', 0) } @{eval(SONOS_Client_Data_Retreive('undef', 'reading', 'AllPlayer', '[]'))};
  7273. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayer', SONOS_Dumper(\@lineInPlayer));
  7274. if (SONOS_Client_Data_Retreive('undef', 'attr', 'getListsDirectlyToReadings', 0)) {
  7275. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayerList', (scalar(@lineInPlayer) ? '-|' : '').join('|', @lineInPlayer));
  7276. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'LineInPlayerListAlias', (scalar(@lineInPlayer) ? 'Auswahl|' : '').join('|', map { my $udn = SONOS_Client_Data_Retreive('udn', 'udn', $_, $_); $_ = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', $udn).' ~ '.SONOS_Client_Data_Retreive($udn, 'reading', 'LineInName', $udn); $_ } @lineInPlayer));
  7277. }
  7278. SONOS_Client_Notifier('ReadingsEndUpdate:undef');
  7279. $SONOS_Client_SendQueue_Suspend = 0;
  7280. SONOS_Log $udn, 3, 'Event: End of AudioIn-Event for Zone "'.$name.'".';
  7281. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  7282. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  7283. SONOS_Log $udn, 1, "AudioIn-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  7284. SONOS_RestartControlPoint();
  7285. }
  7286. return 0;
  7287. }
  7288. ########################################################################################
  7289. #
  7290. # SONOS_MusicServicesCallback - MusicServices-Callback,
  7291. #
  7292. # Parameter $service = Service-Representing Object
  7293. # $properties = Properties, that have been changed in this event
  7294. #
  7295. ########################################################################################
  7296. sub SONOS_MusicServicesCallback($$) {
  7297. my ($service, %properties) = @_;
  7298. my $udn = $SONOS_Locations{$service->base};
  7299. my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
  7300. if (!$udn) {
  7301. SONOS_Log undef, 1, 'MusicServices-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
  7302. return;
  7303. }
  7304. my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  7305. # If the Device is disabled, return here...
  7306. if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
  7307. SONOS_Log $udn, 3, "MusicServices-Event: device '$name' disabled. No Events/Data will be processed!";
  7308. return;
  7309. }
  7310. SONOS_Log $udn, 3, 'Event: Received MusicServices-Event for Zone "'.$name.'".';
  7311. $SONOS_Client_SendQueue_Suspend = 1;
  7312. # Check if the correct ServiceType
  7313. if ($service->serviceType() ne 'urn:schemas-upnp-org:service:MusicServices:1') {
  7314. SONOS_Log $udn, 1, 'MusicServices-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
  7315. return;
  7316. }
  7317. SONOS_Log $udn, 4, "MusicServices-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
  7318. SONOS_Client_Notifier('ReadingsBeginUpdate:undef');
  7319. # ServiceListVersion wurde angepasst?
  7320. my $serviceListVersion = SONOS_Client_Data_Retreive('undef', 'reading', 'MusicServicesListVersion', '');
  7321. if (defined($properties{ServiceListVersion}) && $properties{ServiceListVersion} ne '') {
  7322. if ($serviceListVersion ne $properties{ServiceListVersion}) {
  7323. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MusicServicesListVersion', $properties{ServiceListVersion});
  7324. # Call MusicServiceProxy...
  7325. my $response = $SONOS_MusicServicesProxy{$udn}->ListAvailableServices();
  7326. # ServiceTypes
  7327. my @serviceTypes = split(',', $response->getValue('AvailableServiceTypeList'));
  7328. SONOS_Log undef, 5, 'MusicService-Types: '.join(@serviceTypes, ', ');
  7329. my $servicepos = 0;
  7330. my %musicServices = ();
  7331. my $result = $response->getValue('AvailableServiceDescriptorList');
  7332. SONOS_Log undef, 5, 'MusicService-Call: '.$result;
  7333. while ($result =~ m/<Service.*?Id="(\d+)".*?Name="(.+?)"(.*?)<\/Service>/sgi) {
  7334. my $id = $1;
  7335. my $name = $2;
  7336. my $content = $3;
  7337. # If TuneIn, then jump over
  7338. next if ($name eq 'TuneIn');
  7339. my $serviceType = $serviceTypes[$servicepos++];
  7340. if ($content =~ m/.*?SecureUri="(.+?)".*?Capabilities="(\d+)".*?>.*?(<Strings.*?Uri="(.+?)".*?\/>|).*?<PresentationMap.*?Uri="(.+?)".*?\/>.*?/si) {
  7341. my $smapi = $1;
  7342. my $capabilities = $2;
  7343. my $stringsURL = $4;
  7344. my $presentationMap = $5;
  7345. my $promoString = '';
  7346. if (defined($stringsURL) && ($stringsURL ne '')) {
  7347. my $strings = encode('UTF-8', get($stringsURL));
  7348. if (defined($strings) && ($strings ne '')) {
  7349. $promoString = $1 if ($strings =~ m/<stringtable.*?xml:lang="de-DE">.*?<string.*?stringId="ServicePromo">(.*?)<\/string>.*?<\/stringtable>/si);
  7350. $promoString = $1 if (($promoString eq '') && ($strings =~ m/<stringtable.*?xml:lang="en-US">.*?<string.*?stringId="ServicePromo">(.*?)<\/string>.*?<\/stringtable>/si));
  7351. }
  7352. }
  7353. my $presentationMapData = encode('UTF-8', get($presentationMap));
  7354. if (defined($presentationMapData)) {
  7355. SONOS_Log undef, 5, 'PresentationMap('.$id.' ~ '.$name.' ~ ServiceType: "'.$serviceType.'"): '.$presentationMapData;
  7356. } else {
  7357. SONOS_Log undef, 5, 'PresentationMap('.$id.' ~ '.$name.' ~ ServiceType: "'.$serviceType.'"): undef';
  7358. }
  7359. my ($resolution, $resolutionSubst) = SONOS_ExtractMaxResolution($presentationMapData, 'ArtWorkSizeMap');
  7360. if (!defined($resolution)) {
  7361. ($resolution, $resolutionSubst) = SONOS_ExtractMaxResolution($presentationMapData, 'BrowseIconSizeMap');
  7362. }
  7363. #SONOS_GetMediaMetadata($udn, $id,
  7364. $musicServices{$id}{Name} = $name;
  7365. $musicServices{$id}{ServiceType} = $serviceType;
  7366. $musicServices{$id}{IconQuadraticURL} = 'http://sonos-logo.ws.sonos.com/'.$musicServices{$id}{ServiceType}.'/'.$musicServices{$id}{ServiceType}.'-400x400.png';
  7367. $musicServices{$id}{IconQuadraticURL} = '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/proxy/aa?url='.SONOS_URI_Escape($musicServices{$id}{IconQuadraticURL}) if (SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
  7368. $musicServices{$id}{IconRoundURL} = 'http://sonos-logo.ws.sonos.com/'.$musicServices{$id}{ServiceType}.'/'.$musicServices{$id}{ServiceType}.'-72x72.png';
  7369. $musicServices{$id}{IconRoundURL} = '/'.SONOS_Client_Data_Retreive('undef', 'attr', 'webname', 'fhem').'/sonos/proxy/aa?url='.SONOS_URI_Escape($musicServices{$id}{IconRoundURL}) if (SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
  7370. $musicServices{$id}{SMAPI} = $smapi;
  7371. $musicServices{$id}{Resolution} = $resolution;
  7372. $musicServices{$id}{ResolutionSubstitution} = $resolutionSubst;
  7373. $musicServices{$id}{Capabilities} = $capabilities;
  7374. $promoString =~ s/[\r\n]/ /g;
  7375. $musicServices{$id}{PromoText} = $promoString;
  7376. }
  7377. }
  7378. SONOS_Log undef, 5, 'MusicService-List: '.SONOS_Dumper(\%musicServices);
  7379. SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MusicServicesList', SONOS_Dumper(\%musicServices));
  7380. }
  7381. }
  7382. SONOS_Client_Notifier('ReadingsEndUpdate:undef');
  7383. $SONOS_Client_SendQueue_Suspend = 0;
  7384. SONOS_Log $udn, 3, 'Event: End of MusicServices-Event for Zone "'.$name.'".';
  7385. # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
  7386. if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
  7387. SONOS_Log $udn, 1, "MusicServices-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
  7388. SONOS_RestartControlPoint();
  7389. }
  7390. return 0;
  7391. }
  7392. ########################################################################################
  7393. #
  7394. # SONOS_ExtractMaxResolution - Extracts the available Coversizes
  7395. #
  7396. ########################################################################################
  7397. sub SONOS_ExtractMaxResolution($$) {
  7398. my ($map, $area) = @_;
  7399. return (undef, undef) if (!defined($map));
  7400. my $artworkSizeMap = $1 if ($map =~ m/<PresentationMap type="$area">.*?<Match>.*?<imageSizeMap>(.*?)<\/imageSizeMap>.*?<\/Match>.*?<\/PresentationMap>/is);
  7401. return (undef, undef) if (!defined($artworkSizeMap));
  7402. my @resolutions = ();
  7403. while ($artworkSizeMap =~ m/<sizeEntry size="(\d+)" substitution=".+?".*?\/>.*?/gis) {
  7404. push(@resolutions, $1);
  7405. }
  7406. @resolutions = sort {$b <=> $a} @resolutions;
  7407. my $resolution = $resolutions[0];
  7408. my $resolutionSubst = $1 if ($artworkSizeMap =~ m/<sizeEntry.*?size="$resolution".*?substitution="(.+?)".*?\/>.*?/gis);
  7409. return ($resolution, $resolutionSubst);
  7410. }
  7411. ########################################################################################
  7412. #
  7413. # SONOS_replaceSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal
  7414. #
  7415. # Parameter text = The text, inside that has to be searched and replaced
  7416. #
  7417. ########################################################################################
  7418. sub SONOS_replaceSpecialStringCharacters($) {
  7419. my ($text) = @_;
  7420. $text =~ s/"/'/g;
  7421. return $text;
  7422. }
  7423. ########################################################################################
  7424. #
  7425. # SONOS_maskSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal
  7426. #
  7427. # Parameter text = The text, inside that has to be searched and replaced
  7428. #
  7429. ########################################################################################
  7430. sub SONOS_maskSpecialStringCharacters($) {
  7431. my ($text) = @_;
  7432. $text =~ s/"/\\"/g;
  7433. return $text;
  7434. }
  7435. ########################################################################################
  7436. #
  7437. # SONOS_ProcessInfoSummarize - Process the InfoSummarize-Fields (XML-Alike Structure)
  7438. # Example for Minimal neccesary structure:
  7439. # <NormalAudio></NormalAudio> <StreamAudio></StreamAudio>
  7440. #
  7441. # Complex Example:
  7442. # <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>
  7443. # OR
  7444. # <NormalAudio><TransportState/><InfoSummarize1 prefix=" => "/></NormalAudio> <StreamAudio><TransportState/><InfoSummarize1 prefix=" => "/></StreamAudio>
  7445. #
  7446. # Parameter name = The name of the SonosPlayer-Device
  7447. # current = The Current-Values hashset
  7448. # summarizeVariableName = The variable-name to process (e.g. "InfoSummarize1")
  7449. #
  7450. ########################################################################################
  7451. sub SONOS_ProcessInfoSummarize($$$$) {
  7452. my ($hash, $current, $summarizeVariableName, $bulkUpdate) = @_;
  7453. if (($current->{$summarizeVariableName} = AttrVal($hash->{NAME}, 'generate'.$summarizeVariableName, '')) ne '') {
  7454. # Only pick up the current Audio-Type-Part, if one is available...
  7455. if ($current->{NormalAudio}) {
  7456. $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<NormalAudio>(.*?)<\/NormalAudio>/i);
  7457. } else {
  7458. $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<StreamAudio>(.*?)<\/StreamAudio>/i);
  7459. }
  7460. # Replace placeholder with variables (list defined in 21_SONOSPLAYER ~ stateVariable)
  7461. my $availableVariables = ($2) if (getAllAttr($hash->{NAME}) =~ m/(^|\s+)stateVariable:(.*?)(\s+|$)/);
  7462. foreach (split(/,/, $availableVariables)) {
  7463. $current->{$summarizeVariableName} = SONOS_ReplaceTextToken($current->{$summarizeVariableName}, $_, $current->{$_});
  7464. }
  7465. if ($bulkUpdate) {
  7466. # Enqueue the event
  7467. SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName});
  7468. } else {
  7469. SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName}, 1);
  7470. }
  7471. } else {
  7472. if ($bulkUpdate) {
  7473. # Enqueue the event
  7474. SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), '');
  7475. } else {
  7476. SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), '', 1);
  7477. }
  7478. }
  7479. }
  7480. ########################################################################################
  7481. #
  7482. # SONOS_ReplaceTextToken - Search and replace any occurency of the given tokenName with the value of tokenValue
  7483. #
  7484. # Parameter text = The text, inside that has to be searched and replaced
  7485. # tokenName = The name, that has to be searched for
  7486. # tokenValue = The value, the has to be insert instead of tokenName
  7487. #
  7488. ########################################################################################
  7489. sub SONOS_ReplaceTextToken($$$) {
  7490. my ($text, $tokenName, $tokenValue) = @_;
  7491. # Hier das Token mit Prefix, Suffix, Instead und IfEmpty ersetzen, wenn entsprechend vorhanden
  7492. $text =~ s/<\s*?$tokenName(\s.*?\/|\/)>/SONOS_ReplaceTextTokenRegReplacer($tokenValue, $1)/eig;
  7493. return $text;
  7494. }
  7495. ########################################################################################
  7496. #
  7497. # SONOS_ReplaceTextTokenRegReplacer - Internal procedure for replacing TagValues
  7498. #
  7499. # Parameter tokenValue = The value, the has to be insert instead of tokenName
  7500. # $matcher = The values of the searched and found tag
  7501. #
  7502. ########################################################################################
  7503. sub SONOS_ReplaceTextTokenRegReplacer($$) {
  7504. my ($tokenValue, $matcher) = @_;
  7505. my $emptyVal = SONOS_DealToken($matcher, 'emptyVal', '');
  7506. return SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'prefix', ''), $emptyVal).
  7507. SONOS_ReturnIfEmpty($tokenValue, SONOS_DealToken($matcher, 'ifempty', $emptyVal), $emptyVal).
  7508. SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'instead', $tokenValue), $emptyVal).
  7509. SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'suffix', ''), $emptyVal);
  7510. }
  7511. ########################################################################################
  7512. #
  7513. # SONOS_DealToken - Extracts the content of the given tokenName if exist in checkText
  7514. #
  7515. # Parameter checkText = The text, that has to be search in
  7516. # tokenName = The value, of which the content has to be returned
  7517. #
  7518. ########################################################################################
  7519. sub SONOS_DealToken($$$) {
  7520. my ($checkText, $tokenName, $emptyVal) = @_;
  7521. my $returnText = $1 if($checkText =~ m/$tokenName\s*=\s*"(.*?)"/i);
  7522. return $emptyVal if (not defined($returnText));
  7523. return $returnText;
  7524. }
  7525. ########################################################################################
  7526. #
  7527. # SONOS_ReturnIfEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is* empty
  7528. #
  7529. # Parameter checkText = The text, that has to be checked
  7530. # returnValue = The value, the has to be returned
  7531. #
  7532. ########################################################################################
  7533. sub SONOS_ReturnIfEmpty($$$) {
  7534. my ($checkText, $returnValue, $emptyVal) = @_;
  7535. return '' if not defined($returnValue);
  7536. return $returnValue if ((not defined($checkText)) || $checkText eq $emptyVal);
  7537. return '';
  7538. }
  7539. ########################################################################################
  7540. #
  7541. # SONOS_ReturnIfNotEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is NOT* empty
  7542. #
  7543. # Parameter checkText = The text, that has to be checked
  7544. # returnValue = The value, the has to be returned
  7545. #
  7546. ########################################################################################
  7547. sub SONOS_ReturnIfNotEmpty($$$) {
  7548. my ($checkText, $returnValue, $emptyVal) = @_;
  7549. return '' if not defined($returnValue);
  7550. return $returnValue if (defined($checkText) && $checkText ne $emptyVal);
  7551. return '';
  7552. }
  7553. ########################################################################################
  7554. #
  7555. # SONOS_ImageDownloadTypeExtension - Gives the appropriate extension for the retrieved mimetype of the content of the given url
  7556. #
  7557. # Parameter url = The URL of the content
  7558. #
  7559. ########################################################################################
  7560. sub SONOS_ImageDownloadTypeExtension($) {
  7561. my ($url) = @_;
  7562. # Wenn Spotify, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  7563. if ($url =~ m/x-sonos-spotify/) {
  7564. return 'jpg';
  7565. }
  7566. # Wenn Napster, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  7567. if ($url =~ m/npsdy/) {
  7568. return 'jpg';
  7569. }
  7570. # Wenn Radio, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer GIF
  7571. if ($url =~ m/x-sonosapi-stream/) {
  7572. return 'gif';
  7573. }
  7574. # Wenn Google Music oder Simfy, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG
  7575. if ($url =~ m/x-sonos-http/) {
  7576. return 'jpg';
  7577. }
  7578. # Server abfragen
  7579. my ($content_type, $document_length, $modified_time, $expires, $server);
  7580. eval {
  7581. $SIG{ALRM} = sub { die "Connection Timeout\n" };
  7582. alarm(AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'coverLoadTimeout', $SONOS_DEFAULTCOVERLOADTIMEOUT));
  7583. ($content_type, $document_length, $modified_time, $expires, $server) = head($url);
  7584. alarm(0);
  7585. };
  7586. return 'ERROR' if (!defined($content_type) || ($content_type =~ m/<head>.*?<\/head>/));
  7587. if ($content_type =~ m/png/) {
  7588. return 'png';
  7589. } elsif (($content_type =~ m/jpeg/) || ($content_type =~ m/jpg/)) {
  7590. return 'jpg';
  7591. } elsif ($content_type =~ m/gif/) {
  7592. return 'gif';
  7593. } else {
  7594. $content_type =~ s/\//-/g;
  7595. return $content_type;
  7596. }
  7597. }
  7598. ########################################################################################
  7599. #
  7600. # SONOS_ImageDownloadMimeType - Retrieves the mimetype of the content of the given url
  7601. #
  7602. # Parameter url = The URL of the content
  7603. #
  7604. ########################################################################################
  7605. sub SONOS_ImageDownloadMimeType($) {
  7606. my ($url) = @_;
  7607. eval {
  7608. local $SIG{ALRM} = sub { die "Connection Timeout\n" };
  7609. alarm(AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'coverLoadTimeout', $SONOS_DEFAULTCOVERLOADTIMEOUT));
  7610. my ($content_type, $document_length, $modified_time, $expires, $server) = head($url);
  7611. alarm(0);
  7612. return $content_type;
  7613. };
  7614. return '';
  7615. }
  7616. ########################################################################################
  7617. #
  7618. # SONOS_DownloadReplaceIfChanged - Overwrites the file only if its changed
  7619. #
  7620. # Parameter url = The URL of the new file
  7621. # dest = The local file-uri of the old file
  7622. #
  7623. # Return 1 = New file have been written
  7624. # 0 = nothing happened, because the filecontents are identical or an error has occurred
  7625. #
  7626. ########################################################################################
  7627. sub SONOS_DownloadReplaceIfChanged($$) {
  7628. my ($url, $dest) = @_;
  7629. SONOS_Log undef, 5, 'Call of SONOS_DownloadReplaceIfChanged("'.$url.'", "'.$dest.'")';
  7630. # Be sure URL is absolute
  7631. return 0 if ($url !~ m/^http:\/\//i);
  7632. # Reading new file
  7633. my $newFile = '';
  7634. eval {
  7635. local $SIG{ALRM} = sub { die "Connection Timeout\n" };
  7636. alarm(AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'coverLoadTimeout', $SONOS_DEFAULTCOVERLOADTIMEOUT));
  7637. $newFile = get $url;
  7638. alarm(0);
  7639. if (not defined($newFile)) {
  7640. SONOS_Log undef, 4, 'Couldn\'t retrieve file "'.$url.'" via web. Trying to copy directly...';
  7641. $newFile = SONOS_ReadFile($url);
  7642. if (not defined($newFile)) {
  7643. SONOS_Log undef, 4, 'Couldn\'t even copy file "'.$url.'" directly... exiting...';
  7644. return 0;
  7645. }
  7646. }
  7647. };
  7648. if ($@) {
  7649. SONOS_Log undef, 2, 'Error during SONOS_DownloadReplaceIfChanged("'.$url.'", "'.$dest.'"): '.$@;
  7650. return 0;
  7651. }
  7652. # Wenn keine neue Datei ermittelt wurde, dann abbrechen...
  7653. return 0 if (!defined($newFile) || ($newFile eq ''));
  7654. # Reading old file (if it exists)
  7655. my $oldFile = SONOS_ReadFile($dest);
  7656. $oldFile = '' if (!defined($oldFile));
  7657. # compare those files, and overwrite old file, if it has to be changed
  7658. if ($newFile ne $oldFile) {
  7659. # Hier jetzt alle Dateien dieses Players entfernen, damit nichts überflüssiges rumliegt, falls sich die Endung geändert haben sollte
  7660. if (($dest =~ m/(.*\.).*?/) && ($1 ne '')) {
  7661. unlink(<$1*>);
  7662. }
  7663. # Hier jetzt die neue Datei herunterladen
  7664. SONOS_Log undef, 4, "New filecontent for '$dest'!";
  7665. if (defined(open IMGFILE, '>'.$dest)) {
  7666. binmode IMGFILE ;
  7667. print IMGFILE $newFile;
  7668. close IMGFILE;
  7669. } else {
  7670. SONOS_Log undef, 1, "Error creating file $dest";
  7671. }
  7672. return 1;
  7673. } else {
  7674. SONOS_Log undef, 4, "Identical filecontent for '$dest'!";
  7675. return 0;
  7676. }
  7677. }
  7678. ########################################################################################
  7679. #
  7680. # SONOS_GetRadioMediaMetadata - Read the Radio-Metadata from the Sonos-Webservice
  7681. #
  7682. ########################################################################################
  7683. sub SONOS_GetRadioMediaMetadata($$) {
  7684. my ($udn, $id) = @_;
  7685. my $udnKey = "$1-$2-$3-$4-$5-$6:D" if ($udn =~ m/RINCON_([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})01400(_MR|)/);
  7686. return '' if (!defined($udnKey));
  7687. my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
  7688. my $response = $ua->request(POST 'http://legato.radiotime.com/Radio.asmx', 'content-type' => 'text/xml; charset="utf-8"',
  7689. Content => "<?xml version=\"1.0\" encoding=\"utf-8\"?>
  7690. <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">
  7691. <s:Header>
  7692. <credentials xmlns=\"http://www.sonos.com/Services/1.1\">
  7693. <deviceId>$udnKey</deviceId>
  7694. <deviceProvider>Sonos</deviceProvider>
  7695. </credentials>
  7696. </s:Header>
  7697. <s:Body>
  7698. <getMediaMetadata xmlns=\"http://www.sonos.com/Services/1.1\">
  7699. <id>$id</id>
  7700. </getMediaMetadata>
  7701. </s:Body>
  7702. </s:Envelope>");
  7703. SONOS_Log $udn, 5, 'Radioservice-Metadata: '.$response->content;
  7704. my $title = $1 if ($response->content =~ m/<title>(.*?)<\/title>/i);
  7705. my $genreId = $1 if ($response->content =~ m/<genreId>(.*?)<\/genreId>/i);
  7706. my $genre = $1 if ($response->content =~ m/<genre>(.*?)<\/genre>/i);
  7707. my $bitrate = $1 if ($response->content =~ m/<bitrate>(.*?)<\/bitrate>/i);
  7708. my $logo = $1 if ($response->content =~ m/<logo>(.*?)<\/logo>/i); $logo =~ s/(.*?)q(\..*)/$1g$2/;
  7709. my $subtitle = $1 if ($response->content =~ m/<subtitle>(.*?)<\/subtitle>/i);
  7710. return $logo;
  7711. }
  7712. ########################################################################################
  7713. #
  7714. # SONOS_GetMediaMetadata - Read the Media-Metadata from the Sonos-Webservice
  7715. #
  7716. ########################################################################################
  7717. sub SONOS_GetMediaMetadata($$$) {
  7718. my ($udn, $sid, $id) = @_;
  7719. my $udnKey = "$1-$2-$3-$4-$5-$6:D" if ($udn =~ m/RINCON_([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})01400(_MR|)/);
  7720. # Shorthand to special handling for TuneIn-Artwork
  7721. return SONOS_GetRadioMediaMetadata($udn, $id) if ($sid == 254);
  7722. # Normal Artwork...
  7723. $udn =~ s/RINCON_(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)01400(_MR|)/$1-$2-$3-$4-$5-$6:D/;
  7724. my $musicServicesList = SONOS_Client_Data_Retreive($udn, 'reading', 'MusicServicesList', '()');
  7725. return '' if (!$musicServicesList);
  7726. my %musicService = %{eval($musicServicesList)->{$sid}};
  7727. my $url = $musicService{SMAPI};
  7728. if ($url) {
  7729. my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
  7730. my $response = $ua->request(POST $url, 'content-type' => 'text/xml; charset="utf-8"',
  7731. Content => "<?xml version=\"1.0\" encoding=\"utf-8\"?>
  7732. <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">
  7733. <s:Header>
  7734. <credentials xmlns=\"http://www.sonos.com/Services/1.1\">
  7735. <deviceId>$udnKey</deviceId>
  7736. <deviceProvider>Sonos</deviceProvider>
  7737. </credentials>
  7738. </s:Header>
  7739. <s:Body>
  7740. <getMediaMetadata xmlns=\"http://www.sonos.com/Services/1.1\">
  7741. <id>$id</id>
  7742. </getMediaMetadata>
  7743. </s:Body>
  7744. </s:Envelope>");
  7745. SONOS_Log $udn, 0, 'MediaMetadata: '.$response->content;
  7746. my $title = $1 if ($response->content =~ m/<title>(.*?)<\/title>/i);
  7747. my $genreId = $1 if ($response->content =~ m/<genreId>(.*?)<\/genreId>/i);
  7748. my $genre = $1 if ($response->content =~ m/<genre>(.*?)<\/genre>/i);
  7749. my $bitrate = $1 if ($response->content =~ m/<bitrate>(.*?)<\/bitrate>/i);
  7750. my $logo = $1 if ($response->content =~ m/<logo>(.*?)<\/logo>/i);
  7751. if ($musicService{ResolutionSubstitution}) {
  7752. $logo =~ s//$musicService{ResolutionSubstitution}/;
  7753. }
  7754. my $subtitle = $1 if ($response->content =~ m/<subtitle>(.*?)<\/subtitle>/i);
  7755. return $logo;
  7756. } else {
  7757. return '';
  7758. }
  7759. }
  7760. ########################################################################################
  7761. #
  7762. # SONOS_ReadURL - Read the content of the given URL
  7763. #
  7764. # Parameter $url = The url, that has to be read
  7765. #
  7766. ########################################################################################
  7767. sub SONOS_ReadURL($) {
  7768. my ($url) = @_;
  7769. my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
  7770. my $response = $ua->get($url);
  7771. if ($response->is_success) {
  7772. return $response->content;
  7773. }
  7774. return undef;
  7775. }
  7776. ########################################################################################
  7777. #
  7778. # SONOS_ReadFile - Read the content of the given filename
  7779. #
  7780. # Parameter $fileName = The filename, that has to be read
  7781. #
  7782. ########################################################################################
  7783. sub SONOS_ReadFile($) {
  7784. my ($fileName) = @_;
  7785. if (-e $fileName) {
  7786. my $fileContent = '';
  7787. open IMGFILE, '<'.$fileName;
  7788. binmode IMGFILE;
  7789. while (<IMGFILE>){
  7790. $fileContent .= $_;
  7791. }
  7792. close IMGFILE;
  7793. return $fileContent;
  7794. }
  7795. return undef;
  7796. }
  7797. ########################################################################################
  7798. #
  7799. # SONOS_WriteFile - Write the content to the given filename
  7800. #
  7801. # Parameter $fileName = The filename, that has to be read
  7802. #
  7803. ########################################################################################
  7804. sub SONOS_WriteFile($$) {
  7805. my ($fileName, $data) = @_;
  7806. open IMGFILE, '>'.$fileName;
  7807. binmode IMGFILE;
  7808. print IMGFILE $data;
  7809. close IMGFILE;
  7810. }
  7811. ########################################################################################
  7812. #
  7813. # SONOS_readingsBulkUpdateIfChanged - Wrapper for readingsBulkUpdate. Do only things if value has changed.
  7814. #
  7815. ########################################################################################
  7816. sub SONOS_readingsBulkUpdateIfChanged($$$) {
  7817. my ($hash, $readingName, $readingValue) = @_;
  7818. return if (!defined($hash) || !defined($readingName) || !defined($readingValue));
  7819. readingsBulkUpdate($hash, $readingName, $readingValue) if ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue;
  7820. }
  7821. ########################################################################################
  7822. #
  7823. # SONOS_readingsBeginUpdate - Wrapper for readingsBeginUpdate.
  7824. #
  7825. ########################################################################################
  7826. sub SONOS_readingsBeginUpdate($;$) {
  7827. my ($hash, $fromSubProcess) = @_;
  7828. readingsBeginUpdate($hash);
  7829. if (defined($fromSubProcess)) {
  7830. $SONOS_Module_BulkUpdateFromSubProcessInWork{$hash->{NAME}} = 1;
  7831. SONOS_Log undef, 4, 'ReadingsBeginUpdate from SubProcess for "'.$hash->{NAME}.'"';
  7832. } else {
  7833. SONOS_Log undef, 4, 'ReadingsBeginUpdate from Module for "'.$hash->{NAME}.'"';
  7834. }
  7835. }
  7836. ########################################################################################
  7837. #
  7838. # SONOS_readingsEndUpdate - Wrapper for readingsEndUpdate.
  7839. #
  7840. ########################################################################################
  7841. sub SONOS_readingsEndUpdate($$;$) {
  7842. my ($hash, $doTrigger, $fromSubProcess) = @_;
  7843. if ($SONOS_Module_BulkUpdateFromSubProcessInWork{$hash->{NAME}}) {
  7844. if (defined($fromSubProcess)) {
  7845. readingsEndUpdate($hash, $doTrigger);
  7846. delete($SONOS_Module_BulkUpdateFromSubProcessInWork{$hash->{NAME}});
  7847. SONOS_Log undef, 4, 'ReadingsEndUpdate from SubProcess for "'.$hash->{NAME}.'"';
  7848. } else {
  7849. SONOS_Log undef, 4, 'Supress ReadingsEndUpdate from Module due to running BulkUpdate from SubProcess for "'.$hash->{NAME}.'"';
  7850. }
  7851. } else {
  7852. readingsEndUpdate($hash, $doTrigger);
  7853. delete($SONOS_Module_BulkUpdateFromSubProcessInWork{$hash->{NAME}});
  7854. if (defined($fromSubProcess)) {
  7855. SONOS_Log undef, 4, 'ReadingsEndUpdate from SubProcess for "'.$hash->{NAME}.'"';
  7856. } else {
  7857. SONOS_Log undef, 4, 'ReadingsEndUpdate from Module for "'.$hash->{NAME}.'"';
  7858. }
  7859. }
  7860. }
  7861. ########################################################################################
  7862. #
  7863. # SONOS_readingsSingleUpdate - Wrapper for readingsSingleUpdate.
  7864. #
  7865. ########################################################################################
  7866. sub SONOS_readingsSingleUpdate($$$$) {
  7867. my ($hash, $readingName, $readingValue, $doTrigger) = @_;
  7868. if (defined($hash->{".updateTimestamp"})) {
  7869. readingsBulkUpdate($hash, $readingName, $readingValue);
  7870. } else {
  7871. readingsSingleUpdate($hash, $readingName, $readingValue, $doTrigger);
  7872. }
  7873. }
  7874. ########################################################################################
  7875. #
  7876. # SONOS_readingsSingleUpdateIfChanged - Wrapper for readingsSingleUpdate. Do things only if value has changed.
  7877. #
  7878. ########################################################################################
  7879. sub SONOS_readingsSingleUpdateIfChanged($$$$) {
  7880. my ($hash, $readingName, $readingValue, $doTrigger) = @_;
  7881. if (ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue) {
  7882. if (defined($hash->{".updateTimestamp"})) {
  7883. readingsBulkUpdate($hash, $readingName, $readingValue);
  7884. } else {
  7885. readingsSingleUpdate($hash, $readingName, $readingValue, $doTrigger);
  7886. }
  7887. }
  7888. }
  7889. ########################################################################################
  7890. #
  7891. # SONOS_RefreshIconsInFHEMWEB - Refreshs Iconcache in all FHEMWEB-Instances
  7892. #
  7893. ########################################################################################
  7894. sub SONOS_RefreshIconsInFHEMWEB($) {
  7895. my ($dir) = @_;
  7896. $dir = $attr{global}{modpath}.$dir;
  7897. foreach my $fhem_dev (sort keys %main::defs) {
  7898. if ($main::defs{$fhem_dev}{TYPE} eq 'FHEMWEB') {
  7899. eval('fhem(\'set '.$main::defs{$fhem_dev}{NAME}.' rereadicons\');');
  7900. last; # Die Icon-Liste ist global, muss also nur einmal neu gelesen werden
  7901. }
  7902. }
  7903. }
  7904. ########################################################################################
  7905. #
  7906. # SONOS_getAllSonosplayerDevices - Retreives all available/defined Sonosplayer-Devices
  7907. #
  7908. ########################################################################################
  7909. sub SONOS_getAllSonosplayerDevices() {
  7910. my @devices = ();
  7911. foreach my $fhem_dev (sort keys %main::defs) {
  7912. push @devices, $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER');
  7913. }
  7914. return @devices;
  7915. }
  7916. ########################################################################################
  7917. #
  7918. # SONOS_getSonosPlayerByName - Retrieves the Def-Hash for the SONOS-Device (only one should exists, so this is OK)
  7919. # or, if $devicename is given, the Def-Hash for the SONOSPLAYER with the given name.
  7920. #
  7921. # Parameter $devicename = SONOSPLAYER devicename to be searched for, undef if searching for SONOS instead
  7922. #
  7923. ########################################################################################
  7924. sub SONOS_getSonosPlayerByName(;$) {
  7925. my ($devicename) = @_;
  7926. if (defined($devicename)) {
  7927. foreach my $fhem_dev (keys %main::defs) {
  7928. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{NAME} eq $devicename);
  7929. }
  7930. } else {
  7931. foreach my $fhem_dev (keys %main::defs) {
  7932. next if (!defined($main::defs{$fhem_dev}{TYPE}));
  7933. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOS');
  7934. }
  7935. }
  7936. SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByName' cannot find the FHEM-Device according to '".(defined($devicename) ? $devicename : 'undef')."'. This should not happen!";
  7937. return undef;
  7938. }
  7939. ########################################################################################
  7940. #
  7941. # SONOS_getSonosPlayerByUDN - Retrieves the Def-Hash for the SONOS-Device with the given UDN
  7942. #
  7943. ########################################################################################
  7944. sub SONOS_getSonosPlayerByUDN(;$) {
  7945. my ($udn) = @_;
  7946. if (defined($udn)) {
  7947. foreach my $fhem_dev (keys %main::defs) {
  7948. next if (!defined($main::defs{$fhem_dev}{TYPE}));
  7949. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{UDN} eq $udn);
  7950. }
  7951. } else {
  7952. return SONOS_getSonosPlayerByName();
  7953. }
  7954. SONOS_Log $udn, 0, "The Method 'SONOS_getSonosPlayerByUDN' cannot find the FHEM-Device according to '".(defined($udn) ? $udn : 'undef')."'. This should not happen!";
  7955. return undef;
  7956. }
  7957. ########################################################################################
  7958. #
  7959. # SONOS_getSonosPlayerByRoomName - Retrieves the Def-Hash for the SONOS-Device with the given RoomName
  7960. #
  7961. ########################################################################################
  7962. sub SONOS_getSonosPlayerByRoomName($) {
  7963. my ($roomName) = @_;
  7964. foreach my $fhem_dev (keys %main::defs) {
  7965. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{READINGS}{roomName}{VAL} eq $roomName);
  7966. }
  7967. SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByRoomName' cannot find the FHEM-Device according to '".(defined($roomName) ? $roomName : 'undef')."'. This should not happen!";
  7968. return undef;
  7969. }
  7970. ########################################################################################
  7971. #
  7972. # SONOS_Undef - Implements UndefFn function
  7973. #
  7974. # Parameter hash = hash of the master, name
  7975. #
  7976. ########################################################################################
  7977. sub SONOS_Undef($$) {
  7978. my ($hash, $name) = @_;
  7979. # Alle Timer entfernen...
  7980. RemoveInternalTimer($hash);
  7981. # SubProzess beenden, und Verbindung kappen...
  7982. SONOS_StopSubProcess($hash);
  7983. return undef;
  7984. }
  7985. ########################################################################################
  7986. #
  7987. # SONOS_Delete - Implements DeleteFn function
  7988. #
  7989. # Parameter hash = hash of the master, name
  7990. #
  7991. ########################################################################################
  7992. sub SONOS_Delete($$) {
  7993. my ($hash, $name) = @_;
  7994. # Erst alle SonosPlayer-Devices löschen
  7995. for my $player (SONOS_getAllSonosplayerDevices()) {
  7996. CommandDelete(undef, $player->{NAME});
  7997. }
  7998. # Etwas warten...
  7999. select(undef, undef, undef, 1);
  8000. # Das Entfernen des Sonos-Devices selbst übernimmt Fhem
  8001. return undef;
  8002. }
  8003. ########################################################################################
  8004. #
  8005. # SONOS_Shutdown - Implements ShutdownFn function
  8006. #
  8007. # Parameter hash = hash of the master, name
  8008. #
  8009. ########################################################################################
  8010. sub SONOS_Shutdown ($$) {
  8011. my ($hash) = @_;
  8012. RemoveInternalTimer($hash);
  8013. # Wenn wir einen eigenen UPnP-Server gestartet haben, diesen hier auch wieder beenden,
  8014. # ansonsten nur die Verbindung kappen
  8015. if ($SONOS_StartedOwnUPnPServer) {
  8016. DevIo_SimpleWrite($hash, "shutdown\n", 2);
  8017. } else {
  8018. DevIo_SimpleWrite($hash, "disconnect\n", 2);
  8019. }
  8020. DevIo_CloseDev($hash);
  8021. select(undef, undef, undef, 2);
  8022. return undef;
  8023. }
  8024. ########################################################################################
  8025. #
  8026. # SONOS_posInList - Checks, at which position the given value is in the given list
  8027. # Results in -1 if element not found
  8028. #
  8029. ########################################################################################
  8030. sub SONOS_posInList {
  8031. my($search, @list) = @_;
  8032. $search = '' if (!defined($search));
  8033. for (my $i = 0; $i <= $#list; $i++) {
  8034. return $i if ($list[$i] && $search eq $list[$i]);
  8035. }
  8036. return -1;
  8037. }
  8038. ########################################################################################
  8039. #
  8040. # SONOS_isInList - Checks, if the given value is in the given list
  8041. #
  8042. ########################################################################################
  8043. sub SONOS_isInList {
  8044. my($search, @list) = @_;
  8045. return 1 if SONOS_posInList($search, @list) >= 0;
  8046. return 0;
  8047. }
  8048. ########################################################################################
  8049. #
  8050. # SONOS_Min - Retrieves the minimum of two values
  8051. #
  8052. ########################################################################################
  8053. sub SONOS_Min($$) {
  8054. $_[$_[0] > $_[1]]
  8055. }
  8056. ########################################################################################
  8057. #
  8058. # SONOS_Max - Retrieves the maximum of two values
  8059. #
  8060. ########################################################################################
  8061. sub SONOS_Max($$) {
  8062. $_[$_[0] < $_[1]]
  8063. }
  8064. ########################################################################################
  8065. #
  8066. # SONOS_URI_Escape - Escapes the given string.
  8067. #
  8068. ########################################################################################
  8069. sub SONOS_URI_Escape($) {
  8070. my ($txt) = @_;
  8071. eval {
  8072. $txt = uri_escape($txt);
  8073. };
  8074. if ($@) {
  8075. $txt = uri_escape_utf8($txt);
  8076. };
  8077. return $txt;
  8078. }
  8079. ########################################################################################
  8080. #
  8081. # SONOS_GetRealPath - Retrieves the real (complete and absolute) path of the given file
  8082. # and converts all '\' to '/'
  8083. #
  8084. ########################################################################################
  8085. sub SONOS_GetRealPath($) {
  8086. my ($filename) = @_;
  8087. my $realFilename = realpath($filename);
  8088. $realFilename =~ s/\\/\//g;
  8089. return $realFilename
  8090. }
  8091. ########################################################################################
  8092. #
  8093. # SONOS_GetAbsolutePath - Retreives the absolute path (without filename)
  8094. #
  8095. ########################################################################################
  8096. sub SONOS_GetAbsolutePath($) {
  8097. my ($filename) = @_;
  8098. my $absFilename = SONOS_GetRealPath($filename);
  8099. return substr($absFilename, 0, rindex($absFilename, '/'));
  8100. }
  8101. ########################################################################################
  8102. #
  8103. # SONOS_GetTimeFromString - Parse the given DateTime-String e.g. created by TimeNow().
  8104. #
  8105. ########################################################################################
  8106. sub SONOS_GetTimeFromString($) {
  8107. my ($timeStr) = @_;
  8108. return 0 if (!defined($timeStr));
  8109. eval {
  8110. use Time::Local;
  8111. if($timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})( |_)([0-2]\d):([0-5]\d):([0-5]\d)$/) {
  8112. return timelocal($7, $6, $5, $3, $2 - 1, $1 - 1900);
  8113. }
  8114. }
  8115. }
  8116. ########################################################################################
  8117. #
  8118. # SONOS_GetTimeString - Gets the String for the given time
  8119. #
  8120. ########################################################################################
  8121. sub SONOS_GetTimeString($) {
  8122. my ($time) = @_;
  8123. my @t = localtime($time);
  8124. return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
  8125. }
  8126. ########################################################################################
  8127. #
  8128. # SONOS_TimeNow - Same as FHEM.PL-TimeNow. Neccessary due to forked process...
  8129. #
  8130. ########################################################################################
  8131. sub SONOS_TimeNow() {
  8132. return SONOS_GetTimeString(time());
  8133. }
  8134. ########################################################################################
  8135. #
  8136. # SONOS_Log - Log to the normal Log-command with additional Infomations like Thread-ID and the prefix 'SONOS'
  8137. #
  8138. ########################################################################################
  8139. sub SONOS_Log($$$) {
  8140. my ($udn, $level, $text) = @_;
  8141. if (defined($SONOS_ListenPort)) {
  8142. if ($SONOS_Client_LogLevel >= $level) {
  8143. my ($seconds, $microseconds) = gettimeofday();
  8144. my @t = localtime($seconds);
  8145. my $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d", $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0]);
  8146. if($SONOS_mseclog) {
  8147. $tim .= sprintf(".%03d", $microseconds / 1000);
  8148. }
  8149. if ($SONOS_Client_LogfileName eq '-') {
  8150. print "$tim $level: SONOS".threads->tid().": $text\n";
  8151. } else {
  8152. open(my $fh, '>>', $SONOS_Client_LogfileName);
  8153. print $fh "$tim $level: SONOS".threads->tid().": $text\n";
  8154. close $fh;
  8155. }
  8156. }
  8157. } else {
  8158. my $hash = SONOS_getSonosPlayerByUDN($udn);
  8159. eval {
  8160. Log3 $hash->{NAME}, $level, 'SONOS'.threads->tid().': '.$text;
  8161. };
  8162. if ($@) {
  8163. Log $level, 'SONOS'.threads->tid().': '.$text;
  8164. }
  8165. }
  8166. }
  8167. ########################################################################################
  8168. ########################################################################################
  8169. ##
  8170. ## Start of Telnet-Server-Part for Sonos UPnP-Messages
  8171. ##
  8172. ## If SONOS_ListenPort is defined, then we have to start a listening server
  8173. ##
  8174. ########################################################################################
  8175. ########################################################################################
  8176. # Here starts the main-loop of the telnet-server
  8177. ########################################################################################
  8178. if (defined($SONOS_ListenPort)) {
  8179. $| = 1;
  8180. my $runEndlessLoop = 1;
  8181. my $lastRenewSubscriptionCheckTime = time();
  8182. if ($startedbyfhem) {
  8183. SONOS_Log undef, 1, "$0 is started by fhem...";
  8184. } else {
  8185. SONOS_Log undef, 1, "$0 is started as standalone process...";
  8186. }
  8187. $SIG{'PIPE'} = 'IGNORE';
  8188. $SIG{'CHLD'} = 'IGNORE';
  8189. $SIG{'INT'} = sub {
  8190. # Hauptschleife beenden
  8191. $runEndlessLoop = 0;
  8192. # Sub-Threads beenden, sofern vorhanden
  8193. if (($SONOS_Thread != -1) && defined(threads->object($SONOS_Thread))) {
  8194. threads->object($SONOS_Thread)->kill('INT')->detach();
  8195. }
  8196. if (($SONOS_Thread_LongJobs != -1) && defined(threads->object($SONOS_Thread_LongJobs))) {
  8197. threads->object($SONOS_Thread_LongJobs)->kill('INT')->detach();
  8198. }
  8199. if (($SONOS_Thread_IsAlive != -1) && defined(threads->object($SONOS_Thread_IsAlive))) {
  8200. threads->object($SONOS_Thread_IsAlive)->kill('INT')->detach();
  8201. }
  8202. if (($SONOS_Thread_PlayerRestore != -1) && defined(threads->object($SONOS_Thread_PlayerRestore))) {
  8203. threads->object($SONOS_Thread_PlayerRestore)->kill('INT')->detach();
  8204. }
  8205. };
  8206. my $sock;
  8207. my $retryCounter = 10;
  8208. do {
  8209. eval {
  8210. socket($sock, AF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "Could not create socket: $!";
  8211. bind($sock, sockaddr_in($SONOS_ListenPort, INADDR_ANY)) or die "Bind failed: $!";
  8212. setsockopt($sock, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0)) or die "Setsockopt failed: $!";
  8213. listen($sock, 10);
  8214. };
  8215. if ($@) {
  8216. SONOS_Log undef, 0, "Can't bind Port $SONOS_ListenPort: $@";
  8217. SONOS_Log undef, 0, 'Retries left (wait 30s): '.--$retryCounter;
  8218. if (!$retryCounter) {
  8219. die 'Bind failed...';
  8220. }
  8221. select(undef, undef, undef, 30);
  8222. }
  8223. } while ($@);
  8224. SONOS_Log undef, 1, "$0 is listening to Port $SONOS_ListenPort";
  8225. # Accept incoming connections and talk to clients
  8226. $SONOS_Client_Selector = IO::Select->new($sock);
  8227. while ($runEndlessLoop) {
  8228. # Nachschauen, ob Subscriptions erneuert werden müssen
  8229. if (time() - $lastRenewSubscriptionCheckTime > 1800) {
  8230. $lastRenewSubscriptionCheckTime = time ();
  8231. foreach my $udn (@{$SONOS_Client_Data{PlayerUDNs}}) {
  8232. my %data;
  8233. $data{WorkType} = 'renewSubscription';
  8234. $data{UDN} = $udn;
  8235. my @params = ();
  8236. $data{Params} = \@params;
  8237. $SONOS_Client_ReceiveQueue->enqueue(\%data);
  8238. }
  8239. }
  8240. # Alle Bereit-Schreibenden verarbeiten
  8241. if ($SONOS_Client_SendQueue->pending() && !$SONOS_Client_SendQueue_Suspend) {
  8242. my @receiver = $SONOS_Client_Selector->can_write(0);
  8243. # Prüfen, ob überhaupt ein Empfänger bereit ist. Sonst würden Befehle verloren gehen...
  8244. if (scalar(@receiver) > 0) {
  8245. while ($SONOS_Client_SendQueue->pending()) {
  8246. my $line = $SONOS_Client_SendQueue->dequeue();
  8247. foreach my $so (@receiver) {
  8248. send($so, $line, 0);
  8249. }
  8250. }
  8251. }
  8252. }
  8253. # Alle Bereit-Lesenden verarbeiten
  8254. # Das ganze blockiert eine kurze Zeit, um nicht 100% CPU-Last zu erzeugen
  8255. # Das bedeutet aber auch, dass Sende-Vorgänge um maximal den Timeout-Wert verzögert werden
  8256. my @ready = $SONOS_Client_Selector->can_read(0.1);
  8257. for (my $i = 0; $i < scalar(@ready); $i++) {
  8258. my $so = $ready[$i];
  8259. if ($so == $sock) { # New Connection read
  8260. my $client;
  8261. my $addrinfo = accept($client, $sock);
  8262. setsockopt($client, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0));
  8263. my ($port, $iaddr) = sockaddr_in($addrinfo);
  8264. my $name = gethostbyaddr($iaddr, AF_INET);
  8265. $name = $iaddr if (!defined($name) || $name eq '');
  8266. SONOS_Log undef, 3, "Connection accepted from $name:$port";
  8267. # Send Welcome-Message
  8268. send($client, "This is UPnP-Server listening for commands\r\n", 0);
  8269. select(undef, undef, undef, 0.5);
  8270. # Antwort lesen, und nur wenn es eine dauerhaft gedachte Verbindung ist, dann auch merken...
  8271. my $answer = '';
  8272. recv($client, $answer, 500, 0);
  8273. if ($answer eq "Establish connection\r\n") {
  8274. $SONOS_Client_Selector->add($client);
  8275. SONOS_Log undef, 4, 'A new Connector to list added. There are now '.$SONOS_Client_Selector->count().' connectors in list.';
  8276. }
  8277. } else { # Existing client calling
  8278. if (!$so->opened()) {
  8279. $SONOS_Client_Selector->remove($so);
  8280. last;
  8281. }
  8282. my $inp = <$so>;
  8283. if (defined($inp)) {
  8284. # Abschließende Zeilenumbrüche abschnippeln
  8285. $inp =~ s/[\r\n]*$//;
  8286. # Consume and send evt. reply
  8287. SONOS_Log undef, 5, "Received: '$inp'";
  8288. SONOS_Client_ConsumeMessage($so, $inp);
  8289. } else {
  8290. # Wenn es der letzte war, dann Threads beenden, sonst nur aus der Liste werfen...
  8291. # Größer Zwei, weil der Listener und der aktuell verstorbene Connector enthalten sind...
  8292. if ($SONOS_Client_Selector->count() > 2) {
  8293. SONOS_Log undef, 1, "A Listener seems to be died, but other Listeners exists... leaving Threads alive...";
  8294. $SONOS_Client_Selector->remove($so);
  8295. } else {
  8296. if ($startedbyfhem) {
  8297. SONOS_Log undef, 1, "Last Listener seems to be died and process started by fhem... stopping Threads and process...";
  8298. SONOS_Client_ConsumeMessage($so, 'shutdown');
  8299. } else {
  8300. SONOS_Log undef, 1, "Last Listener seems to be died... stopping Threads...";
  8301. SONOS_Client_ConsumeMessage($so, 'disconnect');
  8302. }
  8303. }
  8304. last;
  8305. }
  8306. }
  8307. }
  8308. }
  8309. SONOS_Log undef, 0, 'Das Lauschen auf der Schnittstelle wurde beendet. Prozess endet nun auch...';
  8310. # Alle Handles entfernen und schliessen...
  8311. for my $cl ($SONOS_Client_Selector->handles()) {
  8312. $SONOS_Client_Selector->remove($cl);
  8313. shutdown($cl, 2);
  8314. close($cl);
  8315. }
  8316. # Prozess beenden...
  8317. exit(0);
  8318. }
  8319. # Wird für den FHEM-Modulpart benötigt
  8320. 1;
  8321. ########################################################################################
  8322. # SONOS_Client_Thread_Notifier: Notifies all clients with the given message
  8323. ########################################################################################
  8324. sub SONOS_Client_Notifier($) {
  8325. my ($msg) = @_;
  8326. $| = 1;
  8327. state $setCurrentUDN = '';
  8328. # Wenn hier ein SetCurrent ausgeführt werden soll, dann auch den lokalen Puffer aktualisieren
  8329. if ($msg =~ m/SetCurrent:(.*?):(.*)/) {
  8330. my $udnBuffer = ($setCurrentUDN eq 'undef') ? 'SONOS' : $setCurrentUDN;
  8331. $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$1} = $2;
  8332. } elsif ($msg =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) {
  8333. $setCurrentUDN = $1;
  8334. }
  8335. SONOS_Log undef, 4, "SONOS_Client_Notifier($msg)" if ($msg !~ m/^Readings(Bulk|Single)Update/i);
  8336. # Immer ein Zeilenumbruch anfügen...
  8337. $msg .= "\n" if (substr($msg, -1, 1) ne "\n");
  8338. $SONOS_Client_SendQueue->enqueue($msg);
  8339. }
  8340. ########################################################################################
  8341. # SONOS_Client_Data_Retreive: Retrieves stored data.
  8342. ########################################################################################
  8343. sub SONOS_Client_Data_Retreive($$$$;$) {
  8344. my ($udn, $reading, $name, $default, $nologging) = @_;
  8345. $udn = '' if (!defined($udn));
  8346. $nologging = 0 if (!defined($nologging));
  8347. my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn;
  8348. return $default if (!defined($SONOS_Client_Data{Buffer}));
  8349. # Prüfen, ob die Anforderung überhaupt bedient werden darf
  8350. if ($reading eq 'attr') {
  8351. if (SONOS_posInList($name, @SONOS_PossibleAttributes) == -1) {
  8352. SONOS_Log undef, 0, "Ungültige Attribut-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  8353. exit(1);
  8354. }
  8355. } elsif ($reading eq 'def') {
  8356. if (SONOS_posInList($name, @SONOS_PossibleDefinitions) == -1) {
  8357. SONOS_Log undef, 0, "Ungültige Definitions-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  8358. exit(1);
  8359. }
  8360. } elsif ($reading eq 'udn') {
  8361. } else {
  8362. if (SONOS_posInList($name, @SONOS_PossibleReadings) == -1) {
  8363. SONOS_Log undef, 0, "Ungültige Reading-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!";
  8364. exit(1);
  8365. }
  8366. }
  8367. # Anfrage zulässig, also ausliefern...
  8368. if (defined($SONOS_Client_Data{Buffer}->{$udnBuffer}) && defined($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name})) {
  8369. SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> ".$SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} if (!$nologging);
  8370. return $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name};
  8371. } else {
  8372. SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> DEFAULT" if (!$nologging);
  8373. return $default;
  8374. }
  8375. }
  8376. ########################################################################################
  8377. # SONOS_Client_Data_Refresh: Send data and refreshs buffer
  8378. ########################################################################################
  8379. sub SONOS_Client_Data_Refresh($$$$) {
  8380. my ($sendCommand, $udn, $name, $value) = @_;
  8381. my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn;
  8382. SONOS_Log undef, 4, 'SONOS_Client_Data_Refresh('.(defined($sendCommand) ? $sendCommand : 'undef').", $udn, $name, ".(defined($value) ? $value : 'undef').')';
  8383. if (!defined($value)) {
  8384. undef($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name});
  8385. } else {
  8386. $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} = $value;
  8387. }
  8388. if (defined($sendCommand) && ($sendCommand ne '')) {
  8389. SONOS_Client_Notifier($sendCommand.':'.$udn.':'.$name.':'.(defined($value) ? $value : 'undef'));
  8390. }
  8391. }
  8392. ########################################################################################
  8393. # SONOS_Client_ConsumeMessage: Consumes the given message and give an evt. return
  8394. ########################################################################################
  8395. sub SONOS_Client_ConsumeMessage($$) {
  8396. my ($client, $msg) = @_;
  8397. if (lc($msg) eq 'disconnect' || lc($msg) eq 'shutdown') {
  8398. SONOS_Log undef, 3, "Disconnecting client and shutdown server..." if (lc($msg) eq 'shutdown');
  8399. SONOS_Log undef, 3, "Disconnecting client..." if (lc($msg) ne 'shutdown');
  8400. $SONOS_Client_Selector->remove($client);
  8401. if ($SONOS_Thread != -1) {
  8402. my $thr = threads->object($SONOS_Thread);
  8403. if ($thr) {
  8404. SONOS_Log undef, 3, 'Trying to kill Sonos_Thread...';
  8405. $thr->kill('INT')->detach();
  8406. } else {
  8407. SONOS_Log undef, 3, 'Sonos_Thread is already killed!';
  8408. }
  8409. }
  8410. if ($SONOS_Thread_LongJobs != -1) {
  8411. my $thr = threads->object($SONOS_Thread_LongJobs);
  8412. if ($thr) {
  8413. SONOS_Log undef, 3, 'Trying to kill LongJobs_Thread...';
  8414. $thr->kill('INT')->detach();
  8415. } else {
  8416. SONOS_Log undef, 3, 'LongJobs_Thread is already killed!';
  8417. }
  8418. }
  8419. if ($SONOS_Thread_IsAlive != -1) {
  8420. my $thr = threads->object($SONOS_Thread_IsAlive);
  8421. if ($thr) {
  8422. SONOS_Log undef, 3, 'Trying to kill IsAlive_Thread...';
  8423. $thr->kill('INT')->detach();
  8424. } else {
  8425. SONOS_Log undef, 3, 'IsAlive_Thread is already killed!';
  8426. }
  8427. }
  8428. if ($SONOS_Thread_PlayerRestore != -1) {
  8429. my $thr = threads->object($SONOS_Thread_PlayerRestore);
  8430. if ($thr) {
  8431. SONOS_Log undef, 3, 'Trying to kill PlayerRestore_Thread...';
  8432. $thr->kill('INT')->detach();
  8433. } else {
  8434. SONOS_Log undef, 3, 'PlayerRestore_Thread is already killed!';
  8435. }
  8436. }
  8437. shutdown($client, 2);
  8438. close($client);
  8439. threads->self()->kill('INT') if (lc($msg) eq 'shutdown');
  8440. } elsif (lc($msg) eq 'hello') {
  8441. send($client, "OK\r\n", 0);
  8442. } elsif (lc($msg) eq 'goaway') {
  8443. $SONOS_Client_Selector->remove($client);
  8444. shutdown($client, 2);
  8445. close($client);
  8446. } elsif ($msg =~ m/SetData:(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*)/i) {
  8447. $SONOS_Client_Data{SonosDeviceName} = $1;
  8448. $SONOS_Client_LogLevel = $2;
  8449. $SONOS_Client_LogfileName = $3;
  8450. $SONOS_Client_Data{pingType} = $4;
  8451. @usedonlyIPs = split(/,/, $5);
  8452. $SONOS_Client_Data{usedonlyIPs} = shared_clone(\@usedonlyIPs);
  8453. @ignoredIPs = split(/,/, $6);
  8454. $SONOS_Client_Data{ignoredIPs} = shared_clone(\@ignoredIPs);
  8455. $reusePort = $7;
  8456. my @names = split(/,/, $8);
  8457. $SONOS_Client_Data{PlayerNames} = shared_clone(\@names);
  8458. my @udns = split(/,/, $9);
  8459. $SONOS_Client_Data{PlayerUDNs} = shared_clone(\@udns);
  8460. my @playeralive = ();
  8461. $SONOS_Client_Data{PlayerAlive} = shared_clone(\@playeralive);
  8462. my %player = ();
  8463. $SONOS_Client_Data{Buffer} = shared_clone(\%player);
  8464. my %udnValues = ();
  8465. $SONOS_Client_Data{Buffer}->{udn} = shared_clone(\%udnValues);
  8466. push @udns, 'SONOS';
  8467. foreach my $elem (@udns) {
  8468. my %elemValues = ();
  8469. $SONOS_Client_Data{Buffer}->{$elem} = shared_clone(\%elemValues);
  8470. $SONOS_Client_Data{Buffer}->{udn}->{$1} = $elem;
  8471. }
  8472. } elsif ($msg =~ m/SetValues:(.*?):(.*)/i) {
  8473. my $deviceName = $1;
  8474. my $deviceValues = $2;
  8475. my %elemValues = ();
  8476. # Werte aus der Übergabe belegen
  8477. foreach my $elem (split(/\|/, $deviceValues)) {
  8478. if ($elem =~ m/(.*?)=(.*)/) {
  8479. $elemValues{$1} = uri_unescape($2);
  8480. if ($1 eq 'bookmarkPlaylistDefinition') {
  8481. # <Gruppenname>:<PlayerDeviceRegEx>:<MinListLength>:<MaxListLength>:<MaxAge> [...]
  8482. %SONOS_BookmarkQueueDefinition = ();
  8483. my $def = $elemValues{$1};
  8484. foreach my $elem (split(/ /, $def)) {
  8485. # Sicherstellen, das alle Stellen vorhanden sind...
  8486. $elem .= ':' while (SONOS_CountInString(':', $elem) < 5);
  8487. # Zerlegen
  8488. if ($elem =~ m/(.*?):(.*?):(\d*):(\d*):(.*?):(.*)/) {
  8489. my $key = $1;
  8490. $SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx} = ($2 ne '') ? $2 : '.*';
  8491. $SONOS_BookmarkQueueDefinition{$key}{MinListLength} = ($3 ne '') ? $3 : 0;
  8492. $SONOS_BookmarkQueueDefinition{$key}{MaxListLength} = ($4 ne '') ? $4 : 99999;
  8493. $SONOS_BookmarkQueueDefinition{$key}{MaxAge} = ($5 ne '') ? $5 : '28*24*60*60';
  8494. $SONOS_BookmarkQueueDefinition{$key}{ReadOnly} = ($6 ne '') ? $6 : 0;
  8495. # RegEx prüfen
  8496. eval { "" =~ m/$SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx}/ };
  8497. if($@) {
  8498. SONOS_Log undef, 0, 'SetData - bookmarkPlaylistDefinition: Bad PlayerDeviceRegExp "'.$SONOS_BookmarkQueueDefinition{$key}{PlayerDeviceRegEx}.'": '.$@;
  8499. delete($SONOS_BookmarkQueueDefinition{$key});
  8500. next;
  8501. }
  8502. # MaxAge berechnen...
  8503. eval { $SONOS_BookmarkQueueDefinition{$key}{MaxAge} = eval($SONOS_BookmarkQueueDefinition{$key}{MaxAge}); };
  8504. if($@) {
  8505. SONOS_Log undef, 0, 'SetData - bookmarkPlaylistDefinition: Bad MaxAge "'.$SONOS_BookmarkQueueDefinition{$key}{MaxAge}.'": '.$@;
  8506. delete($SONOS_BookmarkQueueDefinition{$key});
  8507. next;
  8508. }
  8509. }
  8510. }
  8511. SONOS_Log undef, 4, 'BookmarkPlaylistDefinition: '.Dumper(\%SONOS_BookmarkQueueDefinition);
  8512. }
  8513. if ($1 eq 'bookmarkTitleDefinition') {
  8514. # <Gruppenname>:<PlayerdeviceRegEx>:<TrackURIRegEx>:<MinTitleLength>:<RemainingLength>:<MaxAge>:<ReadOnly> [...]
  8515. %SONOS_BookmarkTitleDefinition = ();
  8516. my $def = $elemValues{$1};
  8517. foreach my $elem (split(/ /, $def)) {
  8518. # Sicherstellen, das alle Stellen vorhanden sind...
  8519. $elem .= ':' while (SONOS_CountInString(':', $elem) < 6);
  8520. # Zerlegen
  8521. if ($elem =~ m/(.*?):(.*?):(.*?):(\d*):(\d*):(.*?):(.*)/) {
  8522. my $key = $1;
  8523. $SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx} = ($2 ne '') ? $2 : '.*';
  8524. $SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx} = ($3 ne '') ? $3 : '.*';
  8525. $SONOS_BookmarkTitleDefinition{$key}{MinTitleLength} = ($4 ne '') ? $4 : 60;
  8526. $SONOS_BookmarkTitleDefinition{$key}{RemainingLength} = ($5 ne '') ? $5 : 10;
  8527. $SONOS_BookmarkTitleDefinition{$key}{MaxAge} = ($6 ne '') ? $6 : '28*24*60*60';
  8528. $SONOS_BookmarkTitleDefinition{$key}{ReadOnly} = 0;
  8529. $SONOS_BookmarkTitleDefinition{$key}{ReadOnly} = 1 if (lc($7) eq 'readonly');
  8530. $SONOS_BookmarkTitleDefinition{$key}{Chapter} = 0;
  8531. $SONOS_BookmarkTitleDefinition{$key}{Chapter} = 1 if (lc($7) eq 'chapter');
  8532. # RegEx prüfen...
  8533. eval { "" =~ m/$SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx}/ };
  8534. if($@) {
  8535. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad PlayerDeviceRegExp "'.$SONOS_BookmarkTitleDefinition{$key}{PlayerDeviceRegEx}.'": '.$@;
  8536. delete($SONOS_BookmarkTitleDefinition{$key});
  8537. next;
  8538. }
  8539. # RegEx prüfen...
  8540. eval { "" =~ m/$SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx}/ };
  8541. if($@) {
  8542. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad TrackURIRegEx "'.$SONOS_BookmarkTitleDefinition{$key}{TrackURIRegEx}.'": '.$@;
  8543. delete($SONOS_BookmarkTitleDefinition{$key});
  8544. next;
  8545. }
  8546. # MaxAge berechnen...
  8547. eval { $SONOS_BookmarkTitleDefinition{$key}{MaxAge} = eval($SONOS_BookmarkTitleDefinition{$key}{MaxAge}); };
  8548. if($@) {
  8549. SONOS_Log undef, 0, 'SetData - bookmarkTitleDefinition: Bad MaxAge "'.$SONOS_BookmarkTitleDefinition{$key}{MaxAge}.'": '.$@;
  8550. delete($SONOS_BookmarkTitleDefinition{$key});
  8551. next;
  8552. }
  8553. }
  8554. }
  8555. SONOS_Log undef, 4, 'BookmarkTitleDefinition: '.Dumper(\%SONOS_BookmarkTitleDefinition);
  8556. }
  8557. }
  8558. }
  8559. $SONOS_Client_Data{Buffer}->{$deviceName} = shared_clone(\%elemValues);
  8560. } elsif ($msg =~ m/DoWork:(.*?):(.*?):(.*)/i) {
  8561. my %data;
  8562. $data{WorkType} = $2;
  8563. $data{UDN} = $1;
  8564. if (defined($3)) {
  8565. my @params = split(/--#--/, decode_utf8($3));
  8566. $data{Params} = \@params;
  8567. } else {
  8568. my @params = ();
  8569. $data{Params} = \@params;
  8570. }
  8571. # Auf die Queue legen, wenn Thread läuft...
  8572. if ($SONOS_Thread != -1) {
  8573. $SONOS_Client_ReceiveQueue->enqueue(\%data);
  8574. }
  8575. } elsif (lc($msg) eq 'startthread') {
  8576. # Discover-Thread
  8577. $SONOS_Thread = threads->create(\&SONOS_Discover)->tid();
  8578. # LongJobs-Thread
  8579. $SONOS_Thread_LongJobs = threads->create(\&SONOS_Client_LongJobs)->tid();
  8580. # IsAlive-Checker-Thread
  8581. if (lc($SONOS_Client_Data{pingType}) ne 'none') {
  8582. $SONOS_Thread_IsAlive = threads->create(\&SONOS_Client_IsAlive)->tid();
  8583. }
  8584. # Playerrestore-Thread
  8585. $SONOS_Thread_PlayerRestore = threads->create(\&SONOS_RestoreOldPlaystate)->tid();
  8586. } else {
  8587. SONOS_Log undef, 2, "ConsumMessage: Sorry. I don't understand you - '$msg'.";
  8588. send($client, "Sorry. I don't understand you - '$msg'.\r\n", 0);
  8589. }
  8590. }
  8591. ########################################################################################
  8592. # SONOS_getBookmarkGroupKeys: Retrieves the approbriate GroupKeys to the given UDN
  8593. ########################################################################################
  8594. sub SONOS_getBookmarkGroupKeys($$;$) {
  8595. my ($type, $udn, $disabled) = @_;
  8596. $disabled = 0 if (!defined($disabled));
  8597. my $deviceName = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
  8598. my @result = ();
  8599. my $hashList = \%SONOS_BookmarkTitleDefinition;
  8600. $hashList = \%SONOS_BookmarkQueueDefinition if (lc($type) eq 'queue');
  8601. foreach my $key (keys %{$hashList}) {
  8602. if ($deviceName =~ m/$hashList->{$key}{PlayerDeviceRegEx}/) {
  8603. push(@result, $key) if (!$disabled && (!defined($hashList->{$key}{Disabled}) || !$hashList->{$key}{Disabled}));
  8604. push(@result, $key) if ($disabled && defined($hashList->{$key}{Disabled}) && $hashList->{$key}{Disabled});
  8605. }
  8606. }
  8607. return @result;
  8608. }
  8609. ########################################################################################
  8610. # SONOS_Client_LongJobs: Longjobs-Thread
  8611. ########################################################################################
  8612. sub SONOS_Client_LongJobs() {
  8613. my $stepInterval = 0.5;
  8614. SONOS_Log undef, 1, 'LongJobs-Thread gestartet. Prüfe auf LongJobs...';
  8615. my $runEndlessLoop = 1;
  8616. $SIG{'PIPE'} = 'IGNORE';
  8617. $SIG{'CHLD'} = 'IGNORE';
  8618. $SIG{'INT'} = sub {
  8619. $runEndlessLoop = 0;
  8620. };
  8621. # Endlos-Schleife für Jobs...
  8622. while($runEndlessLoop) {
  8623. select(undef, undef, undef, $stepInterval);
  8624. if ($SONOS_LongJobsQueue->pending() != 0) {
  8625. my $job = $SONOS_LongJobsQueue->dequeue();
  8626. next if !defined($job);
  8627. SONOS_Log undef, 3, 'LongJobs: DoQueue of "'.$job->{WorkType}.'"';
  8628. SONOS_Client_LongJobs_DoQueue($job);
  8629. }
  8630. }
  8631. SONOS_Log undef, 1, 'LongJobs-Thread wurde beendet.';
  8632. $SONOS_Thread_LongJobs = -1;
  8633. }
  8634. ########################################################################################
  8635. # SONOS_AddToLongJobsQueue: Adds a job to Longjobs
  8636. ########################################################################################
  8637. sub SONOS_AddToLongJobsQueue($$;$) {
  8638. my ($udn, $workType, $params) = @_;
  8639. # Grunddaten
  8640. my %data;
  8641. $data{WorkType} = $workType;
  8642. $data{UDN} = $udn;
  8643. # Location mitsichern, damit die Proxies neu geholt werden können
  8644. my %revUDNs = reverse %SONOS_Locations;
  8645. $data{Location} = $revUDNs{$udn};
  8646. # Parmeter übergeben
  8647. if (defined($params)) {
  8648. my @params = @{$params};
  8649. $data{Params} = \@params;
  8650. } else {
  8651. my @params = ();
  8652. $data{Params} = \@params;
  8653. }
  8654. # Auf die Queue legen, wenn Thread läuft...
  8655. if ($SONOS_Thread_LongJobs != -1) {
  8656. $SONOS_LongJobsQueue->enqueue(\%data);
  8657. }
  8658. }
  8659. ########################################################################################
  8660. # SONOS_LongJobsAnswer: Adds an answer from Longjobs
  8661. ########################################################################################
  8662. #sub SONOS_LongJobsAnswer() {
  8663. # my ($udn, $workType, $params) = @_;
  8664. #
  8665. # my %data;
  8666. # $data{WorkType} = $workType;
  8667. # $data{UDN} = $udn;
  8668. # my @params = ();
  8669. # @params = split(/--#--/, decode_utf8($3));
  8670. # $data{Params} = \@params;
  8671. #
  8672. # $SONOS_Client_ReceiveQueue->enqueue(\%data);
  8673. #}
  8674. ########################################################################################
  8675. # SONOS_Client_LongJobs_DoQueue: Do Longjobs
  8676. ########################################################################################
  8677. sub SONOS_Client_LongJobs_DoQueue($) {
  8678. my ($data) = @_;
  8679. my $workType = $data->{WorkType};
  8680. return if (!defined($workType));
  8681. my $udn = $data->{UDN};
  8682. my @params = ();
  8683. @params = @{$data->{Params}} if (defined($data->{Params}));
  8684. # Hier die ursprünglichen Proxies wiederherstellen/neu verbinden...
  8685. my $controlPoint = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, SubscriptionURL => '/fhemmodule', MaxWait => 20, LogLevel => $SONOS_Client_LogLevel, UsedOnlyIP => \@usedonlyIPs, IgnoreIP => \@ignoredIPs, ReusePort => $reusePort);
  8686. my $device = $controlPoint->_createDevice($data->{Location});
  8687. for my $subdevice ($device->children) {
  8688. if ($subdevice->UDN =~ /.*_MR/i) {
  8689. #$AVProxy = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1')->controlProxy();
  8690. #$GRProxy = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')->controlProxy();
  8691. }
  8692. if ($subdevice->UDN =~ /.*_MS/i) {
  8693. $SONOS_ContentDirectoryControlProxy{$udn} = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')->controlProxy();
  8694. }
  8695. }
  8696. eval {
  8697. if ($workType eq 'exportSonosBibliothek') {
  8698. my $filename = $params[0];
  8699. # Anfragen durchführen...
  8700. if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
  8701. my $exports = {'Structure' => {}, 'Titles' => {}};
  8702. SONOS_Log undef, 4, 'ExportSonosBibliothek-Start';
  8703. my $startTime = gettimeofday();
  8704. SONOS_RecursiveStructure($udn, 'A:', $exports->{Structure}, $exports->{Titles});
  8705. SONOS_Log undef, 4, 'ExportSonosBibliothek-End. Runtime (in seconds): '.int(gettimeofday() - $startTime);
  8706. my $countTitles = scalar(keys %{$exports->{Titles}});
  8707. SONOS_Log undef, 5, 'ExportSonosBibliothek. Titles: '.$countTitles;
  8708. # In Datei wegschreiben
  8709. eval {
  8710. open FILE, '>'.$filename;
  8711. SONOS_Log undef, 5, 'ExportSonosBibliothek. File "'.$filename.'" opened.';;
  8712. binmode(FILE, ':encoding(utf-8)');
  8713. print FILE SONOS_Dumper($exports);
  8714. SONOS_Log undef, 5, 'ExportSonosBibliothek. File written.';
  8715. close FILE;
  8716. SONOS_Log undef, 5, 'ExportSonosBibliothek. File closed.';
  8717. };
  8718. if ($@) {
  8719. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during filewriting: '.$@);
  8720. return;
  8721. }
  8722. $exports = undef;
  8723. SONOS_Log undef, 5, 'ExportSonosBibliothek. Internal Data deleted.';
  8724. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Successfully written to file "'.$filename.'", Titles: '.$countTitles.', Duration: '.int(gettimeofday() - $startTime).'s');
  8725. }
  8726. }
  8727. };
  8728. if ($@) {
  8729. SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'LongJobs-DoWork-Exception ERROR: '.$@);
  8730. }
  8731. $SONOS_ContentDirectoryControlProxy{$udn} = 0;
  8732. undef($controlPoint);
  8733. }
  8734. ########################################################################################
  8735. # SONOS_Client_IsAlive: Checks if the clients (Zoneplayers) are already available
  8736. ########################################################################################
  8737. sub SONOS_Client_IsAlive() {
  8738. my $interval = SONOS_Max(10, SONOS_Client_Data_Retreive('undef', 'def', 'INTERVAL', 0));
  8739. my $stepInterval = 0.5;
  8740. SONOS_Log undef, 1, 'IsAlive-Thread gestartet. Warte 120 Sekunden und pruefe dann alle '.$interval.' Sekunden...';
  8741. my $runEndlessLoop = 1;
  8742. $SIG{'PIPE'} = 'IGNORE';
  8743. $SIG{'CHLD'} = 'IGNORE';
  8744. $SIG{'INT'} = sub {
  8745. $runEndlessLoop = 0;
  8746. };
  8747. # Erst nach einer Weile wartens anfangen zu arbeiten. Bis dahin sollten alle Player im Netz erkannt, und deren Konfigurationen bekannt sein.
  8748. my $counter = 0;
  8749. do {
  8750. select(undef, undef, undef, 0.5);
  8751. } while (($counter++ < 240) && $runEndlessLoop);
  8752. my $stepCounter = 0;
  8753. while($runEndlessLoop) {
  8754. select(undef, undef, undef, $stepInterval);
  8755. next if (($stepCounter += $stepInterval) < $interval);
  8756. $stepCounter = 0;
  8757. # Alle bekannten Player durchgehen, wenn der Thread nicht beendet werden soll
  8758. if ($runEndlessLoop) {
  8759. my @list = @{$SONOS_Client_Data{PlayerAlive}};
  8760. my @toAnnounce = ();
  8761. for(my $i = 0; $i <= $#list; $i++) {
  8762. next if (!$list[$i]);
  8763. if (!SONOS_IsAlive($list[$i])) {
  8764. # Auf die Entfernen-Meldeliste setzen
  8765. push @toAnnounce, $list[$i];
  8766. # Wenn er nicht mehr am Leben ist, dann auch aus der Aktiven-Liste entfernen
  8767. delete @{$SONOS_Client_Data{PlayerAlive}}[$i];
  8768. }
  8769. }
  8770. # Wenn ein Player gerade verschwunden ist, dann dem (verbleibenden) Sonos-System das mitteilen
  8771. foreach my $toDeleteElem (@toAnnounce) {
  8772. if ($toDeleteElem =~ m/(^.*)_/) {
  8773. $toDeleteElem = $1;
  8774. SONOS_Log undef, 3, 'ReportUnresponsiveDevice: '.$toDeleteElem;
  8775. foreach my $udn (@{$SONOS_Client_Data{PlayerAlive}}) {
  8776. next if (!$udn);
  8777. my %data;
  8778. $data{WorkType} = 'reportUnresponsiveDevice';
  8779. $data{UDN} = $udn;
  8780. my @params = ();
  8781. push @params, $toDeleteElem;
  8782. $data{Params} = \@params;
  8783. $SONOS_Client_ReceiveQueue->enqueue(\%data);
  8784. # Da ich das nur an den ersten verfügbaren Player senden muss, kann hier die Schleife direkt beendet werden
  8785. last;
  8786. }
  8787. }
  8788. }
  8789. }
  8790. }
  8791. SONOS_Log undef, 1, 'IsAlive-Thread wurde beendet.';
  8792. $SONOS_Thread_IsAlive = -1;
  8793. }
  8794. ########################################################################################
  8795. ########################################################################################
  8796. ##
  8797. ## End of Telnet-Server-Part for Sonos UPnP-Messages
  8798. ##
  8799. ########################################################################################
  8800. ########################################################################################
  8801. =pod
  8802. =item summary Module to commmunicate with a Sonos-System via UPnP
  8803. =item summary_DE Modul für die Kommunikation mit einem Sonos-System mittels UPnP
  8804. =begin html
  8805. <a name="SONOS"></a>
  8806. <h3>SONOS</h3>
  8807. <p>FHEM-Module to communicate with the Sonos-System via UPnP</p>
  8808. <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>
  8809. <p>For correct functioning of this module it is neccessary to have some Perl-Modules installed, which are eventually installed already manually:<ul>
  8810. <li><code>LWP::Simple</code></li>
  8811. <li><code>LWP::UserAgent</code></li>
  8812. <li><code>SOAP::Lite</code></li>
  8813. <li><code>HTTP::Request</code></li></ul>
  8814. Installation e.g. as Debian-Packages (via "sudo apt-get install &lt;packagename&gt;"):<ul>
  8815. <li>LWP::Simple-Packagename (incl. LWP::UserAgent and HTTP::Request): libwww-perl</li>
  8816. <li>SOAP::Lite-Packagename: libsoap-lite-perl</li></ul>
  8817. <br />Installation e.g. as Windows ActivePerl (via Perl-Packagemanager)<ul>
  8818. <li>Install Package LWP (incl. LWP::UserAgent and HTTP::Request)</li>
  8819. <li>Install Package SOAP::Lite</li>
  8820. <li>SOAP::Lite-Special for Versions after 5.18:<ul>
  8821. <li>Add another Packagesource from suggestions or manual: Bribes de Perl (http://www.bribes.org/perl/ppm)</li>
  8822. <li>Install Package: SOAP::Lite</li></ul></li></ul>
  8823. <b>Windows ActivePerl 5.20 does currently not work due to missing SOAP::Lite</b></p>
  8824. <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>
  8825. <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>
  8826. <p>The system consists of two different components:<br />
  8827. 1. A UPnP-Client which runs as a standalone process in the background and takes the communications to the sonos-components.<br />
  8828. 2. The FHEM-module itself which connects to the UPnP-client to make fhem able to work with sonos.<br /><br />
  8829. The client will be started by the module itself if not done in another way.<br />
  8830. You can start this client on your own (to let it run instantly and independent from FHEM):<br />
  8831. <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>
  8832. <h4>Example</h4>
  8833. <p>
  8834. Simplest way to define:<br />
  8835. <b><code>define Sonos SONOS</code></b>
  8836. </p>
  8837. <p>
  8838. Example with control over the used port and the isalive-checker-interval:<br />
  8839. <b><code>define Sonos SONOS localhost:4711 45</code></b>
  8840. </p>
  8841. <a name="SONOSdefine"></a>
  8842. <h4>Define</h4>
  8843. <b><code>define &lt;name&gt; SONOS [upnplistener [interval [waittime [delaytime]]]]</code></b>
  8844. <br /><br /> Define a Sonos interface to communicate with a Sonos-System.<br />
  8845. <p>
  8846. <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>
  8847. <p>
  8848. <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>
  8849. <p>
  8850. <b><code>[waittime]</code></b><br /> With this value you can configure the waiting time for the starting of the Subprocess.</p>
  8851. <p>
  8852. <b><code>[delaytime]</code></b><br /> With this value you can configure a delay time before starting the network-part.</p>
  8853. <a name="SONOSset"></a>
  8854. <h4>Set</h4>
  8855. <ul>
  8856. <li><b>Common Tasks</b><ul>
  8857. <li><a name="SONOS_setter_RefreshShareIndex">
  8858. <b><code>RefreshShareIndex</code></b></a>
  8859. <br />Starts the refreshing of the library.</li>
  8860. <li><a name="SONOS_setter_RescanNetwork">
  8861. <b><code>RescanNetwork</code></b></a>
  8862. <br />Restarts the player discovery.</li>
  8863. </ul></li>
  8864. <li><b>Control-Commands</b><ul>
  8865. <li><a name="SONOS_setter_Mute">
  8866. <b><code>Mute &lt;state&gt;</code></b></a>
  8867. <br />Sets the mute-state on all players.</li>
  8868. <li><a name="SONOS_setter_PauseAll">
  8869. <b><code>PauseAll</code></b></a>
  8870. <br />Pause all Zoneplayer.</li>
  8871. <li><a name="SONOS_setter_Pause">
  8872. <b><code>Pause</code></b></a>
  8873. <br />Alias for PauseAll.</li>
  8874. <li><a name="SONOS_setter_StopAll">
  8875. <b><code>StopAll</code></b></a>
  8876. <br />Stops all Zoneplayer.</li>
  8877. <li><a name="SONOS_setter_Stop">
  8878. <b><code>Stop</code></b></a>
  8879. <br />Alias for StopAll.</li>
  8880. </ul></li>
  8881. <li><b>Bookmark-Commands</b><ul>
  8882. <li><a name="SONOS_setter_DisableBookmark">
  8883. <b><code>DisableBookmark &lt;Groupname&gt;</code></b></a>
  8884. <br />Disables the group with the given name.</li>
  8885. <li><a name="SONOS_setter_EnableBookmark">
  8886. <b><code>EnableBookmark &lt;Groupname&gt;</code></b></a>
  8887. <br />Enables the group with the given name.</li>
  8888. <li><a name="SONOS_setter_LoadBookmarks">
  8889. <b><code>LoadBookmarks [Groupname]</code></b></a>
  8890. <br />Loads the given group (or all if parameter not set) from the filesystem.</li>
  8891. <li><a name="SONOS_setter_SaveBookmarks">
  8892. <b><code>SaveBookmarks [Groupname]</code></b></a>
  8893. <br />Saves the given group (or all if parameter not set) to the filesystem.</li>
  8894. </ul></li>
  8895. <li><b>Group-Commands</b><ul>
  8896. <li><a name="SONOS_setter_Groups">
  8897. <b><code>Groups &lt;GroupDefinition&gt;</code></b></a>
  8898. <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>
  8899. </ul></li>
  8900. </ul>
  8901. <a name="SONOSget"></a>
  8902. <h4>Get</h4>
  8903. <ul>
  8904. <li><b>Group-Commands</b><ul>
  8905. <li><a name="SONOS_getter_Groups">
  8906. <b><code>Groups</code></b></a>
  8907. <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 />
  8908. 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>
  8909. </ul></li>
  8910. </ul>
  8911. <a name="SONOSattr"></a>
  8912. <h4>Attributes</h4>
  8913. '''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.
  8914. <ul>
  8915. <li><b>Common</b><ul>
  8916. <li><a name="SONOS_attribut_coverLoadTimeout"><b><code>coverLoadTimeout &lt;value&gt;</code></b>
  8917. </a><br />One of (0..10,15,20,25,30). Defines the timeout for waiting of the Sonosplayer for Cover-Downloads. Defaults to 5.</li>
  8918. <li><a name="SONOS_attribut_deviceRoomView"><b><code>deviceRoomView &lt;Both|DeviceLineOnly&gt;</code></b>
  8919. </a><br /> Defines the style of the Device in the room overview. <code>Both</code> means "normal" Deviceline incl. Cover-/Titleview and maybe the control area, <code>DeviceLineOnly</code> means only the "normal" Deviceline-view.</li>
  8920. <li><a name="SONOS_attribut_disable"><b><code>disable &lt;value&gt;</code></b>
  8921. </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>
  8922. <li><a name="SONOS_attribut_getFavouritesListAtNewVersion"><b><code>getFavouritesListAtNewVersion &lt;value&gt;</code></b>
  8923. </a><br />One of (0,1). With this attribute set, the module will refresh the Favourites-List automatically upon changes (if the Attribute <code>getListsDirectlyToReadings</code> is set).</li>
  8924. <li><a name="SONOS_attribut_getPlaylistsListAtNewVersion"><b><code>getPlaylistsListAtNewVersion &lt;value&gt;</code></b>
  8925. </a><br />One of (0,1). With this attribute set, the module will refresh the Playlists-List automatically upon changes (if the Attribute <code>getListsDirectlyToReadings</code> is set).</li>
  8926. <li><a name="SONOS_attribut_getQueueListAtNewVersion"><b><code>getQueueListAtNewVersion &lt;value&gt;</code></b>
  8927. </a><br />One of (0,1). With this attribute set, the module will refresh the current Queue-List automatically upon changes (if the Attribute <code>getListsDirectlyToReadings</code> is set).</li>
  8928. <li><a name="SONOS_attribut_getRadiosListAtNewVersion"><b><code>getRadiosListAtNewVersion &lt;value&gt;</code></b>
  8929. </a><br />One of (0,1). With this attribute set, the module will refresh the Radios-List automatically upon changes (if the Attribute <code>getListsDirectlyToReadings</code> is set).</li>
  8930. <li><a name="SONOS_attribut_getListsDirectlyToReadings"><b><code>getListsDirectlyToReadings &lt;value&gt;</code></b>
  8931. </a><br />One of (0,1). With this attribute you can define that the module fills the readings for the lists of Favourites, Playlists, Radios and the Queue directly without the need of userReadings.</li>
  8932. <li><a name="SONOS_attribut_getLocalCoverArt"><b><code>getLocalCoverArt &lt;value&gt;</code></b>
  8933. </a><br />One of (0,1). With this attribute the loads and saves the Coverart locally (default till now).</li>
  8934. <li><a name="SONOS_attribut_ignoredIPs"><b><code>ignoredIPs &lt;IP-Address&gt;[,IP-Address]</code></b>
  8935. </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>
  8936. <li><a name="SONOS_attribut_pingType"><b><code>pingType &lt;string&gt;</code></b>
  8937. </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>
  8938. <li><a name="SONOS_attribut_reusePort"><b><code>reusePort &lt;int&gt;</code></b>
  8939. </a><br /> One of (0,1). If defined the socket-Attribute 'reuseport' will be used for SSDP Discovery-Port. Can solve restart-problems. If you don't have such problems don't use this attribute.</li>
  8940. <li><a name="SONOS_attribut_SubProcessLogfileName"><b><code>SubProcessLogfileName &lt;Path&gt;</code></b>
  8941. </a><br /> If given, the subprocess logs into its own logfile. Under Windows this is a recommended way for logging, because the two Loggings (Fehm and the SubProcess) overwrite each other. If "-" is given, the logging goes to STDOUT (and therefor in the Fhem-log) as usual. The main purpose of this attribute is the short-use of separated logging. No variables are substituted. The value is used as configured.</li>
  8942. <li><a name="SONOS_attribut_usedonlyIPs"><b><code>usedonlyIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  8943. </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>
  8944. </ul></li>
  8945. <li><b>Bookmark Configuration</b><ul>
  8946. <li><a name="SONOS_attribut_bookmarkSaveDir"><b><code>bookmarkSaveDir &lt;path&gt;</code></b>
  8947. </a><br /> Defines a directory where the saved bookmarks can be placed. If not defined, "." will be used.</li>
  8948. <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>
  8949. </a><br /> Definition of Bookmarks for titles.</li>
  8950. <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>
  8951. </a><br /> Definition of bookmarks for playlists.</li>
  8952. </ul></li>
  8953. <li><b>Proxy Configuration</b><ul>
  8954. <li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><b><code>generateProxyAlbumArtURLs &lt;int&gt;</code></b>
  8955. </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>
  8956. <li><a name="SONOS_attribut_proxyCacheDir"><b><code>proxyCacheDir &lt;Path&gt;</code></b>
  8957. </a><br />Defines a directory where the cached Coverfiles can be placed. If not defined "/tmp" will be used.</li>
  8958. <li><a name="SONOS_attribut_proxyCacheTime"><b><code>proxyCacheTime &lt;int&gt;</code></b>
  8959. </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>
  8960. <li><a name="SONOS_attribut_webname"><b><code>webname &lt;String&gt;</code></b>
  8961. </a><br /> With the attribute you can define the used webname for coverlinks. Defaults to 'fhem' if not given.</li>
  8962. </ul></li>
  8963. <li><b>Speak Configuration</b><ul>
  8964. <li><a name="SONOS_attribut_targetSpeakDir"><b><code>targetSpeakDir &lt;string&gt;</code></b>
  8965. </a><br /> Defines, which Directory has to be used for the Speakfiles</li>
  8966. <li><a name="SONOS_attribut_targetSpeakMP3FileConverter"><b><code>targetSpeakMP3FileConverter &lt;string&gt;</code></b>
  8967. </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>
  8968. <li><a name="SONOS_attribut_targetSpeakMP3FileDir"><b><code>targetSpeakMP3FileDir &lt;string&gt;</code></b>
  8969. </a><br /> The directory which should be used as a default for text-embedded MP3-Files.</li>
  8970. <li><a name="SONOS_attribut_targetSpeakURL"><b><code>targetSpeakURL &lt;string&gt;</code></b>
  8971. </a><br /> Defines, which URL has to be used for accessing former stored Speakfiles as seen from the SonosPlayer</li>
  8972. <li><a name="SONOS_attribut_targetSpeakFileTimestamp"><b><code>targetSpeakFileTimestamp &lt;int&gt;</code></b>
  8973. </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>
  8974. <li><a name="SONOS_attribut_targetSpeakFileHashCache"><b><code>targetSpeakFileHashCache &lt;int&gt;</code></b>
  8975. </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>
  8976. <li><a name="SONOS_attribut_Speak1"><b><code>Speak1 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  8977. </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>
  8978. <li><a name="SONOS_attribut_Speak2"><b><code>Speak2 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  8979. </a><br />See Speak1</li>
  8980. <li><a name="SONOS_attribut_Speak3"><b><code>Speak3 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  8981. </a><br />See Speak1</li>
  8982. <li><a name="SONOS_attribut_Speak4"><b><code>Speak4 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  8983. </a><br />See Speak1</li>
  8984. <li><a name="SONOS_attribut_SpeakCover"><b><code>SpeakCover &lt;Filename&gt;</code></b>
  8985. </a><br />Defines a Cover for use by the speak generation process. If not defined the Fhem-logo will be used.</li>
  8986. <li><a name="SONOS_attribut_Speak1Cover"><b><code>Speak1Cover &lt;Filename&gt;</code></b>
  8987. </a><br />See SpeakCover</li>
  8988. <li><a name="SONOS_attribut_Speak2Cover"><b><code>Speak2Cover &lt;Filename&gt;</code></b>
  8989. </a><br />See SpeakCover</li>
  8990. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Filename&gt;</code></b>
  8991. </a><br />See SpeakCover</li>
  8992. <li><a name="SONOS_attribut_Speak4Cover"><b><code>Speak4Cover &lt;Filename&gt;</code></b>
  8993. </a><br />See SpeakCover</li>
  8994. <li><a name="SONOS_attribut_SpeakGoogleURL"><b><code>SpeakGoogleURL &lt;GoogleURL&gt;</code></b>
  8995. </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>
  8996. </ul></li>
  8997. </ul>
  8998. =end html
  8999. =begin html_DE
  9000. <a name="SONOS"></a>
  9001. <h3>SONOS</h3>
  9002. <p>FHEM-Modul für die Anbindung des Sonos-Systems via UPnP</p>
  9003. <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>
  9004. <p>Für die Verwendung sind Perlmodule notwendig, die unter Umständen noch nachinstalliert werden müssen:<ul>
  9005. <li><code>LWP::Simple</code></li>
  9006. <li><code>LWP::UserAgent</code></li>
  9007. <li><code>SOAP::Lite</code></li>
  9008. <li><code>HTTP::Request</code></li></ul>
  9009. Installation z.B. als Debian-Pakete (mittels "sudo apt-get install &lt;packagename&gt;"):<ul>
  9010. <li>LWP::Simple-Packagename (inkl. LWP::UserAgent und HTTP::Request): libwww-perl</li>
  9011. <li>SOAP::Lite-Packagename: libsoap-lite-perl</li></ul>
  9012. <br />Installation z.B. als Windows ActivePerl (mittels Perl-Packagemanager)<ul>
  9013. <li>Package LWP (incl. LWP::UserAgent and HTTP::Request)</li>
  9014. <li>Package SOAP::Lite</li>
  9015. <li>SOAP::Lite-Special für Versionen nach 5.18:<ul>
  9016. <li>Eine andere Paketquelle von den Vorschlägen oder manuell hinzufügen: Bribes de Perl (http://www.bribes.org/perl/ppm)</li>
  9017. <li>Package: SOAP::Lite</li></ul></li></ul>
  9018. <b>Windows ActivePerl 5.20 kann momentan nicht verwendet werden, da es das Paket SOAP::Lite dort momentan nicht gibt.</b></p>
  9019. <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>
  9020. <p>Mehr Informationen im (deutschen) Wiki-Artikel: <a href="http://www.fhemwiki.de/wiki/SONOS">http://www.fhemwiki.de/wiki/SONOS</a></p>
  9021. <p>Das System besteht aus zwei Komponenten:<br />
  9022. 1. Einem UPnP-Client, der als eigener Prozess im Hintergrund ständig läuft, und die Kommunikation mit den Sonos-Geräten übernimmt.<br />
  9023. 2. Dem eigentlichen FHEM-Modul, welches mit dem UPnP-Client zusammenarbeitet, um die Funktionalität in FHEM zu ermöglichen.<br /><br />
  9024. Der Client wird im Notfall automatisch von Modul selbst gestartet.<br />
  9025. Man kann den Server unabhängig von FHEM selbst starten (um ihn dauerhaft und unabh&auml;ngig von FHEM laufen zu lassen):<br />
  9026. <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>
  9027. <h4>Beispiel</h4>
  9028. <p>
  9029. Einfachste Definition:<br />
  9030. <b><code>define Sonos SONOS</code></b>
  9031. </p>
  9032. <p>
  9033. Definition mit Kontrolle über den verwendeten Port und das Intervall der IsAlive-Prüfung:<br />
  9034. <b><code>define Sonos SONOS localhost:4711 45</code></b>
  9035. </p>
  9036. <a name="SONOSdefine"></a>
  9037. <h4>Definition</h4>
  9038. <b><code>define &lt;name&gt; SONOS [upnplistener [interval [waittime [delaytime]]]]</code></b>
  9039. <br /><br /> Definiert das Sonos interface für die Kommunikation mit dem Sonos-System.<br />
  9040. <p>
  9041. <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>
  9042. <p>
  9043. <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>
  9044. <p>
  9045. <b><code>[waittime]</code></b><br /> Hiermit wird die Wartezeit eingestellt, die nach dem Starten des SubProzesses darauf gewartet wird.</p>
  9046. <p>
  9047. <b><code>[delaytime]</code></b><br /> Hiermit kann eine Verzögerung eingestellt werden, die vor dem Starten des Netzwerks gewartet wird.</p>
  9048. <a name="SONOSset"></a>
  9049. <h4>Set</h4>
  9050. <ul>
  9051. <li><b>Grundsätzliches</b><ul>
  9052. <li><a name="SONOS_setter_RefreshShareIndex">
  9053. <b><code>RefreshShareIndex</code></b></a>
  9054. <br />Startet die Aktualisierung der Bibliothek.</li>
  9055. <li><a name="SONOS_setter_RescanNetwork">
  9056. <b><code>RescanNetwork</code></b></a>
  9057. <br />Startet die Erkennung der im Netzwerk vorhandenen Player erneut.</li>
  9058. </ul></li>
  9059. <li><b>Steuerbefehle</b><ul>
  9060. <li><a name="SONOS_setter_Mute">
  9061. <b><code>Mute &lt;state&gt;</code></b></a>
  9062. <br />Setzt den Mute-Zustand bei allen Playern.</li>
  9063. <li><a name="SONOS_setter_PauseAll">
  9064. <b><code>PauseAll</code></b></a>
  9065. <br />Pausiert die Wiedergabe in allen Zonen.</li>
  9066. <li><a name="SONOS_setter_Pause">
  9067. <b><code>Pause</code></b></a>
  9068. <br />Synonym für PauseAll.</li>
  9069. <li><a name="SONOS_setter_StopAll">
  9070. <b><code>StopAll</code></b></a>
  9071. <br />Stoppt die Wiedergabe in allen Zonen.</li>
  9072. <li><a name="SONOS_setter_Stop">
  9073. <b><code>Stop</code></b></a>
  9074. <br />Synonym für StopAll.</li>
  9075. </ul></li>
  9076. <li><b>Bookmark-Befehle</b><ul>
  9077. <li><a name="SONOS_setter_DisableBookmark">
  9078. <b><code>DisableBookmark &lt;Groupname&gt;</code></b></a>
  9079. <br />Deaktiviert die angegebene Gruppe.</li>
  9080. <li><a name="SONOS_setter_EnableBookmark">
  9081. <b><code>EnableBookmark &lt;Groupname&gt;</code></b></a>
  9082. <br />Aktiviert die angegebene Gruppe.</li>
  9083. <li><a name="SONOS_setter_LoadBookmarks">
  9084. <b><code>LoadBookmarks [Groupname]</code></b></a>
  9085. <br />Lädt die angegebene Gruppe (oder alle Gruppen, wenn nicht angegeben) aus den entsprechenden Dateien.</li>
  9086. <li><a name="SONOS_setter_SaveBookmarks">
  9087. <b><code>SaveBookmarks [Groupname]</code></b></a>
  9088. <br />Speichert die angegebene Gruppe (oder alle Gruppen, wenn nicht angegeben) in die entsprechenden Dateien.</li>
  9089. </ul></li>
  9090. <li><b>Gruppenbefehle</b><ul>
  9091. <li><a name="SONOS_setter_Groups">
  9092. <b><code>Groups &lt;GroupDefinition&gt;</code></b></a>
  9093. <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>
  9094. </ul></li>
  9095. </ul>
  9096. <a name="SONOSget"></a>
  9097. <h4>Get</h4>
  9098. <ul>
  9099. <li><b>Gruppenbefehle</b><ul>
  9100. <li><a name="SONOS_getter_Groups">
  9101. <b><code>Groups</code></b></a>
  9102. <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 />
  9103. 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>
  9104. </ul></li>
  9105. </ul>
  9106. <a name="SONOSattr"></a>
  9107. <h4>Attribute</h4>
  9108. '''Hinweis'''<br />Die Attribute werden erst bei einem Neustart von Fhem verwendet, da diese dem SubProzess initial zur Verfügung gestellt werden müssen.
  9109. <ul>
  9110. <li><b>Grundsätzliches</b><ul>
  9111. <li><a name="SONOS_attribut_coverLoadTimeout"><b><code>coverLoadTimeout &lt;value&gt;</code></b>
  9112. </a><br />Eines von (0..10,15,20,25,30). Definiert den Timeout der für die Abfrage des Covers beim Sonosplayer verwendet wird. Wenn nicht angegeben, dann wird 5 verwendet.</li>
  9113. <li><a name="SONOS_attribut_deviceRoomView"><b><code>deviceRoomView &lt;Both|DeviceLineOnly&gt;</code></b>
  9114. </a><br /> Gibt an, was in der Raumansicht zum Sonosplayer-Device angezeigt werden soll. <code>Both</code> bedeutet "normale" Devicezeile zzgl. Cover-/Titelanzeige und u.U. Steuerbereich, <code>DeviceLineOnly</code> bedeutet nur die Anzeige der "normalen" Devicezeile.</li>
  9115. <li><a name="SONOS_attribut_disable"><b><code>disable &lt;value&gt;</code></b>
  9116. </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>
  9117. <li><a name="SONOS_attribut_getFavouritesListAtNewVersion"><b><code>getFavouritesListAtNewVersion &lt;value&gt;</code></b>
  9118. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die Favoriten (bei definiertem Attribut <code>getListsDirectlyToReadings</code>) bei Aktualisierung automatisch herunterzuladen.</li>
  9119. <li><a name="SONOS_attribut_getPlaylistsListAtNewVersion"><b><code>getPlaylistsListAtNewVersion &lt;value&gt;</code></b>
  9120. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die Playlisten (bei definiertem Attribut <code>getListsDirectlyToReadings</code>) bei Aktualisierung automatisch herunterzuladen.</li>
  9121. <li><a name="SONOS_attribut_getQueueListAtNewVersion"><b><code>getQueueListAtNewVersion &lt;value&gt;</code></b>
  9122. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die aktuelle Abspielliste (bei definiertem Attribut <code>getListsDirectlyToReadings</code>) bei Aktualisierung automatisch herunterzuladen.</li>
  9123. <li><a name="SONOS_attribut_getRadiosListAtNewVersion"><b><code>getRadiosListAtNewVersion &lt;value&gt;</code></b>
  9124. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die Radioliste (bei definiertem Attribut <code>getListsDirectlyToReadings</code>) bei Aktualisierung automatisch herunterzuladen.</li>
  9125. <li><a name="SONOS_attribut_getListsDirectlyToReadings"><b><code>getListsDirectlyToReadings &lt;value&gt;</code></b>
  9126. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die Listen für Favoriten, Playlists, Radios und Queue direkt in die entsprechenden Readings zu schreiben. Dafür sind dann keine Userreadings mehr notwendig.</li>
  9127. <li><a name="SONOS_attribut_getLocalCoverArt"><b><code>getLocalCoverArt &lt;value&gt;</code></b>
  9128. </a><br />Eines von (0,1). Mit diesem Attribut kann das Modul aufgefordert werden, die Cover lokal herunterzuladen (bisheriges Standardverhalten).</li>
  9129. <li><a name="SONOS_attribut_ignoredIPs"><b><code>ignoredIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  9130. </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>
  9131. <li><a name="SONOS_attribut_pingType"><b><code>pingType &lt;string&gt;</code></b>
  9132. </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>
  9133. <li><a name="SONOS_attribut_reusePort"><b><code>reusePort &lt;int&gt;</code></b>
  9134. </a><br /> Eines von (0,1). Gibt an, ob die Portwiederwendung für SSDP aktiviert werden soll, oder nicht. Kann Restart-Probleme lösen. Wenn man diese Probleme nicht hat, sollte man das Attribut nicht setzen.</li>
  9135. <li><a name="SONOS_attribut_SubProcessLogfileName"><b><code>SubProcessLogfileName &lt;Pfad&gt;</code></b>
  9136. </a><br /> Hiermit kann für den SubProzess eine eigene Logdatei angegeben werden. Unter Windows z.B. überschreiben sich die beiden Logausgaben (von Fhem und SubProzess) sonst gegenseitig. Wenn "-" angegeben wird, wird wie bisher auf STDOUT (und damit im Fhem-Log) geloggt. Der Hauptanwendungsfall ist die mehr oder weniger kurzfristige Fehlersuche. Es werden keinerlei Variablenwerte ersetzt, und der Wert direkt als Dateiname verwendet.</li>
  9137. <li><a name="SONOS_attribut_usedonlyIPs"><b><code>usedonlyIPs &lt;IP-Adresse&gt;[,IP-Adresse]</code></b>
  9138. </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>
  9139. </ul></li>
  9140. <li><b>Bookmark-Einstellungen</b><ul>
  9141. <li><a name="SONOS_attribut_bookmarkSaveDir"><b><code>bookmarkSaveDir &lt;path&gt;</code></b>
  9142. </a><br /> Das Verzeichnis, in dem die Dateien für die gespeicherten Bookmarks abgelegt werden sollen. Wenn nicht festgelegt, dann wird "." verwendet.</li>
  9143. <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>
  9144. </a><br /> Die Definition für die Verwendung von Bookmarks für Titel.</li>
  9145. <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>
  9146. </a><br /> Die Definition für die Verwendung von Bookmarks für aktuelle Abspiellisten/Playlisten.</li>
  9147. </ul></li>
  9148. <li><b>Proxy-Einstellungen</b><ul>
  9149. <li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><b><code>generateProxyAlbumArtURLs &lt;int&gt;</code></b>
  9150. </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>
  9151. <li><a name="SONOS_attribut_proxyCacheDir"><b><code>proxyCacheDir &lt;Path&gt;</code></b>
  9152. </a><br /> Hiermit wird das Verzeichnis festgelegt, in dem die Cober zwischengespeichert werden. Wenn nicht festegelegt, so wird "/tmp" verwendet.</li>
  9153. <li><a name="SONOS_attribut_proxyCacheTime"><b><code>proxyCacheTime &lt;int&gt;</code></b>
  9154. </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>
  9155. <li><a name="SONOS_attribut_webname"><b><code>webname &lt;String&gt;</code></b>
  9156. </a><br /> Hiermit kann der zu verwendende Webname für die Cover-Link-Erzeugung angegeben werden. Da vom Modul Links zu Cover u.ä. erzeugt werden, ohne dass es einen FhemWeb-Aufruf dazu gibt, kann das Modul diesen Pfad nicht selber herausfinden. Wenn das Attribut nicht angegeben wird, dann wird 'fhem' angenommen.</li>
  9157. </ul></li>
  9158. <li><b>Sprachoptionen</b><ul>
  9159. <li><a name="SONOS_attribut_targetSpeakDir"><b><code>targetSpeakDir &lt;string&gt;</code></b>
  9160. </a><br /> Gibt an, welches Verzeichnis für die Ablage des MP3-Files der Textausgabe verwendet werden soll</li>
  9161. <li><a name="SONOS_attribut_targetSpeakMP3FileConverter"><b><code>targetSpeakMP3FileConverter &lt;string&gt;</code></b>
  9162. </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>
  9163. <li><a name="SONOS_attribut_targetSpeakMP3FileDir"><b><code>targetSpeakMP3FileDir &lt;string&gt;</code></b>
  9164. </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>
  9165. <li><a name="SONOS_attribut_targetSpeakURL"><b><code>targetSpeakURL &lt;string&gt;</code></b>
  9166. </a><br /> Gibt an, unter welcher Adresse der ZonePlayer das unter targetSpeakDir angegebene Verzeichnis erreichen kann.</li>
  9167. <li><a name="SONOS_attribut_targetSpeakFileTimestamp"><b><code>targetSpeakFileTimestamp &lt;int&gt;</code></b>
  9168. </a><br /> One of (0, 1). Gibt an, ob die erzeugte MP3-Sprachausgabedatei einen Zeitstempel erhalten soll (1) oder nicht (0).</li>
  9169. <li><a name="SONOS_attribut_targetSpeakFileHashCache"><b><code>targetSpeakFileHashCache &lt;int&gt;</code></b>
  9170. </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>
  9171. <li><a name="SONOS_attribut_Speak1"><b><code>Speak1 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  9172. </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>
  9173. <li><a name="SONOS_attribut_Speak2"><b><code>Speak2 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  9174. </a><br />Siehe Speak1</li>
  9175. <li><a name="SONOS_attribut_Speak3"><b><code>Speak3 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  9176. </a><br />Siehe Speak1</li>
  9177. <li><a name="SONOS_attribut_Speak4"><b><code>Speak4 &lt;Fileextension&gt;:&lt;Commandline&gt;</code></b>
  9178. </a><br />Siehe Speak1</li>
  9179. <li><a name="SONOS_attribut_SpeakCover"><b><code>SpeakCover &lt;Absolute-Imagepath&gt;</code></b>
  9180. </a><br />Hiermit kann ein JPG- oder PNG-Bild als Cover für die Sprachdurchsagen definiert werden.</li>
  9181. <li><a name="SONOS_attribut_Speak1Cover"><b><code>Speak1Cover &lt;Absolute-Imagepath&gt;</code></b>
  9182. </a><br />Analog zu SpeakCover für Speak1.</li>
  9183. <li><a name="SONOS_attribut_Speak2Cover"><b><code>Speak2Cover &lt;Absolute-Imagepath&gt;</code></b>
  9184. </a><br />Analog zu SpeakCover für Speak2.</li>
  9185. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Absolute-Imagepath&gt;</code></b>
  9186. </a><br />Analog zu SpeakCover für Speak3.</li>
  9187. <li><a name="SONOS_attribut_Speak3Cover"><b><code>Speak3Cover &lt;Absolute-Imagepath&gt;</code></b>
  9188. </a><br />Analog zu SpeakCover für Speak3.</li>
  9189. <li><a name="SONOS_attribut_Speak4Cover"><b><code>Speak4Cover &lt;Absolute-Imagepath&gt;</code></b>
  9190. </a><br />Analog zu SpeakCover für Speak4.</li>
  9191. <li><a name="SONOS_attribut_SpeakGoogleURL"><b><code>SpeakGoogleURL &lt;GoogleURL&gt;</code></b>
  9192. </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>
  9193. </ul></li>
  9194. </ul>
  9195. =end html_DE
  9196. =cut