]> mj.ucw.cz Git - ursary.git/blob - ursaryd.c
Better IR control of lights
[ursary.git] / ursaryd.c
1 /*
2  *      The Ursary Control Panel
3  *
4  *      (c) 2014-2023 Martin Mares <mj@ucw.cz>
5  */
6
7 #undef LOCAL_DEBUG
8
9 #include <ucw/lib.h>
10 #include <ucw/clists.h>
11 #include <ucw/daemon.h>
12 #include <ucw/log.h>
13 #include <ucw/mainloop.h>
14 #include <ucw/opt.h>
15 #include <ucw/stkstring.h>
16 #include <ucw/string.h>
17
18 #include <math.h>
19 #include <signal.h>
20 #include <stdio.h>
21 #include <string.h>
22 #include <stdlib.h>
23 #include <syslog.h>
24
25 #include "ursaryd.h"
26 #include "usb.h"
27
28 /*
29  *      Map of all controls
30  *
31  *              rotary          red button      green button
32  *              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33  *      0       sink PCH        mute            switch to PCH
34  *      1       sink BT         mute            switch to BT
35  *      2       ceil brightness mic non-mute    ceiling lights on
36  *      3       desk brightness -               desk lights on
37  *      4       Albireo MPD     mute            MPD play/pause
38  *      5       Albireo MPV     mute            MPD stop
39  *      6       Albireo Zoom    mute            MPD prev
40  *      7       eveyrhing else  mute            MPD next
41  *
42  *      center  rainbow brightness
43  *      slider  light color temperature
44  */
45
46 #define PCH_SINK "alsa_output.pci-0000_07_00.6.analog-stereo"
47 #define BT_SINK "bluez_sink.CC_98_8B_D0_8C_06.a2dp_sink"
48 #define LOGI_SOURCE "alsa_input.usb-046d_Logitech_Webcam_C925e_EF163C5F-02.analog-stereo"
49
50 /*** Sink controls ***/
51
52 static double volume_from_pa(pa_volume_t vol)
53 {
54   return (double) vol / PA_VOLUME_NORM;
55 }
56
57 static pa_volume_t volume_to_pa(double vol)
58 {
59   return vol * PA_VOLUME_NORM + 0.0001;
60 }
61
62 static void update_ring_from_sink(int ring, const char *sink_name)
63 {
64   struct pulse_sink *s = pulse_sink_by_name(sink_name);
65   if (!s)
66     {
67       noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
68       noct_set_button(ring, 0);
69       return;
70     }
71
72   if (s->mute)
73     {
74       noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
75       noct_set_button(ring, 1);
76       return;
77     }
78
79   double vol = CLAMP(volume_from_pa(s->volume), 0, 1);
80   noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f));
81   noct_set_button(ring, 0);
82 }
83
84 static void update_sink_from_rotary(int delta, const char *sink_name)
85 {
86   struct pulse_sink *s = pulse_sink_by_name(sink_name);
87   if (!s)
88     return;
89
90   double vol = volume_from_pa(s->volume) + delta * 0.02;
91   pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1));
92   if (pavol == s->volume)
93     return;
94   pa_cvolume cvol;
95   pa_cvolume_set(&cvol, s->channels, pavol);
96
97   DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
98   pulse_sink_set_volume(s->idx, &cvol);
99 }
100
101 static void update_sink_mute_from_button(int on, const char *sink_name)
102 {
103   if (!on)
104     return;
105
106   struct pulse_sink *s = pulse_sink_by_name(sink_name);
107   if (!s)
108     return;
109
110   DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
111   pulse_sink_set_mute(s->idx, !s->mute);
112 }
113
114 /*** Client controls ***/
115
116 struct client_map {
117   int group;
118   const char *client;
119   const char *host;
120 };
121
122 static struct client_map client_map[] = {
123   { 4, "Music Player Daemon",   "albireo",      },
124   { 5, "mpv",                   "albireo",      },
125   { 6, "ZOOM VoiceEngine",      "albireo",      },
126   { 7, NULL,                    NULL,           },
127 };
128
129 #define CLIENT_MAP_SIZE ARRAY_SIZE(client_map)
130
131 struct group_config {
132   bool enabled;
133   double range;
134 };
135
136 struct group_state {
137   double volume;
138   bool have_muted[2];
139 };
140
141 #define NUM_GROUPS 9
142
143 static struct group_config group_config[NUM_GROUPS] = {
144   [4] = { .enabled = 1, .range = 1.5 },
145   [5] = { .enabled = 1, .range = 1.5 },
146   [6] = { .enabled = 1, .range = 1.5 },
147   [7] = { .enabled = 1, .range = 1.5 },
148 };
149
150 static struct group_state group_state[NUM_GROUPS];
151
152 static void calc_groups(void)
153 {
154   bzero(group_state, sizeof(group_state));
155
156   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
157     {
158       s->noct_group_idx = -1;
159
160       if (s->client_idx < 0 || s->sink_idx < 0)
161         continue;
162
163       struct pulse_client *c = pulse_client_by_idx(s->client_idx);
164       if (!c)
165         continue;
166
167       for (uns i=0; i < CLIENT_MAP_SIZE; i++)
168         {
169           struct client_map *cm = &client_map[i];
170           if ((!cm->client || !strcmp(cm->client, c->name)) &&
171               (!cm->host || !strcmp(cm->host, c->host)))
172             {
173               int g = cm->group;
174               struct group_state *gs = &group_state[g];
175               DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g);
176               s->noct_group_idx = g;
177               gs->volume = MAX(gs->volume, s->volume);
178               gs->have_muted[!!s->mute] = 1;
179               break;
180             }
181         }
182     }
183 }
184
185 static void update_groups(void)
186 {
187   calc_groups();
188
189   for (uns i=0; i < NUM_GROUPS; i++)
190     {
191       struct group_config *gc = &group_config[i];
192       struct group_state *gs = &group_state[i];
193       if (!gc->enabled)
194         continue;
195
196       DBG("@@ Group #%d: mute=%d/%d volume=%.3f/%.3f", i, gs->have_muted[0], gs->have_muted[1], volume_from_pa(gs->volume), gc->range);
197
198       if (!gs->have_muted[0] && !gs->have_muted[1])
199         {
200           noct_set_ring(i, RING_MODE_LEFT, 0);
201           noct_set_button(i, 0);
202         }
203       else if (!gs->have_muted[0])
204         {
205           noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f);
206           noct_set_button(i, 1);
207         }
208       else
209         {
210           double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range);
211           int val = 0x7f * vol / gc->range;
212           val = CLAMP(val, 12, 0x7f);
213           noct_set_ring(i, RING_MODE_LEFT, val);
214           noct_set_button(i, 0);
215         }
216     }
217 }
218
219 static void update_group_from_rotary(int i, int delta)
220 {
221   if (i >= NUM_GROUPS)
222     return;
223   struct group_config *gc = &group_config[i];
224   struct group_state *gs = &group_state[i];
225   if (!gc->enabled)
226     return;
227
228   calc_groups();
229   double vol = volume_from_pa(gs->volume) + delta*0.02;
230   pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range));
231
232   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
233     {
234       if (s->noct_group_idx == i && s->volume != pavol)
235         {
236           DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
237           pa_cvolume cvol;
238           pa_cvolume_set(&cvol, s->channels, pavol);
239           pulse_sink_input_set_volume(s->idx, &cvol);
240         }
241     }
242 }
243
244 static void update_group_from_button(int i, int on)
245 {
246   if (!on)
247     return;
248   if (i >= NUM_GROUPS)
249     return;
250   struct group_config *gc = &group_config[i];
251   struct group_state *gs = &group_state[i];
252   if (!gc->enabled)
253     return;
254
255   calc_groups();
256   if (!gs->have_muted[0] && !gs->have_muted[1])
257     return;
258   uns mute = !gs->have_muted[1];
259
260   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
261     {
262       if (s->noct_group_idx == i)
263         {
264           DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
265           pulse_sink_input_set_mute(s->idx, mute);
266         }
267     }
268 }
269
270 static int find_touched_client(void)
271 {
272   int touched = -1;
273
274   for (uns i=0; i < NUM_GROUPS; i++)
275     if (group_config[i].enabled && noct_rotary_touched[i])
276       {
277         if (touched >= 0)
278           return -1;
279         touched = i;
280       }
281   return touched;
282 }
283
284 /*** Default sink controls ***/
285
286 static const char *get_client_sink(int i)
287 {
288   const char *sink = NULL;
289
290   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
291     if (s->noct_group_idx == i)
292       {
293         struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL;
294         const char *ss = sk ? sk->name : NULL;
295         if (!sink)
296           sink = ss;
297         else if (strcmp(sink, ss))
298           sink = "?";
299       }
300   return sink ? : "?";
301 }
302
303 static void update_default_sink(void)
304 {
305   int i = find_touched_client();
306   const char *sink;
307   if (i >= 0)
308     sink = get_client_sink(i);
309   else
310     sink = pulse_default_sink_name ? : "?";
311
312   if (!strcmp(sink, PCH_SINK))
313     {
314       noct_set_button(8, 1);
315       noct_set_button(9, 0);
316     }
317   else if (!strcmp(sink, BT_SINK))
318     {
319       noct_set_button(8, 0);
320       noct_set_button(9, 1);
321     }
322   else
323     {
324       noct_set_button(8, 0);
325       noct_set_button(9, 0);
326     }
327 }
328
329 static void update_default_sink_from_button(int button, int on)
330 {
331   if (!on)
332     return;
333
334   int i = find_touched_client();
335 #if 0
336   const char *sink;
337   if (i >= 0)
338     sink = get_client_sink(i);
339   else
340     sink = pulse_default_sink_name ? : "?";
341 #endif
342
343   const char *switch_to = NULL;
344   if (button == 8)
345     switch_to = PCH_SINK;
346   else if (button == 9)
347     switch_to = BT_SINK;
348
349   if (!switch_to)
350     return;
351
352   if (i >= 0)
353     {
354       struct pulse_sink *sk = pulse_sink_by_name(switch_to);
355       if (!sk)
356         return;
357
358       CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
359         if (s->noct_group_idx == i)
360           {
361             DBG("Moving input #%d to sink #%d", s->idx, sk->idx);
362             pulse_sink_input_move(s->idx, sk->idx);
363           }
364     }
365   else
366     {
367       DBG("Switching default sink to %s", switch_to);
368       pulse_server_set_default_sink(switch_to);
369     }
370 }
371
372 /*** Source mute controls ***/
373
374 static void update_source_buttons(void)
375 {
376   struct pulse_source *source = pulse_source_by_name(LOGI_SOURCE);
377   if (!source)
378     return;
379
380 #if 0   // Disabled for now
381   // if (source->suspended || source->mute)
382   if (source->mute)
383     noct_set_button(2, 0);
384   else
385     noct_set_button(2, 1);
386 #endif
387 }
388
389 static void update_source_mute_from_button(int on, const char *source_name)
390 {
391   if (!on)
392     return;
393
394   struct pulse_source *s = pulse_source_by_name(source_name);
395   if (!s)
396     return;
397
398 #if 0
399   DBG("## Setting mute of source %s to %d", s->name, !s->mute);
400   pulse_source_set_mute(s->idx, !s->mute);
401 #endif
402 }
403
404 /*** MPD controls ***/
405
406 static bool mpd_flash_state;
407
408 static void mpd_flash_timeout(struct main_timer *t)
409 {
410   mpd_flash_state ^= 1;
411   noct_set_button(12, mpd_flash_state);
412   timer_add_rel(t, 500);
413 }
414
415 static struct main_timer mpd_flash_timer = {
416   .handler = mpd_flash_timeout,
417 };
418
419 static void update_mpd(void)
420 {
421   const char *state = mpd_get_player_state();
422   if (!strcmp(state, "play"))
423     {
424       noct_set_button(12, 1);
425       timer_del(&mpd_flash_timer);
426     }
427   else if (!strcmp(state, "pause"))
428     {
429       if (!timer_is_active(&mpd_flash_timer))
430         {
431           mpd_flash_state = 1;
432           mpd_flash_timeout(&mpd_flash_timer);
433         }
434     }
435   else
436     {
437       noct_set_button(12, 0);
438       timer_del(&mpd_flash_timer);
439     }
440 }
441
442 static void mpd_button_timeout(struct main_timer *t)
443 {
444   DBG("MPD stop");
445   timer_del(t);
446   mpd_stop();
447 }
448
449 static void update_mpd_from_button(int button UNUSED, int on)
450 {
451   static struct main_timer mpd_button_timer = {
452     .handler = mpd_button_timeout,
453   };
454
455   const char *state = mpd_get_player_state();
456
457   if (!on)
458     {
459       if (timer_is_active(&mpd_button_timer))
460         {
461           timer_del(&mpd_button_timer);
462           if (!strcmp(state, "play"))
463             {
464               DBG("MPD pause");
465               mpd_pause(1);
466             }
467           else if (!strcmp(state, "pause"))
468             {
469               DBG("MPD resume");
470               mpd_pause(0);
471             }
472         }
473       return;
474     }
475
476   if (!strcmp(state, "stop"))
477     {
478       DBG("MPD play");
479       mpd_play();
480     }
481   else
482     {
483       DBG("MPD starting button timer");
484       timer_add_rel(&mpd_button_timer, 1000);
485     }
486 }
487
488 /*** Lights ***/
489
490 static bool lights_on[2];
491 static double lights_brightness[2];
492 static double lights_temperature[2];
493 static timestamp_t lights_last_update[2];
494
495 static void update_lights(void)
496 {
497   for (uint ch=0; ch < 2; ch++)
498     {
499       if (lights_on[ch])
500         {
501           noct_set_ring(3-ch, RING_MODE_LEFT, lights_brightness[ch] * 127);
502           noct_set_button(11-ch, 1);
503         }
504       else
505         {
506           noct_set_ring(3-ch, RING_MODE_LEFT, 0);
507           noct_set_button(11-ch, 0);
508         }
509     }
510 }
511
512 static void send_lights(int ch)
513 {
514   DBG("Lights[%d]: on=%d bri=%.3f temp=%.3f", ch, lights_on[ch], lights_brightness[ch], lights_temperature[ch]);
515   double b = lights_on[ch] ? lights_brightness[ch] : 0;
516   double t = lights_on[ch] ? lights_temperature[ch] : 0;
517   char topic[100], val[100];
518   snprintf(topic, sizeof(topic), "burrow/lights/catarium/%s", (ch ? "top" : "bottom"));
519   snprintf(val, sizeof(val), "%.3f %.3f", b, t);
520   mqtt_publish(topic, val);
521   lights_last_update[ch] = main_get_now();
522   update_lights();
523 }
524
525 static void update_lights_from_rotary(int ch, int delta)
526 {
527   if (lights_on[ch])
528     lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.015*delta*abs(delta), 0., 1.);
529   send_lights(ch);
530 }
531
532 static void update_lights_from_slider(int value)
533 {
534   lights_temperature[0] = value / 127.;
535   lights_temperature[1] = value / 127.;
536   send_lights(0);
537   send_lights(1);
538 }
539
540 static void lights_button_timeout(struct main_timer *t)
541 {
542   int ch = (uintptr_t) t->data;
543   DBG("Lights[%d]: Full throttle!", ch);
544   timer_del(t);
545   lights_on[ch] = 1;
546   lights_brightness[ch] = 1;
547   send_lights(ch);
548 }
549
550 static void update_lights_from_button(int ch, int on)
551 {
552   static struct main_timer lights_button_timer[2] = {{
553     .handler = lights_button_timeout,
554     .data = (void *)(uintptr_t) 0,
555   },{
556     .handler = lights_button_timeout,
557     .data = (void *)(uintptr_t) 1,
558   }};
559
560   if (on)
561     timer_add_rel(&lights_button_timer[ch], 500);
562   else if (timer_is_active(&lights_button_timer[ch]))
563     {
564       timer_del(&lights_button_timer[ch]);
565       lights_on[ch] = !lights_on[ch];
566       send_lights(ch);
567     }
568 }
569
570 static void update_lights_from_ir(int ch, int dir)
571 {
572   if (lights_on[ch])
573     lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.07*dir, 0., 1.);
574   else if (dir > 0)
575     {
576       lights_on[ch] = 1;
577       lights_brightness[ch] = 1;
578     }
579   else
580     {
581       lights_on[ch] = 1;
582       lights_brightness[ch] = 0.05;
583     }
584   send_lights(ch);
585 }
586
587 static void update_lights_on_off_ir(int ch)
588 {
589   lights_on[ch] ^= 1;
590   send_lights(ch);
591 }
592
593 static void update_lights_temp_ir(void)
594 {
595   if (!lights_on[0] && !lights_on[1])
596     return;
597
598   double t = (lights_temperature[0] + lights_temperature[1]) / 2;
599   if (t >= 0.66)
600     t = 0;
601   else if (t < 0.33)
602     t = 0.5;
603   else
604     t = 1;
605   lights_temperature[0] = lights_temperature[1] = t;
606
607   send_lights(0);
608   send_lights(1);
609 }
610
611 /*** Rainbow ***/
612
613 static double rainbow_brightness;
614 static timestamp_t rainbow_last_update;
615
616 static void update_rainbow(void)
617 {
618   noct_set_ring(8, RING_MODE_LEFT, rainbow_brightness * 127);
619 }
620
621 static void send_rainbow(void)
622 {
623   char val[100];
624   snprintf(val, sizeof(val), "%.3f", rainbow_brightness);
625   mqtt_publish("burrow/lights/rainbow/brightness", val);
626   rainbow_last_update = main_get_now();
627   update_rainbow();
628 }
629
630 static void update_rainbow_from_rotary(int delta)
631 {
632   rainbow_brightness = CLAMP(rainbow_brightness + 0.015*delta*abs(delta), 0., 1.);
633   send_rainbow();
634 }
635
636 /*** Main update routines ***/
637
638 static struct main_timer update_timer;
639 static timestamp_t last_touch_time;
640
641 enum update_state {
642   US_OFFLINE,
643   US_ONLINE,
644   US_SLEEPING,
645   US_PULSE_DEAD,
646 };
647
648 static enum update_state update_state;
649
650 static bool want_sleep_p(void)
651 {
652   CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list)
653     if (!s->suspended)
654       return 0;
655   return 1;
656 }
657
658 static void do_update(struct main_timer *t)
659 {
660   DBG("## UPDATE in state %u", update_state);
661   timer_del(t);
662
663   // Nocturn dead?
664   if (!noct_is_ready())
665     {
666       DBG("## UPDATE: Nocturn is not ready");
667       update_state = US_OFFLINE;
668       return;
669     }
670   else if (update_state == US_OFFLINE)
671     {
672       DBG("## UPDATE: Going online");
673       update_state = US_ONLINE;
674     }
675
676   // Pulse dead?
677   if (!pulse_is_ready())
678     {
679       DBG("## UPDATE: Pulse is not online");
680       if (update_state != US_PULSE_DEAD)
681         {
682           update_state = US_PULSE_DEAD;
683           noct_clear();
684           for (int i=0; i<8; i++)
685             noct_set_button(i, 1);
686         }
687       return;
688     }
689   else if (update_state == US_PULSE_DEAD)
690     {
691       DBG("## UPDATE: Waking up from the dead");
692       update_state = US_ONLINE;
693       noct_clear();
694     }
695
696 #ifdef LOCAL_DEBUG
697   pulse_dump();
698 #endif
699
700   // Sleeping?
701   bool want_sleep = want_sleep_p();
702   if (!want_sleep)
703     last_touch_time = main_get_now();
704   timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0;
705   timestamp_t sleep_in = 30000;
706   if (since_touch >= sleep_in)
707     {
708       DBG("UPDATE: Sleeping");
709       if (update_state == US_ONLINE)
710         {
711           update_state = US_SLEEPING;
712           noct_clear();
713           noct_set_ring(8, RING_MODE_LEFT, 127);
714         }
715       return;
716     }
717   else
718     {
719       if (update_state == US_SLEEPING)
720         {
721           DBG("UPDATE: Waking up");
722           update_state = US_ONLINE;
723           noct_clear();
724         }
725       if (want_sleep)
726         {
727           timestamp_t t = sleep_in - since_touch + 10;
728           DBG("UPDATE: Scheduling sleep in %d ms", (int) t);
729           timer_add_rel(&update_timer, t);
730         }
731     }
732
733   // Everything normal
734   update_ring_from_sink(0, PCH_SINK);
735   update_ring_from_sink(1, BT_SINK);
736   update_groups();
737   update_default_sink();
738   update_source_buttons();
739   update_mpd();
740   update_lights();
741   update_rainbow();
742 }
743
744 void schedule_update(void)
745 {
746   timer_add_rel(&update_timer, 10);
747 }
748
749 static bool prepare_notify(void)
750 {
751   if (!pulse_is_ready())
752     {
753       DBG("## NOTIFY: Pulse is not online");
754       return 0;
755     }
756
757   last_touch_time = main_get_now();
758   if (update_state == US_SLEEPING)
759     {
760       DBG("## NOTIFY: Scheduling wakeup");
761       schedule_update();
762     }
763
764   return 1;
765 }
766
767 void notify_rotary(int rotary, int delta)
768 {
769   if (!prepare_notify())
770     return;
771
772   switch (rotary)
773     {
774     case 0:
775       update_sink_from_rotary(delta, PCH_SINK);
776       break;
777     case 1:
778       update_sink_from_rotary(delta, BT_SINK);
779       break;
780     case 2:
781       update_lights_from_rotary(1, delta);
782       break;
783     case 3:
784       update_lights_from_rotary(0, delta);
785       break;
786     case 8:
787       update_rainbow_from_rotary(delta);
788       break;
789     case 9:
790       update_lights_from_slider(delta);
791       break;
792     default:
793       update_group_from_rotary(rotary, delta);
794     }
795 }
796
797 void notify_button(int button, int on)
798 {
799   if (!prepare_notify())
800     return;
801
802   switch (button)
803     {
804     case 0:
805       update_sink_mute_from_button(on, PCH_SINK);
806       break;
807     case 1:
808       update_sink_mute_from_button(on, BT_SINK);
809       break;
810     case 2:
811       update_source_mute_from_button(on, LOGI_SOURCE);
812       break;
813     case 8:
814     case 9:
815       update_default_sink_from_button(button, on);
816       break;
817     case 10:
818       update_lights_from_button(1, on);
819       break;
820     case 11:
821       update_lights_from_button(0, on);
822       break;
823     case 12:
824       update_mpd_from_button(button, on);
825       break;
826     case 13:
827       if (on)
828         mpd_stop();
829       break;
830     case 14:
831       if (on)
832         mpd_prev();
833       break;
834     case 15:
835       if (on)
836         mpd_next();
837       break;
838     default:
839       update_group_from_button(button, on);
840     }
841 }
842
843 void notify_touch(int rotary UNUSED, int on UNUSED)
844 {
845   if (!prepare_notify())
846     return;
847
848   // Rotary touches switch meaning of LEDs, this is handled inside display updates
849   if (rotary >= 4 && rotary < 8)
850     schedule_update();
851 }
852
853 static void notify_ir(const char *key)
854 {
855   DBG("Received IR key %s", key);
856
857   // Lights
858   if (!strcmp(key, "preset+"))
859     update_lights_from_ir(1, 1);
860   else if (!strcmp(key, "preset-"))
861     update_lights_from_ir(1, -1);
862   else if (!strcmp(key, "tuning-up"))
863     update_lights_from_ir(0, 1);
864   else if (!strcmp(key, "tuning-down"))
865     update_lights_from_ir(0, -1);
866   else if (!strcmp(key, "band"))
867     update_lights_on_off_ir(1);
868   else if (!strcmp(key, "fm-mode"))
869     update_lights_on_off_ir(0);
870   else if (!strcmp(key, "dimmer"))
871     update_lights_temp_ir();
872
873   // Player
874   else if (!strcmp(key, "play"))
875     mpd_play();
876   else if (!strcmp(key, "stop"))
877     mpd_stop();
878   else if (!strcmp(key, "pause"))
879     mpd_pause(1);
880   else if (!strcmp(key, "prev-song"))
881     mpd_prev();
882   else if (!strcmp(key, "next-song"))
883     mpd_next();
884   else if (!strcmp(key, "rewind"))
885     update_sink_from_rotary(-2, PCH_SINK);
886   else if (!strcmp(key, "ffwd"))
887     update_sink_from_rotary(2, PCH_SINK);
888 }
889
890 void notify_mqtt(const char *topic, const char *val)
891 {
892   const char blc[] = "burrow/lights/catarium/";
893   if (str_has_prefix(topic, blc))
894     {
895       topic += strlen(blc);
896       int ch;
897       if (!strcmp(topic, "top"))
898         ch = 1;
899       else if (!strcmp(topic, "bottom"))
900         ch = 0;
901       else
902         return;
903
904       double b, t;
905       if (sscanf(val, "%lf %lf", &b, &t) != 2)
906         return;
907
908       timestamp_t now = main_get_now();
909       if (!lights_last_update[ch] || lights_last_update[ch] + 1000 < now)
910         {
911           DBG("Received foreign light settings");
912           if (!b)
913             lights_on[ch] = 0;
914           else
915             {
916               lights_on[ch] = 1;
917               lights_brightness[ch] = b;
918               lights_temperature[ch] = t;
919             }
920           update_lights();
921         }
922     }
923
924   if (!strcmp(topic, "burrow/lights/rainbow/brightness"))
925     {
926       double b;
927       if (sscanf(val, "%lf", &b) == 1 && b >= 0 && b <= 1)
928         {
929           timestamp_t now = main_get_now();
930           if (!rainbow_last_update || rainbow_last_update + 1000 < now)
931             {
932               DBG("Received foreign rainbow settings");
933               rainbow_brightness = b;
934               update_rainbow();
935             }
936         }
937     }
938
939   if (!strcmp(topic, "burrow/control/catarium-ir"))
940     notify_ir(val);
941 }
942
943 /*** Main entry point ***/
944
945 static int debug;
946 static int no_fork;
947
948 static struct opt_section options = {
949   OPT_ITEMS {
950     OPT_HELP("Control console for the Ursary"),
951     OPT_HELP(""),
952     OPT_HELP("Options:"),
953     OPT_HELP_OPTION,
954     OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"),
955     OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"),
956     OPT_END
957   }
958 };
959
960 static void sigterm_handler(struct main_signal *ms UNUSED)
961 {
962   main_shut_down();
963 }
964
965 static void daemon_body(struct daemon_params *dp)
966 {
967   main_init();
968   update_timer.handler = do_update;
969
970   usb_init();
971   noct_init();
972   pulse_init();
973   mpd_init();
974   mqtt_init();
975
976   static struct main_signal term_sig = {
977     .signum = SIGTERM,
978     .handler = sigterm_handler,
979   };
980   signal_add(&term_sig);
981
982   msg(L_INFO, "Ursary daemon starting");
983   main_loop();
984   msg(L_INFO, "Ursary daemon shut down");
985   daemon_exit(dp);
986 }
987
988 int main(int argc UNUSED, char **argv)
989 {
990   opt_parse(&options, argv+1);
991   unsetenv("DISPLAY");
992   unsetenv("HOME");
993
994   struct daemon_params dp = {
995     .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0),
996     .pid_file = "/run/ursaryd.pid",
997     .run_as_user = "ursary",
998   };
999   daemon_init(&dp);
1000
1001   log_init(argv[0]);
1002   if (!debug)
1003     {
1004       struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
1005       ls->levels = ~(1U << L_DEBUG);
1006       log_set_default_stream(ls);
1007     }
1008
1009   daemon_run(&dp, daemon_body);
1010   return 0;
1011 }