Calculate event recurrance
authorArjen Baart <arjen@andromeda.nl>
Wed, 17 Jun 2020 06:43:54 +0000 (08:43 +0200)
committerArjen Baart <arjen@andromeda.nl>
Wed, 17 Jun 2020 06:43:54 +0000 (08:43 +0200)
configure.ac
doc/design.xml
doc/wakeup-classes.svg
src/event.cpp
src/event.h
test/Makefile.am
test/event_recurrance.cpp [new file with mode: 0644]
test/event_recurrance.exp [new file with mode: 0644]
test/wakeup-02.xml [new file with mode: 0644]
test/wakeup_load_events.cpp
test/wakeup_load_events.exp

index 081ba58..8922558 100644 (file)
@@ -6,6 +6,7 @@ AC_INIT([wakeup], [0.1], [arjen@andromeda.nl])
 AC_CONFIG_SRCDIR([src/sunrise.cpp])
 AC_CONFIG_HEADERS([config.h])
 AM_INIT_AUTOMAKE([foreign])
+AC_LANG(C++)
 
 # Checks for programs.
 AC_PROG_CXX
index e531d81..6dea068 100644 (file)
     </chapter>
 
     <chapter>
+    <heading>Hardware</heading>
+<para>
+<picture src='../hardware/ledcontrol-sch.png' eps='../hardware/ledcontrol-sch'/>
+</para>
+<para>
+<picture src='../hardware/ledcontrol-pcb.png' eps='../hardware/ledcontrol-pcb'/>
+</para>
+<para>
+
+IO pin assignments:
+<table cpos='rll'>
+  <thead><col>Connector pin</col><col>RPi IO</col><col>Function</col></thead>
+  <row><col> 3</col><col>GPIO 2</col><col>Light switch</col></row>
+  <row><col> 5</col><col>GPIO 3</col><col>Red PWM</col></row>
+  <row><col> 7</col><col>GPIO 4</col><col>White PWM</col></row>
+  <row><col> 8</col><col>GPIO 14</col><col>Green PWM</col></row>
+  <row><col>10</col><col>GPIO 15</col><col>Blue PWM</col></row>
+  <row><col>35</col><col>GPIO 19</col><col>Curtain open out</col></row>
+  <row><col>36</col><col>GPIO 16</col><col>Curtain open in</col></row>
+  <row><col>37</col><col>GPIO 26</col><col>Curtain close out</col></row>
+  <row><col>38</col><col>GPIO 20</col><col>Curtain close in</col></row>
+</table>
+</para>
+
+    </chapter>
+
+    <chapter>
     <heading>Modules</heading>
 <para>
 The dataflow diagram shows the high level design.
@@ -373,12 +400,19 @@ When the next event does happen, meaning the time of the occurance is the curren
 Each action can for example be a change in lights or the opening or closing of curtains.
 Event methods:
 
-FromXML
-ToXML
-add_recurrance
-next_occurance
-add_action
-execute_actions
+<itemize>
+<item>FromXML</item>
+<item>ToXML</item>
+<item>add_recurrance</item>
+<item>next_occurance</item>
+<item>add_action</item>
+<item>execute_actions</item>
+</itemize>
+
+Calculating the next occurance after a certain time is done by adding the number of days or months untill the given time
+is passed or untill the end of the recurrence is reached. A recurrence pattern of weeks is converted to a pattern of n * 7 days.
+The end of the recurrence is specified as an end time or as a maximum number of occurences.
+A recurrence pattern specified as a set of weekdays requires a different calculation.
 </para>
     </section>
     </chapter>
index 054b46d..379d44f 100644 (file)
@@ -25,9 +25,9 @@
      borderopacity="1.0"
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
-     inkscape:zoom="0.86772068"
-     inkscape:cx="140.42641"
-     inkscape:cy="579.0815"
+     inkscape:zoom="1.2271424"
+     inkscape:cx="233.1688"
+     inkscape:cy="743.6865"
      inkscape:document-units="mm"
      inkscape:current-layer="layer1"
      showgrid="false"
     <text
        xml:space="preserve"
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-       x="42.730206"
-       y="41.941666"
+       x="52.182652"
+       y="41.636749"
        id="text5086"><tspan
          sodipodi:role="line"
          id="tspan5084"
-         x="42.730206"
-         y="41.941666"
-         style="stroke-width:0.26458332px">action</tspan></text>
+         x="52.182652"
+         y="41.636749"
+         style="stroke-width:0.26458332px">action_sequnce</tspan></text>
     <text
        xml:space="preserve"
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          id="tspan5128"
          x="132.30797"
          y="37.365841"
-         style="text-align:start;text-anchor:start;stroke-width:0.26458332px">number</tspan></text>
+         style="text-align:start;text-anchor:start;stroke-width:0.26458332px">maximum_number</tspan></text>
     <text
        xml:space="preserve"
        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          id="tspan4519"
          x="44.517975"
          y="139.66251"
-         style="stroke-width:0.26458332px">execute</tspan></text>
+         style="stroke-width:0.26458332px">execute()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="46.628098"
+       y="57.9599"
+       id="text5313"><tspan
+         sodipodi:role="line"
+         id="tspan5311"
+         x="46.628098"
+         y="57.9599"
+         style="stroke-width:0.26458332px">FromXML()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="44.378979"
+       y="62.281059"
+       id="text5317"><tspan
+         sodipodi:role="line"
+         id="tspan5315"
+         x="44.378979"
+         y="62.281059"
+         style="stroke-width:0.26458332px">ToXML()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="48.599228"
+       y="66.817833"
+       id="text5321"><tspan
+         sodipodi:role="line"
+         id="tspan5319"
+         x="48.599228"
+         y="66.817833"
+         style="stroke-width:0.26458332px">add_action()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="52.584824"
+       y="71.659523"
+       id="text5325"><tspan
+         sodipodi:role="line"
+         id="tspan5323"
+         x="52.584824"
+         y="71.659523"
+         style="stroke-width:0.26458332px">next_occurance()</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="46.571613"
+       y="108.98867"
+       id="text5329"><tspan
+         sodipodi:role="line"
+         id="tspan5327"
+         x="46.571613"
+         y="108.98867"
+         style="stroke-width:0.26458332px">parameters</tspan></text>
   </g>
 </svg>
index a2f10d8..dde8372 100644 (file)
@@ -3,6 +3,7 @@
 void Event::FromXML(xml_element x)
 {
    std::vector<xml_element> elems;
+   std::vector<xml_element> recur;
 
    elems = x["start"];
    start_time = elems[0].content();
@@ -12,6 +13,42 @@ void Event::FromXML(xml_element x)
    {
       add_action(elems[i].content());
    }
+
+   recur = x["recurrance"];
+   if (recur.size() != 0)
+   {
+      String number;
+
+      elems   = recur[0]["pattern"];
+      pattern = elems[0].content();
+      elems   = recur[0]["number"];
+      number  = elems[0].content();
+
+      //   Initialize the recurrance interval according to pattern
+
+      if (pattern == "weekdays")
+      {
+         // The number element is a comma separated list of day numbers
+
+         SuperString wkdays = number.split(",");
+         for (int i = 0; i < ~wkdays; i++)
+         {
+            weekdays.insert(int(wkdays[i]));
+         }
+      }
+      else if (pattern == "days")
+      {
+         recurrance_interval = UTC(date(long(number), 0, 0), hour(0));
+      }
+      else if (pattern == "months")
+      {
+         recurrance_interval = UTC(date(0, long(number), 0), hour(0));
+      }
+      else if (pattern == "years")
+      {
+         recurrance_interval = UTC(date(0, 0, long(number)), hour(0));
+      }
+   }
 }
 
 String Event::ToXML(void)
@@ -27,6 +64,42 @@ String Event::ToXML(void)
    xml_text += start_time.format();
    xml_text += "</start>\n";
 
+   if (pattern != "none")
+   {
+      xml_text += "    <pattern>";
+      xml_text += pattern;
+      xml_text += "</pattern>\n";
+      xml_text += "    <number>";
+      if (pattern == "weekdays")
+      {
+         std::set<int>::iterator it;
+         for (it = weekdays.begin(); it != weekdays.end(); it++)
+         {
+            if (it != weekdays.begin())
+            {
+               xml_text += ",";
+            }
+            xml_text += *it;
+         }
+      }
+      else
+      {
+         if (date(recurrance_interval).Year() != 0)
+         {
+            xml_text += int(date(recurrance_interval).Year());
+         }
+         if (date(recurrance_interval).Month() != 0)
+         {
+            xml_text += int(date(recurrance_interval).Month());
+         }
+         if (date(recurrance_interval).Day() != 0)
+         {
+            xml_text += int(date(recurrance_interval).Day());
+         }
+      }
+      xml_text += "</number>\n";
+   }
+
    for (act = sequence.begin(); act != sequence.end(); act++)
    {
       xml_text += "    <action>";
@@ -65,6 +138,40 @@ void Event::add_action(String command)
    }
 }
 
+UTC Event::next_occurance(UTC after)
+{
+   UTC next;
+
+   //std::cout << "Next occ after " << after << ", pattern = " << pattern << "\n";
+   next = start_time;
+
+   if (pattern != "none")
+   {
+      while (next <= after)
+      {
+         if (pattern == "weekdays")
+         {
+            while (next <= after || weekdays.find(date(next).WeekDay()) == weekdays.end())
+            {
+               next += UTC(date(1, 0, 0), hour(0));
+            }
+         }
+         else
+         {
+            next += recurrance_interval;
+         }
+      }
+   }
+
+   if (after >= next)
+   {
+      // The last event has already passed
+      next = UTC(date(0,0,0), hour(0));
+   }
+
+   return next;
+}
+
 std::list<Event> read_alarms(const char filename[])
 {
    std::list<Event>         collected_alarms;
index b4cdd27..72f4f3a 100644 (file)
@@ -1,5 +1,6 @@
 
 #include <list>
+#include <set>
 #include <xml.h>
 #include <date.h>
 
@@ -9,19 +10,33 @@ class Event
 {
    String              label;
    UTC                 start_time;
+
+   String              pattern;    // for recurrance
+   UTC                 recurrance_interval;
+   std::set<int>       weekdays;
+
    std::list<Action *> sequence;
 
 public:
 
+   Event()
+   {
+      label = "";
+      pattern = "none";
+   }
+
    Event(String lbl)
    {
       label = lbl;
+      pattern = "none";
    }
 
    void   FromXML(xml_element x);
    String ToXML(void);
 
    void  add_action(String command);
+
+   UTC   next_occurance(UTC after);
 };
 
 std::list<Event> read_alarms(const char *filename);
index e1a2aa4..6e8b0fe 100644 (file)
@@ -2,7 +2,8 @@ TESTS = lightctrl lightctrl-oor lightctrl-fade $(check_PROGRAMS) check_output
 
 AM_CPPFLAGS = -I../src
 LDADD = -lTachyon -lACL
-check_PROGRAMS = wakeup_load_events sunrise_test
+check_PROGRAMS = wakeup_load_events sunrise_test event_recurrance
 
 wakeup_load_events_SOURCES = wakeup_load_events.cpp ../src/event.cpp
+event_recurrance_SOURCES = event_recurrance.cpp ../src/event.cpp
 sunrise_test_SOURCES = sunrise_test.cpp ../src/sunrise.cpp
diff --git a/test/event_recurrance.cpp b/test/event_recurrance.cpp
new file mode 100644 (file)
index 0000000..3f12c0e
--- /dev/null
@@ -0,0 +1,96 @@
+#include "event.h"
+#include "assert.h"
+
+int main()
+{
+   std::list<Event>   set_alarms;
+   std::list<Event>::iterator alarm;
+   UTC                time_to_check;
+   UTC                time_expected;
+   UTC                next_time;
+
+   set_alarms = read_alarms("wakeup-02.xml");
+
+   // The first alarm at 2020-03-22 06:00 does not recurr
+
+
+   alarm = set_alarms.begin();
+
+   // Check before the alarm time
+   time_to_check = UTC(date(20, 3, 2020), hour(2, 2, 2));
+   time_expected = UTC(date(22, 3, 2020), hour(6, 0, 0));
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << "\n";
+   assert(next_time == time_expected);
+
+   // Check after the alarm time
+   time_to_check = UTC(date(22, 3, 2020), hour(6, 2, 2));
+   time_expected = UTC(date(0, 0, 0), hour(0, 0, 0));
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   // The second alarm at 2020-05-24 08:00 recurs weekly
+
+   alarm++;
+
+   //  Before the first occurance
+
+   time_to_check = UTC(date(20, 5, 2020), hour(2, 2, 2));
+   time_expected = UTC(date(24, 5, 2020), hour(8, 0, 0));
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   //  After the first occurance
+
+   time_to_check = UTC(date(24, 5, 2020), hour(8, 2, 2));
+   time_expected = UTC(date(31, 5, 2020), hour(8, 0, 0));
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   //  Much later after the first occurance
+
+   time_to_check = UTC(date(25, 11, 2020), hour(2, 2, 2));
+   time_expected = UTC(date(29, 11, 2020), hour(8, 0, 0));
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   // The third alarm at 2020-04-01 07:00 (Wednesday) recurs every Tuesday, Thursday and Friday
+
+   alarm++;
+
+   //  Before the first occurance
+
+   time_to_check = UTC(date(20, 3, 2020), hour(2, 2, 2));
+   time_expected = UTC(date( 2, 4, 2020), hour(7, 0, 0)); // the first Thursday
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << ". Expected " << time_expected << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   //  After the first occurance
+
+   time_to_check = UTC(date( 2, 4, 2020), hour(7, 2, 2));
+   time_expected = UTC(date( 3, 4, 2020), hour(7, 0, 0));   // the next Friday
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << ". Expected " << time_expected << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+   //  After the second occurance
+
+   time_to_check = UTC(date( 3, 4, 2020), hour(7, 2, 2));
+   time_expected = UTC(date( 7, 4, 2020), hour(7, 0, 0));   // the next Tuesday
+   next_time = alarm->next_occurance(time_to_check);
+   std::cout << "Next alarm after " << time_to_check << " will be " << next_time << ". Expected " << time_expected << "\n";
+   std::cout.flush();
+   assert(next_time == time_expected);
+
+}
diff --git a/test/event_recurrance.exp b/test/event_recurrance.exp
new file mode 100644 (file)
index 0000000..a5dbe76
--- /dev/null
@@ -0,0 +1,9 @@
+Next alarm after 20-03-2020 02:02:02 will be 22-03-2020 06:00:00
+Next alarm after 22-03-2020 06:02:02 will be 00-00-0 00:00:00
+Next alarm after 20-05-2020 02:02:02 will be 24-05-2020 08:00:00
+Next alarm after 24-05-2020 08:02:02 will be 31-05-2020 08:00:00
+Next alarm after 25-11-2020 02:02:02 will be 29-11-2020 08:00:00
+Next alarm after 20-03-2020 02:02:02 will be 02-04-2020 07:00:00. Expected 02-04-2020 07:00:00
+Next alarm after 02-04-2020 07:02:02 will be 03-04-2020 07:00:00. Expected 03-04-2020 07:00:00
+Next alarm after 03-04-2020 07:02:02 will be 07-04-2020 07:00:00. Expected 07-04-2020 07:00:00
+PASS event_recurrance (exit status: 0)
diff --git a/test/wakeup-02.xml b/test/wakeup-02.xml
new file mode 100644 (file)
index 0000000..5e765a4
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version='1.0'?>
+<wakeup>
+  <event id='non-recurring'>
+    <start>2020-03-22 06:00</start>
+    <action>lightcontrol -r 100 -f 200</action>
+  </event>
+  <event id='recurr-weekly'>
+    <start>2020-05-24 08:00</start>
+    <recurrance>
+       <pattern>days</pattern>
+       <number>7</number>
+    </recurrance>
+    <action>lightcontrol -g 100 -f 200</action>
+  </event>
+  <event id='recurr-somedays'>
+    <start>2020-04-02 07:00</start>
+    <recurrance>
+       <pattern>weekdays</pattern>
+       <number>2,4,5</number>
+    </recurrance>
+    <action>lightcontrol -g 100 -f 200</action>
+  </event>
+</wakeup>
index 219bd1b..6e7a7d0 100644 (file)
@@ -7,4 +7,8 @@ int main()
    set_alarms = read_alarms("wakeup-01.xml");
 
    std::cout << alarms_to_XML(set_alarms);
+
+   set_alarms = read_alarms("wakeup-02.xml");
+
+   std::cout << alarms_to_XML(set_alarms);
 }
index 1acff55..b277957 100644 (file)
@@ -9,4 +9,23 @@
     <action>lightcontrol -r 0 -g 0 -w 0 -f 200</action>
   </event>
 </wakeup>
+<?xml version='1.0'?>
+<wakeup>
+  <event id='name'>
+    <start>2020-03-22 06:00:00</start>
+    <action>lightcontrol -r 100 -f 200</action>
+  </event>
+  <event id='name'>
+    <start>2020-05-24 08:00:00</start>
+    <pattern>days</pattern>
+    <number>7</number>
+    <action>lightcontrol -g 100 -f 200</action>
+  </event>
+  <event id='name'>
+    <start>2020-04-02 07:00:00</start>
+    <pattern>weekdays</pattern>
+    <number>2,4,5</number>
+    <action>lightcontrol -g 100 -f 200</action>
+  </event>
+</wakeup>
 PASS wakeup_load_events (exit status: 0)