butcher butcher butcher
[gregoa/zavai.git] / src / clock.vala
1 /*
2  * clock - clock resource for zavai
3  *
4  * Copyright (C) 2009--2010  Enrico Zini <enrico@enricozini.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 using GLib;
22
23 namespace zavai {
24 namespace clock {
25
26 public enum SourceType
27 {
28     SYSTEM,
29     GPS
30 }
31
32 /*
33 TODO: schedule alarms via at
34
35 Uses the 'z' queue.
36
37 atq -q z  can be used to list the jobs (for example, at startup, or after a job has run)
38 at -c id  can be used to query a job, parsing its contents (which can have
39           comments or variables being set)
40 zavai --notify ...  can be used to notify the job (and start zavai if it's not running)
41
42 Alarm needs to be able to serialize itself to an at invocation and to
43 deserialize itself from the output of at -c
44
45 Alarm needs to deserialize also a job with no special markers whatsoever: a
46 generic at job.
47
48
49
50 refresh_alarms()
51 {
52     oldtime = next_alarm ? next_alarm.time : 0
53     next_alarm = the first alarm from atq
54     if (oldtime != next_alarm.time)
55     {
56         remove existing triggers
57           (triggers can be skipped if we don't need to support non-zavai alarms)
58         schedule a trigger calling refresh_alarms() at next_alarm.time + 30 seconds
59           (triggers can be skipped if we don't need to support non-zavai alarms)
60     }
61 }
62
63 at clock constructor: refresh_alarms()
64 inotifywait -e close /usr/bin/at -> refresh_alarms()  (shows when someone has just used at)
65   (can be skipped if we don't need to support non-zavai alarms)
66 at alarm triggered through zavai: refresh_alarms()
67
68
69 */
70
71
72 public class Alarm : Object
73 {
74     public at.Event ev;
75     public string label;
76
77     // Schedule with at
78     public static void schedule(string timespec, string label) throws Error
79     {
80         string argv[5];
81         argv[0] = "/usr/bin/at";
82         argv[1] = "-q";
83         argv[2] = "z";
84         argv[3] = timespec;
85         argv[4] = null;
86
87         Pid pid;
88         int stdinfd;
89
90         if (!Process.spawn_async_with_pipes("/", argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out pid, out stdinfd, null, null))
91             return;
92
93         {
94             FileStream fs = FileStream.fdopen(stdinfd, "w");
95             string display = GLib.Environment.get_variable("DISPLAY");
96             if (display != null)
97                 fs.printf("DISPLAY=\"%s\"; export DISPLAY\n", display);
98             fs.printf("# Zavai variables start here\n");
99             fs.printf("ZAVAI_LABEL=\"%s\"\n", label.escape(""));
100             fs.printf("# Zavai commands starts here\n");
101             fs.printf("%s notify \"$ZAVAI_LABEL\"", zavai.config.argv0);
102         }
103         
104         Process.close_pid(pid);
105     }
106
107     // Get the label of the job with the given at ID
108     public static string? getLabel(int atID)
109     {
110         string label = null;
111         at.jobContents(atID, fd => {
112             FileStream fs = FileStream.fdopen(fd, "r");
113             while (true)
114             {
115                 string? line = fs.read_line();
116                 if (line == null) break;
117                 if (line.has_prefix("ZAVAI_LABEL=\""))
118                 {
119                     size_t size = line.size();
120                     if (size < 15) continue;
121                     label = line.substring(13, (long)(size - 14));
122                     label = label.compress();
123                     break;
124                 }
125             }
126             return true;
127         });
128         return label;
129     }
130 }
131
132 [DBus (name = "org.enricozini.zavai.Alarm")]
133 public class ZavaiClock : Object {
134     public void Notify (string label) {
135         clock.notify_alarm(label);
136     }
137 }
138
139 public class AlarmTriggerInfo
140 {
141     public zavai.log.Log log;
142     public string label;
143     public bool acked;
144     public bool canceled;
145
146     public AlarmTriggerInfo(string label)
147     {
148         this.label = label;
149         acked = false;
150         canceled = false;
151     }
152 }
153
154 public class AlarmTriggerQueue : zavai.Service
155 {
156     protected List<AlarmTriggerInfo> queue;
157
158     public signal void triggered(AlarmTriggerInfo info);
159     public signal void acked(AlarmTriggerInfo info);
160     public signal void canceled(AlarmTriggerInfo info);
161
162     public AlarmTriggerQueue()
163     {
164         queue = new List<AlarmTriggerInfo>();
165     }
166
167     public void enqueue_trigger(AlarmTriggerInfo info)
168     {
169         // Reuse IDs from the associated logger object
170         info.log = zavai.log.log.start("alarm", "Alarm " + info.label);
171         queue.append(info);
172         if (queue.data == info)
173             triggered(queue.data);
174     }
175
176     protected void done_with_first()
177     {
178         var first = queue.data;
179         queue.remove_link(queue);
180         if (queue != null)
181             triggered(queue.data);
182     }
183
184     public void ack(AlarmTriggerInfo info)
185     {
186         if (queue == null || info != queue.data) return;
187         if (!info.acked && !info.canceled)
188         {
189             info.acked = true;
190             acked(info);
191             info.log.add("alarm acknowledged");
192             info.log.acked = true;
193             zavai.log.log.end(info.log);
194         }
195         done_with_first();
196     }
197
198     public void cancel(AlarmTriggerInfo info)
199     {
200         if (queue == null || info != queue.data) return;
201         if (!info.acked && !info.canceled)
202         {
203             info.canceled = true;
204             canceled(info);
205             info.log.add("alarm canceled");
206             zavai.log.log.end(info.log);
207         }
208         done_with_first();
209     }
210 }
211
212 public class Clock: zavai.Service
213 {
214     protected time_t last_gps_time;
215     protected time_t last_gps_time_system_time;
216     protected time_t last_system_time;
217     protected uint system_time_timeout;
218     protected time_t last_minute;
219     protected time_t chosen_time;
220     protected SourceType chosen_type;
221     protected ZavaiClock dbusClock;
222
223     protected dynamic DBus.Object otimed_alarm;
224     protected dynamic DBus.Object rtc;
225     protected SList<Alarm> alarms;
226
227     // Ticks once a minute
228     public signal void minute_changed(long time, SourceType source);
229     public signal void schedule_changed(Alarm? next);
230
231     public Clock()
232     {
233         Object(name: "clock");
234         alarms = null;
235         dbusClock = new ZavaiClock();
236         last_minute = 0;
237         last_gps_time = 0;
238         last_gps_time_system_time = 0;
239         last_system_time = time_t();
240         chosen_time = last_system_time;
241
242         // FSO alarm system
243         otimed_alarm = zavai.registry.sbus.get_object(
244                 "org.freesmartphone.otimed",
245                 "/org/freesmartphone/Time/Alarm",
246                 "org.freesmartphone.Time.Alarm");
247
248         rtc = zavai.registry.sbus.get_object(
249                 "org.freesmartphone.odeviced",
250                 "/org/freesmartphone/Device/RTC/0",
251                 "org.freesmartphone.Device.RealtimeClock");
252
253         zavai.registry.sbus.register_object("/org/enricozini/Zavai/Clock", dbusClock);
254     }
255
256     public void notify_alarm(string label)
257     {
258         stderr.printf("Notifying %s\n", label);
259         AlarmTriggerInfo info = new AlarmTriggerInfo(label);
260         alarm_trigger_queue.enqueue_trigger(info);
261         schedule_changed(next_alarm());
262     }
263
264     public Alarm? next_alarm()
265     {
266         at.Event ev;
267         ev = at.earliestID("z");
268         if (ev.deadline == 0)
269             return null;
270         string label = Alarm.getLabel(ev.id);
271         Alarm res = new Alarm();
272         res.ev = ev;
273         res.label = label;
274         return res;
275     }
276
277     public void schedule(string timespec, string label) throws Error
278     {
279         Alarm.schedule(timespec, label);
280         schedule_changed(next_alarm());
281     }
282
283     private void on_gps_time(uint t)
284     {
285         if (t == 0)
286         {
287             last_gps_time_system_time = 0;
288             update_time();
289         } else {
290             last_gps_time = (time_t)t;
291             last_gps_time_system_time = time_t();
292             update_time();
293         }
294     }
295
296     private bool on_system_time()
297     {
298         last_system_time = time_t();
299         update_time();
300         return true;
301     }
302
303     private void update_time()
304     {
305         if (last_gps_time_system_time + 10 > last_system_time)
306         {
307             chosen_time = last_gps_time;
308             chosen_type = SourceType.GPS;
309         }
310         else
311         {
312             chosen_time = last_system_time;
313             chosen_type = SourceType.SYSTEM;
314         }
315         if (chosen_time / 60 != last_minute)
316         {
317             last_minute = chosen_time / 60;
318             minute_changed(chosen_time, chosen_type);
319         }
320     }
321
322     /// Request GPS resource
323     public override void start()
324     {
325         if (started) return;
326
327         system_time_timeout = Timeout.add(5000, on_system_time);
328         zavai.gps.gps.time_changed += on_gps_time;
329         last_system_time = time_t();
330         update_time();
331
332         base.start();
333     }
334
335     public override void stop()
336     {
337         if (!started) return;
338
339         Source.remove(system_time_timeout);
340         zavai.gps.gps.time_changed -= on_gps_time;
341
342         base.stop();
343     }
344 }
345
346 public Clock clock = null;
347 public AlarmTriggerQueue alarm_trigger_queue = null;
348
349 public void init()
350 {
351     clock = new Clock();
352     alarm_trigger_queue = new AlarmTriggerQueue();
353 }
354
355 }
356 }