Forgot to add file
[gregoa/zavai.git] / zavai / 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 uint id;
142     public string label;
143     public bool acked;
144     public bool canceled;
145
146     public AlarmTriggerInfo(string label)
147     {
148         id = 0;
149         this.label = label;
150         acked = false;
151         canceled = false;
152     }
153 }
154
155 public class AlarmTriggerQueue : zavai.Service
156 {
157     protected List<AlarmTriggerInfo> queue;
158
159     public signal void triggered(AlarmTriggerInfo info);
160     public signal void acked(AlarmTriggerInfo info);
161     public signal void canceled(AlarmTriggerInfo info);
162
163     public AlarmTriggerQueue()
164     {
165         queue = new List<AlarmTriggerInfo>();
166     }
167
168     public uint enqueue_trigger(AlarmTriggerInfo info)
169     {
170         // Reuse IDs from the associated logger object
171         info.id = zavai.log.log.start("alarm", "Alarm " + info.label);
172         queue.append(info);
173         if (queue.data.id == info.id)
174             triggered(queue.data);
175         return info.id;
176     }
177
178     protected void done_with_first()
179     {
180         var first = queue.data;
181         queue.remove_link(queue);
182         if (queue != null)
183             triggered(queue.data);
184     }
185
186     public void ack(AlarmTriggerInfo info)
187     {
188         if (queue == null || info.id != queue.data.id) return;
189         if (!info.acked && !info.canceled)
190         {
191             info.acked = true;
192             acked(info);
193             zavai.log.log.add(info.id, "alarm acknowledged");
194             zavai.log.log.end(info.id);
195         }
196         done_with_first();
197     }
198
199     public void cancel(AlarmTriggerInfo info)
200     {
201         if (queue == null || info.id != queue.data.id) return;
202         if (!info.acked && !info.canceled)
203         {
204             info.canceled = true;
205             canceled(info);
206             zavai.log.log.add(info.id, "alarm canceled");
207             zavai.log.log.end(info.id);
208         }
209         done_with_first();
210     }
211 }
212
213 public class Clock: zavai.Service
214 {
215     protected time_t last_gps_time;
216     protected time_t last_gps_time_system_time;
217     protected time_t last_system_time;
218     protected uint system_time_timeout;
219     protected time_t last_minute;
220     protected time_t chosen_time;
221     protected SourceType chosen_type;
222     protected ZavaiClock dbusClock;
223
224     protected dynamic DBus.Object otimed_alarm;
225     protected dynamic DBus.Object rtc;
226     protected SList<Alarm> alarms;
227
228     // Ticks once a minute
229     public signal void minute_changed(long time, SourceType source);
230     public signal void schedule_changed(Alarm? next);
231
232     public Clock()
233     {
234         Object(name: "clock");
235         alarms = null;
236         dbusClock = new ZavaiClock();
237         last_minute = 0;
238         last_gps_time = 0;
239         last_gps_time_system_time = 0;
240         last_system_time = time_t();
241         chosen_time = last_system_time;
242
243         // FSO alarm system
244         otimed_alarm = zavai.registry.sbus.get_object(
245                 "org.freesmartphone.otimed",
246                 "/org/freesmartphone/Time/Alarm",
247                 "org.freesmartphone.Time.Alarm");
248
249         rtc = zavai.registry.sbus.get_object(
250                 "org.freesmartphone.odeviced",
251                 "/org/freesmartphone/Device/RTC/0",
252                 "org.freesmartphone.Device.RealtimeClock");
253
254         zavai.registry.sbus.register_object("/org/enricozini/Zavai/Clock", dbusClock);
255     }
256
257     public void notify_alarm(string label)
258     {
259         stderr.printf("Notifying %s\n", label);
260         AlarmTriggerInfo info = new AlarmTriggerInfo(label);
261         alarm_trigger_queue.enqueue_trigger(info);
262         schedule_changed(next_alarm());
263     }
264
265     public Alarm? next_alarm()
266     {
267         at.Event ev;
268         ev = at.earliestID("z");
269         if (ev.deadline == 0)
270             return null;
271         string label = Alarm.getLabel(ev.id);
272         Alarm res = new Alarm();
273         res.ev = ev;
274         res.label = label;
275         return res;
276     }
277
278     public void schedule(string timespec, string label) throws Error
279     {
280         Alarm.schedule(timespec, label);
281         schedule_changed(next_alarm());
282     }
283
284     private void on_gps_time(uint t)
285     {
286         if (t == 0)
287         {
288             last_gps_time_system_time = 0;
289             update_time();
290         } else {
291             last_gps_time = (time_t)t;
292             last_gps_time_system_time = time_t();
293             update_time();
294         }
295     }
296
297     private bool on_system_time()
298     {
299         last_system_time = time_t();
300         update_time();
301         return true;
302     }
303
304     private void update_time()
305     {
306         if (last_gps_time_system_time + 10 > last_system_time)
307         {
308             chosen_time = last_gps_time;
309             chosen_type = SourceType.GPS;
310         }
311         else
312         {
313             chosen_time = last_system_time;
314             chosen_type = SourceType.SYSTEM;
315         }
316         if (chosen_time / 60 != last_minute)
317         {
318             last_minute = chosen_time / 60;
319             minute_changed(chosen_time, chosen_type);
320         }
321     }
322
323     /// Request GPS resource
324     public override void start()
325     {
326         if (started) return;
327
328         system_time_timeout = Timeout.add(5000, on_system_time);
329         zavai.gps.gps.time_changed += on_gps_time;
330         last_system_time = time_t();
331         update_time();
332
333         base.start();
334     }
335
336     public override void stop()
337     {
338         if (!started) return;
339
340         Source.remove(system_time_timeout);
341         zavai.gps.gps.time_changed -= on_gps_time;
342
343         base.stop();
344     }
345 }
346
347 public Clock clock = null;
348 public AlarmTriggerQueue alarm_trigger_queue = null;
349
350 public void init()
351 {
352     clock = new Clock();
353     alarm_trigger_queue = new AlarmTriggerQueue();
354 }
355
356 }
357 }