57_Calendar.pm 145 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234
  1. # $Id: 57_Calendar.pm 17531 2018-10-14 16:19:52Z neubert $
  2. ##############################################################################
  3. #
  4. # 57_Calendar.pm
  5. # Copyright by Dr. Boris Neubert
  6. # e-mail: omega at online dot de
  7. #
  8. # This file is part of fhem.
  9. #
  10. # Fhem is free software: you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation, either version 2 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # Fhem is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License
  21. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  22. #
  23. ##############################################################################
  24. use strict;
  25. use warnings;
  26. use HttpUtils;
  27. use Storable qw(freeze thaw);
  28. use POSIX qw(strftime);
  29. ##############################################
  30. package main;
  31. no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  32. #
  33. # *** Potential isses:
  34. #
  35. # There might be issues when turning to daylight saving time and back that
  36. # need further investigation. For counterpart please see
  37. # http://forum.fhem.de/index.php?topic=18707
  38. # http://forum.fhem.de/index.php?topic=15827
  39. #
  40. # *** Potential future extensions:
  41. #
  42. # sequence of events fired sorted by time
  43. # http://forum.fhem.de/index.php?topic=29112
  44. #
  45. # document ownCloud ical use
  46. # http://forum.fhem.de/index.php?topic=28667
  47. #
  48. =for comment
  49. RFC
  50. ---
  51. https://tools.ietf.org/html/rfc5545
  52. Data structures
  53. ---------------
  54. We call a set of calendar events (short: events) a series, even for sets
  55. consisting only of a single event. A series may consist of only one single
  56. event, a series of regularly reccuring events and reccuring events with
  57. exceptions. A series is identified by a UID.
  58. *** VEVENT record, class ICal::Entry
  59. In the iCalendar, a series is represented by one or more VEVENT records.
  60. The unique key for a VEVENT record is UID, RECURRENCE-ID (3.8.4.4, p. 112) and
  61. SEQUENCE (3.8.7.4, p. 138).
  62. The internal primary key for a VEVENT is ID.
  63. FHEM keeps a set of VEVENT records (record set). When the calendar is updated,
  64. a new record set is retrieved from the iCalendar and updates the old record set
  65. to form the resultant record set.
  66. A record in the resultant record set can be in exactly one of these states:
  67. - deleted:
  68. a record from the old record set for which no record with the same
  69. (UID, RECURRENCE-ID) was in the new record set.
  70. - new:
  71. a record from the new record set for which no record with same
  72. (UID, RECURRENCE-ID) was in the old record set.
  73. - changed-old:
  74. a record from the old record set for which a record with the same
  75. (UID, RECURRENCE-ID) but different SEQUENCE was in the new record
  76. set.
  77. - changed-new:
  78. a record from the new record set for which a record with the same
  79. (UID, RECURRENCE-ID) but different SEQUENCE was in the old record
  80. set.
  81. - known:
  82. a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the
  83. old and in the new record set and both records have the same
  84. LAST-MODIFIED. The record from the old record set was
  85. kept and the record from the new record set was discarded.
  86. - modified-new:
  87. a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the
  88. old and in the new record set and both records differ in
  89. LAST-MODIFIED. This is the record from the new record set.
  90. - modified-old:
  91. a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the
  92. old and in the new record set and both records differ in
  93. LAST-MODIFIED. This is the record from the old record set.
  94. Records in states modified-old and changed-old refer to the corresponding records
  95. in states modified-new and change-new, and vice versa.
  96. Records in state deleted, modified-old or changed-old are removed upon
  97. the next update. They are said to be "obsolete".
  98. A record is said to be "recurring" if it has a RRULE property.
  99. A record is said to be an "exception" if it has a RECURRENCE-ID property.
  100. Each records has a set of events attached.
  101. *** calendar event, class Calendar::Event
  102. Events are attached to single records (VEVENTs).
  103. The uid of the event is the UID of the record with all non-alphanumerical
  104. characters removed.
  105. At a given point in time t, an event is in exactly one of these modes:
  106. - upcoming:
  107. the start time of the event is in the future
  108. - alarm:
  109. alarm time <= t < start time for any of the alarms for the event
  110. - start:
  111. start time <= t <= end time of the event
  112. - end:
  113. end time < t
  114. An event is said to be "changed", when its mode has changed during the most
  115. recent run of calendar event processing.
  116. An event is said to be "hidden", when
  117. - it was in mode end and end time of the event < t - horizonPast, or
  118. - it was in mode upcoming and start time of the event > t + horizonFuture
  119. at the most recent run of calendar event processing. horizonPast defaults to 0,
  120. horizonFuture defaults to 366 days.
  121. Processing of iCalendar
  122. -----------------------
  123. *** Initial situation:
  124. We have an old record set of VEVENTs. It is empty on a restart of FHEM or upon
  125. issueing the "set ... reload" command.
  126. *** Step 1: Retrieval of new record set (Calendar_GetUpdate)
  127. 1) The iCalendar is downloaded from its location into FHEM memory.
  128. 2) It is parsed into a new record set of VEVENTs.
  129. *** Step 2: Update of internal record set (Calendar_UpdateCalendar)
  130. 1) All records in the old record set that are in state deleted or obsolete are
  131. removed.
  132. 2) All states of all records in the old record set are set to blank.
  133. 3) The old and new record sets are merged to create a resultant record set
  134. according to the following procedure:
  135. If the new record set contains a record with the same (UID, RECURRENCE-ID,
  136. SEQUENCE) as a record in the old record set:
  137. - if the two records differ in LAST-MODIFIED, then both records
  138. are kept. The state of the record from the old record set is set to
  139. modified-old, the state of the record from the new record set is set to
  140. modified-new.
  141. - else the record from the old record set is kept, state set to known, and the
  142. record from the new record set is discarded.
  143. If the new record set contains a record with the same (UID, RECURRENCE-ID) but
  144. different SEQUENCE as a record in the old record set, then both records are
  145. kept. The state of the record from the new record set is set to changed-new,
  146. and the state of record from the old record set is set to changed-old.
  147. If the new record set contains a record that differs from any record in the old
  148. record set by both UID and RECURRENCE-ID, the record from the new record set
  149. id added to the resultant record set and its state is set to new.
  150. 4) The state of all records in the old record set that have not been touched
  151. in 3) are set to deleted.
  152. Notes:
  153. - This procedure favors records from the new record set over records from the
  154. old record set, even if the SEQUENCE is lower or LAST-MODIFIED is earlier.
  155. - DTSTAMP is the time stamp of the creation of the iCalendar entry. For Google
  156. Calendar it is the time stamp of the latest retrieval of the calendar.
  157. *** Step 3: Update of calendar events (Calendar_UpdateCalendar)
  158. We walk over all records and treat the corresponding events according to
  159. the state of the record:
  160. - deleted, changed-old, modified-old:
  161. all events are removed
  162. - new, changed-new, modified-new:
  163. all events are removed and events are created anew
  164. - known:
  165. all events are left alone
  166. No events older than 400 days or more than 400 days in the future will be
  167. created.
  168. Creation of events in a series works as follows:
  169. If we have several events in a series, the main series has the RRULE tag and
  170. the exceptions have RECURRENCE-IDs. The RECURRENCE-ID match the start
  171. dates in the series created from the RRULE that need to be exempted. We
  172. therefore collect all events from records with same UID and RECURRENCE-ID set
  173. as they form the list of records with the exceptions for the UID.
  174. Before the regular creation is done, events for RDATEs are added as long as
  175. an RDATE is not superseded by an EXDATE. An RDATE takes precedence over a
  176. regularly created recurring event.
  177. Starting with the start date of the series, one event is created after the
  178. other. Creation stops when the series ends or when an event more than 400 days
  179. in the future has been created. If the event is in the list of exceptions
  180. (either defined by other events with same UID and a RECURRENCE-ID or by the
  181. EXDATE property), it is not added.
  182. What attributes are recognized and which of these are honored or ignored?
  183. The following frequencies (FREQ) are recognized and honored:
  184. SECONDLY
  185. MINUTELY
  186. HOURLY
  187. DAILY
  188. WEEKLY
  189. BYDAY: recognizes and honors one or several weekdays without prefix (e.g. -1SU, 2MO)
  190. MONTHLY
  191. BYDAY: recognizes and honors one or several weekdays with and without prefix (e.g. -1SU, 2MO)
  192. BYMONTHDAY: recognized but ignored
  193. BYMONTH: recognized but ignored
  194. YEARLY
  195. For all of the above:
  196. INTERVAL: recognized and honored
  197. UNTIL: recognized and honored
  198. COUNT: recognized and honored
  199. WKST: recognized but ignored
  200. EXDATE: recognized and honored
  201. RDATE: recognized and honored
  202. *** Step 4: The device readings related to updates are set
  203. - calname
  204. - lastUpdate
  205. - nextUpdate
  206. - nextWakeup
  207. - state
  208. Note: the state... readings from the previous version of this module (2015 and
  209. earlier) are not available any more.
  210. Processing of calendar events
  211. -----------------------------
  212. Calendar_CheckTimes
  213. In case of a series of calendar events, several calendar events may exist for
  214. the same uid which may be in different modes. Therefore only the most
  215. interesting mode is chosen over any other mode of any calendar event with
  216. the same uid. The most interesting mode is the first applicable from the
  217. following list:
  218. - start
  219. - alarm
  220. - upcoming
  221. - end
  222. Apart from these actual modes, virtual modes apply:
  223. - changed: the actual mode has changed during this call of Calendar_CheckTimes
  224. - alarmed: modes are alarm and changed
  225. - started: modes are start and changed
  226. - ended: modes are end and changed
  227. - alarm or start: mode is alarm or start
  228. If the mode has changed to <mode>, the following FHEM events are created:
  229. changed uid <mode>
  230. <mode> uid
  231. Note: there is no colon in these FHEM events.
  232. Program flow
  233. ------------
  234. Calendar_Initialize sets the Calendar_Notify to watch for notifications.
  235. Calendar_Notify acts on the INITIALIZED and REREADCFG events by starting the
  236. timer to call Calendar_Wakeup between 10 and 29 seconds after the
  237. notification.
  238. Calendar_Wakeup starts a processing run.
  239. It sets the current time t as baseline for process.
  240. If the time for the next update has been reached,
  241. Calendar_GetUpdate is called
  242. else
  243. Calendar_CheckTimes
  244. Calendar_RearmTimer
  245. are called.
  246. Calendar_GetUpdate retrieves the iCal file. If the source is url, this is
  247. done asynchronously. Upon successfull retrieval of the iCal file, we
  248. continue with Calendar_ProcessUpdate.
  249. Calendar_ProcessUpdate calls
  250. Calendar_UpdateCalendar
  251. Calendar_CheckTimes
  252. Calendar_RearmTimer
  253. in sequence.
  254. Calendar_UpdateCalendar updates the VEVENT records in the
  255. $hash->{".fhem"}{vevents} hash and creates the associated calendar events.
  256. Calendar_CheckTimes checks for a mode change of the calendar events and
  257. creates the readings and FHEM events.
  258. Calendar_RearmTimer sets the timer to call Calendar_Wakeup to time of the
  259. next mode change or update, whatever comes earlier.
  260. What's new?
  261. -----------
  262. This module version replaces the 2015 version that has been widely. Noteworthy
  263. changes
  264. - No more state... readings; "all" reading has been removed as well.
  265. - The mode... readings (modeAlarm, modeAlarmOrStart, etc.) are deprecated
  266. and will be removed in a future version. Use the mode=<regex> filter instead.
  267. - Handles recurring calendar events with out-of-order events and exceptions
  268. (EXDATE).
  269. - Keeps ALL calendar events within plus/minus 400 days from the date of the
  270. in FHEM: this means that you can have more than one calendar event with the
  271. same UID.
  272. - You can restrict visible calendar events with attributes hideLaterThan,
  273. hideOlderThan.
  274. - Nonblocking retrieval of calendar from URL.
  275. - New get commands:
  276. get <name> vevents
  277. get <name> vcalendar
  278. get <name> <format> <mode>
  279. get <name> <format> mode=<regex>
  280. get <name> <format> uid=<regex>
  281. - The get commands
  282. get <name> <format> ...
  283. may not work as before since several calendar events may exist for a
  284. single UID, particularly the get command
  285. get <name> <format> all
  286. show all calendar events from a series (past, current, and future); you
  287. probably want to replace "all" by "next":
  288. get <name> <format> next
  289. to get only the first (not past but current or future) calendar event from
  290. each series.
  291. - Migration hints:
  292. Replace
  293. get <name> <format> all
  294. by
  295. get <name> <format> next
  296. Replace
  297. get <name> <format> <uid>
  298. by
  299. get <name> <format> uid=<uid> 1
  300. Replace
  301. get <name> <format> modeAlarmOrStart
  302. by
  303. get <name> <format> mode=alarm|start
  304. - The FHEM events created for mode changes of single calendar events have been
  305. amended:
  306. changed: UID <mode>
  307. <mode>: UID (this is new)
  308. <mode> is the current mode of the calendar event after the change. It is
  309. highly advisable to trigger actions based on these FHEM events instead of
  310. notifications for changes of the mode... readings.
  311. =cut
  312. #####################################
  313. #
  314. # Event
  315. #
  316. #####################################
  317. package Calendar::Event;
  318. sub new {
  319. my $class= shift;
  320. my $self= {}; # I am a hash
  321. bless $self, $class;
  322. $self->{_previousMode}= "undefined";
  323. $self->{_mode}= "undefined";
  324. return($self);
  325. }
  326. sub uid {
  327. my ($self)= @_;
  328. return $self->{uid};
  329. }
  330. sub start {
  331. my ($self)= @_;
  332. return $self->{start};
  333. }
  334. sub end {
  335. my ($self)= @_;
  336. return $self->{end};
  337. }
  338. sub setNote($$) {
  339. my ($self,$note)= @_;
  340. $self->{_note}= $note;
  341. return $note;
  342. }
  343. sub getNote($) {
  344. my ($self)= @_;
  345. return $self->{_note};
  346. }
  347. sub hasNote($) {
  348. my ($self)= @_;
  349. return defined($self->{_note}) ? 1 : 0;
  350. }
  351. sub setMode {
  352. my ($self,$mode)= @_;
  353. $self->{_previousMode}= $self->{_mode};
  354. $self->{_mode}= $mode;
  355. #main::Debug "After setMode $mode: Modes(" . $self->uid() . ") " . $self->{_previousMode} . " -> " . $self->{_mode};
  356. return $mode;
  357. }
  358. sub setModeUnchanged {
  359. my ($self)= @_;
  360. $self->{_previousMode}= $self->{_mode};
  361. }
  362. sub getMode {
  363. my ($self)= @_;
  364. return $self->{_mode};
  365. }
  366. sub lastModified {
  367. my ($self)= @_;
  368. return $self->{lastModified};
  369. }
  370. sub modeChanged {
  371. my ($self)= @_;
  372. return (($self->{_mode} ne $self->{_previousMode}) and
  373. ($self->{_previousMode} ne "undefined")) ? 1 : 0;
  374. }
  375. sub summary {
  376. my ($self)= @_;
  377. return $self->{summary};
  378. }
  379. sub location {
  380. my ($self)= @_;
  381. return $self->{location};
  382. }
  383. sub description {
  384. my ($self)= @_;
  385. return $self->{description};
  386. }
  387. sub categories {
  388. my ($self)= @_;
  389. return $self->{categories};
  390. }
  391. sub classfication {
  392. my ($self)= @_;
  393. return $self->{classification};
  394. }
  395. sub ts {
  396. my ($self,$tm,$tf)= @_;
  397. return "" unless($tm);
  398. $tf= $tf // "%d.%m.%Y %H:%M";
  399. return POSIX::strftime($tf, localtime($tm));
  400. }
  401. sub ts0 {
  402. my ($self,$tm)= @_;
  403. return $self->ts($tm, "%d.%m.%y %H:%M");
  404. }
  405. # duration as friendly string
  406. sub td {
  407. # 20d
  408. # 47h
  409. # 5d 12h
  410. # 8d 4:22'04
  411. #
  412. my ($self, $d)= @_;
  413. return "" unless defined($d);
  414. my $s= $d % 60; $d-= $s; $d/= 60;
  415. my $m= $d % 60; $d-= $m; $d/= 60;
  416. my $h= $d % 24; $d-= $h; $d/= 24;
  417. if(24*$d+$h<= 72) { $h+= 24*$d; $d= 0; }
  418. my @r= ();
  419. push @r, sprintf("%dd", $d) if $d> 0;
  420. if($m>0 || $s>0) {
  421. my $t= sprintf("%d:%02d", $h, $m);
  422. $t+= sprintf("\'%02d", $s) if $s> 0;
  423. push @r, $t;
  424. } else {
  425. push @r, sprintf("%dh", $h) if $h> 0;
  426. }
  427. return join(" ", @r);
  428. }
  429. sub asText {
  430. my ($self)= @_;
  431. return sprintf("%s %s",
  432. $self->ts0($self->{start}),
  433. $self->{summary}
  434. );
  435. }
  436. sub asFull {
  437. my ($self)= @_;
  438. return sprintf("%s %9s %s %s-%s %s %s %s",
  439. $self->uid(),
  440. $self->getMode(),
  441. $self->{alarm} ? $self->ts($self->{alarm}) : " ",
  442. $self->ts($self->{start}),
  443. $self->ts($self->{end}),
  444. $self->{summary},
  445. $self->{categories},
  446. $self->{location}
  447. );
  448. }
  449. sub asDebug {
  450. my ($self)= @_;
  451. return sprintf("%s %s %9s %s %s-%s %s %s %s %s",
  452. $self->uid(),
  453. $self->modeChanged() ? "*" : " ",
  454. $self->getMode(),
  455. $self->{alarm} ? $self->ts($self->{alarm}) : " ",
  456. $self->ts($self->{start}),
  457. $self->ts($self->{end}),
  458. $self->{summary},
  459. $self->{categories},
  460. $self->{location},
  461. $self->hasNote() ? $self->getNote() : ""
  462. );
  463. }
  464. sub formatted {
  465. my ($self, $format, $timeformat)= @_;
  466. my $t1= $self->{start};
  467. my $T1= defined($t1) ? $self->ts($t1, $timeformat) : "";
  468. my $t2= $self->{end};
  469. my $T2= defined($t2) ? $self->ts($t2, $timeformat) : "";
  470. my $a= $self->{alarm};
  471. my $A= defined($a) ? $self->ts($a, $timeformat) : "";
  472. my $S= $self->{summary}; $S=~s/\\,/,/g;
  473. my $L= $self->{location}; $L=~s/\\,/,/g;
  474. my $CA= $self->{categories};
  475. my $CL= $self->{classification};
  476. my $DS= $self->{description}; $DS=~s/\\,/,/g;
  477. my $d= defined($t1) && defined($t2) ? $t2-$t1 : undef;
  478. my $D= defined($d) ? $self->td($d) : "";
  479. my $U= $self->uid();
  480. my $M= sprintf("%9s", $self->getMode());
  481. my $r= eval $format;
  482. $r= $@ if $@;
  483. return $r;
  484. }
  485. sub alarmTime {
  486. my ($self)= @_;
  487. return $self->ts($self->{alarm});
  488. }
  489. sub startTime {
  490. my ($self)= @_;
  491. return $self->ts($self->{start});
  492. }
  493. sub endTime {
  494. my ($self)= @_;
  495. return $self->ts($self->{end});
  496. }
  497. # returns 1 if time is before alarm time and before start time, else 0
  498. sub isUpcoming {
  499. my ($self,$t) = @_;
  500. return 0 unless defined($t);
  501. if($self->{alarm}) {
  502. return $t< $self->{alarm} ? 1 : 0;
  503. } else {
  504. return $t< $self->{start} ? 1 : 0;
  505. }
  506. }
  507. # returns 1 if time is between alarm time and start time, else 0
  508. sub isAlarmed {
  509. my ($self,$t) = @_;
  510. return $self->{alarm} ?
  511. (($self->{alarm}<= $t && $t< $self->{start}) ? 1 : 0) : 0;
  512. }
  513. # return 1 if time is between start time and end time, else 0
  514. sub isStarted {
  515. my ($self,$t) = @_;
  516. return 0 unless(defined($self->{start}));
  517. return 0 if($t < $self->{start});
  518. if(defined($self->{end})) {
  519. return 0 if($t>= $self->{end});
  520. }
  521. return 1;
  522. }
  523. sub isSeries {
  524. my ($self)= @_;
  525. #main::Debug " freq= " . $self->{freq};
  526. return exists($self->{freq}) ? 1 : 0;
  527. }
  528. sub isAfterSeriesEnded {
  529. my ($self,$t) = @_;
  530. #main::Debug " isSeries? " . $self->isSeries();
  531. return 0 unless($self->isSeries());
  532. #main::Debug " until= " . $self->{until};
  533. return 0 unless(exists($self->{until}));
  534. #main::Debug " has until!";
  535. return $self->{until}< $t ? 1 : 0;
  536. }
  537. sub isEnded {
  538. my ($self,$t) = @_;
  539. #main::Debug "isEnded for " . $self->asFull();
  540. #main::Debug " isAfterSeriesEnded? " . $self->isAfterSeriesEnded($t);
  541. #return 1 if($self->isAfterSeriesEnded($t));
  542. #main::Debug " has end? " . (defined($self->{end}) ? 1 : 0);
  543. return 0 unless(defined($self->{end}) && defined($t));
  544. return $self->{end}<= $t ? 1 : 0;
  545. }
  546. sub nextTime {
  547. my ($self,$t) = @_;
  548. my @times= ( );
  549. push @times, $self->{start} if(defined($self->{start}));
  550. push @times, $self->{end} if(defined($self->{end}));
  551. unshift @times, $self->{alarm} if($self->{alarm});
  552. if(defined($t)) {
  553. @times= sort grep { $_ > $t } @times;
  554. } else {
  555. @times= sort @times;
  556. }
  557. # main::Debug "Calendar: " . $self->asFull();
  558. # main::Debug "Calendar: Start " . main::FmtDateTime($self->{start});
  559. # main::Debug "Calendar: End " . main::FmtDateTime($self->{end});
  560. # main::Debug "Calendar: Alarm " . main::FmtDateTime($self->{alarm}) if($self->{alarm});
  561. # main::Debug "Calendar: times[0] " . main::FmtDateTime($times[0]);
  562. # main::Debug "Calendar: times[1] " . main::FmtDateTime($times[1]);
  563. # main::Debug "Calendar: times[2] " . main::FmtDateTime($times[2]);
  564. if(@times) {
  565. return $times[0];
  566. } else {
  567. return undef;
  568. }
  569. }
  570. #####################################
  571. #
  572. # Events
  573. #
  574. #####################################
  575. package Calendar::Events;
  576. sub new {
  577. my $class= shift;
  578. my $self= []; # I am an array
  579. bless $self, $class;
  580. return($self);
  581. }
  582. sub addEvent($$) {
  583. my ($self,$event)= @_;
  584. return push @{$self}, $event;
  585. }
  586. sub clear($) {
  587. my ($self)= @_;
  588. return @{$self}= ();
  589. }
  590. #####################################
  591. #
  592. # ICal
  593. # the ical format is governed by RFC2445 http://www.ietf.org/rfc/rfc2445.txt
  594. #
  595. #####################################
  596. package ICal::Entry;
  597. sub getNextMonthlyDateByDay($$$);
  598. sub new($$) {
  599. my $class= shift;
  600. my ($type)= @_;
  601. #main::Debug "new ICal::Entry $type";
  602. my $self= {};
  603. bless $self, $class;
  604. $self->{type}= $type;
  605. #$self->clearState(); set here:
  606. $self->{state}= "<none>";
  607. #$self->clearCounterpart(); unnecessary
  608. #$self->clearReferences(); set here:
  609. $self->{references}= [];
  610. #$self->clearTags(); unnecessary
  611. $self->{entries}= []; # array of subordinated ICal::Entry
  612. $self->{events}= Calendar::Events->new();
  613. $self->{skippedEvents}= Calendar::Events->new();
  614. return($self);
  615. }
  616. #
  617. # keys, properties, values
  618. #
  619. # is key a repeated property?
  620. sub isMultiple($$) {
  621. my ($self,$key)= @_;
  622. return $self->{properties}{$key}{multiple};
  623. }
  624. # has a property named key?
  625. sub hasKey($$) {
  626. my ($self,$key)= @_;
  627. return exists($self->{properties}{$key}) ? 1 : 0;
  628. }
  629. # value for single property key
  630. sub value($$) {
  631. my ($self,$key)= @_;
  632. return undef if($self->isMultiple($key));
  633. return $self->{properties}{$key}{VALUE};
  634. }
  635. # value for property key or default, if non-existant
  636. sub valueOrDefault($$$) {
  637. my ($self,$key,$default)= @_;
  638. return $self->hasKey($key) ? $self->value($key) : $default;
  639. }
  640. # value for multiple property key (array counterpart)
  641. sub values($$) {
  642. my ($self,$key)= @_;
  643. return undef unless($self->isMultiple($key));
  644. return $self->{properties}{$key}{VALUES};
  645. }
  646. # true, if the property exists at both entries and have the same value
  647. # or neither entry has this property
  648. sub sameValue($$$) {
  649. my ($self,$other,$key)= @_;
  650. my $value1= $self->hasKey($key) ? $self->value($key) : "";
  651. my $value2= $other->hasKey($key) ? $other->value($key) : "";
  652. return $value1 eq $value2;
  653. }
  654. sub parts($$) {
  655. my ($self,$key)= @_;
  656. return split(";", $self->{properties}{$key}{PARTS});
  657. }
  658. #
  659. # state
  660. #
  661. sub setState {
  662. my ($self,$state)= @_;
  663. $self->{state}= $state;
  664. return $state;
  665. }
  666. sub clearState {
  667. my ($self)= @_;
  668. $self->{state}= "<none>";
  669. }
  670. sub state($) {
  671. my($self)= @_;
  672. return $self->{state};
  673. }
  674. sub inState($$) {
  675. my($self, $state)= @_;
  676. return ($self->{state} eq $state ? 1 : 0);
  677. }
  678. sub isObsolete($) {
  679. my($self)= @_;
  680. # VEVENT records in these states are obsolete
  681. my @statesObsolete= qw/deleted changed-old modified-old/;
  682. return $self->state() ~~ @statesObsolete ? 1 : 0;
  683. }
  684. sub hasChanged($) {
  685. my($self)= @_;
  686. # VEVENT records in these states have changed
  687. my @statesChanged= qw/new changed-new modified-new/;
  688. return $self->state() ~~ @statesChanged ? 1 : 0;
  689. }
  690. #
  691. # type
  692. #
  693. sub type($) {
  694. my($self)= @_;
  695. return $self->{type};
  696. }
  697. #
  698. # counterpart, for changed or modified records
  699. #
  700. sub counterpart($) {
  701. my($self)= @_;
  702. return $self->{counterpart};
  703. }
  704. sub setCounterpart($$) {
  705. my ($self, $id)= @_;
  706. $self->{counterpart}= $id;
  707. return $id;
  708. }
  709. sub hasCounterpart($) {
  710. my($self)= @_;
  711. return (defined($self->{counterpart}) ? 1 : 0);
  712. }
  713. sub clearCounterpart($) {
  714. my($self)= @_;
  715. delete $self->{counterpart} if(defined($self->{counterpart}));
  716. }
  717. #
  718. # series
  719. #
  720. sub isRecurring($) {
  721. my($self)= @_;
  722. return $self->hasKey("RRULE");
  723. }
  724. sub isException($) {
  725. my($self)= @_;
  726. return $self->hasKey("RECURRENCE-ID");
  727. }
  728. sub isCancelled($) {
  729. my($self)= @_;
  730. return (($self->valueOrDefault("STATUS","CONFIRMED") eq "CANCELLED") ? 1 : 0);
  731. }
  732. sub hasReferences($) {
  733. my($self)= @_;
  734. return scalar(@{$self->references()});
  735. }
  736. sub references($) {
  737. my($self)= @_;
  738. return $self->{references};
  739. }
  740. sub clearReferences($) {
  741. my($self)= @_;
  742. $self->{references}= [];
  743. }
  744. #
  745. # tags
  746. #
  747. # sub tags($) {
  748. # my($self)= @_;
  749. # return $self->{tags};
  750. # }
  751. #
  752. # sub clearTags($) {
  753. # my($self)= @_;
  754. # $self->{tags}= [];
  755. # }
  756. #
  757. # sub tagAs($$) {
  758. # my ($self, $tag)= @_;
  759. # push @{$self->{tags}}, $tag unless($self->isTaggedAs($tag));
  760. # }
  761. #
  762. # sub isTaggedAs($$) {
  763. # my ($self, $tag)= @_;
  764. # return grep { $_ eq $tag } @{$self->{tags}} ? 1 : 0;
  765. # }
  766. #
  767. # sub numTags($) {
  768. # my ($self)= @_;
  769. # return scalar @{$self->{tags}};
  770. # }
  771. #
  772. # parsing
  773. #
  774. sub addproperty($$) {
  775. my ($self,$line)= @_;
  776. # contentline = name *(";" param ) ":" value CRLF [Page 13]
  777. # example:
  778. # TRIGGER;VALUE=DATE-TIME:20120531T150000Z
  779. #main::Debug "line=\'$line\'";
  780. # for DTSTART, DTEND there are several variants:
  781. # DTSTART;TZID=Europe/Berlin:20140205T183600
  782. # * DTSTART;TZID="(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna":20140904T180000
  783. # DTSTART:20140211T212000Z
  784. # DTSTART;VALUE=DATE:20130619
  785. my ($key,$parts,$parameter);
  786. if($line =~ /^([\w\d\-]+)(;(.*))?:(.*)$/) {
  787. $key= $1;
  788. $parts= $3 // "";
  789. $parameter= $4 // "";
  790. } else {
  791. return;
  792. }
  793. return unless($key);
  794. #main::Debug "addproperty for key $key";
  795. # ignore some properties
  796. # commented out: it is faster to add the property than to do the check
  797. # return if(($key eq "ATTENDEE") or ($key eq "TRANSP") or ($key eq "STATUS"));
  798. return if(substr($key,0,2) eq "^X-");
  799. if(($key eq "RDATE") or ($key eq "EXDATE")) {
  800. #main::Debug "addproperty for dates";
  801. # handle multiple properties
  802. my @values;
  803. @values= @{$self->values($key)} if($self->hasKey($key));
  804. push @values, $parameter;
  805. #main::Debug "addproperty pushed parameter $parameter to key $key";
  806. $self->{properties}{$key}= {
  807. multiple => 1,
  808. VALUES => \@values,
  809. }
  810. } else {
  811. # handle single properties
  812. $self->{properties}{$key}= {
  813. multiple => 0,
  814. PARTS => "$parts",
  815. VALUE => "$parameter",
  816. }
  817. };
  818. }
  819. sub parse($$) {
  820. my ($self,$ics)= @_;
  821. # This is the proper way to do it, with \R corresponding to (?>\r\n|\n|\x0b|\f|\r|\x85|\x2028|\x2029)
  822. # my @ical= split /\R/, $ics;
  823. # Tt does not treat some unicode emojis correctly, though.
  824. # We thus go for the the DOS/Windows/Unix/Mac classic variants.
  825. # Suggested reading:
  826. # http://stackoverflow.com/questions/3219014/what-is-a-cross-platform-regex-for-removal-of-line-breaks
  827. my @ical= split /(?>\r\n|[\r\n])/, $ics;
  828. return $self->parseSub(0, \@ical);
  829. }
  830. sub parseSub($$$) {
  831. my ($self,$ln,$icalref)= @_;
  832. my $len= scalar @$icalref;
  833. #main::Debug "lines= $len";
  834. #main::Debug "ENTER @ $ln";
  835. while($ln< $len) {
  836. my $line= $$icalref[$ln];
  837. $ln++;
  838. # check for and handle continuation lines (4.1 on page 12)
  839. while($ln< $len) {
  840. my $line1= $$icalref[$ln];
  841. last if(substr($line1,0,1) ne " ");
  842. $line.= substr($line1,1);
  843. $ln++;
  844. };
  845. #main::Debug "$ln: $line";
  846. next if($line eq ""); # ignore empty line
  847. last if(substr($line,0,4) eq "END:");
  848. if(substr($line,0,6) eq "BEGIN:") {
  849. my $entry= ICal::Entry->new(substr($line,6));
  850. $entry->{ln}= $ln;
  851. push @{$self->{entries}}, $entry;
  852. $ln= $entry->parseSub($ln,$icalref);
  853. } else {
  854. $self->addproperty($line);
  855. }
  856. }
  857. #main::Debug "BACK";
  858. return $ln;
  859. }
  860. #
  861. # events
  862. #
  863. sub events($) {
  864. my ($self)= @_;
  865. return $self->{events};
  866. }
  867. sub clearEvents($) {
  868. my ($self)= @_;
  869. $self->{events}->clear();
  870. }
  871. sub numEvents($) {
  872. my ($self)= @_;
  873. return scalar(@{$self->{events}});
  874. }
  875. sub addEvent($$) {
  876. my ($self, $event)= @_;
  877. $self->{events}->addEvent($event);
  878. }
  879. sub skippedEvents($) {
  880. my ($self)= @_;
  881. return $self->{skippedEvents};
  882. }
  883. sub clearSkippedEvents($) {
  884. my ($self)= @_;
  885. $self->{skippedEvents}->clear();
  886. }
  887. sub numSkippedEvents($) {
  888. my ($self)= @_;
  889. return scalar(@{$self->{skippedEvents}});
  890. }
  891. sub addSkippedEvent($$) {
  892. my ($self, $event)= @_;
  893. $self->{skippedEvents}->addEvent($event);
  894. }
  895. sub createEvent($) {
  896. my ($self)= @_;
  897. my $event= Calendar::Event->new();
  898. $event->{uid}= $self->value("UID");
  899. $event->{uid}=~ s/\W//g; # remove all non-alphanumeric characters, this makes life easier for perl specials
  900. return $event;
  901. }
  902. # converts a date/time string to the number of non-leap seconds since the epoch
  903. # 20120520T185202Z: date/time string in ISO8601 format, time zone GMT
  904. # 20121129T222200: date/time string in ISO8601 format, time zone local
  905. # 20120520: a date string has no time zone associated
  906. sub tm($$) {
  907. my ($self, $t)= @_;
  908. return undef if(!$t);
  909. #main::Debug "convert >$t<";
  910. my ($year,$month,$day)= (substr($t,0,4), substr($t,4,2),substr($t,6,2));
  911. if(length($t)>8) {
  912. my ($hour,$minute,$second)= (substr($t,9,2), substr($t,11,2),substr($t,13,2));
  913. my $z;
  914. $z= substr($t,15,1) if(length($t) == 16);
  915. #main::Debug "$day.$month.$year $hour:$minute:$second $z";
  916. if($z) {
  917. return main::fhemTimeGm($second,$minute,$hour,$day,$month-1,$year-1900);
  918. } else {
  919. return main::fhemTimeLocal($second,$minute,$hour,$day,$month-1,$year-1900);
  920. }
  921. } else {
  922. #main::Debug "$day.$month.$year";
  923. return main::fhemTimeLocal(0,0,0,$day,$month-1,$year-1900);
  924. }
  925. }
  926. # DURATION RFC2445
  927. # dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
  928. #
  929. # dur-date = dur-day [dur-time]
  930. # dur-time = "T" (dur-hour / dur-minute / dur-second)
  931. # dur-week = 1*DIGIT "W"
  932. # dur-hour = 1*DIGIT "H" [dur-minute]
  933. # dur-minute = 1*DIGIT "M" [dur-second]
  934. # dur-second = 1*DIGIT "S"
  935. # dur-day = 1*DIGIT "D"
  936. #
  937. # example: -P0DT0H30M0S
  938. sub d($$) {
  939. my ($self, $d)= @_;
  940. #main::Debug "Duration $d";
  941. my $sign= 1;
  942. my $t= 0;
  943. my @c= split("P", $d);
  944. $sign= -1 if($c[0] eq "-");
  945. my ($dw,$dt)= split("T", $c[1]);
  946. $dt="" unless defined($dt);
  947. if($dw =~ m/(\d+)D$/) {
  948. $t+= 86400*$1; # days
  949. } elsif($dw =~ m/(\d+)W$/) {
  950. $t+= 604800*$1; # weeks
  951. }
  952. if($dt =~ m/(\d+)H/) {
  953. $t+= $1*3600;
  954. }
  955. if($dt =~ m/(\d+)M/) {
  956. $t+= $1*60;
  957. }
  958. if($dt =~ m/(\d+)S/) {
  959. $t+= $1;
  960. }
  961. $t*= $sign;
  962. #main::Debug "sign: $sign dw: $dw dt: $dt t= $t";
  963. return $t;
  964. }
  965. sub dt($$$$) {
  966. my ($self,$t0,$value,$parts)= @_;
  967. #main::Debug "t0= $t0 parts= $parts value= $value";
  968. if(defined($parts) && $parts =~ m/VALUE=DATE/) {
  969. return $self->tm($value);
  970. } else {
  971. return $t0+$self->d($value);
  972. }
  973. }
  974. sub makeEventDetails($$) {
  975. my ($self, $event)= @_;
  976. $event->{summary}= $self->valueOrDefault("SUMMARY", "");
  977. $event->{location}= $self->valueOrDefault("LOCATION", "");
  978. $event->{description}= $self->valueOrDefault("DESCRIPTION", "");
  979. $event->{categories}= $self->valueOrDefault("CATEGORIES", "");
  980. $event->{classification}= $self->valueOrDefault("CLASS", "PUBLIC");
  981. return $event;
  982. }
  983. sub makeEventAlarms($$) {
  984. my ($self, $event)= @_;
  985. # alarms
  986. my @valarms= grep { $_->{type} eq "VALARM" } @{$self->{entries}};
  987. my @alarmtimes= sort map { $self->dt($event->{start}, $_->value("TRIGGER"), $_->parts("TRIGGER")) } @valarms;
  988. if(@alarmtimes) {
  989. $event->{alarm}= $alarmtimes[0];
  990. } else {
  991. $event->{alarm}= undef;
  992. }
  993. return $event;
  994. }
  995. sub DSTOffset($$) {
  996. my ($t1,$t2)= @_;
  997. my @lt1 = localtime($t1);
  998. my @lt2 = localtime($t2);
  999. return 3600 *($lt1[8] - $lt2[8]);
  1000. }
  1001. # This function adds $n times $seconds to $t1 (seconds from the epoch).
  1002. # A correction of 3600 seconds (one hour) is applied if and only if
  1003. # one of $t1 and $t1+$n*$seconds falls into wintertime and the other
  1004. # into summertime. Thus, e.g., adding a multiple of 24*60*60 seconds
  1005. # to 5 o'clock always gives 5 o'clock and not 4 o'clock or 6 o'clock
  1006. # upon a change of summertime to wintertime or vice versa.
  1007. sub plusNSeconds($$$) {
  1008. my ($t1, $seconds, $n)= @_;
  1009. $n= 1 unless defined($n);
  1010. my $t2= $t1+$n*$seconds;
  1011. return $t2+DSTOffset($t1,$t2);
  1012. }
  1013. sub plusNMonths($$) {
  1014. my ($tm, $n)= @_;
  1015. my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm);
  1016. #main::Debug "Adding $n months to $day.$month.$year $hour:$minute:$second= " . ts($tm);
  1017. $month+= $n;
  1018. $year+= int($month / 12);
  1019. $month %= 12;
  1020. #main::Debug " gives $day.$month.$year $hour:$minute:$second= " . ts(main::fhemTimeLocal($second,$minute,$hour,$day,$month,$year));
  1021. return main::fhemTimeLocal($second,$minute,$hour,$day,$month,$year);
  1022. }
  1023. # This function gets the next date according to interval and byDate
  1024. # Alex, 2016-11-24
  1025. # 1. parameter: startTime
  1026. # 2. parameter: interval (months)
  1027. # 3. parameter: byDay (string with byDay-value(s), e.g. "FR" or "4SA" or "-1SU" or "4SA,4SU" (not sure if this is possible, i just take the first byDay))
  1028. sub getNextMonthlyDateByDay($$$) {
  1029. my ( $ipTimeLocal, $ipByDays, $ipInterval )= @_;
  1030. my ($lSecond, $lMinute, $lHour, $lDay, $lMonth, $lYear, $lWday, $lYday, $lIsdst )= localtime( $ipTimeLocal );
  1031. #main::Debug "getNextMonthlyDateByDay($ipTimeLocal, $ipByDays, $ipInterval)";
  1032. my @lByDays = split(",", $ipByDays);
  1033. my $lByDay = $lByDays[0]; #only get first day element within string
  1034. my $lByDayLength = length( $lByDay );
  1035. my $lDayStr; # which day to set the date
  1036. my $lDayInterval; # e.g. 2 = 2nd $lDayStr of month or -1 = last $lDayStr of month
  1037. if ( $lByDayLength > 2 ) {
  1038. $lDayStr= substr( $lByDay, -2 );
  1039. $lDayInterval= int( substr( $lByDay, 0, $lByDayLength - 2 ) );
  1040. } else {
  1041. $lDayStr= $lByDay;
  1042. $lDayInterval= 1;
  1043. }
  1044. my @weekdays = qw(SU MO TU WE TH FR SA);
  1045. my ($lDayOfWeek)= grep { $weekdays[$_] eq $lDayStr } 0..$#weekdays;
  1046. # get next day from beginning of the month, e.g. "4FR" = 4th friday of the month
  1047. my $lNextMonth;
  1048. my $lNextYear;
  1049. my $lDayOfWeekNew;
  1050. my $lDaysToAddOrSub;
  1051. my $lNewTime;
  1052. if ( $lDayInterval > 0 ) {
  1053. #get next month and year according to $ipInterval
  1054. $lNextMonth= $lMonth + $ipInterval;
  1055. $lNextYear= $lYear;
  1056. $lNextYear += int( $lNextMonth / 12);
  1057. $lNextMonth %= 12;
  1058. my $lFirstOfNextMonth = main::fhemTimeLocal( $lSecond, $lMinute, $lHour, 1, $lNextMonth, $lNextYear );
  1059. ($lSecond, $lMinute, $lHour, $lDay, $lMonth, $lYear, $lDayOfWeekNew, $lYday, $lIsdst )= localtime( $lFirstOfNextMonth );
  1060. if ( $lDayOfWeekNew <= $lDayOfWeek ) {
  1061. $lDaysToAddOrSub = $lDayOfWeek - $lDayOfWeekNew;
  1062. } else {
  1063. $lDaysToAddOrSub = 7 - $lDayOfWeekNew + $lDayOfWeek;
  1064. }
  1065. $lDaysToAddOrSub += ( 7 * ( $lDayInterval - 1 ) ); #add day interval, e.g. 4th friday...
  1066. $lNewTime = plusNSeconds( $lFirstOfNextMonth, 24*60*60*$lDaysToAddOrSub, 1);
  1067. ($lSecond, $lMinute, $lHour, $lDay, $lMonth, $lYear, $lWday, $lYday, $lIsdst )= localtime( $lNewTime );
  1068. if ( $lMonth ne $lNextMonth ) { #skip this date and move on to the next interval...
  1069. $lNewTime = getNextMonthlyDateByDay( $lFirstOfNextMonth, $ipByDays, $ipInterval );
  1070. }
  1071. } else { #calculate date from end of month
  1072. #get next month and year according to ipInterval
  1073. $lNextMonth = $lMonth + $ipInterval + 1; #first get the month after the desired month
  1074. $lNextYear = $lYear;
  1075. $lNextYear += int( $lNextMonth / 12);
  1076. $lNextMonth %= 12;
  1077. my $lLastOfNextMonth = main::fhemTimeLocal( $lSecond, $lMinute, $lHour, 1, $lNextMonth, $lNextYear ); # get time
  1078. $lLastOfNextMonth = plusNSeconds( $lLastOfNextMonth, -24*60*60, 1 ); #subtract one day
  1079. ($lSecond, $lMinute, $lHour, $lDay, $lMonth, $lYear, $lDayOfWeekNew, $lYday, $lIsdst )= localtime( $lLastOfNextMonth );
  1080. if ( $lDayOfWeekNew >= $lDayOfWeek )
  1081. {
  1082. $lDaysToAddOrSub = $lDayOfWeekNew - $lDayOfWeek;
  1083. }
  1084. else
  1085. {
  1086. $lDaysToAddOrSub = 7 - $lDayOfWeek + $lDayOfWeekNew;
  1087. }
  1088. $lDaysToAddOrSub += ( 7 * ( abs( $lDayInterval ) - 1 ) );
  1089. $lNewTime = plusNSeconds( $lLastOfNextMonth, -24*60*60*$lDaysToAddOrSub, 1);
  1090. }
  1091. #main::Debug "lByDay = $lByDay, lByDayLength = $lByDayLength, lDay = $lDay, lDayInterval = $lDayInterval, lDayOfWeek = $lDayOfWeek, lFirstOfNextMonth = $lFirstOfNextMonth, lNextYear = $lNextYear, lNextMonth = $lNextMonth";
  1092. #main::Debug main::FmtDateTime($lNewTime);
  1093. return $lNewTime;
  1094. }
  1095. use constant eventsLimitMinus => -34560000; # -400d
  1096. use constant eventsLimitPlus => 34560000; # +400d
  1097. sub addEventLimited($$$) {
  1098. my ($self, $t, $event)= @_;
  1099. return -1 if($event->start()< $t+eventsLimitMinus);
  1100. return 1 if($event->start()> $t+eventsLimitPlus);
  1101. $self->addEvent($event);
  1102. #main::Debug " addEventLimited: " . $event->asDebug();
  1103. return 0;
  1104. }
  1105. # 0= SU ... 6= SA
  1106. sub weekdayOf($$) {
  1107. my ($self, $t)= @_;
  1108. my (undef, undef, undef, undef, undef, undef, $weekday, undef, undef) = localtime($t);
  1109. return $weekday;
  1110. }
  1111. sub createSingleEvent($$$$) {
  1112. my ($self, $nextstart, $onCreateEvent)= @_;
  1113. my $event= $self->createEvent();
  1114. my $start= $self->tm($self->value("DTSTART"));
  1115. $nextstart= $start unless(defined($nextstart));
  1116. $event->{start}= $nextstart;
  1117. if($self->hasKey("DTEND")) {
  1118. my $end= $self->tm($self->value("DTEND"));
  1119. $event->{end}= $nextstart+($end-$start);
  1120. } elsif($self->hasKey("DURATION")) {
  1121. my $duration= $self->d($self->value("DURATION"));
  1122. $event->{end}= $nextstart + $duration;
  1123. }
  1124. $self->makeEventDetails($event);
  1125. $self->makeEventAlarms($event);
  1126. #main::Debug "createSingleEvent DTSTART=" . $self->value("DTSTART") . " DTEND=" . $self->value("DTEND");
  1127. #main::Debug "createSingleEvent Start " . main::FmtDateTime($event->{start});
  1128. #main::Debug "createSingleEvent End " . main::FmtDateTime($event->{end});
  1129. # plug-in
  1130. if(defined($onCreateEvent)) {
  1131. my $e= $event;
  1132. #main::Debug "Executing $onCreateEvent for " . $e->asDebug();
  1133. eval $onCreateEvent;
  1134. if($@) {
  1135. main::Log3 undef, 2, "Erroneous onCreateEvent $onCreateEvent: $@";
  1136. } else {
  1137. $event= $e;
  1138. }
  1139. }
  1140. return $event;
  1141. }
  1142. sub excludeByExdate($$) {
  1143. my ($self, $event)= @_;
  1144. my $skip= 0;
  1145. if($self->hasKey('EXDATE')) {
  1146. foreach my $exdate (@{$self->values("EXDATE")}) {
  1147. if($self->tm($exdate) == $event->start()) {
  1148. $skip++;
  1149. $event->setNote("EXDATE: $exdate");
  1150. $self->addSkippedEvent($event);
  1151. last;
  1152. }
  1153. } # end of foreach exdate
  1154. } # end of EXDATE checking
  1155. return $skip;
  1156. }
  1157. sub excludeByReference($$$) {
  1158. my ($self, $event, $veventsref)= @_;
  1159. my $skip= 0;
  1160. # check if superseded by out-of-series event
  1161. if($self->hasReferences()) {
  1162. foreach my $id (@{$self->references()}) {
  1163. my $vevent= $veventsref->{$id};
  1164. my $recurrenceid= $vevent->value("RECURRENCE-ID");
  1165. my $originalstart= $vevent->tm($recurrenceid);
  1166. if($originalstart == $event->start()) {
  1167. $event->setNote("RECURRENCE-ID: $recurrenceid");
  1168. $self->addSkippedEvent($event);
  1169. $skip++;
  1170. last;
  1171. }
  1172. }
  1173. }
  1174. return $skip;
  1175. }
  1176. sub excludeByRdate($$) {
  1177. my ($self, $event)= @_;
  1178. my $skip= 0;
  1179. # check if excluded by a duplicate RDATE
  1180. # this is only to avoid duplicates from previously added RDATEs
  1181. if($self->hasKey('RDATE')) {
  1182. foreach my $rdate (@{$self->values("RDATE")}) {
  1183. if($self->tm($rdate) == $event->start()) {
  1184. $event->setNote("RDATE: $rdate");
  1185. $self->addSkippedEvent($event);
  1186. $skip++;
  1187. last;
  1188. }
  1189. }
  1190. }
  1191. return $skip;
  1192. }
  1193. # we return 0 if the storage limit is exceeded or the number of occurances is reached
  1194. # we return 1 else no matter if this evevent was added or skipped
  1195. sub addOrSkipSeriesEvent($$$$$$) {
  1196. my ($self, $event, $t0, $until, $count, $veventsref)= @_;
  1197. #main::Debug " addOrSkipSeriesEvent: " . $event->asDebug();
  1198. return if($event->{start} > $until); # return if we are after end of series
  1199. my $skip= 0;
  1200. # check if superseded by out-of-series event
  1201. $skip+= $self->excludeByReference($event, $veventsref);
  1202. # RFC 5545 p. 120
  1203. # The final recurrence set is generated by gathering all of the
  1204. # start DATE-TIME values generated by any of the specified "RRULE"
  1205. # and "RDATE" properties, and then excluding any start DATE-TIME
  1206. # values specified by "EXDATE" properties. This implies that start
  1207. # DATE-TIME values specified by "EXDATE" properties take precedence
  1208. # over those specified by inclusion properties (i.e., "RDATE" and
  1209. # "RRULE"). Where duplicate instances are generated by the "RRULE"
  1210. # and "RDATE" properties, only one recurrence is considered.
  1211. # Duplicate instances are ignored.
  1212. # check if excluded by EXDATE
  1213. $skip+= $self->excludeByExdate($event);
  1214. # check if excluded by a duplicate RDATE
  1215. # this is only to avoid duplicates from previously added RDATEs
  1216. $skip+= $self->excludeByRdate($event);
  1217. if(!$skip) {
  1218. # add event
  1219. # and return if we exceed storage limit
  1220. my $x= $self->addEventLimited($t0, $event);
  1221. #main::Debug "addEventLimited returned $x";
  1222. return 0 if($x> 0);
  1223. #return 0 if($self->addEventLimited($t0, $event) > 0);
  1224. }
  1225. my $occurances= scalar(@{$self->{events}})+scalar(@{$self->{skippedEvents}});
  1226. #main::Debug("$occurances occurances so far");
  1227. return($occurances< $count);
  1228. }
  1229. sub createEvents($$$%) {
  1230. my ($self, $t0, $onCreateEvent, %vevents)= @_; # t0 is today (for limits)
  1231. $self->clearEvents();
  1232. $self->clearSkippedEvents();
  1233. if($self->isRecurring()) {
  1234. #
  1235. # recurring event creates a series
  1236. #
  1237. my $rrule= $self->value("RRULE");
  1238. my @rrparts= split(";", $rrule);
  1239. my %r= map { split("=", $_); } @rrparts;
  1240. my @keywords= qw(FREQ INTERVAL UNTIL COUNT BYMONTHDAY BYDAY BYMONTH WKST);
  1241. foreach my $k (keys %r) {
  1242. if(not($k ~~ @keywords)) {
  1243. main::Log3 undef, 2, "Calendar: keyword $k in RRULE $rrule is not supported";
  1244. } else {
  1245. #main::Debug "keyword $k in RRULE $rrule has value $r{$k}";
  1246. }
  1247. }
  1248. # Valid values for freq: SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY
  1249. my $freq = $r{"FREQ"};
  1250. #main::Debug "FREQ= $freq";
  1251. # According to RFC, interval defaults to 1
  1252. my $interval = exists($r{"INTERVAL"}) ? $r{"INTERVAL"} : 1;
  1253. my $until = exists($r{"UNTIL"}) ? $self->tm($r{"UNTIL"}) : 99999999999999999;
  1254. my $count = exists($r{"COUNT"}) ? $r{"COUNT"} : 999999;
  1255. my $bymonthday = $r{"BYMONTHDAY"} if(exists($r{"BYMONTHDAY"})); # stored but ignored
  1256. my $byday = exists($r{"BYDAY"}) ? $r{"BYDAY"} : "";
  1257. #main::Debug "byday is $byday";
  1258. my $bymonth = $r{"BYMONTH"} if(exists($r{"BYMONTH"})); # stored but ignored
  1259. my $wkst = $r{"WKST"} if(exists($r{"WKST"})); # stored but ignored
  1260. my @weekdays = qw(SU MO TU WE TH FR SA);
  1261. #main::Debug "createEvents: " . $self->asString();
  1262. #
  1263. # we first add all RDATEs
  1264. #
  1265. if($self->hasKey('RDATE')) {
  1266. foreach my $rdate (@{$self->values("RDATE")}) {
  1267. my $event= $self->createSingleEvent($self->tm($rdate), $onCreateEvent);
  1268. my $skip= 0;
  1269. if($self->hasKey('EXDATE')) {
  1270. foreach my $exdate (@{$self->values("EXDATE")}) {
  1271. if($self->tm($exdate) == $event->start()) {
  1272. $event->setNote("EXDATE: $exdate for RDATE: $rdate");
  1273. $self->addSkippedEvent($event);
  1274. $skip++;
  1275. last;
  1276. }
  1277. }
  1278. }
  1279. if(!$skip) {
  1280. # add event
  1281. $event->setNote("RDATE: $rdate");
  1282. $self->addEventLimited($t0, $event);
  1283. }
  1284. }
  1285. }
  1286. #
  1287. # now we build the series
  1288. #
  1289. #main::Debug "building series...";
  1290. # first event in the series
  1291. my $event= $self->createSingleEvent(undef, $onCreateEvent);
  1292. return if(!$self->addOrSkipSeriesEvent($event, $t0, $until, $count, \%vevents));
  1293. my $nextstart = $event->{start};
  1294. #main::Debug "start: " . $event->ts($nextstart);
  1295. if(($freq eq "WEEKLY") && ($byday ne "")) {
  1296. # special handling for WEEKLY and BYDAY
  1297. # BYDAY with prefix (e.g. -1SU or 2MO) is not recognized
  1298. #main::Debug "weekly event, BYDAY= $byday";
  1299. my @bydays= split(',', $byday);
  1300. # we assume a week from MO to SU
  1301. # we need to cover situations similar to:
  1302. # BYDAY= TU,WE,TH and start is WE or end is WE
  1303. # loop over days, skip over weeks
  1304. # e.g. TH, FR, SA, SU / ... / MO, TU, WE
  1305. while(1) {
  1306. # next day
  1307. $nextstart= plusNSeconds($nextstart, 24*60*60, 1);
  1308. my $weekday= $self->weekdayOf($nextstart);
  1309. # if we reach MO, then skip ($interval-1) weeks
  1310. $nextstart= plusNSeconds($nextstart, 7*24*60*60, $interval-1) if($weekday==1);
  1311. #main::Debug "Skip to: start " . $event->ts($nextstart) . " = " . $weekdays[$weekday];
  1312. if($weekdays[$weekday] ~~ @bydays) {
  1313. my $event= $self->createSingleEvent($nextstart, $onCreateEvent);
  1314. return if(!$self->addOrSkipSeriesEvent($event, $t0, $until, $count, \%vevents));
  1315. }
  1316. }
  1317. } else {
  1318. # handling for events with equal time spacing
  1319. while(1) {
  1320. # advance to next occurance
  1321. if($freq eq "SECONDLY") {
  1322. $nextstart = plusNSeconds($nextstart, 1, $interval);
  1323. } elsif($freq eq "MINUTELY") {
  1324. $nextstart = plusNSeconds($nextstart, 60, $interval);
  1325. } elsif($freq eq "HOURLY") {
  1326. $nextstart = plusNSeconds($nextstart, 60*60, $interval);
  1327. } elsif($freq eq "DAILY") {
  1328. $nextstart = plusNSeconds($nextstart, 24*60*60, $interval);
  1329. } elsif($freq eq "WEEKLY") {
  1330. # default WEEKLY handling
  1331. $nextstart = plusNSeconds($nextstart, 7*24*60*60, $interval);
  1332. } elsif($freq eq "MONTHLY") {
  1333. if ( $byday ne "" ) {
  1334. $nextstart = getNextMonthlyDateByDay( $nextstart, $byday, $interval );
  1335. } else {
  1336. # here we ignore BYMONTHDAY as we consider the day of month of $self->{start}
  1337. # to be equal to BYMONTHDAY.
  1338. $nextstart= plusNMonths($nextstart, $interval);
  1339. }
  1340. } elsif($freq eq "YEARLY") {
  1341. $nextstart= plusNMonths($nextstart, 12*$interval);
  1342. } else {
  1343. main::Log3 undef, 2, "Calendar: event frequency '$freq' not implemented";
  1344. return;
  1345. }
  1346. # the next event
  1347. #main::Debug "Skip to: start " . $event->ts($nextstart);
  1348. $event= $self->createSingleEvent($nextstart, $onCreateEvent);
  1349. return if(!$self->addOrSkipSeriesEvent($event, $t0, $until, $count, \%vevents));
  1350. }
  1351. }
  1352. } else {
  1353. #
  1354. # single event
  1355. #
  1356. my $event= $self->createSingleEvent(undef, $onCreateEvent);
  1357. $self->addEventLimited($t0, $event);
  1358. }
  1359. }
  1360. #
  1361. # friendly string
  1362. #
  1363. sub asString($$) {
  1364. my ($self,$level)= @_;
  1365. $level= "" unless(defined($level));
  1366. my $s= $level . $self->{type};
  1367. $s.= " @" . $self->{ln} if(defined($self->{ln}));
  1368. $s.= " [";
  1369. $s.= "obsolete, " if($self->isObsolete());
  1370. $s.= $self->state();
  1371. $s.= ", refers to " . $self->counterpart() if($self->hasCounterpart());
  1372. $s.= ", in a series with " . join(",", sort @{$self->references()}) if($self->hasReferences());
  1373. $s.= "]";
  1374. #$s.= " (tags: " . join(",", @{$self->tags()}) . ")" if($self->numTags());
  1375. $s.= "\n";
  1376. $level .= " ";
  1377. for my $key (sort keys %{$self->{properties}}) {
  1378. $s.= $level . "$key: ";
  1379. if($self->{properties}{$key}{multiple}) {
  1380. $s.= "(" . join(" ", @{$self->values($key)}) . ")";
  1381. } else {
  1382. $s.= $self->value($key);
  1383. }
  1384. $s.= "\n";
  1385. }
  1386. if($self->{type} eq "VEVENT") {
  1387. if($self->isRecurring()) {
  1388. $s.= $level . ">>> is a series\n";
  1389. }
  1390. if($self->isException()) {
  1391. $s.= $level . ">>> is an exception\n";
  1392. }
  1393. $s.= $level . ">>> Events:\n";
  1394. foreach my $event (@{$self->{events}}) {
  1395. $s.= "$level " . $event->asDebug() . "\n";
  1396. }
  1397. $s.= $level . ">>> Skipped events:\n";
  1398. foreach my $event (@{$self->{skippedEvents}}) {
  1399. $s.= "$level " . $event->asDebug() . "\n";
  1400. }
  1401. }
  1402. my @entries= @{$self->{entries}};
  1403. for(my $i= 0; $i<=$#entries; $i++) {
  1404. $s.= $entries[$i]->asString($level);
  1405. }
  1406. return $s;
  1407. }
  1408. ##########################################################################
  1409. #
  1410. # main
  1411. #
  1412. ##########################################################################
  1413. package main;
  1414. #####################################
  1415. sub Calendar_Initialize($) {
  1416. my ($hash) = @_;
  1417. $hash->{DefFn} = "Calendar_Define";
  1418. $hash->{UndefFn} = "Calendar_Undef";
  1419. $hash->{GetFn} = "Calendar_Get";
  1420. $hash->{SetFn} = "Calendar_Set";
  1421. $hash->{AttrFn} = "Calendar_Attr";
  1422. $hash->{NotifyFn}= "Calendar_Notify";
  1423. $hash->{AttrList}= "update:sync,async,none removevcalendar:0,1 " .
  1424. "cutoffOlderThan hideOlderThan hideLaterThan onCreateEvent " .
  1425. "ignoreCancelled:0,1 quirks " .
  1426. "SSLVerify:0,1 defaultFormat defaultTimeFormat " .
  1427. $readingFnAttributes;
  1428. }
  1429. #####################################
  1430. sub Calendar_Define($$) {
  1431. my ($hash, $def) = @_;
  1432. # define <name> Calendar ical URL [interval]
  1433. my @a = split("[ \t][ \t]*", $def);
  1434. return "syntax: define <name> Calendar ical url <URL> [interval]\n".\
  1435. " define <name> Calendar ical file <FILENAME> [interval]"
  1436. if(($#a < 4 && $#a > 5) || ($a[2] ne 'ical') || (($a[3] ne 'url') && ($a[3] ne 'file')));
  1437. $hash->{NOTIFYDEV} = "global";
  1438. readingsSingleUpdate($hash, "state", "initialized", 1);
  1439. my $name = $a[0];
  1440. my $type = $a[3];
  1441. my $url = $a[4];
  1442. my $interval = 3600;
  1443. $interval= $a[5] if($#a==5);
  1444. $hash->{".fhem"}{type}= $type;
  1445. $hash->{".fhem"}{url}= $url;
  1446. $hash->{".fhem"}{interval}= $interval;
  1447. $hash->{".fhem"}{lastid}= 0;
  1448. $hash->{".fhem"}{vevents}= {};
  1449. $hash->{".fhem"}{nxtUpdtTs}= 0;
  1450. #$attr{$name}{"hideOlderThan"}= 0;
  1451. #main::Debug "Interval: ${interval}s";
  1452. # if initialization is not yet done, we do not wake up at this point already to
  1453. # avoid the following race condition:
  1454. # events are loaded from fhem.save and data are updated asynchronousy from
  1455. # non-blocking Http get
  1456. Calendar_Wakeup($hash, 0) if($init_done);
  1457. return undef;
  1458. }
  1459. #####################################
  1460. sub Calendar_Undef($$) {
  1461. my ($hash, $arg) = @_;
  1462. Calendar_DisarmTimer($hash);
  1463. if(exists($hash->{".fhem"}{subprocess})) {
  1464. my $subprocess= $hash->{".fhem"}{subprocess};
  1465. $subprocess->terminate();
  1466. $subprocess->wait();
  1467. }
  1468. return undef;
  1469. }
  1470. #####################################
  1471. sub Calendar_Attr(@) {
  1472. my ($cmd, $name, @a) = @_;
  1473. return undef unless($cmd eq "set");
  1474. my $hash= $defs{$name};
  1475. return "attr $name needs at least one argument." if(!@a);
  1476. my $arg= $a[1];
  1477. if($a[0] eq "onCreateEvent") {
  1478. if($arg !~ m/^{.*}$/s) {
  1479. return "$arg must be a perl command in curly brackets but you supplied $arg.";
  1480. }
  1481. } elsif($a[0] eq "update") {
  1482. my @args= qw/none sync async/;
  1483. return "Argument for update must be one of " . join(" ", @args) .
  1484. " instead of $arg." unless($arg ~~ @args);
  1485. }
  1486. return undef;
  1487. }
  1488. ###################################
  1489. sub Calendar_Notify($$)
  1490. {
  1491. my ($hash,$dev) = @_;
  1492. my $name = $hash->{NAME};
  1493. my $type = $hash->{TYPE};
  1494. return if($dev->{NAME} ne "global");
  1495. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  1496. return if($attr{$name} && $attr{$name}{disable});
  1497. # update calendar after initialization or change of configuration
  1498. # wait 10 to 29 seconds to avoid congestion due to concurrent activities
  1499. Calendar_DisarmTimer($hash);
  1500. my $delay= 10+int(rand(20));
  1501. # delay removed until further notice
  1502. $delay= 2;
  1503. Log3 $hash, 5, "Calendar $name: FHEM initialization or rereadcfg triggered update, delay $delay seconds.";
  1504. InternalTimer(time()+$delay, "Calendar_Wakeup", $hash, 0) ;
  1505. return undef;
  1506. }
  1507. ###################################
  1508. sub Calendar_Set($@) {
  1509. my ($hash, @a) = @_;
  1510. my $cmd= $a[1];
  1511. $cmd= "?" unless($cmd);
  1512. my $t= time();
  1513. # usage check
  1514. if((@a == 2) && ($a[1] eq "update")) {
  1515. Calendar_DisarmTimer($hash);
  1516. Calendar_GetUpdate($hash, $t, 0);
  1517. return undef;
  1518. } elsif((@a == 2) && ($a[1] eq "reload")) {
  1519. Calendar_DisarmTimer($hash);
  1520. Calendar_GetUpdate($hash, $t, 1); # remove all events before update
  1521. return undef;
  1522. } else {
  1523. return "Unknown argument $cmd, choose one of update:noArg reload:noArg";
  1524. }
  1525. }
  1526. ###################################
  1527. # everything within matching single or double quotes is literally copied
  1528. # everything within braces is literally copied, nesting braces is allowed
  1529. # use \ to mask quotes and braces
  1530. # parts are separated by one or more spaces
  1531. sub Calendar_simpleParseWords($;$) {
  1532. my ($p,$separator)= @_;
  1533. $separator= " " unless defined($separator);
  1534. my $quote= undef;
  1535. my $braces= 0;
  1536. my @parts= (); # resultant array of space-separated parts
  1537. my @chars= split(//, $p); # split into characters
  1538. my $escape= 0; # escape mode off
  1539. my @part= (); # the current part
  1540. for my $c (@chars) {
  1541. #Debug "checking $c, quote is " . (defined($quote) ? $quote : "empty") . ", braces is $braces";
  1542. push @part, $c; # append the character to the current part
  1543. if($escape) { $escape= 0; next; } # continue and turn escape mode off if escape mode is on
  1544. if(($c eq $separator) && !$braces && !defined($quote)) { # we have encountered a space outside quotes and braces
  1545. #Debug " break";
  1546. pop @part; # remove the space
  1547. push @parts, join("", @part) if(@part); # add the completed part if non-empty
  1548. @part= ();
  1549. next;
  1550. }
  1551. $escape= ($c eq "\\"); next if($escape); # escape mode on
  1552. #Debug " not escaped";
  1553. if(($c eq "\"") || ($c eq "\'")) {
  1554. #Debug " quote";
  1555. if(defined($quote)) {
  1556. if($c eq $quote) { $quote= undef; }
  1557. } else {
  1558. $quote= $c;
  1559. }
  1560. next;
  1561. }
  1562. next if defined($quote);
  1563. if($c eq "{") { $braces++; next; } # opening brace
  1564. if($c eq "}") { # closing brace
  1565. return("closing brace without matching opening brace", undef) unless($braces);
  1566. $braces--;
  1567. }
  1568. }
  1569. return("opening quote $quote without matching closing quote", undef) if(defined($quote));
  1570. return("$braces opening brace(s) without matching closing brace(s)", undef) if($braces);
  1571. push @parts, join("", @part) if(@part); # add the completed part
  1572. return(undef, \@parts);
  1573. }
  1574. sub Calendar_Get($@) {
  1575. my ($hash, @a) = @_;
  1576. my $name= $hash->{NAME};
  1577. my $t= time();
  1578. my $eventsObj= $hash->{".fhem"}{events};
  1579. my @events;
  1580. #Debug "Command line: " . join(" ", @a);
  1581. my $cmd= $a[1];
  1582. $cmd= "?" unless($cmd);
  1583. # --------------------------------------------------------------------------
  1584. if($cmd eq "update") {
  1585. # this is the same as set update for convenience
  1586. Calendar_DisarmTimer($hash);
  1587. Calendar_GetUpdate($hash, $t, 0, 1);
  1588. return undef;
  1589. }
  1590. # --------------------------------------------------------------------------
  1591. if($cmd eq "reload") {
  1592. # this is the same as set reload for convenience
  1593. Calendar_DisarmTimer($hash);
  1594. Calendar_GetUpdate($hash, $t, 1, 1); # remove all events before update
  1595. return undef;
  1596. }
  1597. # --------------------------------------------------------------------------
  1598. if($cmd eq "events") {
  1599. # see https://forum.fhem.de/index.php/topic,46608.msg397309.html#msg397309 for ideas
  1600. # get myCalendar events
  1601. # filter:mode=alarm|start|upcoming
  1602. # format:custom={ sprintf("...") }
  1603. # series:next=3
  1604. # attr myCalendar defaultFormat <format>
  1605. my $format= AttrVal($name, "defaultFormat", '"$T1 $D $S"');
  1606. my $timeFormat= AttrVal($name, "defaultTimeFormat",'%d.%m.%Y %H:%M');
  1607. my @filters= ();
  1608. my $next= undef;
  1609. my $count= undef;
  1610. my ($paramerror, $arrayref)= Calendar_simpleParseWords(join(" ", @a));
  1611. return "$name: Parameter parse error: $paramerror" if(defined($paramerror));
  1612. my @a= @{$arrayref};
  1613. shift @a; shift @a; # remove name and "events"
  1614. for my $p (@a) {
  1615. ### format
  1616. if($p =~ /^format:(.+)$/) {
  1617. my $v= $1;
  1618. if($v eq "default") {
  1619. # as if it were not there at all
  1620. } elsif($v eq "full") {
  1621. $format= '"$U $M $A $T1-$T2 $S $CA $L"';
  1622. } elsif($v eq "text") {
  1623. $format= '"$T1 $S"';
  1624. } elsif($v =~ /^custom=['"](.+)['"]$/) {
  1625. $format= '"'.$1.'"';
  1626. } elsif($v =~ /^custom=(\{.+\})$/) {
  1627. $format= $1;
  1628. #Debug "Format=$format";
  1629. } else {
  1630. return "$name: Illegal format specification: $v";
  1631. }
  1632. ### timeFormat
  1633. } elsif($p =~ /^timeFormat:['"](.+)['"]$/) {
  1634. $timeFormat= $1;
  1635. ### filter
  1636. } elsif($p =~ /^filter:(.+)$/) {
  1637. my ($filtererror, $filterarrayref)= Calendar_simpleParseWords($1, ",");
  1638. return "$name: Filter parse error: $filtererror" if(defined($filtererror));
  1639. my @filterspecs= @{$filterarrayref};
  1640. for my $filterspec (@filterspecs) {
  1641. #Debug "Filter specification: $filterspec";
  1642. if($filterspec =~ /^mode==['"](.+)['"]$/) {
  1643. push @filters, { ref => \&filter_mode, param => $1 }
  1644. } elsif($filterspec =~ /^mode=~['"](.+)['"]$/) {
  1645. push @filters, { ref => \&filter_modes, param => $1 }
  1646. } elsif($filterspec =~ /^uid==['"](.+)['"]$/) {
  1647. push @filters, { ref => \&filter_uid, param => $1 }
  1648. } elsif($filterspec =~ /^uid=~['"](.+)['"]$/) {
  1649. push @filters, { ref => \&filter_uids, param => $1 }
  1650. } elsif($filterspec =~ /^field\((uid|mode|summary|description|location|categories|classification)\)==['"](.+)['"]$/) {
  1651. push @filters, { ref => \&filter_field, field => $1, param => $2 }
  1652. } elsif($filterspec =~ /^field\((uid|mode|summary|description|location|categories|classification)\)=~['"](.+)['"]$/) {
  1653. push @filters, { ref => \&filter_fields, field => $1, param => $2 }
  1654. } else {
  1655. return "$name: Illegal filter specification: $filterspec";
  1656. }
  1657. }
  1658. ### series
  1659. } elsif($p =~ /^series:(.+)$/) {
  1660. my ($serieserror,$seriesarrayref)= Calendar_simpleParseWords($1, ",");
  1661. return "$name: Series parse error: $serieserror" if(defined($serieserror));
  1662. my @seriesspecs= @{$seriesarrayref};
  1663. for my $seriesspec (@seriesspecs) {
  1664. if($seriesspec eq "next") {
  1665. $next= 1;
  1666. push(@filters, { ref => \&filter_notend });
  1667. } elsif($seriesspec =~ /next=([1-9]+\d*)/) {
  1668. $next= $1;
  1669. push(@filters, { ref => \&filter_notend });
  1670. } else {
  1671. return "$name: Illegal series specification: $seriesspec";
  1672. }
  1673. }
  1674. ### limit
  1675. } elsif($p =~ /^limit:(.+)$/) {
  1676. my ($limiterror, $limitarrayref)= Calendar_simpleParseWords($1, ",");
  1677. return "$name: Limit parse error: $limiterror" if(defined($limiterror));
  1678. my @limits= @{$limitarrayref};
  1679. for my $limit (@limits) {
  1680. if($limit =~ /count=([1-9]+\d*)/) {
  1681. $count= $1;
  1682. } elsif($limit =~ /from=([+-]?)(.+)/ ) {
  1683. my $sign= $1 eq "-" ? -1 : 1;
  1684. my ($error, $from)= Calendar_GetSecondsFromTimeSpec($2);
  1685. return "$name: $error" if($error);
  1686. push @filters, { ref => \&filter_endafter, param => $t+$sign*$from };
  1687. } elsif($limit =~ /to=([+-]?)(.+)/ ) {
  1688. my $sign= $1 eq "-" ? -1 : 1;
  1689. my ($error, $to)= Calendar_GetSecondsFromTimeSpec($2);
  1690. return "$name: $error" if($error);
  1691. push @filters, { ref => \&filter_startbefore, param => $t+$sign*$to };
  1692. } else {
  1693. return "$name: Illegal limit specification: $limit";
  1694. }
  1695. }
  1696. } else {
  1697. return "$name: Illegal parameter: $p";
  1698. }
  1699. }
  1700. my @texts;
  1701. my @events= Calendar_GetEvents($hash, $t, @filters);
  1702. # special treatment for next
  1703. if(defined($next)) {
  1704. my %uids; # remember the UIDs
  1705. # the @events are ordered by start time ascending
  1706. # they do contain all events that have not ended
  1707. @events = grep {
  1708. my $seen= $uids{$_->uid()} // 0;
  1709. $uids{$_->uid()}= ++$seen;
  1710. #Debug $_->uid() . " => " . $seen . ", next= $next";
  1711. $seen <= $next;
  1712. } @events;
  1713. }
  1714. my $n= 0;
  1715. foreach my $event (@events) {
  1716. push @texts, $event->formatted($format, $timeFormat);
  1717. last if(defined($count) && (++$n>= $count));
  1718. }
  1719. return "" if($#texts<0);
  1720. return join("\n", @texts);
  1721. }
  1722. # --------------------------------------------------------------------------
  1723. my @cmds2= qw/text full summary location description categories alarm start end uid debug/;
  1724. if($cmd ~~ @cmds2) {
  1725. return "argument is missing" if($#a < 2);
  1726. Log3 $hash, 2, "get $name $cmd is deprecated and will be removed soon. Use get $name events instead.";
  1727. my $filter= $a[2];
  1728. # $reading is alarm, all, changed, start, end, upcoming
  1729. my $filterref;
  1730. my $param= undef;
  1731. my $keeppos= 3;
  1732. if($filter eq "changed") {
  1733. $filterref= \&filter_changed;
  1734. } elsif($filter eq "alarm") {
  1735. $filterref= \&filter_alarm;
  1736. } elsif($filter eq "start") {
  1737. $filterref= \&filter_start;
  1738. } elsif($filter eq "end") {
  1739. $filterref= \&filter_end;
  1740. } elsif($filter eq "upcoming") {
  1741. $filterref= \&filter_upcoming;
  1742. } elsif($filter =~ /^uid=(.+)$/) {
  1743. $filterref= \&filter_uids;
  1744. $param= $1;
  1745. } elsif($filter =~ /^mode=(.+)$/) {
  1746. $filterref= \&filter_modes;
  1747. $param= $1;
  1748. } elsif(($filter =~ /^mode\w+$/) and (defined($hash->{READINGS}{$filter}))) {
  1749. #main::Debug "apply filter_reading";
  1750. $filterref= \&filter_reading;
  1751. my @uids= split(";", $hash->{READINGS}{$filter}{VAL});
  1752. $param= \@uids;
  1753. } elsif($filter eq "all") {
  1754. $filterref= \&filter_true;
  1755. } elsif($filter eq "next") {
  1756. $filterref= \&filter_notend;
  1757. $param= { }; # reference to anonymous (unnamed) empty hash, thus $ in $param
  1758. } else { # everything else is interpreted as uid
  1759. $filterref= \&filter_uid;
  1760. $param= $a[2];
  1761. }
  1762. my @filters= ( { ref => $filterref, param => $param } );
  1763. @events= Calendar_GetEvents($hash, $t, @filters);
  1764. # special treatment for next
  1765. if($filter eq "next") {
  1766. my %uids; # remember the UIDs
  1767. # the @events are ordered by start time ascending
  1768. # they do contain all events that have not ended
  1769. @events= grep {
  1770. my $seen= defined($uids{$_->uid()});
  1771. $uids{$_->uid()}= 1;
  1772. not $seen;
  1773. } @events;
  1774. }
  1775. my @texts;
  1776. if(@events) {
  1777. foreach my $event (sort { $a->start() <=> $b->start() } @events) {
  1778. push @texts, $event->uid() if $cmd eq "uid";
  1779. push @texts, $event->asText() if $cmd eq "text";
  1780. push @texts, $event->asFull() if $cmd eq "full";
  1781. push @texts, $event->asDebug() if $cmd eq "debug";
  1782. push @texts, $event->summary() if $cmd eq "summary";
  1783. push @texts, $event->location() if $cmd eq "location";
  1784. push @texts, $event->description() if $cmd eq "description";
  1785. push @texts, $event->categories() if $cmd eq "categories";
  1786. push @texts, $event->alarmTime() if $cmd eq "alarm";
  1787. push @texts, $event->startTime() if $cmd eq "start";
  1788. push @texts, $event->endTime() if $cmd eq "end";
  1789. }
  1790. }
  1791. if(defined($a[$keeppos])) {
  1792. my $keep= $a[$keeppos];
  1793. return "Argument $keep is not a number." unless($keep =~ /\d+/);
  1794. $keep= $#texts+1 if($keep> $#texts);
  1795. splice @texts, $keep if($keep>= 0);
  1796. }
  1797. return "" if($#texts<0);
  1798. return join("\n", @texts);
  1799. } elsif($cmd eq "vevents") {
  1800. my %vevents= %{$hash->{".fhem"}{vevents}};
  1801. my $s= "";
  1802. foreach my $key (sort {$a<=>$b} keys %vevents) {
  1803. $s .= "$key: ";
  1804. $s .= $vevents{$key}->asString();
  1805. $s .= "\n";
  1806. }
  1807. return $s;
  1808. } elsif($cmd eq "vcalendar") {
  1809. return undef unless(defined($hash->{".fhem"}{iCalendar}));
  1810. return $hash->{".fhem"}{iCalendar}
  1811. } elsif($cmd eq "find") {
  1812. return "argument is missing" if($#a != 2);
  1813. my $regexp= $a[2];
  1814. my %vevents= %{$hash->{".fhem"}{vevents}};
  1815. my %uids;
  1816. foreach my $id (keys %vevents) {
  1817. my $v= $vevents{$id};
  1818. my @events= @{$v->{events}};
  1819. if(@events) {
  1820. eval {
  1821. if($events[0]->summary() =~ m/$regexp/) {
  1822. $uids{$events[0]->uid()}= 1; #
  1823. }
  1824. }
  1825. }
  1826. Log3($hash, 2, "Calendar " . $hash->{NAME} .
  1827. ": The regular expression $regexp caused a problem: $@") if($@);
  1828. }
  1829. return join(";", keys %uids);
  1830. } else {
  1831. return "Unknown argument $cmd, choose one of update:noArg reload:noArg events find text full summary location description categories alarm start end vcalendar:noArg vevents:noArg";
  1832. }
  1833. }
  1834. ###################################
  1835. sub Calendar_Wakeup($$) {
  1836. my ($hash, $removeall) = @_;
  1837. Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Wakeup";
  1838. my $t= time(); # baseline
  1839. # we could arrive here 1 second before nextWakeTs for unknown reasons
  1840. use constant delta => 5; # avoid waking up again in a few seconds
  1841. if(defined($t) && ($t>= $hash->{".fhem"}{nxtUpdtTs} - delta)) {
  1842. # GetUpdate does CheckTimes and RearmTimer asynchronously
  1843. Calendar_GetUpdate($hash, $t, $removeall);
  1844. } else {
  1845. Calendar_CheckTimes($hash, $t);
  1846. Calendar_RearmTimer($hash, $t);
  1847. }
  1848. }
  1849. ###################################
  1850. sub Calendar_RearmTimer($$) {
  1851. my ($hash, $t) = @_;
  1852. #main::Debug "RearmTimer now " . FmtDateTime($t);
  1853. my $nt= $hash->{".fhem"}{nxtUpdtTs};
  1854. #main::Debug "RearmTimer next update " . FmtDateTime($nt);
  1855. # find next event
  1856. my %vevents= %{$hash->{".fhem"}{vevents}};
  1857. foreach my $uid (keys %vevents) {
  1858. my $v= $vevents{$uid};
  1859. foreach my $e (@{$v->{events}}) {
  1860. my $et= $e->nextTime($t);
  1861. # we only consider times in the future to avoid multiple
  1862. # invocations for calendar events with the event time
  1863. $nt= $et if(defined($et) && defined($t) && ($et< $nt) && ($et > $t));
  1864. }
  1865. }
  1866. $hash->{".fhem"}{nextWakeTs}= $nt;
  1867. $hash->{".fhem"}{nextWake}= FmtDateTime($nt);
  1868. #main::Debug "RearmTimer for " . $hash->{".fhem"}{nextWake};
  1869. readingsSingleUpdate($hash, "nextWakeup", $hash->{".fhem"}{nextWake}, 1);
  1870. if($nt< $t) { $nt= $t+1 }; # sanity check / do not wake-up at or before the same second
  1871. InternalTimer($nt, "Calendar_Wakeup", $hash, 0) ;
  1872. }
  1873. sub Calendar_DisarmTimer($) {
  1874. my ($hash)= @_;
  1875. RemoveInternalTimer($hash);
  1876. }
  1877. #
  1878. ###################################
  1879. sub Calendar_GetSecondsFromTimeSpec($) {
  1880. my ($tspec) = @_;
  1881. # days
  1882. if($tspec =~ m/^([0-9]+)d$/) {
  1883. return ("", $1*86400);
  1884. }
  1885. # seconds
  1886. if($tspec =~ m/^[0-9]+s?$/) {
  1887. return ("", $tspec);
  1888. }
  1889. # D:HH:MM:SS
  1890. if($tspec =~ m/^([0-9]+):([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/) {
  1891. return ("", $4+60*($3+60*($2+24*$1)));
  1892. }
  1893. # HH:MM:SS
  1894. if($tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { # HH:MM:SS
  1895. return ("", $3+60*($2+(60*$1)));
  1896. }
  1897. # HH:MM
  1898. if($tspec =~ m/^([0-9]+):([0-5][0-9])$/) {
  1899. return ("", 60*($2+60*$1));
  1900. }
  1901. return ("Wrong time specification $tspec", undef);
  1902. }
  1903. ###################################
  1904. # Filters
  1905. sub filter_true($$) {
  1906. return 1;
  1907. }
  1908. sub filter_mode($$) {
  1909. my ($event,$value)= @_;
  1910. my $hit;
  1911. eval { $hit= ($event->getMode() eq $value); };
  1912. return 0 if($@);
  1913. return $hit ? 1 : 0;
  1914. }
  1915. sub filter_modes($$) {
  1916. my ($event,$regex)= @_;
  1917. my $hit;
  1918. eval { $hit= ($event->getMode() =~ $regex); };
  1919. return 0 if($@);
  1920. return $hit ? 1 : 0;
  1921. }
  1922. sub filter_uids($$) {
  1923. my ($event,$regex)= @_;
  1924. my $hit;
  1925. eval { $hit= ($event->uid() =~ $regex); };
  1926. return 0 if($@);
  1927. #Debug "filter_uids: " . $event->uid() . " $regex: $hit";
  1928. return $hit ? 1 : 0;
  1929. }
  1930. sub filter_field($$$) {
  1931. my ($event,$value,$field)= @_;
  1932. my $hit;
  1933. eval { $hit= ($event->{$field} eq $value); };
  1934. return 0 if($@);
  1935. return $hit ? 1 : 0;
  1936. }
  1937. sub filter_fields($$$) {
  1938. my ($event,$regex,$field)= @_;
  1939. my $hit;
  1940. eval { $hit= ($event->{$field} =~ $regex); };
  1941. return 0 if($@);
  1942. return $hit ? 1 : 0;
  1943. }
  1944. sub filter_changed($) {
  1945. my ($event)= @_;
  1946. return $event->modeChanged();
  1947. }
  1948. sub filter_alarm($) {
  1949. my ($event)= @_;
  1950. return $event->getMode() eq "alarm" ? 1 : 0;
  1951. }
  1952. sub filter_start($) {
  1953. my ($event)= @_;
  1954. return $event->getMode() eq "start" ? 1 : 0;
  1955. }
  1956. sub filter_startbefore($$) {
  1957. my ($event, $param)= @_;
  1958. return $event->start() < $param ? 1 : 0;
  1959. }
  1960. sub filter_end($) {
  1961. my ($event)= @_;
  1962. return $event->getMode() eq "end" ? 1 : 0;
  1963. }
  1964. sub filter_endafter($$) {
  1965. my ($event, $param)= @_;
  1966. return $event->end() > $param ? 1 : 0;
  1967. }
  1968. sub filter_notend($) {
  1969. my ($event)= @_;
  1970. #Debug "filter_notend: event " . $event->{summary} . ", mode= " . $event->getMode();
  1971. return $event->getMode() eq "end" ? 0 : 1;
  1972. }
  1973. sub filter_upcoming($) {
  1974. my ($event)= @_;
  1975. return $event->getMode() eq "upcoming" ? 1 : 0;
  1976. }
  1977. sub filter_uid($$) {
  1978. my ($event, $param)= @_;
  1979. return $event->uid() eq "$param" ? 1 : 0;
  1980. }
  1981. sub filter_reading($$) {
  1982. my ($event, $param)= @_;
  1983. my @uids= @{$param};
  1984. #foreach my $u (@uids) { main::Debug "UID $u"; }
  1985. my $uid= $event->uid();
  1986. #main::Debug "SUCHE $uid";
  1987. #main::Debug "GREP: " . grep(/^$uid$/, @uids);
  1988. return grep(/^$uid$/, @uids);
  1989. }
  1990. ###################################
  1991. sub Calendar_GetEvents($$@) {
  1992. my ($hash, $t, @filters)= @_;
  1993. my $name= $hash->{NAME};
  1994. my @result= ();
  1995. # time window
  1996. my ($error, $t1, $t2)= (undef, undef, undef);
  1997. my $hideOlderThan= AttrVal($name, "hideOlderThan", undef);
  1998. my $hideLaterThan= AttrVal($name, "hideLaterThan", undef);
  1999. # start of time window
  2000. if(defined($hideOlderThan)) {
  2001. ($error, $t1)= Calendar_GetSecondsFromTimeSpec($hideOlderThan);
  2002. if($error) {
  2003. Log3 $hash, 2, "$name: attribute hideOlderThan: $error";
  2004. } else {
  2005. $t1= $t- $t1;
  2006. }
  2007. }
  2008. # end of time window
  2009. if(defined($hideLaterThan)) {
  2010. ($error, $t2)= Calendar_GetSecondsFromTimeSpec($hideLaterThan);
  2011. if($error) {
  2012. Log3 $hash, 2, "$name: attribute hideLaterThan: $error";
  2013. } else {
  2014. $t2= $t+ $t2;
  2015. }
  2016. }
  2017. # get and filter events
  2018. my %vevents= %{$hash->{".fhem"}{vevents}};
  2019. foreach my $id (keys %vevents) {
  2020. my $v= $vevents{$id};
  2021. my @events= @{$v->{events}};
  2022. foreach my $event (@events) {
  2023. if(@filters) {
  2024. my $match= 0;
  2025. for my $h (@filters) {
  2026. my $filter= \%$h;
  2027. my $filterref= $filter->{ref};
  2028. my $param = $filter->{param};
  2029. my $field = $filter->{field};
  2030. last unless(&$filterref($event, $param, $field));
  2031. $match++;
  2032. }
  2033. #Debug "Filter $filterref, Parameter $param, Match $match";
  2034. next unless $match==@filters;
  2035. }
  2036. if(defined($t1)) { next if(defined($event->end()) && $event->end() < $t1); }
  2037. if(defined($t2)) { next if(defined($event->start()) && $event->start() > $t2); }
  2038. push @result, $event;
  2039. }
  2040. }
  2041. return sort { $a->start() <=> $b->start() } @result;
  2042. }
  2043. ###################################
  2044. sub Calendar_GetUpdate($$$;$) {
  2045. my ($hash, $t, $removeall, $force) = @_;
  2046. my $name= $hash->{NAME};
  2047. $hash->{".fhem"}{lstUpdtTs}= $t;
  2048. $hash->{".fhem"}{lastUpdate}= FmtDateTime($t);
  2049. my $nut= $t+ $hash->{".fhem"}{interval};
  2050. $hash->{".fhem"}{nxtUpdtTs}= $nut;
  2051. $hash->{".fhem"}{nextUpdate}= FmtDateTime($nut);
  2052. #main::Debug "Getting update now: " . $hash->{".fhem"}{lastUpdate};
  2053. #main::Debug "Next Update is at : " . $hash->{".fhem"}{nextUpdate};
  2054. # If update is disable, shortcut to time checking and rearming timer.
  2055. # Why is this here and not in Calendar_Wakeup? Because the next update time needs to be set
  2056. if(!$force && (AttrVal($hash->{NAME},"update","") eq "none")) {
  2057. Calendar_CheckTimes($hash, $t);
  2058. Calendar_RearmTimer($hash, $t);
  2059. return;
  2060. }
  2061. Log3 $hash, 4, "Calendar $name: Updating...";
  2062. my $type = $hash->{".fhem"}{type};
  2063. my $url= $hash->{".fhem"}{url};
  2064. my $errmsg= "";
  2065. my $ics;
  2066. if($type eq "url") {
  2067. my $SSLVerify= AttrVal($name, "SSLVerify", undef);
  2068. my $SSLArgs= { };
  2069. if(defined($SSLVerify)) {
  2070. eval "use IO::Socket::SSL";
  2071. if($@) {
  2072. Log3 $hash, 2, $@;
  2073. } else {
  2074. my $SSLVerifyMode= eval("$SSLVerify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE");
  2075. Log3 $hash, 5, "SSL verify mode set to $SSLVerifyMode";
  2076. $SSLArgs= { SSL_verify_mode => $SSLVerifyMode };
  2077. }
  2078. }
  2079. HttpUtils_NonblockingGet({
  2080. url => $url,
  2081. hideurl => 1,
  2082. noshutdown => 1,
  2083. hash => $hash,
  2084. timeout => 30,
  2085. type => 'caldata',
  2086. removeall => $removeall,
  2087. sslargs => $SSLArgs,
  2088. t => $t,
  2089. callback => \&Calendar_ProcessUpdate,
  2090. });
  2091. Log3 $hash, 4, "Calendar $name: Getting data from URL <hidden>"; # $url
  2092. } elsif($type eq "file") {
  2093. Log3 $hash, 4, "Calendar $name: Getting data from file $url";
  2094. if(open(ICSFILE, $url)) {
  2095. while(<ICSFILE>) {
  2096. $ics .= $_;
  2097. }
  2098. close(ICSFILE);
  2099. my $paramhash;
  2100. $paramhash->{hash} = $hash;
  2101. $paramhash->{removeall} = $removeall;
  2102. $paramhash->{t} = $t;
  2103. $paramhash->{type} = 'caldata';
  2104. Calendar_ProcessUpdate($paramhash, '', $ics);
  2105. return undef;
  2106. } else {
  2107. Log3 $hash, 1, "Calendar $name: Could not open file $url";
  2108. readingsSingleUpdate($hash, "state", "error (could not open file)", 1);
  2109. return 0;
  2110. }
  2111. } else {
  2112. # this case never happens by virtue of _Define, so just
  2113. die "Software Error";
  2114. }
  2115. }
  2116. ###################################
  2117. sub Calendar_ProcessUpdate($$$) {
  2118. my ($param, $errmsg, $ics) = @_;
  2119. my $hash = $param->{hash};
  2120. my $name = $hash->{NAME};
  2121. my $removeall = $param->{removeall};
  2122. my $t= $param->{t};
  2123. my $type= $hash->{".fhem"}{type};
  2124. if(exists($hash->{".fhem"}{subprocess})) {
  2125. Log3 $hash, 2, "Calendar $name: update in progress, process aborted.";
  2126. return 0;
  2127. }
  2128. # not for the developer:
  2129. # we must be sure that code that starts here ends with Calendar_CheckAndRearm()
  2130. # no matter what branch is taken in the following
  2131. delete($hash->{".fhem"}{iCalendar});
  2132. my $httpresponsecode= $param->{code};
  2133. if($errmsg) {
  2134. Log3 $name, 1, "Calendar $name: retrieval failed with error message $errmsg";
  2135. readingsSingleUpdate($hash, "state", "error ($errmsg)", 1);
  2136. } else {
  2137. if($type eq "url") {
  2138. if($httpresponsecode != 200) {
  2139. $errmsg= "retrieval failed with HTTP response code $httpresponsecode";
  2140. Log3 $name, 1, "Calendar $name: $errmsg";
  2141. readingsSingleUpdate($hash, "state", "error ($errmsg)", 1);
  2142. Log3 $name, 5, "Calendar $name: HTTP response header:\n" .
  2143. $param->{httpheader};
  2144. } else {
  2145. Log3 $name, 5, "Calendar $name: HTTP response code $httpresponsecode";
  2146. readingsSingleUpdate($hash, "state", "retrieved", 1);
  2147. }
  2148. } elsif($type eq "file") {
  2149. Log3 $name, 5, "Calendar $name: file retrieval successful";
  2150. readingsSingleUpdate($hash, "state", "retrieved", 1);
  2151. } else {
  2152. # this case never happens by virtue of _Define, so just
  2153. die "Software Error";
  2154. }
  2155. }
  2156. $hash->{".fhem"}{t}= $t;
  2157. if($errmsg or !defined($ics) or ("$ics" eq "") ) {
  2158. Log3 $hash, 1, "Calendar $name: retrieved no or empty data";
  2159. readingsSingleUpdate($hash, "state", "error (no or empty data)", 1);
  2160. Calendar_CheckAndRearm($hash);
  2161. } else {
  2162. $hash->{".fhem"}{iCalendar}= $ics; # the plain text iCalendar
  2163. $hash->{".fhem"}{removeall}= $removeall;
  2164. if(AttrVal($name, "update", "sync") eq "async") {
  2165. Calendar_AsynchronousUpdateCalendar($hash);
  2166. } else {
  2167. Calendar_SynchronousUpdateCalendar($hash);
  2168. }
  2169. }
  2170. }
  2171. sub Calendar_Cleanup($) {
  2172. my ($hash)= @_;
  2173. delete($hash->{".fhem"}{t});
  2174. delete($hash->{".fhem"}{removeall});
  2175. delete($hash->{".fhem"}{serialized});
  2176. delete($hash->{".fhem"}{subprocess});
  2177. my $name= $hash->{NAME};
  2178. delete($hash->{".fhem"}{iCalendar}) if(AttrVal($name,"removevcalendar",0));
  2179. Log3 $hash, 4, "Calendar $name: process ended.";
  2180. }
  2181. sub Calendar_CheckAndRearm($) {
  2182. my ($hash)= @_;
  2183. my $t= $hash->{".fhem"}{t};
  2184. Calendar_CheckTimes($hash, $t);
  2185. Calendar_RearmTimer($hash, $t);
  2186. }
  2187. sub Calendar_SynchronousUpdateCalendar($) {
  2188. my ($hash) = @_;
  2189. my $name= $hash->{NAME};
  2190. Log3 $hash, 4, "Calendar $name: parsing data synchronously";
  2191. my $ical= Calendar_ParseICS($hash->{".fhem"}{iCalendar});
  2192. Calendar_UpdateCalendar($hash, $ical);
  2193. Calendar_CheckAndRearm($hash);
  2194. Calendar_Cleanup($hash);
  2195. }
  2196. use constant POLLINTERVAL => 1;
  2197. sub Calendar_AsynchronousUpdateCalendar($) {
  2198. require "SubProcess.pm";
  2199. my ($hash) = @_;
  2200. my $name= $hash->{NAME};
  2201. my $subprocess= SubProcess->new({ onRun => \&Calendar_OnRun });
  2202. $subprocess->{ics}= $hash->{".fhem"}{iCalendar};
  2203. my $pid= $subprocess->run();
  2204. if(!defined($pid)) {
  2205. Log3 $hash, 1, "Calendar $name: Cannot parse asynchronously";
  2206. Calendar_CheckAndRearm($hash);
  2207. Calendar_Cleanup($hash);
  2208. return undef;
  2209. }
  2210. Log3 $hash, 4, "Calendar $name: parsing data asynchronously (PID= $pid)";
  2211. $hash->{".fhem"}{subprocess}= $subprocess;
  2212. $hash->{".fhem"}{serialized}= "";
  2213. InternalTimer(gettimeofday()+POLLINTERVAL, "Calendar_PollChild", $hash, 0);
  2214. # go and do your thing while the timer polls and waits for the child to terminate
  2215. Log3 $hash, 5, "Calendar $name: control passed back to main loop.";
  2216. }
  2217. sub Calendar_OnRun() {
  2218. # This routine runs in a process separate from the main process.
  2219. my $subprocess= shift;
  2220. my $ical= Calendar_ParseICS($subprocess->{ics});
  2221. my $serialized= freeze $ical;
  2222. $subprocess->writeToParent($serialized);
  2223. }
  2224. sub Calendar_PollChild($) {
  2225. my ($hash)= @_;
  2226. my $name= $hash->{NAME};
  2227. my $subprocess= $hash->{".fhem"}{subprocess};
  2228. my $data= $subprocess->readFromChild();
  2229. if(!defined($data)) {
  2230. Log3 $name, 4, "Calendar $name: still waiting (". $subprocess->{lasterror} .").";
  2231. InternalTimer(gettimeofday()+POLLINTERVAL, "Calendar_PollChild", $hash, 0);
  2232. return;
  2233. } else {
  2234. Log3 $name, 4, "Calendar $name: got result from asynchronous parsing.";
  2235. $subprocess->wait();
  2236. Log3 $name, 4, "Calendar $name: asynchronous parsing finished.";
  2237. my $ical= thaw($data);
  2238. Calendar_UpdateCalendar($hash, $ical);
  2239. Calendar_CheckAndRearm($hash);
  2240. Calendar_Cleanup($hash);
  2241. }
  2242. }
  2243. sub Calendar_ParseICS($) {
  2244. #main::Debug "Calendar $name: parsing data";
  2245. my ($ics)= @_;
  2246. my ($error, $state)= (undef, "");
  2247. # we parse the calendar into a recursive ICal::Entry structure
  2248. my $ical= ICal::Entry->new("root");
  2249. $ical->parse($ics);
  2250. #main::Debug "*** Result:";
  2251. #main::Debug $ical->asString();
  2252. my $numentries= scalar @{$ical->{entries}};
  2253. if($numentries<= 0) {
  2254. eval { require Compress::Zlib; };
  2255. if($@) {
  2256. $error= "data not in ICal format; maybe gzip data, but cannot load Compress::Zlib";
  2257. }
  2258. else {
  2259. $ics = Compress::Zlib::memGunzip($ics);
  2260. $ical->parse($ics);
  2261. $numentries= scalar @{$ical->{entries}};
  2262. if($numentries<= 0) {
  2263. $error= "data not in ICal format; even not gzip data";
  2264. } else {
  2265. $state= "parsed (gzip data)";
  2266. }
  2267. }
  2268. } else {
  2269. $state= "parsed";
  2270. };
  2271. $ical->{error}= $error;
  2272. $ical->{state}= $state;
  2273. return $ical;
  2274. }
  2275. ###################################
  2276. sub Calendar_UpdateCalendar($$) {
  2277. my ($hash, $ical)= @_;
  2278. my $name= $hash->{NAME};
  2279. my @quirks= split(",", AttrVal($name, "quirks", ""));
  2280. my $nodtstamp= "ignoreDtStamp" ~~ @quirks;
  2281. # *******************************
  2282. # *** Step 1 Digest Parser Result
  2283. # *******************************
  2284. my $error= $ical->{error};
  2285. my $state= $ical->{state};
  2286. if(defined($error)) {
  2287. Log3 $hash, 2, "Calendar $name: error ($error)";
  2288. readingsSingleUpdate($hash, "state", "error ($error)", 1);
  2289. return 0;
  2290. } else {
  2291. readingsSingleUpdate($hash, "state", $state, 1);
  2292. }
  2293. my $t= $hash->{".fhem"}{t};
  2294. my $removeall= $hash->{".fhem"}{removeall};
  2295. my @entries= @{$ical->{entries}};
  2296. my $root= @{$ical->{entries}}[0];
  2297. my $calname= "?";
  2298. if($root->{type} ne "VCALENDAR") {
  2299. Log3 $hash, 1, "Calendar $name: root element is not VCALENDAR";
  2300. readingsSingleUpdate($hash, "state", "error (root element is not VCALENDAR)", 1);
  2301. return 0;
  2302. } else {
  2303. $calname= $root->value("X-WR-CALNAME");
  2304. }
  2305. # *********************
  2306. # *** Step 2 Merging
  2307. # *********************
  2308. Log3 $hash, 4, "Calendar $name: merging data";
  2309. #main::Debug "Calendar $name: merging data";
  2310. # this the hash of VEVENTs that have been created on the previous update
  2311. my %vevents;
  2312. %vevents= %{$hash->{".fhem"}{vevents}} if(!$removeall);
  2313. # the keys to the hash are numbers taken from a sequence
  2314. my $lastid= $hash->{".fhem"}{lastid};
  2315. #
  2316. # 1, 2, 4
  2317. #
  2318. # we first discard all VEVENTs that have been tagged as deleted in the previous run
  2319. # and untag the rest
  2320. foreach my $key (keys %vevents) {
  2321. #main::Debug "Preparing id $key...";
  2322. if($vevents{$key}->isObsolete() ) {
  2323. delete($vevents{$key});
  2324. } else {
  2325. $vevents{$key}->setState("deleted"); # will be changed if record is touched in the next step
  2326. $vevents{$key}->clearCounterpart();
  2327. $vevents{$key}->clearReferences();
  2328. }
  2329. }
  2330. #
  2331. # 3
  2332. #
  2333. # we now run through the list of freshly retrieved VEVENTs and merge them into
  2334. # the hash
  2335. my ($n, $nknown, $nmodified, $nnew, $nchanged)= (0,0,0,0,0,0);
  2336. # this code is O(n^2) and stalls FHEM for large numbers of VEVENTs
  2337. # to speed up the code we first build a reverse hash (UID,RECURRENCE-ID) -> id
  2338. sub kf($) { my ($v)= @_; return $v->value("UID").$v->valueOrDefault("RECURRENCE-ID","") }
  2339. my %lookup;
  2340. foreach my $id (keys %vevents) {
  2341. my $k= kf($vevents{$id});
  2342. Log3 $hash, 2, "Calendar $name: Duplicate VEVENT" if(defined($lookup{$k}));
  2343. $lookup{$k}= $id;
  2344. #main::Debug "Adding event $id with key $k to lookup hash.";
  2345. }
  2346. # start of time window for cutoff
  2347. my $cutoffOlderThan = AttrVal($name, "cutoffOlderThan", undef);
  2348. my $cutoffT= 0;
  2349. my $cutoff;
  2350. if(defined($cutoffOlderThan)) {
  2351. ($error, $cutoffT)= Calendar_GetSecondsFromTimeSpec($cutoffOlderThan);
  2352. if($error) {
  2353. Log3 $hash, 2, "$name: attribute cutoffOlderThan: $error";
  2354. };
  2355. $cutoff= $t- $cutoffT;
  2356. }
  2357. foreach my $v (grep { $_->{type} eq "VEVENT" } @{$root->{entries}}) {
  2358. # totally skip outdated calendar entries
  2359. if($cutoffOlderThan) {
  2360. if(!$v->isRecurring()) {
  2361. # non recurring event
  2362. next if(
  2363. defined($cutoffOlderThan) &&
  2364. $v->hasKey("DTEND") &&
  2365. $v->tm($v->value("DTEND")) < $cutoff
  2366. );
  2367. } else {
  2368. # recurring event, inspect
  2369. my $rrule= $v->value("RRULE");
  2370. my @rrparts= split(";", $rrule);
  2371. my %r= map { split("=", $_); } @rrparts;
  2372. if(exists($r{"UNTIL"})) {
  2373. next if($v->tm($r{"UNTIL"}) < $cutoff)
  2374. }
  2375. }
  2376. }
  2377. #main::Debug "Merging " . $v->asString();
  2378. my $found= 0;
  2379. my $added= 0; # flag to prevent multiple additions
  2380. $n++;
  2381. # some braindead calendars provide no UID - add one:
  2382. $v->addproperty(sprintf("UID:synthetic-%06d", $v->{ln}))
  2383. unless($v->hasKey("UID") or !defined($v->{ln}));
  2384. # look for related records in the old record set
  2385. my $k= kf($v);
  2386. #main::Debug "Looking for event with key $k";
  2387. my $id= $lookup{$k};
  2388. if(defined($id)) {
  2389. my $v0= $vevents{$id};
  2390. #main::Debug "Found $id";
  2391. #
  2392. # same UID and RECURRENCE-ID
  2393. #
  2394. $found++;
  2395. if($v0->sameValue($v, "SEQUENCE")) {
  2396. #
  2397. # and same SEQUENCE
  2398. #
  2399. if($v0->sameValue($v, "LAST-MODIFIED") &&
  2400. ($nodtstamp || $v0->sameValue($v, "DTSTAMP"))) {
  2401. #
  2402. # is not modified
  2403. #
  2404. # we only keep the record from the old record set
  2405. $v0->setState("known");
  2406. $nknown++;
  2407. } else {
  2408. #
  2409. # is modified
  2410. #
  2411. # we keep both records
  2412. next if($added);
  2413. $added++;
  2414. $vevents{++$lastid}= $v;
  2415. $v->setState("modified-new");
  2416. $v->setCounterpart($id);
  2417. $v0->setState("modified-old");
  2418. $v0->setCounterpart($lastid);
  2419. $nmodified++;
  2420. }
  2421. } else {
  2422. #
  2423. # and different SEQUENCE
  2424. #
  2425. # we keep both records
  2426. next if($added);
  2427. $added++;
  2428. $vevents{++$lastid}= $v;
  2429. $v->setState("changed-new");
  2430. $v->setCounterpart($id);
  2431. $v0->setState("changed-old");
  2432. $v0->setCounterpart($lastid);
  2433. $nchanged++;
  2434. }
  2435. }
  2436. if(!$found) {
  2437. $v->setState("new");
  2438. $vevents{++$lastid}= $v;
  2439. $added++;
  2440. $nnew++;
  2441. }
  2442. }
  2443. #
  2444. # Cross-referencing series
  2445. #
  2446. # this code is O(n^2) and stalls FHEM for large numbers of VEVENTs
  2447. # to speed up the code we build a hash of a hash UID => {id => VEVENT}
  2448. %lookup= ();
  2449. foreach my $id (keys %vevents) {
  2450. my $v= $vevents{$id};
  2451. $lookup{$v->value("UID")}{$id}= $v unless($v->isObsolete);
  2452. }
  2453. for my $idref (values %lookup) {
  2454. my %vs= %{$idref};
  2455. foreach my $v (values %vs) {
  2456. foreach my $id (keys %vs) {
  2457. push @{$v->references()}, $id unless($vs{$id} eq $v);
  2458. }
  2459. }
  2460. }
  2461. # foreach my $id (keys %vevents) {
  2462. # my $v= $vevents{$id};
  2463. # next if($v->isObsolete());
  2464. # foreach my $id0 (keys %vevents) {
  2465. # next if($id==$id0);
  2466. # my $v0= $vevents{$id0};
  2467. # next if($v0->isObsolete());
  2468. # push @{$v0->references()}, $id if($v->sameValue($v0, "UID"));
  2469. # }
  2470. # }
  2471. Log3 $hash, 4, "Calendar $name: $n records processed, $nnew new, ".
  2472. "$nknown known, $nmodified modified, $nchanged changed.";
  2473. # save the VEVENTs hash and lastid
  2474. $hash->{".fhem"}{vevents}= \%vevents;
  2475. $hash->{".fhem"}{lastid}= $lastid;
  2476. # *********************
  2477. # *** Step 3 Events
  2478. # *********************
  2479. #
  2480. # Recreating the events
  2481. #
  2482. Log3 $hash, 4, "Calendar $name: creating calendar events";
  2483. #main::Debug "Calendar $name: creating calendar events";
  2484. my $ignoreCancelled= AttrVal($name, "ignoreCancelled", 0);
  2485. foreach my $id (keys %vevents) {
  2486. my $v= $vevents{$id};
  2487. if($v->isObsolete() or ($ignoreCancelled and $v->isCancelled())) {
  2488. $v->clearEvents();
  2489. next;
  2490. }
  2491. my $onCreateEvent= AttrVal($name, "onCreateEvent", undef);
  2492. if($v->hasChanged() or !$v->numEvents()) {
  2493. #main::Debug "createEvents";
  2494. $v->createEvents($t, $onCreateEvent, %vevents);
  2495. }
  2496. }
  2497. #main::Debug "*** Result:";
  2498. #main::Debug $ical->asString();
  2499. # *********************
  2500. # *** Step 4 Readings
  2501. # *********************
  2502. readingsBeginUpdate($hash);
  2503. readingsBulkUpdate($hash, "calname", $calname);
  2504. readingsBulkUpdate($hash, "lastUpdate", $hash->{".fhem"}{lastUpdate});
  2505. readingsBulkUpdate($hash, "nextUpdate", $hash->{".fhem"}{nextUpdate});
  2506. readingsEndUpdate($hash, 1); # DoTrigger, because sub is called by a timer instead of dispatch
  2507. return 1;
  2508. }
  2509. ###################################
  2510. sub Calendar_CheckTimes($$) {
  2511. my ($hash, $t) = @_;
  2512. Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Checking times...";
  2513. #
  2514. # determine the uids of all events and their most interesting mode
  2515. #
  2516. my %priority= (
  2517. "none" => 0,
  2518. "end" => 1,
  2519. "upcoming" => 2,
  2520. "alarm" => 3,
  2521. "start" => 4,
  2522. );
  2523. my %mim; # most interesting mode per id
  2524. my %changed; # changed per id
  2525. my %vevents= %{$hash->{".fhem"}{vevents}};
  2526. foreach my $uid (keys %vevents) {
  2527. my $v= $vevents{$uid};
  2528. foreach my $e (@{$v->{events}}) {
  2529. my $uid= $e->uid();
  2530. my $mode= defined($mim{$uid}) ? $mim{$uid} : "none";
  2531. if($e->isEnded($t)) {
  2532. $e->setMode("end");
  2533. } elsif($e->isUpcoming($t)) {
  2534. $e->setMode("upcoming");
  2535. } elsif($e->isStarted($t)) {
  2536. $e->setMode("start");
  2537. } elsif($e->isAlarmed($t)) {
  2538. $e->setMode("alarm");
  2539. }
  2540. if($priority{$e->getMode()} > $priority{$mode}) {
  2541. $mim{$uid}= $e->getMode();
  2542. }
  2543. $changed{$uid}= 0 unless(defined($changed{$uid}));
  2544. # create the FHEM event
  2545. if($e->modeChanged()) {
  2546. $changed{$uid}= 1;
  2547. addEvent($hash, "changed: $uid " . $e->getMode());
  2548. addEvent($hash, $e->getMode() . ": $uid ");
  2549. }
  2550. }
  2551. }
  2552. #
  2553. # determine the uids of events in certain modes
  2554. #
  2555. my @changed;
  2556. my @upcoming;
  2557. my @start;
  2558. my @started;
  2559. my @alarm;
  2560. my @alarmed;
  2561. my @end;
  2562. my @ended;
  2563. foreach my $uid (keys %mim) {
  2564. push @changed, $uid if($changed{$uid});
  2565. push @upcoming, $uid if($mim{$uid} eq "upcoming");
  2566. if($mim{$uid} eq "alarm") {
  2567. push @alarm, $uid;
  2568. push @alarmed, $uid if($changed{$uid});
  2569. }
  2570. if($mim{$uid} eq "start") {
  2571. push @start, $uid;
  2572. push @started, $uid if($changed{$uid});
  2573. }
  2574. if($mim{$uid} eq "end") {
  2575. push @end, $uid;
  2576. push @ended, $uid if($changed{$uid});
  2577. }
  2578. }
  2579. #sub uniq { my %uids; return grep {!$uids{$_->uid()}++} @_; }
  2580. #@allevents= sort { $a->start() <=> $b->start() } uniq(@allevents);
  2581. #foreach my $event (@allevents) {
  2582. # main::Debug $event->asFull();
  2583. #}
  2584. sub es(@) {
  2585. my (@events)= @_;
  2586. return join(";", @events);
  2587. }
  2588. sub rbu($$$) {
  2589. my ($hash, $reading, $value)= @_;
  2590. if(!defined($hash->{READINGS}{$reading}) or
  2591. ($hash->{READINGS}{$reading}{VAL} ne $value)) {
  2592. readingsBulkUpdate($hash, $reading, $value);
  2593. }
  2594. }
  2595. # clears all events in CHANGED, thus must be called first
  2596. readingsBeginUpdate($hash);
  2597. # we update the readings
  2598. rbu($hash, "modeUpcoming", es(@upcoming));
  2599. rbu($hash, "modeAlarm", es(@alarm));
  2600. rbu($hash, "modeAlarmed", es(@alarmed));
  2601. rbu($hash, "modeAlarmOrStart", es(@alarm,@start));
  2602. rbu($hash, "modeChanged", es(@changed));
  2603. rbu($hash, "modeStart", es(@start));
  2604. rbu($hash, "modeStarted", es(@started));
  2605. rbu($hash, "modeEnd", es(@end));
  2606. rbu($hash, "modeEnded", es(@ended));
  2607. readingsBulkUpdate($hash, "state", "triggered");
  2608. # DoTrigger, because sub is called by a timer instead of dispatch
  2609. readingsEndUpdate($hash, 1);
  2610. }
  2611. #####################################
  2612. sub CalendarAsHtml($;$) {
  2613. my ($d,$o) = @_;
  2614. $d = "<none>" if(!$d);
  2615. return "$d is not a Calendar instance<br>"
  2616. if(!$defs{$d} || $defs{$d}{TYPE} ne "Calendar");
  2617. my $l= Calendar_Get($defs{$d}, split("[ \t]+", "- text $o"));
  2618. my @lines= split("\n", $l);
  2619. my $ret = '<table class="calendar">';
  2620. foreach my $line (@lines) {
  2621. my @fields= split(" ", $line, 3);
  2622. $ret.= sprintf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>", @fields);
  2623. }
  2624. $ret .= '</table>';
  2625. return $ret;
  2626. }
  2627. sub CalendarEventsAsHtml($;$) {
  2628. my ($d,$parameters) = @_;
  2629. $d = "<none>" if(!$d);
  2630. return "$d is not a Calendar instance<br>"
  2631. if(!$defs{$d} || $defs{$d}{TYPE} ne "Calendar");
  2632. my $l= Calendar_Get($defs{$d}, split("[ \t]+", "- events $parameters"));
  2633. my @lines= split("\n", $l);
  2634. my $ret = '<table class="calendar">';
  2635. foreach my $line (@lines) {
  2636. my @fields= split(" ", $line, 3);
  2637. $ret.= sprintf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>", @fields);
  2638. }
  2639. $ret .= '</table>';
  2640. return $ret;
  2641. }
  2642. #####################################
  2643. 1;
  2644. =pod
  2645. =item device
  2646. =item summary handles calendar events from iCal file or URL
  2647. =item summary_DE handhabt Kalendertermine aus iCal-Dateien und URLs
  2648. =begin html
  2649. <a name="Calendar"></a>
  2650. <h3>Calendar</h3>
  2651. <ul>
  2652. <br>
  2653. <a name="Calendardefine"></a>
  2654. <b>Define</b><br><br>
  2655. <ul>
  2656. <code>define &lt;name&gt; Calendar ical url &lt;URL&gt; [&lt;interval&gt;]</code><br>
  2657. <code>define &lt;name&gt; Calendar ical file &lt;FILENAME&gt; [&lt;interval&gt;]</code><br>
  2658. <br>
  2659. Defines a calendar device.<br><br>
  2660. A calendar device periodically gathers calendar events from the source calendar at the given URL or from a file.
  2661. The file must be in ICal format.<br><br>
  2662. If the URL
  2663. starts with <code>https://</code>, the perl module IO::Socket::SSL must be installed
  2664. (use <code>cpan -i IO::Socket::SSL</code>).<br><br>
  2665. Note for users of Google Calendar: You can literally use the private ICal URL from your Google Calendar.
  2666. If your Google Calendar
  2667. URL starts with <code>https://</code> and the perl module IO::Socket::SSL is not installed on your system, you can
  2668. replace it by <code>http://</code> if and only if there is no redirection to the <code>https://</code> URL.
  2669. Check with your browser first if unsure.<br><br>
  2670. Note for users of Netxtcloud Calendar: you can use an URL of the form
  2671. <code>https://admin:admin@demo.nextcloud.com/wid0ohgh/remote.php/dav/calendars/admin/personal/?export</code>.
  2672. <p>
  2673. The optional parameter <code>interval</code> is the time between subsequent updates
  2674. in seconds. It defaults to 3600 (1 hour).<br><br>
  2675. Examples:
  2676. <pre>
  2677. define MyCalendar Calendar ical url https://www.google.com&shy;/calendar/ical/john.doe%40example.com&shy;/private-foo4711/basic.ics
  2678. define YourCalendar Calendar ical url http://www.google.com&shy;/calendar/ical/jane.doe%40example.com&shy;/private-bar0815/basic.ics 86400
  2679. define SomeCalendar Calendar ical file /home/johndoe/calendar.ics
  2680. </pre>
  2681. </ul>
  2682. <a name="Calendarset"></a>
  2683. <b>Set </b><br><br>
  2684. <ul>
  2685. <li><code>set &lt;name&gt; update</code><br>
  2686. Forces the retrieval of the calendar from the URL. The next automatic retrieval is scheduled to occur <code>interval</code> seconds later.<br><br></li>
  2687. <li><code>set &lt;name&gt; reload</code><br>
  2688. Same as <code>update</code> but all calendar events are removed first.<br><br></li>
  2689. </ul>
  2690. <br>
  2691. <a name="Calendarget"></a>
  2692. <b>Get</b><br><br>
  2693. <ul>
  2694. <li><code>get &lt;name&gt; update</code><br>
  2695. Same as <code>set &lt;name&gt; update</code><br><br></li>
  2696. <li><code>get &lt;name&gt; reload</code><br>
  2697. Same as <code>set &lt;name&gt; update</code><br><br></li>
  2698. <li><code>get &lt;name&gt; events [format:&lt;formatSpec&gt;] [timeFormat:&lt;timeFormatSpec&gt;] [filter:&lt;filterSpecs&gt;] [series:next[=&lt;max&gt;]] [limit:&lt;limitSpecs&gt;]</code><br><br>
  2699. The swiss army knife for displaying calendar events.
  2700. Returns, line by line, information on the calendar events in the calendar &lt;name&gt;
  2701. according to formatting and filtering rules.
  2702. You can give none, one or several of the <code>format</code>,
  2703. <code>timeFormat</code>, <code>filter</code>, <code>series</code> and <code>limit</code>
  2704. parameters and it makes even sense to give the <code>filter</code>
  2705. parameter several times.
  2706. <br><br>
  2707. The <u><code>format</code></u> parameter determines the overall formatting of the calendar event.
  2708. The following format specifications are available:<br><br>
  2709. <table>
  2710. <tr><th align="left">&lt;formatSpec&gt;</th><th align="left">content</th></tr>
  2711. <tr><td><code>default</code></td><td>the default format (see below)</td></tr>
  2712. <tr><td><code>full</code></td><td>same as <code>custom="$U $M $A $T1-$T2 $S $CA $L"</code></td></tr>
  2713. <tr><td><code>text</code></td><td>same as <code>custom="$T1 $S"</code></td></tr>
  2714. <tr><td><code>custom="&lt;formatString&gt;"</code></td><td> a custom format (see below)</td></tr>
  2715. <tr><td><code>custom="{ &lt;perl-code&gt; }"</code></td><td>a custom format (see below)</td></tr>
  2716. </table><br>
  2717. Single quotes (<code>'</code>) can be used instead of double quotes (<code>"</code>) in the
  2718. custom format.<br><br>
  2719. You can use the following variables in the <code>&lt;formatString&gt;</code> and in
  2720. the <code>&lt;perl-code&gt;</code>:<br><br>
  2721. <table>
  2722. <tr><th align="left">variable</th><th align="left">meaning</th></tr>
  2723. <tr><td><code>$t1</code></td><td>the start time in seconds since the epoch</td></tr>
  2724. <tr><td><code>$T1</code></td><td>the start time according to the time format</td></tr>
  2725. <tr><td><code>$t2</code></td><td>the end time in seconds since the epoch</td></tr>
  2726. <tr><td><code>$T2</code></td><td>the end time according to the time format</td></tr>
  2727. <tr><td><code>$a</code></td><td>the alarm time in seconds since the epoch</td></tr>
  2728. <tr><td><code>$A</code></td><td>the alarm time according to the time format</td></tr>
  2729. <tr><td><code>$d</code></td><td>the duration in seconds</td></tr>
  2730. <tr><td><code>$D</code></td><td>the duration in human-readable form</td></tr>
  2731. <tr><td><code>$S</code></td><td>the summary</td></tr>
  2732. <tr><td><code>$L</code></td><td>the location</td></tr>
  2733. <tr><td><code>$CA</code></td><td>the categories</td></tr>
  2734. <tr><td><code>$CL</code></td><td>the classification</td></tr>
  2735. <tr><td><code>$DS</code></td><td>the description</td></tr>
  2736. <tr><td><code>$U</code></td><td>the UID</td></tr>
  2737. <tr><td><code>$M</code></td><td>the mode</td></tr>
  2738. </table><br>
  2739. \, (masked comma) in summary, location and description is replaced by a comma but \n
  2740. (indicates newline) is untouched.<br><br>
  2741. If the <code>format</code> parameter is omitted, the custom format string
  2742. from the <code>defaultFormat</code> attribute is used. If this attribute
  2743. is not set, <code>"$T1 $D $S"</code> is used as default custom format string.
  2744. The last occurance wins if the <code>format</code>
  2745. parameter is given several times.<br><br>
  2746. Examples:<br>
  2747. <code>get MyCalendar events format:full</code><br>
  2748. <code>get MyCalendar events format:custom="$T1-$T2 $S \@ $L"</code><br>
  2749. <code>get MyCalendar events format:custom={ sprintf("%20s %8s", $S, $D) }</code><br><br>
  2750. The <u><code>timeFormat</code></u> parameter determines the formatting of
  2751. start, end and alarm times.<br><br>
  2752. You use the POSIX conversion specifications in the <code>&lt;timeFormatSpec&gt;</code>.
  2753. The web page <a href="http://strftime.net">strftime.net</a> has a nice builder
  2754. for <code>&lt;timeFormatSpec&gt;</code>.<br><br>
  2755. If the <code>timeFormat</code> parameter is omitted, the time format specification
  2756. from the <code>defaultTimeFormat</code> attribute is used. If this attribute
  2757. is not set, <code>"%d.%m.%Y %H:%M"</code> is used as default time format
  2758. specification.
  2759. Single quotes (<code>'</code>) or double quotes (<code>"</code>) can be
  2760. used to enclose the format specification.<br><br>
  2761. The last occurance wins if the parameter is given several times.<br><br>
  2762. Example:<br>
  2763. <code>get MyCalendar events timeFormat:"%e-%b-%Y" format:full</code><br><br>
  2764. The <u><code>filter</code></u> parameter restricts the calendar
  2765. events displayed to a subset. <code>&lt;filterSpecs&gt;</code> is a comma-separated
  2766. list of <code>&lt;filterSpec&gt;</code> specifications. All filters must apply for a
  2767. calendar event to be displayed. The parameter is cumulative: all separate
  2768. occurances of the parameter add to the list of filters.<br><br>
  2769. <table>
  2770. <tr><th align="left"><code>&lt;filterSpec&gt;</code></th><th align="left">description</th></tr>
  2771. <tr><td><code>uid=="&lt;uid&gt;"</code></td><td>UID is <code>&lt;uid&gt;</code><br>
  2772. same as <code>field(uid)=="&lt;uid&gt;"</code></td></tr>
  2773. <tr><td><code>uid=~"&lt;regex&gt;"</code></td><td>UID matches regular expression <code>&lt;regex&gt;</code><br>
  2774. same as <code>field(uid)=~"&lt;regex&gt;"</code></td></tr>
  2775. <tr><td><code>mode=="&lt;mode&gt;"</code></td><td>mode is <code>&lt;mode&gt;</code><br>
  2776. same as <code>field(mode)=="&lt;mode&gt;"</code></td></tr>
  2777. <tr><td><code>mode=~"&lt;regex&gt;"</code></td><td>mode matches regular expression <code>&lt;regex&gt;</code><br>
  2778. same as <code>field(mode)=~"&lt;regex&gt;"</code></td></tr>
  2779. <tr><td><code>field(&lt;field&gt;)=="&lt;value&gt;"</code></td><td>content of the field <code>&lt;field&gt;</code> is <code>&lt;value&gt;</code><br>
  2780. &lt;field&gt; is one of <code>uid</code>, <code>mode</code>, <code>summary</code>, <code>location</code>,
  2781. <code>description</code>, <code>categories</code>, <code>classification</code>
  2782. </td></tr>
  2783. <tr><td><code>field(&lt;field&gt;)=~"&lt;regex&gt;"</code></td><td>content of the field &lt;field&gt; matches &lt;regex&gt;<br>
  2784. &lt;field&gt; is one of <code>uid</code>, <code>mode</code>, <code>summary</code>, <code>location</code>,
  2785. <code>description</code>, <code>categories</code>, <code>classification</code><br>
  2786. </td></tr>
  2787. </table><br>
  2788. The double quotes (<code>"</code>) on the right hand side of a <code>&lt;filterSpec&gt;</code>
  2789. are not part of the value or regular expression. Single quotes (<code>'</code>) can be
  2790. used instead.<br><br>
  2791. Examples:<br>
  2792. <code>get MyCalendar events filter:uid=="432dsafweq64yehdbwqhkd"</code><br>
  2793. <code>get MyCalendar events filter:uid=~"^7"</code><br>
  2794. <code>get MyCalendar events filter:mode=="alarm"</code><br>
  2795. <code>get MyCalendar events filter:mode=~"alarm|upcoming"</code><br>
  2796. <code>get MyCalendar events filter:field(summary)=~"Mama"</code><br>
  2797. <code>get MyCalendar events filter:field(classification)=="PUBLIC"</code><br>
  2798. <code>get MyCalendar events filter:field(summary)=~"Gelber Sack",mode=~"upcoming|start"</code><br>
  2799. <code>get MyCalendar events filter:field(summary)=~"Gelber Sack" filter:mode=~"upcoming|start"</code>
  2800. <br><br>
  2801. The <u><code>series</code></u> parameter determines the display of
  2802. recurring events. <code>series:next</code> limits the display to the
  2803. next calendar event out of all calendar events in the series that have
  2804. not yet ended. <code>series:next=&lt;max&gt;</code> shows at most the
  2805. <code>&lt;max&gt;</code> next calendar events in the series. This applies
  2806. per series. To limit the total amount of events displayed see the <code>limit</code>
  2807. parameter below.<br><br>
  2808. The <u><code>limit</code></u> parameter limits the number of events displayed.
  2809. <code>&lt;limitSpecs&gt;</code> is a comma-separated list of <code>&lt;limitSpec&gt;</code>
  2810. specifications.<br><br>
  2811. <table>
  2812. <tr><th align="left"><code>&lt;limitSpec&gt;</code></th><th align="left">description</th></tr>
  2813. <tr><td><code>count=&lt;n&gt;</code></td><td>shows at most <code>&lt;n&gt;</code> events, <code>&lt;n&gt;</code> is a positive integer</td></tr>
  2814. <tr><td><code>from=[+|-]&lt;timespec&gt;</code></td><td>shows only events that end after
  2815. a timespan &lt;timespec&gt; from now; use a minus sign for events in the
  2816. past; &lt;timespec&gt; is described below in the Attributes section</td></tr>
  2817. <tr><td><code>to=[+|-]&lt;timespec&gt;</code></td><td>shows only events that start before
  2818. a timespan &lt;timespec&gt; from now; use a minus sign for events in the
  2819. past; &lt;timespec&gt; is described below in the Attributes section</td></tr>
  2820. </table><br>
  2821. Examples:<br>
  2822. <code>get MyCalendar events limit:count=10</code><br>
  2823. <code>get MyCalendar events limit:from=-2d</code><br>
  2824. <code>get MyCalendar events limit:count=10,from=0,to=+10d</code><br>
  2825. <br><br>
  2826. </li>
  2827. <!-- DEPRECATED
  2828. <li><code>get &lt;name&gt; &lt;format&gt; &lt;filter&gt; [&lt;max&gt;]</code><br>
  2829. This command is deprecated. Use <code>get &lt;name&gt; events ...</code>
  2830. instead. Please inform the author of the module if you think that there
  2831. is anything this command can do what <code>get &lt;name&gt; events ...</code>
  2832. cannot.<br><br>
  2833. Returns, line by line, information on the calendar events in the calendar &lt;name&gt;. The content depends on the
  2834. &lt;format&gt specifier:<br><br>
  2835. <table>
  2836. <tr><th align="left">&lt;format&gt;</th><th align="left">content</th></tr>
  2837. <tr><td>uid</td><td>the UID of the event</td></tr>
  2838. <tr><td>text</td><td>a user-friendly textual representation, best suited for display</td></tr>
  2839. <tr><td>summary</td><td>the content of the summary field (subject, title)</td></tr>
  2840. <tr><td>location</td><td>the content of the location field</td></tr>
  2841. <tr><td>categories</td><td>the content of the categories field</td></tr>
  2842. <tr><td>alarm</td><td>alarm time in human-readable format</td></tr>
  2843. <tr><td>start</td><td>start time in human-readable format</td></tr>
  2844. <tr><td>end</td><td>end time in human-readable format</td></tr>
  2845. <tr><td>categories</td><td>the content of the categories field</td></tr>
  2846. <tr><td>full</td><td>the full state</td></tr>
  2847. <tr><td>debug</td><td>like full with additional information for debugging purposes</td></tr>
  2848. </table><br>
  2849. The &lt;filter&gt; specifier determines the seriesed subset of calendar events:<br><br>
  2850. <table>
  2851. <tr><th align="left">&lt;filter&gt;</th align="left"><th>seriesion</th></tr>
  2852. <tr><td>mode=&lt;regex&gt;</td><td>all calendar events with mode matching the regular expression &lt;regex&gt</td></tr>
  2853. <tr><td>&lt;mode&gt;</td><td>all calendar events in the mode &lt;mode&gt</td></tr>
  2854. <tr><td>uid=&lt;regex&gt;</td><td>all calendar events identified by UIDs that match the regular expression &lt;regex&gt;.</td></tr>
  2855. <tr><td>&lt;uid&gt;</td><td>all calendar events identified by the UID &lt;uid&gt;</td></tr>
  2856. <tr><td>&lt;reading&gt;</td><td>all calendar events listed in the reading &lt;reading&gt; (modeAlarm, modeAlarmed, modeStart, etc.) - this is deprecated and will be removed in a future version, use mode=&lt;regex&gt; instead.</td></tr>
  2857. <tr><td>all</td><td>all calendar events (past, current and future)</td></tr>
  2858. <tr><td>next</td><td>only calendar events that have not yet ended and among these only the first in a series, best suited for display</td></tr>
  2859. </table><br>
  2860. The <code>mode=&lt;regex&gt;</code> and <code>uid=&lt;regex&gt;</code> filters should be preferred over the
  2861. <code>&lt;mode&gt;</code> and <code>&lt;uid&gt;</code> filters.<br><br>
  2862. The optional parameter <code>&lt;max&gt;</code> limits
  2863. the number of returned lines.<br><br>
  2864. See attributes <code>hideOlderThan</code> and
  2865. <code>hideLaterThan</code> for how to return events within a certain time window.
  2866. Please remember that the global &pm;400 days limits apply.<br><br>
  2867. Examples:<br>
  2868. <code>get MyCalendar text next</code><br>
  2869. <code>get MyCalendar summary uid:435kjhk435googlecom 1</code><br>
  2870. <code>get MyCalendar summary 435kjhk435googlecom 1</code><br>
  2871. <code>get MyCalendar full all</code><br>
  2872. <code>get MyCalendar text mode=alarm|start</code><br>
  2873. <code>get MyCalendar text uid=.*6286.*</code><br>
  2874. <br>
  2875. </li>
  2876. -->
  2877. <li><code>get &lt;name&gt; find &lt;regexp&gt;</code><br>
  2878. Returns, line by line, the UIDs of all calendar events whose summary matches the regular expression
  2879. &lt;regexp&gt;.<br><br></li>
  2880. <li><code>get &lt;name&gt; vcalendar</code><br>
  2881. Returns the calendar in ICal format as retrieved from the source.<br><br></li>
  2882. <li><code>get &lt;name&gt; vevents</code><br>
  2883. Returns a list of all VEVENT entries in the calendar with additional information for
  2884. debugging. Only properties that have been kept during processing of the source
  2885. are shown. The list of calendar events created from each VEVENT entry is shown as well
  2886. as the list of calendar events that have been omitted.</li>
  2887. </ul>
  2888. <br><br>
  2889. <a name="Calendarattr"></a>
  2890. <b>Attributes</b>
  2891. <br><br>
  2892. <ul>
  2893. <li><code>defaultFormat &lt;formatSpec&gt;</code><br>
  2894. Sets the default format for the <code>get &lt;name&gt; events</code>
  2895. command. The specification is explained there. You must enclose
  2896. the &lt;formatSpec&gt; in double quotes (") like input
  2897. in <code>attr myCalendar defaultFormat "$T1 $D $S"</code>.</li></p>
  2898. <li><code>defaultTimeFormat &lt;timeFormatSpec&gt;</code><br>
  2899. Sets the default time format for the <code>get &lt;name&gt;events</code>
  2900. command. The specification is explained there. Do not enclose
  2901. the &lt;timeFormatSpec&gt; in quotes.</li></p>
  2902. <li><code>update sync|async|none</code><br>
  2903. If this attribute is not set or if it is set to <code>sync</code>, the processing of
  2904. the calendar is done in the foreground. Large calendars will block FHEM on slow
  2905. systems. If this attribute is set to <code>async</code>, the processing is done in the
  2906. background and FHEM will not block during updates. If this attribute is set to
  2907. <code>none</code>, the calendar will not be updated at all.
  2908. </li><p>
  2909. <li><code>removevcalendar 0|1</code><br>
  2910. If this attribute is set to 1, the vCalendar will be discarded after the processing to reduce the memory consumption of the module.
  2911. A retrieval via <code>get &lt;name&gt; vcalendar</code> is then no longer possible.
  2912. </li><p>
  2913. <li><code>hideOlderThan &lt;timespec&gt;</code><br>
  2914. <code>hideLaterThan &lt;timespec&gt;</code><br><p>
  2915. These attributes limit the list of events shown by
  2916. <code>get &lt;name&gt; full|debug|text|summary|location|alarm|start|end ...</code>.<p>
  2917. The time is specified relative to the current time t. If hideOlderThan is set,
  2918. calendar events that ended before t-hideOlderThan are not shown. If hideLaterThan is
  2919. set, calendar events that will start after t+hideLaterThan are not shown.<p>
  2920. Please note that an action triggered by a change to mode "end" cannot access the calendar event
  2921. if you set hideOlderThan to 0 because the calendar event will already be hidden at that time. Better set
  2922. hideOlderThan to 10.<p>
  2923. <code>&lt;timespec&gt;</code> must have one of the following formats:<br>
  2924. <table>
  2925. <tr><th>format</th><th>description</th><th>example</th></tr>
  2926. <tr><td>SSS</td><td>seconds</td><td>3600</td></tr>
  2927. <tr><td>SSSs</td><td>seconds</td><td>3600s</td></tr>
  2928. <tr><td>HH:MM</td><td>hours:minutes</td><td>02:30</td></tr>
  2929. <tr><td>HH:MM:SS</td><td>hours:minutes:seconds</td><td>00:01:30</td></tr>
  2930. <tr><td>D:HH:MM:SS</td><td>days:hours:minutes:seconds</td><td>122:10:00:00</td></tr>
  2931. <tr><td>DDDd</td><td>days</td><td>100d</td></tr>
  2932. </table></li>
  2933. <p>
  2934. <li><code>cutoffOlderThan &lt;timespec&gt;</code><br>
  2935. This attribute cuts off all calendar events that ended a timespan cutoffOlderThan
  2936. before the last update of the calendar. The purpose of setting this attribute is to save memory.
  2937. Such calendar events cannot be accessed at all from FHEM. Calendar events are not cut off if
  2938. they are recurring with no end of series (UNTIL) or if they have no end time (DTEND).
  2939. </li><p>
  2940. <li><code>onCreateEvent &lt;perl-code&gt;</code><br>
  2941. This attribute allows to run the Perl code &lt;perl-code&gt; for every
  2942. calendar event that is created. See section <a href="#CalendarPlugIns">Plug-ins</a> below.
  2943. </li><p>
  2944. <li><code>SSLVerify</code><br>
  2945. This attribute sets the verification mode for the peer certificate for connections secured by
  2946. SSL. Set attribute either to 0 for SSL_VERIFY_NONE (no certificate verification) or
  2947. to 1 for SSL_VERIFY_PEER (certificate verification). Disabling verification is useful
  2948. for local calendar installations (e.g. OwnCloud, NextCloud) without valid SSL certificate.
  2949. </li><p>
  2950. <li><code>ignoreCancelled</code><br>
  2951. Set to 1 to ignore events with status "CANCELLED".
  2952. Set this attribute to 1 if calanedar events of a series are returned
  2953. although they are cancelled.
  2954. </li><p>
  2955. <li><code>quirks &lt;values&gt;</code><br>
  2956. Parameters to handle special situations. <code>&lt;values&gt;</code> is
  2957. a comma-separated list of the following keywords:
  2958. <ul>
  2959. <li><code>ignoreDtStamp</code>: if present, a modified DTSTAMP attribute of a calendar event
  2960. does not signify that the calendar event was modified.</li>
  2961. </ul>
  2962. </li><p>
  2963. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  2964. </ul>
  2965. <br><br>
  2966. <b>Description</b>
  2967. <ul>
  2968. <br>
  2969. A calendar is a set of calendar events. The calendar events are
  2970. fetched from the source calendar at the given URL on a regular basis.<p>
  2971. A calendar event has a summary (usually the title shown in a visual
  2972. representation of the source calendar), a start time, an end time, and zero, one or more alarm times. In case of multiple alarm times for a calendar event, only the
  2973. earliest alarm time is kept.<p>
  2974. Recurring calendar events (series) are currently supported to an extent:
  2975. FREQ INTERVAL UNTIL COUNT are interpreted, BYMONTHDAY BYMONTH WKST
  2976. are recognized but not interpreted. BYDAY is correctly interpreted for weekly and monthly events.
  2977. The module will get it most likely wrong
  2978. if you have recurring calendar events with unrecognized or uninterpreted keywords.
  2979. Out-of-order events and events excluded from a series (EXDATE) are handled.
  2980. Calendar events are only created within &pm;400 days around the time of the
  2981. last update.
  2982. <p>
  2983. Calendar events are created when FHEM is started or when the respective entry in the source
  2984. calendar has changed and the calendar is updated or when the calendar is reloaded with
  2985. <code>get &lt;name&gt; reload</code>.
  2986. Only calendar events within &pm;400 days around the event creation time are created. Consider
  2987. reloading the calendar from time to time to avoid running out of upcoming events. You can use something like <code>define reloadCalendar at +*240:00:00 set MyCalendar reload</code> for that purpose.<p>
  2988. Some dumb calendars do not use LAST-MODIFIED. This may result in modifications in the source calendar
  2989. go unnoticed. Reload the calendar if you experience this issue.<p>
  2990. A calendar event is identified by its UID. The UID is taken from the source calendar.
  2991. All events in a series including out-of-order events habe the same UID.
  2992. All non-alphanumerical characters
  2993. are stripped off the original UID to make your life easier.<p>
  2994. A calendar event can be in one of the following modes:
  2995. <table>
  2996. <tr><td>upcoming</td><td>Neither the alarm time nor the start time of the calendar event is reached.</td></tr>
  2997. <tr><td>alarm</td><td>The alarm time has passed but the start time of the calendar event is not yet reached.</td></tr>
  2998. <tr><td>start</td><td>The start time has passed but the end time of the calendar event is not yet reached.</td></tr>
  2999. <tr><td>end</td><td>The end time of the calendar event has passed.</td></tr>
  3000. </table><br>
  3001. A calendar event transitions from one mode to another immediately when the time for the change has come. This is done by waiting
  3002. for the earliest future time among all alarm, start or end times of all calendar events.
  3003. <p>
  3004. A calendar device has several readings. Except for <code>calname</code>, each reading is a semicolon-separated list of UIDs of
  3005. calendar events that satisfy certain conditions:
  3006. <table>
  3007. <tr><td>calname</td><td>name of the calendar</td></tr>
  3008. <tr><td>modeAlarm</td><td>events in alarm mode</td></tr>
  3009. <tr><td>modeAlarmOrStart</td><td>events in alarm or start mode</td></tr>
  3010. <tr><td>modeAlarmed</td><td>events that have just transitioned from upcoming to alarm mode</td></tr>
  3011. <tr><td>modeChanged</td><td>events that have just changed their mode somehow</td></tr>
  3012. <tr><td>modeEnd</td><td>events in end mode</td></tr>
  3013. <tr><td>modeEnded</td><td>events that have just transitioned from start to end mode</td></tr>
  3014. <tr><td>modeStart</td><td>events in start mode</td></tr>
  3015. <tr><td>modeStarted</td><td>events that have just transitioned to start mode</td></tr>
  3016. <tr><td>modeUpcoming</td><td>events in upcoming mode</td></tr>
  3017. </table>
  3018. <p>
  3019. For recurring events, usually several calendar events exists with the same UID. In such a case,
  3020. the UID is only shown in the mode reading for the most interesting mode. The most
  3021. interesting mode is the first applicable of start, alarm, upcoming, end.<p>
  3022. In particular, you will never see the UID of a series in modeEnd or modeEnded as long as the series
  3023. has not yet ended - the UID will be in one of the other mode... readings. This means that you better
  3024. do not trigger FHEM events for series based on mode... readings. See below for a recommendation.<p>
  3025. </ul>
  3026. <br>
  3027. <b>Events</b>
  3028. <br><br>
  3029. <ul>
  3030. When the calendar was reloaded or updated or when an alarm, start or end time was reached, one
  3031. FHEM event is created:<p>
  3032. <code>triggered</code><br><br>
  3033. When you receive this event, you can rely on the calendar's readings being in a consistent and
  3034. most recent state.<p>
  3035. When a calendar event has changed, two FHEM events are created:<p>
  3036. <code>changed: UID &lt;mode&gt;</code><br>
  3037. <code>&lt;mode&gt;: UID</code><br><br>
  3038. &lt;mode&gt; is the current mode of the calendar event after the change. Note: there is a
  3039. colon followed by a single space in the FHEM event specification.<p>
  3040. The recommended way of reacting on mode changes of calendar events is to get notified
  3041. on the aforementioned FHEM events and do not check for the FHEM events triggered
  3042. by a change of a mode reading.
  3043. <br><br>
  3044. </ul>
  3045. <a name="CalendarPlugIns"></a>
  3046. <b>Plug-ins</b>
  3047. <ul>
  3048. <br>
  3049. A plug-in is a piece of Perl code that modifies a calendar event on the fly. The Perl code operates on the
  3050. hash reference <code>$e</code>. The most important elements are as follows:
  3051. <table>
  3052. <tr><th>code</th><th>description</th></tr>
  3053. <tr><td>$e->{start}</td><td>the start time of the calendar event, in seconds since the epoch</td></tr>
  3054. <tr><td>$e->{end}</td><td>the end time of the calendar event, in seconds since the epoch</td></tr>
  3055. <tr><td>$e->{alarm}</td><td>the alarm time of the calendar event, in seconds since the epoch</td></tr>
  3056. <tr><td>$e->{summary}</td><td>the summary (caption, title) of the calendar event</td></tr>
  3057. <tr><td>$e->{location}</td><td>the location of the calendar event</td></tr>
  3058. </table><br>
  3059. To add or change the alarm time of a calendar event for all events with the string "Tonne" in the
  3060. summary, the following plug-in can be used:<br><br>
  3061. <code>attr MyCalendar onCreateEvent { $e->{alarm}= $e->{start}-86400 if($e->{summary} =~ /Tonne/);; }</code><br>
  3062. <br>The double semicolon masks the semicolon. <a href="#perl">Perl specials</a> cannot be used.<br>
  3063. <br>
  3064. To add a missing end time, the following plug-in can be used:<br><br>
  3065. <code>attr MyCalendar onCreateEvent { $e->{end}= $e->{start}+86400 unless(defined($e->{end})) }</code><br>
  3066. </ul>
  3067. <br><br>
  3068. <b>Usage scenarios</b>
  3069. <ul><br>
  3070. <i>Show all calendar events with details</i><br><br>
  3071. <ul>
  3072. <code>
  3073. get MyCalendar events format:full<br>
  3074. 2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom alarm 31.05.2012 17:00:00 07.06.2012 16:30:00-07.06.2012 18:00:00 Erna for coffee<br>
  3075. 992hydf4y44awer5466lhfdsr&shy;gl7tin6b6mckf8glmhui4&shy;googlecom upcoming 08.06.2012 00:00:00-09.06.2012 00:00:00 Vacation
  3076. </code><br><br>
  3077. </ul>
  3078. <i>Show calendar events in your photo frame</i><br><br>
  3079. <ul>
  3080. Put a line in the <a href="#RSSlayout">layout description</a> to show calendar events in alarm or start mode:<br><br>
  3081. <code>text 20 60 { fhem("get MyCalendar events timeFormat:'%d.%m.%Y %H:%M' format:custom='$T1 $S' filter:mode=~'alarm|start') }</code><br><br>
  3082. This may look like:<br><br>
  3083. <code>
  3084. 07.06.12 16:30 Erna for coffee<br>
  3085. 08.06.12 00:00 Vacation
  3086. </code><br><br>
  3087. </ul>
  3088. <i>Switch the light on when Erna comes</i><br><br>
  3089. <ul>
  3090. First find the UID of the calendar event:<br><br>
  3091. <code>
  3092. get MyCalendar find .*Erna.*<br>
  3093. 2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom
  3094. </code><br><br>
  3095. Then define a notify (the dot after the second colon matches the space):<br><br>
  3096. <code>
  3097. define ErnaComes notify MyCalendar:start:.2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom set MyLight on
  3098. </code><br><br>
  3099. You can also do some logging:<br><br>
  3100. <code>
  3101. define LogErna notify MyCalendar:alarm:.2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" }
  3102. </code><br><br>
  3103. </ul>
  3104. <i>Switch actors on and off</i><br><br>
  3105. <ul>
  3106. Think about a calendar with calendar events whose summaries (subjects, titles) are the names of devices in your fhem installation.
  3107. You want the respective devices to switch on when the calendar event starts and to switch off when the calendar event ends.<br><br>
  3108. <code>
  3109. define SwitchActorOn notify MyCalendar:start:.* { \<br>
  3110. my $reading="$EVTPART0";; \<br>
  3111. my $uid= "$EVTPART1";; \<br>
  3112. my $actor= fhem('get MyCalendar filter:uid=="'.$uid.'" format:custom="$S"');; \<br>
  3113. if(defined $actor) {
  3114. fhem("set $actor on")
  3115. } \<br>
  3116. }<br><br>
  3117. define SwitchActorOff notify MyCalendar:end:.* { \<br>
  3118. my $reading="$EVTPART0";; \<br>
  3119. my $uid= "$EVTPART1";; \<br>
  3120. my $actor= fhem('get MyCalendar filter:uid=="'.$uid.'" format:custom="$S"');; \<br>
  3121. if(defined $actor) {
  3122. fhem("set $actor off")
  3123. } \<br>
  3124. }
  3125. </code><br><br>
  3126. You can also do some logging:<br><br>
  3127. <code>
  3128. define LogActors notify MyCalendar:(start|end):.*
  3129. { my $reading= "$EVTPART0";; my $uid= "$EVTPART1";; \<br>
  3130. my $actor= fhem('get MyCalendar filter:uid=="'.$uid.'" format:custom="$S"');; \<br>
  3131. Log3 $NAME, 1, "Actor: $actor, Reading $reading" }
  3132. </code><br><br>
  3133. </ul>
  3134. <i>Inform about garbage collection</i><br><br>
  3135. <ul>
  3136. We assume the <code>GarbageCalendar</code> has all the dates of the
  3137. garbage collection with the type of garbage collected in the summary. The
  3138. following notify can be used to inform about the garbage collection:
  3139. <br><br><code>
  3140. define GarbageCollectionNotifier notify GarbageCalendar:alarm:.* { \<br>
  3141. my $uid= "$EVTPART1";; \<br>
  3142. my $summary= fhem('get MyCalendar events filter:uid=="'.$uid.'" format:custom="$S"');; \<br>
  3143. # e.g. mail $summary to someone \<br>
  3144. }</code><br><br>
  3145. If the garbage calendar has no reminders, you can set these to one day
  3146. before the date of the collection:<br><br><code>
  3147. attr GarbageCalendar onCreateEvent { $e->{alarm}= $e->{start}-86400 }
  3148. </code><br><br>
  3149. The following code realizes a HTML display of the upcoming collection
  3150. dates (see below):<br><br>
  3151. <code>{ CalendarEventsAsHtml('GarbageCalendar','format:text filter:mode=~"alarm|start"') }</code>
  3152. <br>
  3153. </ul>
  3154. </ul>
  3155. <br>
  3156. <b>Embedded HTML</b>
  3157. <ul><br>
  3158. The module provides two functions which return HTML code.<br><br>
  3159. <code>CalendarAsHtml(&lt;name&gt;,&lt;options&gt;)</code>
  3160. returns the HTML code for a list of calendar events. <code>&lt;name&gt;</code> is the name of the
  3161. Calendar device and <code>&lt;options&gt;</code> is what you would write
  3162. after <code>get &lt;name&gt; text ...</code>. This function is deprecated.
  3163. <br><br>
  3164. Example: <code>define MyCalendarWeblink weblink htmlCode { CalendarAsHtml("MyCalendar","next 3") }</code>
  3165. <br><br>
  3166. <code>CalendarEventsAsHtml(&lt;name&gt;,&lt;parameters&gt;)</code>
  3167. returns the HTML code for a list of calendar events. <code>&lt;name&gt;</code> is the name of the
  3168. Calendar device and <code>&lt;parameters&gt;</code> is what you would write
  3169. in <code>get &lt;name&gt; events &lt;parameters&gt;</code>.
  3170. <br><br>
  3171. Example: <code>define MyCalendarWeblink weblink htmlCode
  3172. { CalendarEventsAsHtml('F','format:custom="$T1 $D $S" timeFormat="%d.%m" series:next=3') }</code>
  3173. <br><br>
  3174. Tip: use single quotes as outer quotes.
  3175. <p>
  3176. </ul>
  3177. </ul>
  3178. =end html
  3179. =begin html_DE
  3180. <a name="Calendar"></a>
  3181. <h3>Calendar</h3>
  3182. <ul>
  3183. <br>
  3184. <b>Diese deutsche &Uuml;bersetzung ist nicht mehr aktuell
  3185. (siehe bitte <a href="https://forum.fhem.de/index.php/topic,86148.msg786290.html#msg786290">Forumsbeitrag</a>).
  3186. Bitte hilf bei FHEM mit und aktualisiere diese &Uuml;bersetzung. Die englischsprachige
  3187. Dokumentation ist immer aktuell.</b><p>
  3188. <a name="Calendardefine"></a>
  3189. <b>Define</b>
  3190. <ul>
  3191. <code>define &lt;name&gt; Calendar ical url &lt;URL&gt; [&lt;interval&gt;]</code><br>
  3192. <code>define &lt;name&gt; Calendar ical file &lt;FILENAME&gt; [&lt;interval&gt;]</code><br>
  3193. <br>
  3194. Definiert ein Kalender-Device.<br><br>
  3195. Ein Kalender-Device ermittelt (Serien-) Termine aus einem Quell-Kalender. Dieser kann eine URL oder eine Datei sein.
  3196. Die Datei muss im iCal-Format vorliegen.<br><br>
  3197. Beginnt die URL mit <code>https://</code>, muss das Perl-Modul IO::Socket::SSL installiert sein
  3198. (use <code>cpan -i IO::Socket::SSL</code>).<br><br>
  3199. Hinweis f&uuml;r Nutzer des Google-Kalenders: Du kann direkt die private iCal-URL des Google Kalender nutzen.
  3200. Sollte Deine Google-Kalender-URL mit <code>https://</code> beginnen und das Perl-Modul IO::Socket::SSL ist nicht auf Deinem Systeme installiert,
  3201. kannst Du in der URL <code>https://</code> durch <code>http://</code> ersetzen, falls keine automatische Umleitung auf die <code>https://</code> URL erfolgt.
  3202. Solltest Du unsicher sein, ob dies der Fall ist, &uuml;berpr&uuml;fe es bitte zuerst mit Deinem Browser.<br><br>
  3203. Hinweis f&uuml;r Nutzer des Nextcloud-Kalenders: Du kannst eine URL der folgenden Form benutzen:
  3204. <code>https://admin:admin@demo.nextcloud.com/wid0ohgh/remote.php/dav/calendars/admin/personal/?export</code>.<p>
  3205. Der optionale Parameter <code>interval</code> bestimmt die Zeit in Sekunden zwischen den Updates. Default-Wert ist 3600 (1 Stunde).<br><br>
  3206. Beispiele:
  3207. <pre>
  3208. define MeinKalender Calendar ical url https://www.google.com&shy;/calendar/ical/john.doe%40example.com&shy;/private-foo4711/basic.ics
  3209. define DeinKalender Calendar ical url http://www.google.com&shy;/calendar/ical/jane.doe%40example.com&shy;/private-bar0815/basic.ics 86400
  3210. define IrgendeinKalender Calendar ical file /home/johndoe/calendar.ics
  3211. </pre>
  3212. </ul>
  3213. <br>
  3214. <a name="Calendarset"></a>
  3215. <b>Set </b><br><br>
  3216. <ul>
  3217. <code>set &lt;name&gt; update</code><br>
  3218. Erzwingt das Einlesen des Kalenders von der definierten URL. Das n&auml;chste automatische Einlesen erfolgt in
  3219. <code>interval</code> Sekunden sp&auml;ter.<br><br>
  3220. <code>set &lt;name&gt; reload</code><br>
  3221. Dasselbe wie <code>update</code>, jedoch werden zuerst alle Termine entfernt.<br><br>
  3222. </ul>
  3223. <br>
  3224. <a name="Calendarget"></a>
  3225. <b>Get</b><br><br>
  3226. <ul>
  3227. <code>get &lt;name&gt; update</code><br>
  3228. Entspricht <code>set &lt;name&gt; update</code><br><br>
  3229. <code>get &lt;name&gt; reload</code><br>
  3230. Entspricht <code>set &lt;name&gt; reload</code><br><br>
  3231. <code>get &lt;name&gt; &lt;format&gt; &lt;filter&gt; [&lt;max&gt;]</code><br>
  3232. Die Termine f&uuml;r den Kalender &lt;name&gt; werden Zeile f&uuml;r Zeile ausgegeben.<br><br>
  3233. Folgende Selektoren/Filter stehen zur Verf&uuml;gung:<br><br>
  3234. Der Selektor &lt;format&gt legt den zur&uuml;ckgegeben Inhalt fest:<br><br>
  3235. <table>
  3236. <tr><th>&lt;format&gt;</th><th>Inhalt</th></tr>
  3237. <tr><td>uid</td><td>UID des Termins</td></tr>
  3238. <tr><td>text</td><td>Benutzer-/Monitorfreundliche Textausgabe.</td></tr>
  3239. <tr><td>summary</td><td>&Uuml;bersicht (Betreff, Titel)</td></tr>
  3240. <tr><td>location</td><td>Ort</td></tr>
  3241. <tr><td>categories</td><td>Kategorien</td></tr>
  3242. <tr><td>alarm</td><td>Alarmzeit</td></tr>
  3243. <tr><td>start</td><td>Startzeit</td></tr>
  3244. <tr><td>end</td><td>Endezeit</td></tr>
  3245. <tr><td>full</td><td>Vollst&auml;ndiger Status</td></tr>
  3246. <tr><td>debug</td><td>wie &lt;full&gt; mit zus&auml;tzlichen Informationen zur Fehlersuche</td></tr>
  3247. </table><br>
  3248. Der Filter &lt;filter&gt; grenzt die Termine ein:<br><br>
  3249. <table>
  3250. <tr><th>&lt;filter&gt;</th><th>Inhalt</th></tr>
  3251. <tr><td>mode=&lt;regex&gt;</td><td>alle Termine, deren Modus durch den regul&auml;ren Ausdruck &lt;regex&gt beschrieben werden.</td></tr>
  3252. <tr><td>&lt;mode&gt;</td><td>alle Termine mit Modus &lt;mode&gt.</td></tr>
  3253. <tr><td>uid=&lt;regex&gt;</td><td>Alle Termine, deren UIDs durch den regul&auml;ren Ausdruck &lt;regex&gt beschrieben werden.</td></tr>
  3254. <tr><td>&lt;uid&gt;</td><td>Alle Termine mit der UID &lt;uid&gt;</td></tr>
  3255. <tr><td>&lt;reading&gt;</td><td>Alle Termine die im Reading &lt;reading&gt; aufgelistet werden (modeAlarm, modeAlarmed, modeStart, etc.)
  3256. - dieser Filter ist abgek&uuml;ndigt und steht in einer zuk&uuml;nftigen Version nicht mehr zur Verf&uuml;gung, bitte mode=&lt;regex&gt; benutzen.</td></tr>
  3257. <tr><td>all</td><td>Alle Termine (vergangene, aktuelle und zuk&uuml;nftige)</td></tr>
  3258. <tr><td>next</td><td>Alle Termine, die noch nicht beendet sind. Bei Serienterminen der erste Termin. Benutzer-/Monitorfreundliche Textausgabe</td></tr>
  3259. </table><br>
  3260. Die Filter <code>mode=&lt;regex&gt;</code> und <code>uid=&lt;regex&gt;</code> sollten den Filtern
  3261. <code>&lt;mode&gt;</code> und <code>&lt;uid&gt;</code> vorgezogen werden.<br><br>
  3262. Der optionale Parameter <code>&lt;max&gt;</code> schr&auml;nkt die Anzahl der zur&uuml;ckgegebenen Zeilen ein.<br><br>
  3263. Bitte beachte die Attribute <code>hideOlderThan</code> und
  3264. <code>hideLaterThan</code> f&uuml;r die Seletion von Terminen in einem bestimmten Zeitfenster.
  3265. Bitte ber&uuml;cksichtige, dass das globale &pm;400 Tageslimit gilt .<br><br>
  3266. Beispiele:<br>
  3267. <code>get MyCalendar text next</code><br>
  3268. <code>get MyCalendar summary uid:435kjhk435googlecom 1</code><br>
  3269. <code>get MyCalendar summary 435kjhk435googlecom 1</code><br>
  3270. <code>get MyCalendar full all</code><br>
  3271. <code>get MyCalendar text mode=alarm|start</code><br>
  3272. <code>get MyCalendar text uid=.*6286.*</code><br>
  3273. <br>
  3274. <code>get &lt;name&gt; find &lt;regexp&gt;</code><br>
  3275. Gibt Zeile f&uuml;r Zeile die UIDs aller Termine deren Zusammenfassungen durch den regul&auml;ren Ausdruck &lt;regex&gt beschrieben werden.
  3276. &lt;regexp&gt;.<br><br>
  3277. <code>get &lt;name&gt; vcalendar</code><br>
  3278. Gibt den Kalender ICal-Format, so wie er von der Quelle gelesen wurde, zur&uuml;ck.<br><br>
  3279. <code>get &lt;name&gt; vevents</code><br>
  3280. Gibt eine Liste aller VEVENT-Eintr&auml;ge des Kalenders &lt;name&gt;, angereichert um Ausgaben f&uuml;r die Fehlersuche, zur&uuml;ck.
  3281. Es werden nur Eigenschaften angezeigt, die w&auml;hrend der Programmausf&uuml;hrung beibehalten wurden. Es wird sowohl die Liste
  3282. der Termine, die von jedem VEVENT-Eintrag erzeugt wurden, als auch die Liste der ausgelassenen Termine angezeigt.
  3283. </ul>
  3284. <br>
  3285. <a name="Calendarattr"></a>
  3286. <b>Attributes</b>
  3287. <br><br>
  3288. <ul>
  3289. <li><code>update sync|async|none</code><br>
  3290. Wenn dieses Attribut nicht gesetzt ist oder wenn es auf <code>sync</code> gesetzt ist,
  3291. findet die Verarbeitung des Kalenders im Vordergrund statt. Gro&szlig;e Kalender werden FHEM
  3292. auf langsamen Systemen blockieren. Wenn das Attribut auf <code>async</code> gesetzt ist,
  3293. findet die Verarbeitung im Hintergrund statt, und FHEM wird w&auml;hrend der Verarbeitung
  3294. nicht blockieren. Wenn dieses Attribut auf <code>none</code> gesetzt ist, wird der
  3295. Kalender &uuml;berhaupt nicht aktualisiert.
  3296. </li><p>
  3297. <li><code>removevcalendar 0|1</code><br>
  3298. Wenn dieses Attribut auf 1 gesetzt ist, wird der vCalendar nach der Verarbeitung verworfen,
  3299. gleichzeitig reduziert sich der Speicherverbrauch des Moduls.
  3300. Ein Abruf &uuml;ber <code>get &lt;name&gt; vcalendar</code> ist dann nicht mehr m&ouml;glich.
  3301. </li><p>
  3302. <li><code>hideOlderThan &lt;timespec&gt;</code><br>
  3303. <code>hideLaterThan &lt;timespec&gt;</code><br><p>
  3304. Dieses Attribut grenzt die Liste der durch <code>get &lt;name&gt; full|debug|text|summary|location|alarm|start|end ...</code> gezeigten Termine ein.
  3305. Die Zeit wird relativ zur aktuellen Zeit t angegeben.<br>
  3306. Wenn &lt;hideOlderThan&gt; gesetzt ist, werden Termine, die vor &lt;t-hideOlderThan&gt; enden, ingnoriert.<br>
  3307. Wenn &lt;hideLaterThan&gt; gesetzt ist, werden Termine, die nach &lt;t+hideLaterThan&gt; anfangen, ignoriert.<p>
  3308. Bitte beachten, dass eine Aktion, die durch einen Wechsel in den Modus "end" ausgel&ouml;st wird, nicht auf den Termin
  3309. zugreifen kann, wenn hideOlderThan 0 ist, weil der Termin dann schon versteckt ist. Besser hideOlderThan auf 10 setzen.<p>
  3310. <code>&lt;timespec&gt;</code> muss einem der folgenden Formate entsprechen:<br>
  3311. <table>
  3312. <tr><th>Format</th><th>Beschreibung</th><th>Beispiel</th></tr>
  3313. <tr><td>SSS</td><td>Sekunden</td><td>3600</td></tr>
  3314. <tr><td>SSSs</td><td>Sekunden</td><td>3600s</td></tr>
  3315. <tr><td>HH:MM</td><td>Stunden:Minuten</td><td>02:30</td></tr>
  3316. <tr><td>HH:MM:SS</td><td>Stunden:Minuten:Sekunden</td><td>00:01:30</td></tr>
  3317. <tr><td>D:HH:MM:SS</td><td>Tage:Stunden:Minuten:Sekunden</td><td>122:10:00:00</td></tr>
  3318. <tr><td>DDDd</td><td>Tage</td><td>100d</td></tr>
  3319. </table></li>
  3320. <p>
  3321. <li><code>cutoffOlderThan &lt;timespec&gt;</code><br>
  3322. Dieses Attribut schneidet alle Termine weg, die eine Zeitspanne cutoffOlderThan
  3323. vor der letzten Aktualisierung des Kalenders endeten. Der Zweck dieses Attributs ist es Speicher zu
  3324. sparen. Auf solche Termine kann gar nicht mehr aus FHEM heraus zugegriffen
  3325. werden. Serientermine ohne Ende (UNTIL) und
  3326. Termine ohne Endezeitpunkt (DTEND) werden nicht weggeschnitten.
  3327. </li><p>
  3328. <li><code>onCreateEvent &lt;perl-code&gt;</code><br>
  3329. Dieses Attribut f&uuml;hrt ein Perlprogramm &lt;perl-code&gt; f&uuml;r jeden erzeugten Termin aus.
  3330. Weitere Informationen unter <a href="#CalendarPlugIns">Plug-ins</a> im Text.
  3331. </li><p>
  3332. <li><code>SSLVerify</code><br>
  3333. Dieses Attribut setzt die Art der &Uuml;berpr&uuml;fung des Zertifikats des Partners
  3334. bei mit SSL gesicherten Verbindungen. Entweder auf 0 setzen f&uuml;r
  3335. SSL_VERIFY_NONE (keine &Uuml;berpr&uuml;fung des Zertifikats) oder auf 1 f&uuml;r
  3336. SSL_VERIFY_PEER (&Uuml;berpr&uuml;fung des Zertifikats). Die &Uuml;berpr&uuml;fung auszuschalten
  3337. ist n&uuml;tzlich f&uuml;r lokale Kalenderinstallationen(e.g. OwnCloud, NextCloud)
  3338. ohne g&uuml;tiges SSL-Zertifikat.
  3339. </li><p>
  3340. <li><code>ignoreCancelled</code><br>
  3341. Wenn dieses Attribut auf 1 gesetzt ist, werden Termine im Status "CANCELLED" ignoriert.
  3342. Dieses Attribut auf 1 setzen, falls Termine in einer
  3343. Serie zur&uuml;ckgegeben werden, die gel&ouml;scht sind.
  3344. </li><p>
  3345. <li><code>quirks &lt;values&gt;</code><br>
  3346. Parameter f&uuml;r spezielle Situationen. <code>&lt;values&gt;</code> ist
  3347. eine mit Kommas getrennte Liste der folgenden Schl&uuml;sselw&ouml;rter:
  3348. <ul>
  3349. <li><code>ignoreDtStamp</code>: wenn gesetzt, dann zeigt
  3350. ein ver&auml;ndertes DTSTAMP Attribut eines Termins nicht an, dass
  3351. der Termin ver&auml;ndert wurde.</li>
  3352. </ul>
  3353. </li><p>
  3354. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  3355. </ul>
  3356. <br>
  3357. <b>Beschreibung</b>
  3358. <ul><br>
  3359. Ein Kalender ist eine Menge von Terminen. Ein Termin hat eine Zusammenfassung (normalerweise der Titel, welcher im Quell-Kalender angezeigt wird), eine Startzeit, eine Endzeit und keine, eine oder mehrere Alarmzeiten. Die Termine werden
  3360. aus dem Quellkalender ermittelt, welcher &uuml;ber die URL angegeben wird. Sollten mehrere Alarmzeiten f&uuml;r einen Termin existieren, wird nur der fr&uuml;heste Alarmzeitpunkt beibehalten. Wiederkehrende Kalendereintr&auml;ge werden in einem gewissen Umfang unterst&uuml;tzt:
  3361. FREQ INTERVAL UNTIL COUNT werden ausgewertet, BYMONTHDAY BYMONTH WKST
  3362. werden erkannt aber nicht ausgewertet. BYDAY wird f&uuml;r w&ouml;chentliche und monatliche Termine
  3363. korrekt behandelt. Das Modul wird es sehr wahrscheinlich falsch machen, wenn Du wiederkehrende Termine mit unerkannten oder nicht ausgewerteten Schl&uuml;sselw&ouml;rtern hast.<p>
  3364. Termine werden erzeugt, wenn FHEM gestartet wird oder der betreffende Eintrag im Quell-Kalender ver&auml;ndert
  3365. wurde oder der Kalender mit <code>get &lt;name&gt; reload</code> neu geladen wird. Es werden nur Termine
  3366. innerhalb &pm;400 Tage um die Erzeugungs des Termins herum erzeugt. Ziehe in Betracht, den Kalender von Zeit zu Zeit
  3367. neu zu laden, um zu vermeiden, dass die k&uuml;nftigen Termine ausgehen. Du kann so etwas wie <code>define reloadCalendar at +*240:00:00 set MyCalendar reload</code> daf&uuml;r verwenden.<p>
  3368. Manche dummen Kalender benutzen LAST-MODIFIED nicht. Das kann dazu f&uuml;hren, dass Ver&auml;nderungen im
  3369. Quell-Kalender unbemerkt bleiben. Lade den Kalender neu, wenn Du dieses Problem hast.<p>
  3370. Ein Termin wird durch seine UID identifiziert. Die UID wird vom Quellkalender bezogen. Um das Leben leichter zu machen, werden alle nicht-alphanumerischen Zeichen automatisch aus der UID entfernt.<p>
  3371. Ein Termin kann sich in einem der folgenden Modi befinden:
  3372. <table>
  3373. <tr><td>upcoming</td><td>Weder die Alarmzeit noch die Startzeit des Kalendereintrags ist erreicht.</td></tr>
  3374. <tr><td>alarm</td><td>Die Alarmzeit ist &uuml;berschritten, aber die Startzeit des Kalender-Ereignisses ist noch nicht erreicht.</td></tr>
  3375. <tr><td>start</td><td>Die Startzeit ist &uuml;berschritten, aber die Ende-Zeit des Kalender-Ereignisses ist noch nicht erreicht.</td></tr>
  3376. <tr><td>end</td><td>Die Ende-Zeit des Kalender-Ereignisses wurde &uuml;berschritten.</td></tr>
  3377. </table><br>
  3378. Ein Kalender-Ereignis wechselt umgehend von einem Modus zum Anderen, wenn die Zeit f&uuml;r eine &Auml;nderung erreicht wurde. Dies wird dadurch erreicht, dass auf die fr&uuml;heste zuk&uuml;nftige Zeit aller Alarme, Start- oder Endezeiten aller Kalender-Ereignisse gewartet wird.
  3379. <p>
  3380. Ein Kalender-Device hat verschiedene Readings. Mit Ausnahme von <code>calname</code> stellt jedes Reading eine Semikolon-getrennte Liste von UIDs von Kalender-Ereignisse dar, welche bestimmte Zust&auml;nde haben:
  3381. <table>
  3382. <tr><td>calname</td><td>Name des Kalenders</td></tr>
  3383. <tr><td>modeAlarm</td><td>Ereignisse im Alarm-Modus</td></tr>
  3384. <tr><td>modeAlarmOrStart</td><td>Ereignisse im Alarm- oder Startmodus</td></tr>
  3385. <tr><td>modeAlarmed</td><td>Ereignisse, welche gerade in den Alarmmodus gewechselt haben</td></tr>
  3386. <tr><td>modeChanged</td><td>Ereignisse, welche gerade in irgendeiner Form ihren Modus gewechselt haben</td></tr>
  3387. <tr><td>modeEnd</td><td>Ereignisse im Endemodus</td></tr>
  3388. <tr><td>modeEnded</td><td>Ereignisse, welche gerade vom Start- in den Endemodus gewechselt haben</td></tr>
  3389. <tr><td>modeStart</td><td>Ereignisse im Startmodus</td></tr>
  3390. <tr><td>modeStarted</td><td>Ereignisse, welche gerade in den Startmodus gewechselt haben</td></tr>
  3391. <tr><td>modeUpcoming</td><td>Ereignisse im zuk&uuml;nftigen Modus</td></tr>
  3392. </table>
  3393. <p>
  3394. F&uuml;r Serientermine werden mehrere Termine mit der selben UID erzeugt. In diesem Fall
  3395. wird die UID nur im interessantesten gelesenen Modus-Reading angezeigt.
  3396. Der interessanteste Modus ist der erste zutreffende Modus aus der Liste der Modi start, alarm, upcoming, end.<p>
  3397. Die UID eines Serientermins wird nicht angezeigt, solange sich der Termin im Modus: modeEnd oder modeEnded befindet
  3398. und die Serie nicht beendet ist. Die UID befindet sich in einem der anderen mode... Readings.
  3399. Hieraus ergibts sich, das FHEM-Events nicht auf einem mode... Reading basieren sollten.
  3400. Weiter unten im Text gibt es hierzu eine Empfehlung.<p>
  3401. </ul>
  3402. <b>Events</b>
  3403. <ul><br>
  3404. Wenn der Kalendar neu geladen oder aktualisiert oder eine Alarm-, Start- oder Endezeit
  3405. erreicht wurde, wird ein FHEM-Event erzeugt:<p>
  3406. <code>triggered</code><br><br>
  3407. Man kann sich darauf verlassen, dass alle Readings des Kalenders in einem konsistenten und aktuellen
  3408. Zustand befinden, wenn dieses Event empfangen wird.<p>
  3409. Wenn ein Termin ge&auml;ndert wurde, werden zwei FHEM-Events erzeugt:<p>
  3410. <code>changed: UID &lt;mode&gt;</code><br>
  3411. <code>&lt;mode&gt;: UID</code><br><br>
  3412. &lt;mode&gt; ist der aktuelle Modus des Termins nach der &auml;nderung. Bitte beachten: Im FHEM-Event befindet sich ein Doppelpunkt gefolgt von einem Leerzeichen.<p>
  3413. FHEM-Events sollten nur auf den vorgenannten Events basieren und nicht auf FHEM-Events, die durch &auml;ndern eines mode... Readings ausgel&ouml;st werden.
  3414. <p>
  3415. </ul>
  3416. <a name="CalendarPlugIns"></a>
  3417. <b>Plug-ins</b>
  3418. <ul>
  3419. <br>
  3420. Experimentell, bitte mit Vorsicht nutzen.<p>
  3421. Ein Plug-In ist ein kleines Perl-Programm, dass Termine nebenher ver&auml;ndern kann.
  3422. Das Perl-Programm arbeitet mit der Hash-Referenz <code>$e</code>.<br>
  3423. Die wichtigsten Elemente sind:
  3424. <table>
  3425. <tr><th>code</th><th>description</th></tr>
  3426. <tr><td>$e->{start}</td><td>Startzeit des Termins, in Sekunden seit 1.1.1970</td></tr>
  3427. <tr><td>$e->{end}</td><td>Endezeit des Termins, in Sekunden seit 1.1.1970</td></tr>
  3428. <tr><td>$e->{alarm}</td><td>Alarmzeit des Termins, in Sekunden seit 1.1.1970</td></tr>
  3429. <tr><td>$e->{summary}</td><td>die Zusammenfassung (Betreff, Titel) des Termins</td></tr>
  3430. <tr><td>$e->{location}</td><td>Der Ort des Termins</td></tr>
  3431. </table><br>
  3432. Um f&uuml;r alle Termine mit dem Text "Tonne" in der Zusammenfassung die Alarmzeit zu erg&auml;nzen / zu &auml;ndern,
  3433. kann folgendes Plug-In benutzt werden:<br><br>
  3434. <code>attr MyCalendar onCreateEvent { $e->{alarm}= $e->{start}-86400 if($e->{summary} =~ /Tonne/);; }</code><br>
  3435. <br>Das doppelte Semikolon maskiert das Semikolon. <a href="#perl">Perl specials</a> k&ouml;nnen nicht genutzt werden.<br>
  3436. </ul>
  3437. <br><br>
  3438. <b>Anwendungsbeispiele</b>
  3439. <ul><br>
  3440. <i>Alle Termine inkl. Details anzeigen</i><br><br>
  3441. <ul>
  3442. <code>
  3443. get MyCalendar full all<br>
  3444. 2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom known alarm 31.05.2012 17:00:00 07.06.2012 16:30:00-07.06.2012 18:00:00 Erna for coffee<br>
  3445. 992hydf4y44awer5466lhfdsr&shy;gl7tin6b6mckf8glmhui4&shy;googlecom known upcoming 08.06.2012 00:00:00-09.06.2012 00:00:00 Vacation
  3446. </code><br><br>
  3447. </ul>
  3448. <i>Zeige Termine in Deinem Bilderrahmen</i><br><br>
  3449. <ul>
  3450. F&uuml;ge eine Zeile in die <a href="#RSSlayout">layout description</a> ein, um Termine im Alarm- oder Startmodus anzuzeigen:<br><br>
  3451. <code>text 20 60 { fhem("get MyCalendar text next 2") }</code><br><br>
  3452. Dies kann dann z.B. so aussehen:<br><br>
  3453. <code>
  3454. 07.06.12 16:30 Erna zum Kaffee<br>
  3455. 08.06.12 00:00 Urlaub
  3456. </code><br><br>
  3457. </ul>
  3458. <i>Schalte das Licht ein, wenn Erna kommt</i><br><br>
  3459. <ul>
  3460. Finde zuerst die UID des Termins:<br><br>
  3461. <code>
  3462. get MyCalendar find .*Erna.*<br>
  3463. 2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom
  3464. </code><br><br>
  3465. Definiere dann ein notify: (Der Punkt nach dem zweiten Doppelpunkt steht f&uuml;r ein Leerzeichen)<br><br>
  3466. <code>
  3467. define ErnaComes notify MyCalendar:start:.2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom.* set MyLight on
  3468. </code><br><br>
  3469. Du kannst auch ein Logging aufsetzen:<br><br>
  3470. <code>
  3471. define LogErna notify MyCalendar:alarm:.2767324dsfretfvds7dsfn3e4&shy;dsa234r234sdfds6bh874&shy;googlecom.* { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" }
  3472. </code><br><br>
  3473. </ul>
  3474. <i>Schalte die Aktoren an und aus</i><br><br>
  3475. <ul>
  3476. Stell Dir einen Kalender vor, dessen Zusammenfassungen (Betreff, Titel) die Namen von Devices in Deiner fhem-Installation sind.
  3477. Du willst nun die entsprechenden Devices an- und ausschalten, wenn das Kalender-Ereignis beginnt bzw. endet.<br><br>
  3478. <code>
  3479. define SwitchActorOn notify MyCalendar:start:.* {}<br>
  3480. </code>
  3481. Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben:
  3482. <code>
  3483. my $reading="$EVTPART0";
  3484. my $uid= "$EVTPART1";
  3485. my $actor= fhem("get MyCalendar summary $uid");
  3486. if(defined $actor) {
  3487. fhem("set $actor on")
  3488. }
  3489. <br><br>
  3490. define SwitchActorOff notify MyCalendar:end:.* {}<br>
  3491. </code>
  3492. Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben:
  3493. <code>
  3494. my $reading="$EVTPART0";
  3495. my $uid= "$EVTPART1";
  3496. my $actor= fhem("get MyCalendar summary $uid");
  3497. if(defined $actor) {
  3498. fhem("set $actor off")
  3499. }
  3500. </code><br><br>
  3501. Auch hier kann ein Logging aufgesetzt werden:<br><br>
  3502. <code>
  3503. define LogActors notify MyCalendar:(start|end).* {}<br>
  3504. </code>
  3505. Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben:
  3506. <code>
  3507. my $reading= "$EVTPART0";
  3508. my $uid= "$EVTPART1";
  3509. my $actor= fhem("get MyCalendar summary $uid");
  3510. Log 3 $NAME, 1, "Actor: $actor, Reading $reading";
  3511. </code><br><br>
  3512. </ul>
  3513. </ul>
  3514. <b>Eingebettetes HTML</b>
  3515. <ul><br>
  3516. Das Modul stellt eine zus&auml;tzliche Funktion <code>CalendarAsHtml(&lt;name&gt;,&lt;options&gt;)</code> bereit.
  3517. Diese gibt den HTML-Kode f&uuml;r eine Liste von Terminen zur&uuml;ck. <code>&lt;name&gt;</code> ist der Name des
  3518. Kalendar-Device und <code>&lt;options&gt;</code> ist das, was Du hinter <code>get &lt;name&gt; text ...</code>
  3519. schreiben w&uuml;rdest.
  3520. <br><br>
  3521. Beispiel: <code>define MyCalendarWeblink weblink htmlCode { CalendarAsHtml("MyCalendar","next 3") }</code>
  3522. <br><br>
  3523. Dies ist eine rudiment&auml;re Funktion, die vielleicht in k&uuml;nftigen Versionen erweitert wird.
  3524. <p>
  3525. </ul>
  3526. </ul>
  3527. =end html_DE
  3528. =cut