]> mj.ucw.cz Git - ursary.git/blobdiff - ursaryd.c
Better IR control of lights
[ursary.git] / ursaryd.c
index bb057e16a1f105278bbb048d4fe24487d55bfe99..1d0e5ff4ed56ea8881a1506f44f1a4908d5f048b 100644 (file)
--- a/ursaryd.c
+++ b/ursaryd.c
-#define LOCAL_DEBUG
+/*
+ *     The Ursary Control Panel
+ *
+ *     (c) 2014-2023 Martin Mares <mj@ucw.cz>
+ */
+
+#undef LOCAL_DEBUG
 
 #include <ucw/lib.h>
 #include <ucw/clists.h>
+#include <ucw/daemon.h>
+#include <ucw/log.h>
 #include <ucw/mainloop.h>
+#include <ucw/opt.h>
 #include <ucw/stkstring.h>
+#include <ucw/string.h>
 
+#include <math.h>
+#include <signal.h>
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
-
-#include <pulse/pulseaudio.h>
+#include <syslog.h>
 
 #include "ursaryd.h"
+#include "usb.h"
 
-/*** Interface to PulseAudio ***/
+/*
+ *     Map of all controls
+ *
+ *             rotary          red button      green button
+ *             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *     0       sink PCH        mute            switch to PCH
+ *     1       sink BT         mute            switch to BT
+ *     2       ceil brightness mic non-mute    ceiling lights on
+ *     3       desk brightness -               desk lights on
+ *     4       Albireo MPD     mute            MPD play/pause
+ *     5       Albireo MPV     mute            MPD stop
+ *     6       Albireo Zoom    mute            MPD prev
+ *     7       eveyrhing else  mute            MPD next
+ *
+ *     center  rainbow brightness
+ *     slider  light color temperature
+ */
 
-static pa_context *pulse_ctx;
-static struct main_timer pulse_connect_timer;
+#define PCH_SINK "alsa_output.pci-0000_07_00.6.analog-stereo"
+#define BT_SINK "bluez_sink.CC_98_8B_D0_8C_06.a2dp_sink"
+#define LOGI_SOURCE "alsa_input.usb-046d_Logitech_Webcam_C925e_EF163C5F-02.analog-stereo"
 
-static void pulse_dump(void);
-
-enum pulse_state {
-  PS_OFFLINE,
-  PS_SUBSCRIBE,
-  PS_GET_CLIENTS,
-  PS_GET_SINKS,
-  PS_GET_SINK_INPUTS,
-  PS_ONLINE,
-};
-
-static enum pulse_state pulse_state;
-#define PULSE_STATE(s) do { pulse_state = s; DBG("Pulse: " #s); } while (0)
-
-// Tracking of currently running asynchronous operations
-struct pulse_op {
-  cnode n;
-  pa_operation *o;
-  bool is_init;
-};
+/*** Sink controls ***/
 
-static clist pulse_op_list;
-
-static struct pulse_op *pulse_op_new(void)
+static double volume_from_pa(pa_volume_t vol)
 {
-  struct pulse_op *op = xmalloc_zero(sizeof(*op));
-  clist_add_tail(&pulse_op_list, &op->n);
-  return op;
+  return (double) vol / PA_VOLUME_NORM;
 }
 
-static void pulse_op_done(struct pulse_op *op)
+static pa_volume_t volume_to_pa(double vol)
 {
-  if (op->o)
-    pa_operation_unref(op->o);
-  clist_remove(&op->n);
-  xfree(op);
+  return vol * PA_VOLUME_NORM + 0.0001;
 }
 
-static void pulse_op_cancel_all(void)
+static void update_ring_from_sink(int ring, const char *sink_name)
 {
-  struct pulse_op *op;
-  while (op = (struct pulse_op *) clist_head(&pulse_op_list))
+  struct pulse_sink *s = pulse_sink_by_name(sink_name);
+  if (!s)
     {
-      DBG("Pulse: Cancelling pending operation");
-      pa_operation_cancel(op->o);
-      pulse_op_done(op);
+      noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
+      noct_set_button(ring, 0);
+      return;
+    }
+
+  if (s->mute)
+    {
+      noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
+      noct_set_button(ring, 1);
+      return;
     }
-}
 
-#define PULSE_ASYNC_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->o = name(__VA_ARGS__, _op); } while (0)
-#define PULSE_ASYNC_INIT_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->is_init = 1; _op->o = name(__VA_ARGS__, _op); } while (0)
+  double vol = CLAMP(volume_from_pa(s->volume), 0, 1);
+  noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f));
+  noct_set_button(ring, 0);
+}
 
-static void pulse_success_cb(pa_context *ctx UNUSED, int success, void *userdata)
+static void update_sink_from_rotary(int delta, const char *sink_name)
 {
-  if (!success)
-    msg(L_ERROR, "Pulse: Failure reported");
-  pulse_op_done(userdata);
+  struct pulse_sink *s = pulse_sink_by_name(sink_name);
+  if (!s)
+    return;
+
+  double vol = volume_from_pa(s->volume) + delta * 0.02;
+  pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1));
+  if (pavol == s->volume)
+    return;
+  pa_cvolume cvol;
+  pa_cvolume_set(&cvol, s->channels, pavol);
+
+  DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
+  pulse_sink_set_volume(s->idx, &cvol);
 }
 
-static void pulse_dump_proplist(pa_proplist *pl UNUSED)
+static void update_sink_mute_from_button(int on, const char *sink_name)
 {
-#if 0
-  void *iterator = NULL;
-  const char *key;
+  if (!on)
+    return;
 
-  while (key = pa_proplist_iterate(pl, &iterator))
-    {
-      const char *val = pa_proplist_gets(pl, key);
-      DBG("   %s = %s", key, val);
-    }
-#endif
+  struct pulse_sink *s = pulse_sink_by_name(sink_name);
+  if (!s)
+    return;
+
+  DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
+  pulse_sink_set_mute(s->idx, !s->mute);
 }
 
-struct pulse_sink_input {
-  int idx;
-  char *name;
-  int client_idx;
-  int sink_idx;
-  uns channels;
-  uns volume;
-  uns mute;
-  int noct_client_idx;         // Used by the high-level logic below
+/*** Client controls ***/
+
+struct client_map {
+  int group;
+  const char *client;
+  const char *host;
+};
+
+static struct client_map client_map[] = {
+  { 4, "Music Player Daemon",  "albireo",      },
+  { 5, "mpv",                  "albireo",      },
+  { 6, "ZOOM VoiceEngine",     "albireo",      },
+  { 7, NULL,                   NULL,           },
 };
 
-#define HASH_NODE struct pulse_sink_input
-#define HASH_PREFIX(x) pulse_sink_input_##x
-#define HASH_KEY_ATOMIC idx
-#define HASH_WANT_CLEANUP
-#define HASH_WANT_LOOKUP
-#define HASH_WANT_REMOVE
-#define HASH_ZERO_FILL
-#include <ucw/hashtable.h>
+#define CLIENT_MAP_SIZE ARRAY_SIZE(client_map)
 
-#define SET_STRING(_field, _val) do { if (!_field || strcmp(_field, _val)) { xfree(_field); _field = xstrdup(_val); } } while (0)
+struct group_config {
+  bool enabled;
+  double range;
+};
 
-static void pulse_sink_input_cb(pa_context *ctx UNUSED, const pa_sink_input_info *i, int eol, void *userdata)
+struct group_state {
+  double volume;
+  bool have_muted[2];
+};
+
+#define NUM_GROUPS 9
+
+static struct group_config group_config[NUM_GROUPS] = {
+  [4] = { .enabled = 1, .range = 1.5 },
+  [5] = { .enabled = 1, .range = 1.5 },
+  [6] = { .enabled = 1, .range = 1.5 },
+  [7] = { .enabled = 1, .range = 1.5 },
+};
+
+static struct group_state group_state[NUM_GROUPS];
+
+static void calc_groups(void)
 {
-  struct pulse_op *op = userdata;
+  bzero(group_state, sizeof(group_state));
 
-  if (eol)
+  CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
     {
-      if (op->is_init)
+      s->noct_group_idx = -1;
+
+      if (s->client_idx < 0 || s->sink_idx < 0)
+       continue;
+
+      struct pulse_client *c = pulse_client_by_idx(s->client_idx);
+      if (!c)
+       continue;
+
+      for (uns i=0; i < CLIENT_MAP_SIZE; i++)
        {
-         PULSE_STATE(PS_ONLINE);
-         schedule_update();
+         struct client_map *cm = &client_map[i];
+         if ((!cm->client || !strcmp(cm->client, c->name)) &&
+             (!cm->host || !strcmp(cm->host, c->host)))
+           {
+             int g = cm->group;
+             struct group_state *gs = &group_state[g];
+             DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g);
+             s->noct_group_idx = g;
+             gs->volume = MAX(gs->volume, s->volume);
+             gs->have_muted[!!s->mute] = 1;
+             break;
+           }
        }
-      pulse_op_done(op);
-      return;
     }
+}
+
+static void update_groups(void)
+{
+  calc_groups();
+
+  for (uns i=0; i < NUM_GROUPS; i++)
+    {
+      struct group_config *gc = &group_config[i];
+      struct group_state *gs = &group_state[i];
+      if (!gc->enabled)
+       continue;
 
-  DBG("Pulse: SINK INPUT #%u: %s client=%d sink=%d chans=%d has_vol=%d vol_rw=%d volume=%u mute=%d",
-    i->index, i->name, i->client, i->sink, i->channel_map.channels, i->has_volume, i->volume_writable, i->volume.values[0], i->mute);
-  pulse_dump_proplist(i->proplist);
+      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);
 
-  struct pulse_sink_input *s = pulse_sink_input_lookup(i->index);
-  SET_STRING(s->name, i->name);
-  s->client_idx = i->client;
-  s->sink_idx = i->sink;
-  s->channels = i->channel_map.channels;
-  s->volume = pa_cvolume_avg(&i->volume);
-  s->mute = i->mute;
-  schedule_update();
+      if (!gs->have_muted[0] && !gs->have_muted[1])
+       {
+         noct_set_ring(i, RING_MODE_LEFT, 0);
+         noct_set_button(i, 0);
+       }
+      else if (!gs->have_muted[0])
+       {
+         noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f);
+         noct_set_button(i, 1);
+       }
+      else
+       {
+         double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range);
+         int val = 0x7f * vol / gc->range;
+         val = CLAMP(val, 12, 0x7f);
+         noct_set_ring(i, RING_MODE_LEFT, val);
+         noct_set_button(i, 0);
+       }
+    }
 }
 
-static void pulse_sink_input_gone(int idx)
+static void update_group_from_rotary(int i, int delta)
 {
-  DBG("Pulse: REMOVE SINK INPUT #%d", idx);
-  struct pulse_sink_input *s = pulse_sink_input_lookup(idx);
-  pulse_sink_input_remove(s);
-  schedule_update();
-}
+  if (i >= NUM_GROUPS)
+    return;
+  struct group_config *gc = &group_config[i];
+  struct group_state *gs = &group_state[i];
+  if (!gc->enabled)
+    return;
 
-struct pulse_sink {
-  int idx;
-  char *name;
-  uns channels;
-  uns volume;
-  uns base_volume;
-  int mute;
-};
+  calc_groups();
+  double vol = volume_from_pa(gs->volume) + delta*0.02;
+  pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range));
 
-#define HASH_NODE struct pulse_sink
-#define HASH_PREFIX(x) pulse_sink_##x
-#define HASH_KEY_ATOMIC idx
-#define HASH_WANT_CLEANUP
-#define HASH_WANT_LOOKUP
-#define HASH_WANT_REMOVE
-#define HASH_ZERO_FILL
-#include <ucw/hashtable.h>
+  CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
+    {
+      if (s->noct_group_idx == i && s->volume != pavol)
+       {
+         DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
+         pa_cvolume cvol;
+         pa_cvolume_set(&cvol, s->channels, pavol);
+         pulse_sink_input_set_volume(s->idx, &cvol);
+       }
+    }
+}
 
-static void pulse_sink_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata)
+static void update_group_from_button(int i, int on)
 {
-  struct pulse_op *op = userdata;
+  if (!on)
+    return;
+  if (i >= NUM_GROUPS)
+    return;
+  struct group_config *gc = &group_config[i];
+  struct group_state *gs = &group_state[i];
+  if (!gc->enabled)
+    return;
 
-  if (eol)
+  calc_groups();
+  if (!gs->have_muted[0] && !gs->have_muted[1])
+    return;
+  uns mute = !gs->have_muted[1];
+
+  CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
     {
-      if (op->is_init)
+      if (s->noct_group_idx == i)
        {
-         PULSE_STATE(PS_GET_SINK_INPUTS);
-         PULSE_ASYNC_INIT_RUN(pa_context_get_sink_input_info_list, ctx, pulse_sink_input_cb);
+         DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
+         pulse_sink_input_set_mute(s->idx, mute);
        }
-      pulse_op_done(op);
-      return;
     }
+}
 
-  DBG("Pulse: SINK #%u: %s (%s) flags=%08x channels=%u volume=%u mute=%d base_vol=%u state=%u",
-    i->index, i->name, i->description, i->flags, i->channel_map.channels, i->volume.values[0], i->mute, i->base_volume, i->state);
-  pulse_dump_proplist(i->proplist);
+static int find_touched_client(void)
+{
+  int touched = -1;
 
-  struct pulse_sink *s = pulse_sink_lookup(i->index);
-  SET_STRING(s->name, i->name);
-  s->channels = i->channel_map.channels;
-  s->volume = pa_cvolume_avg(&i->volume);
-  s->base_volume = i->base_volume;
-  s->mute = i->mute;
-  schedule_update();
+  for (uns i=0; i < NUM_GROUPS; i++)
+    if (group_config[i].enabled && noct_rotary_touched[i])
+      {
+       if (touched >= 0)
+         return -1;
+       touched = i;
+      }
+  return touched;
 }
 
-static void pulse_sink_gone(int idx)
+/*** Default sink controls ***/
+
+static const char *get_client_sink(int i)
 {
-  DBG("Pulse: REMOVE SINK #%d", idx);
-  struct pulse_sink *s = pulse_sink_lookup(idx);
-  pulse_sink_remove(s);
-  schedule_update();
+  const char *sink = NULL;
+
+  CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
+    if (s->noct_group_idx == i)
+      {
+       struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL;
+       const char *ss = sk ? sk->name : NULL;
+       if (!sink)
+         sink = ss;
+       else if (strcmp(sink, ss))
+         sink = "?";
+      }
+  return sink ? : "?";
 }
 
-static struct pulse_sink *pulse_sink_by_name(const char *name)
+static void update_default_sink(void)
 {
-  HASH_FOR_ALL(pulse_sink, s)
+  int i = find_touched_client();
+  const char *sink;
+  if (i >= 0)
+    sink = get_client_sink(i);
+  else
+    sink = pulse_default_sink_name ? : "?";
+
+  if (!strcmp(sink, PCH_SINK))
     {
-      if (!strcmp(s->name, name))
-       return s;
+      noct_set_button(8, 1);
+      noct_set_button(9, 0);
+    }
+  else if (!strcmp(sink, BT_SINK))
+    {
+      noct_set_button(8, 0);
+      noct_set_button(9, 1);
+    }
+  else
+    {
+      noct_set_button(8, 0);
+      noct_set_button(9, 0);
     }
-  HASH_END_FOR;
-  return NULL;
 }
 
-struct pulse_client {
-  int idx;
-  char *name;
-  char *host;
-};
+static void update_default_sink_from_button(int button, int on)
+{
+  if (!on)
+    return;
 
-#define HASH_NODE struct pulse_client
-#define HASH_PREFIX(x) pulse_client_##x
-#define HASH_KEY_ATOMIC idx
-#define HASH_WANT_CLEANUP
-#define HASH_WANT_LOOKUP
-#define HASH_WANT_REMOVE
-#define HASH_ZERO_FILL
-#include <ucw/hashtable.h>
+  int i = find_touched_client();
+#if 0
+  const char *sink;
+  if (i >= 0)
+    sink = get_client_sink(i);
+  else
+    sink = pulse_default_sink_name ? : "?";
+#endif
 
-static void pulse_client_cb(pa_context *ctx, const pa_client_info *i, int eol, void *userdata)
-{
-  struct pulse_op *op = userdata;
+  const char *switch_to = NULL;
+  if (button == 8)
+    switch_to = PCH_SINK;
+  else if (button == 9)
+    switch_to = BT_SINK;
+
+  if (!switch_to)
+    return;
 
-  if (eol)
+  if (i >= 0)
     {
-      if (op->is_init)
-       {
-         PULSE_STATE(PS_GET_SINKS);
-         PULSE_ASYNC_INIT_RUN(pa_context_get_sink_info_list, ctx, pulse_sink_cb);
-       }
-      pulse_op_done(op);
-      return;
+      struct pulse_sink *sk = pulse_sink_by_name(switch_to);
+      if (!sk)
+       return;
+
+      CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
+        if (s->noct_group_idx == i)
+         {
+           DBG("Moving input #%d to sink #%d", s->idx, sk->idx);
+           pulse_sink_input_move(s->idx, sk->idx);
+         }
+    }
+  else
+    {
+      DBG("Switching default sink to %s", switch_to);
+      pulse_server_set_default_sink(switch_to);
     }
-
-  char *host = stk_strdup(pa_proplist_gets(i->proplist, "application.process.host") ? : "?");
-  DBG("Pulse: CLIENT #%u: %s mod=%u drv=%s host=%s",
-    i->index, i->name, i->owner_module, i->driver, host);
-  pulse_dump_proplist(i->proplist);
-
-  struct pulse_client *c = pulse_client_lookup(i->index);
-  SET_STRING(c->name, i->name);
-  SET_STRING(c->host, host);
-  schedule_update();
 }
 
-static void pulse_client_gone(int idx)
-{
-  DBG("Pulse: REMOVE CLIENT #%d", idx);
-  struct pulse_client *c = pulse_client_lookup(idx);
-  pulse_client_remove(c);
-  schedule_update();
-}
+/*** Source mute controls ***/
 
-static void pulse_shutdown(void)
+static void update_source_buttons(void)
 {
-  DBG("Pulse: Shutting down");
-  pulse_client_cleanup();
-  pulse_sink_cleanup();
-  pulse_sink_input_cleanup();
+  struct pulse_source *source = pulse_source_by_name(LOGI_SOURCE);
+  if (!source)
+    return;
+
+#if 0  // Disabled for now
+  // if (source->suspended || source->mute)
+  if (source->mute)
+    noct_set_button(2, 0);
+  else
+    noct_set_button(2, 1);
+#endif
 }
 
-static void pulse_subscribe_done_cb(pa_context *ctx, int success, void *userdata)
+static void update_source_mute_from_button(int on, const char *source_name)
 {
-  pulse_op_done(userdata);
+  if (!on)
+    return;
 
-  if (!success)
-    msg(L_ERROR, "pa_context_subscribe failed: success=%d", success);
+  struct pulse_source *s = pulse_source_by_name(source_name);
+  if (!s)
+    return;
 
-  PULSE_STATE(PS_GET_CLIENTS);
-  PULSE_ASYNC_INIT_RUN(pa_context_get_client_info_list, ctx, pulse_client_cb);
+#if 0
+  DBG("## Setting mute of source %s to %d", s->name, !s->mute);
+  pulse_source_set_mute(s->idx, !s->mute);
+#endif
 }
 
-static void pulse_event_cb(pa_context *ctx, pa_subscription_event_type_t type, uint32_t idx, void *userdata UNUSED)
-{
-  DBG("Pulse: SUBSCRIBE EVENT type=%08x idx=%u", type, idx);
+/*** MPD controls ***/
 
-  uns object = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
-  uns action = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
-  switch (object)
-    {
-    case PA_SUBSCRIPTION_EVENT_CLIENT:
-      if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE)
-       PULSE_ASYNC_RUN(pa_context_get_client_info, ctx, idx, pulse_client_cb);
-      else if (action == PA_SUBSCRIPTION_EVENT_REMOVE)
-       pulse_client_gone(idx);
-      break;
-    case PA_SUBSCRIPTION_EVENT_SINK:
-      if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE)
-       PULSE_ASYNC_RUN(pa_context_get_sink_info_by_index, ctx, idx, pulse_sink_cb);
-      else if (action == PA_SUBSCRIPTION_EVENT_REMOVE)
-       pulse_sink_gone(idx);
-      break;
-    case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
-      if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE)
-       PULSE_ASYNC_RUN(pa_context_get_sink_input_info, ctx, idx, pulse_sink_input_cb);
-      else if (action == PA_SUBSCRIPTION_EVENT_REMOVE)
-       pulse_sink_input_gone(idx);
-      break;
-    }
+static bool mpd_flash_state;
+
+static void mpd_flash_timeout(struct main_timer *t)
+{
+  mpd_flash_state ^= 1;
+  noct_set_button(12, mpd_flash_state);
+  timer_add_rel(t, 500);
 }
 
-static void pulse_state_cb(pa_context *ctx, void *userdata UNUSED)
+static struct main_timer mpd_flash_timer = {
+  .handler = mpd_flash_timeout,
+};
+
+static void update_mpd(void)
 {
-  int state = pa_context_get_state(ctx);
-  DBG("Pulse: State callback, new state = %d", state);
-  if (state == PA_CONTEXT_READY)
+  const char *state = mpd_get_player_state();
+  if (!strcmp(state, "play"))
     {
-      if (pulse_state == PS_OFFLINE)
+      noct_set_button(12, 1);
+      timer_del(&mpd_flash_timer);
+    }
+  else if (!strcmp(state, "pause"))
+    {
+      if (!timer_is_active(&mpd_flash_timer))
        {
-         PULSE_STATE(PS_SUBSCRIBE);
-         pa_context_set_subscribe_callback(ctx, pulse_event_cb, NULL);
-         PULSE_ASYNC_INIT_RUN(pa_context_subscribe, ctx, PA_SUBSCRIPTION_MASK_ALL, pulse_subscribe_done_cb);
+         mpd_flash_state = 1;
+         mpd_flash_timeout(&mpd_flash_timer);
        }
     }
   else
     {
-      if (pulse_state != PS_OFFLINE)
-       {
-         PULSE_STATE(PS_OFFLINE);
-         pulse_op_cancel_all();
-         pulse_shutdown();
-         schedule_update();
-       }
-      if (state == PA_CONTEXT_FAILED && !timer_is_active(&pulse_connect_timer))
-       timer_add_rel(&pulse_connect_timer, 2000);
+      noct_set_button(12, 0);
+      timer_del(&mpd_flash_timer);
     }
 }
 
-static void pulse_dump(void)
+static void mpd_button_timeout(struct main_timer *t)
+{
+  DBG("MPD stop");
+  timer_del(t);
+  mpd_stop();
+}
+
+static void update_mpd_from_button(int button UNUSED, int on)
 {
-  HASH_FOR_ALL(pulse_client, c)
+  static struct main_timer mpd_button_timer = {
+    .handler = mpd_button_timeout,
+  };
+
+  const char *state = mpd_get_player_state();
+
+  if (!on)
     {
-      DBG("## Client #%d: %s host=%s", c->idx, c->name, c->host);
+      if (timer_is_active(&mpd_button_timer))
+       {
+         timer_del(&mpd_button_timer);
+         if (!strcmp(state, "play"))
+           {
+             DBG("MPD pause");
+             mpd_pause(1);
+           }
+         else if (!strcmp(state, "pause"))
+           {
+             DBG("MPD resume");
+             mpd_pause(0);
+           }
+       }
+      return;
     }
-  HASH_END_FOR;
 
-  HASH_FOR_ALL(pulse_sink, s)
+  if (!strcmp(state, "stop"))
     {
-      DBG("## Sink #%d: %s channels=%u volume=%u base_vol=%u mute=%u",
-       s->idx, s->name, s->channels, s->volume, s->base_volume, s->mute);
+      DBG("MPD play");
+      mpd_play();
     }
-  HASH_END_FOR;
-
-  HASH_FOR_ALL(pulse_sink_input, s)
+  else
     {
-      DBG("## Sink input #%d: %s client=%d sink=%d channels=%u volume=%u mute=%u",
-       s->idx, s->name, s->client_idx, s->sink_idx, s->channels, s->volume, s->mute);
+      DBG("MPD starting button timer");
+      timer_add_rel(&mpd_button_timer, 1000);
     }
-  HASH_END_FOR;
 }
 
-static void pulse_connect(struct main_timer *t)
-{
-  DBG("Pulse: Connecting");
-  timer_del(t);
-
-  clist_init(&pulse_op_list);
-  pulse_client_init();
-  pulse_sink_init();
-  pulse_sink_input_init();
+/*** Lights ***/
 
-  if (pulse_ctx)
-    pa_context_unref(pulse_ctx);
-  pulse_ctx = pa_context_new(&pmain_api, "ursaryd");
+static bool lights_on[2];
+static double lights_brightness[2];
+static double lights_temperature[2];
+static timestamp_t lights_last_update[2];
 
-  pa_context_set_state_callback(pulse_ctx, pulse_state_cb, NULL);
-  pa_context_connect(pulse_ctx, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
+static void update_lights(void)
+{
+  for (uint ch=0; ch < 2; ch++)
+    {
+      if (lights_on[ch])
+       {
+         noct_set_ring(3-ch, RING_MODE_LEFT, lights_brightness[ch] * 127);
+         noct_set_button(11-ch, 1);
+       }
+      else
+       {
+         noct_set_ring(3-ch, RING_MODE_LEFT, 0);
+         noct_set_button(11-ch, 0);
+       }
+    }
 }
 
-static void pulse_init(void)
+static void send_lights(int ch)
 {
-  pmain_init();
+  DBG("Lights[%d]: on=%d bri=%.3f temp=%.3f", ch, lights_on[ch], lights_brightness[ch], lights_temperature[ch]);
+  double b = lights_on[ch] ? lights_brightness[ch] : 0;
+  double t = lights_on[ch] ? lights_temperature[ch] : 0;
+  char topic[100], val[100];
+  snprintf(topic, sizeof(topic), "burrow/lights/catarium/%s", (ch ? "top" : "bottom"));
+  snprintf(val, sizeof(val), "%.3f %.3f", b, t);
+  mqtt_publish(topic, val);
+  lights_last_update[ch] = main_get_now();
+  update_lights();
+}
 
-  pulse_connect_timer.handler = pulse_connect;
-  timer_add_rel(&pulse_connect_timer, 0);
+static void update_lights_from_rotary(int ch, int delta)
+{
+  if (lights_on[ch])
+    lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.015*delta*abs(delta), 0., 1.);
+  send_lights(ch);
 }
 
-/*** High-level logic ***/
+static void update_lights_from_slider(int value)
+{
+  lights_temperature[0] = value / 127.;
+  lights_temperature[1] = value / 127.;
+  send_lights(0);
+  send_lights(1);
+}
 
-static struct main_timer update_timer;
+static void lights_button_timeout(struct main_timer *t)
+{
+  int ch = (uintptr_t) t->data;
+  DBG("Lights[%d]: Full throttle!", ch);
+  timer_del(t);
+  lights_on[ch] = 1;
+  lights_brightness[ch] = 1;
+  send_lights(ch);
+}
 
-static void update_ring_from_sink(int ring, const char *sink_name)
+static void update_lights_from_button(int ch, int on)
 {
-  struct pulse_sink *s = pulse_sink_by_name(sink_name);
-  if (!s)
+  static struct main_timer lights_button_timer[2] = {{
+    .handler = lights_button_timeout,
+    .data = (void *)(uintptr_t) 0,
+  },{
+    .handler = lights_button_timeout,
+    .data = (void *)(uintptr_t) 1,
+  }};
+
+  if (on)
+    timer_add_rel(&lights_button_timer[ch], 500);
+  else if (timer_is_active(&lights_button_timer[ch]))
     {
-      noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
-      noct_set_button(ring, 0);
-      return;
+      timer_del(&lights_button_timer[ch]);
+      lights_on[ch] = !lights_on[ch];
+      send_lights(ch);
     }
+}
 
-  if (s->mute)
+static void update_lights_from_ir(int ch, int dir)
+{
+  if (lights_on[ch])
+    lights_brightness[ch] = CLAMP(lights_brightness[ch] + 0.07*dir, 0., 1.);
+  else if (dir > 0)
     {
-      noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
-      noct_set_button(ring, 1);
-      return;
+      lights_on[ch] = 1;
+      lights_brightness[ch] = 1;
+    }
+  else
+    {
+      lights_on[ch] = 1;
+      lights_brightness[ch] = 0.05;
     }
+  send_lights(ch);
+}
 
-  double vol = pa_sw_volume_to_linear(s->volume);
-  vol = CLAMP(vol, 0, 1);
-  int val = 0x7f * vol;
-  val = CLAMP(val, 12, 0x7f);
-  noct_set_ring(ring, RING_MODE_LEFT, val);
-  noct_set_button(ring, 0);
+static void update_lights_on_off_ir(int ch)
+{
+  lights_on[ch] ^= 1;
+  send_lights(ch);
 }
 
-struct client_map {
-  int rotary;
-  const char *client;
-  const char *host;
-  double range;
-};
+static void update_lights_temp_ir(void)
+{
+  if (!lights_on[0] && !lights_on[1])
+    return;
 
-static struct client_map client_map[] = {
-  { 4, "Music Player Daemon",  "albireo",      1 },
-  { 5, "MPlayer",              NULL,           1 },
-  { 6, NULL,                   "ogion",        1 },
-};
+  double t = (lights_temperature[0] + lights_temperature[1]) / 2;
+  if (t >= 0.66)
+    t = 0;
+  else if (t < 0.33)
+    t = 0.5;
+  else
+    t = 1;
+  lights_temperature[0] = lights_temperature[1] = t;
 
-#define NUM_CLIENTS ARRAY_SIZE(client_map)
+  send_lights(0);
+  send_lights(1);
+}
 
-struct client_state {
-  double volume;
-  bool have_muted[2];
-};
+/*** Rainbow ***/
 
-static struct client_state client_state[NUM_CLIENTS];
+static double rainbow_brightness;
+static timestamp_t rainbow_last_update;
 
-static int find_client_by_rotary(int rotary)
+static void update_rainbow(void)
 {
-  uns i;
-  for (i=0; i < NUM_CLIENTS; i++)
-    if (client_map[i].rotary == rotary)
-      return i;
-  return -1;
+  noct_set_ring(8, RING_MODE_LEFT, rainbow_brightness * 127);
 }
 
-static void calc_clients(void)
+static void send_rainbow(void)
 {
-  bzero(client_state, sizeof(client_state));
+  char val[100];
+  snprintf(val, sizeof(val), "%.3f", rainbow_brightness);
+  mqtt_publish("burrow/lights/rainbow/brightness", val);
+  rainbow_last_update = main_get_now();
+  update_rainbow();
+}
 
-  HASH_FOR_ALL(pulse_sink_input, s)
-    {
-      s->noct_client_idx = -1;
+static void update_rainbow_from_rotary(int delta)
+{
+  rainbow_brightness = CLAMP(rainbow_brightness + 0.015*delta*abs(delta), 0., 1.);
+  send_rainbow();
+}
 
-      if (s->client_idx < 0 || s->sink_idx < 0)
-       continue;
+/*** Main update routines ***/
 
-      struct pulse_client *c = pulse_client_lookup(s->client_idx);
-      if (!c)
-       continue;
+static struct main_timer update_timer;
+static timestamp_t last_touch_time;
 
-      for (uns i=0; i < NUM_CLIENTS; i++)
-       {
-         struct client_map *cm = &client_map[i];
-         struct client_state *cs = &client_state[i];
-         if ((!cm->client || !strcmp(cm->client, c->name)) &&
-             (!cm->host || !strcmp(cm->host, c->host)))
-           {
-             // DBG("@@ Client #%d, sink input #%d -> rotary %d", s->client_idx, s->idx, cm->rotary);
-             s->noct_client_idx = i;
-             cs->volume = MAX(cs->volume, s->volume);
-             cs->have_muted[!!s->mute] = 1;
-             break;
-           }
-       }
-    }
-  HASH_END_FOR;
-}
+enum update_state {
+  US_OFFLINE,
+  US_ONLINE,
+  US_SLEEPING,
+  US_PULSE_DEAD,
+};
 
-static void update_clients(void)
-{
-  calc_clients();
+static enum update_state update_state;
 
-  for (uns i=0; i < NUM_CLIENTS; i++)
-    {
-      struct client_map *cm = &client_map[i];
-      struct client_state *cs = &client_state[i];
-      if (!cs->have_muted[0] && !cs->have_muted[1])
-       {
-         noct_set_ring(cm->rotary, RING_MODE_LEFT, 0);
-         noct_set_button(cm->rotary, 0);
-       }
-      else if (!cs->have_muted[0])
-       {
-         noct_set_ring(cm->rotary, RING_MODE_SINGLE_ON, 0x7f);
-         noct_set_button(cm->rotary, 1);
-       }
-      else
-       {
-         double vol = pa_sw_volume_to_linear(cs->volume);
-         vol = CLAMP(vol, 0, cm->range);
-         int val = 0x7f * vol / cm->range;
-         val = CLAMP(val, 12, 0x7f);
-         noct_set_ring(cm->rotary, RING_MODE_LEFT, val);
-         noct_set_button(cm->rotary, 0);
-       }
-    }
+static bool want_sleep_p(void)
+{
+  CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list)
+    if (!s->suspended)
+      return 0;
+  return 1;
 }
 
 static void do_update(struct main_timer *t)
 {
+  DBG("## UPDATE in state %u", update_state);
   timer_del(t);
+
+  // Nocturn dead?
   if (!noct_is_ready())
     {
       DBG("## UPDATE: Nocturn is not ready");
+      update_state = US_OFFLINE;
       return;
     }
+  else if (update_state == US_OFFLINE)
+    {
+      DBG("## UPDATE: Going online");
+      update_state = US_ONLINE;
+    }
 
-  static bool dead;
-  if (pulse_state != PS_ONLINE)
+  // Pulse dead?
+  if (!pulse_is_ready())
     {
       DBG("## UPDATE: Pulse is not online");
-      for (int i=0; i<=8; i++)
-       noct_set_ring(i, RING_MODE_LEFT, 0);
-      for (int i=0; i<8; i++)
+      if (update_state != US_PULSE_DEAD)
        {
-         noct_set_button(i, 1);
-         noct_set_button(i+8, 0);
+         update_state = US_PULSE_DEAD;
+         noct_clear();
+         for (int i=0; i<8; i++)
+           noct_set_button(i, 1);
        }
-      dead = 1;
       return;
     }
-  if (dead)
+  else if (update_state == US_PULSE_DEAD)
     {
       DBG("## UPDATE: Waking up from the dead");
-      for (int i=0; i<=8; i++)
-       noct_set_ring(i, RING_MODE_LEFT, 0);
-      for (int i=0; i<16; i++)
-       noct_set_button(i, 0);
-      dead = 0;
+      update_state = US_ONLINE;
+      noct_clear();
     }
 
-  DBG("## UPDATE");
+#ifdef LOCAL_DEBUG
   pulse_dump();
+#endif
+
+  // Sleeping?
+  bool want_sleep = want_sleep_p();
+  if (!want_sleep)
+    last_touch_time = main_get_now();
+  timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0;
+  timestamp_t sleep_in = 30000;
+  if (since_touch >= sleep_in)
+    {
+      DBG("UPDATE: Sleeping");
+      if (update_state == US_ONLINE)
+       {
+         update_state = US_SLEEPING;
+         noct_clear();
+         noct_set_ring(8, RING_MODE_LEFT, 127);
+       }
+      return;
+    }
+  else
+    {
+      if (update_state == US_SLEEPING)
+       {
+         DBG("UPDATE: Waking up");
+         update_state = US_ONLINE;
+         noct_clear();
+       }
+      if (want_sleep)
+       {
+         timestamp_t t = sleep_in - since_touch + 10;
+         DBG("UPDATE: Scheduling sleep in %d ms", (int) t);
+         timer_add_rel(&update_timer, t);
+       }
+    }
 
-  update_ring_from_sink(0, "ursarium");
-  update_ring_from_sink(1, "catarium");
-  update_clients();
+  // Everything normal
+  update_ring_from_sink(0, PCH_SINK);
+  update_ring_from_sink(1, BT_SINK);
+  update_groups();
+  update_default_sink();
+  update_source_buttons();
+  update_mpd();
+  update_lights();
+  update_rainbow();
 }
 
 void schedule_update(void)
@@ -557,150 +746,266 @@ void schedule_update(void)
   timer_add_rel(&update_timer, 10);
 }
 
-static void update_sink_from_rotary(int delta, const char *sink_name)
+static bool prepare_notify(void)
 {
-  struct pulse_sink *s = pulse_sink_by_name(sink_name);
-  if (!s)
-    return;
-
-  double vol = pa_sw_volume_to_linear(s->volume);
-  vol += delta * 0.02;
-  vol = CLAMP(vol, 0, 1);
-  pa_volume_t pavol = pa_sw_volume_from_linear(vol);
-  if (pavol == s->volume)
-    return;
-  pa_cvolume cvol;
-  pa_cvolume_set(&cvol, s->channels, pavol);
-
-  DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
-  PULSE_ASYNC_RUN(pa_context_set_sink_volume_by_index, pulse_ctx, s->idx, &cvol, pulse_success_cb);
-}
-
-static void update_client_from_rotary(int rotary, int delta)
-{
-  int i = find_client_by_rotary(rotary);
-  if (i < 0)
-    return;
-  struct client_map *cm = &client_map[i];
-  struct client_state *cs = &client_state[i];
-
-  calc_clients();
-  double vol = pa_sw_volume_to_linear(cs->volume);
-  vol += delta * 0.02;
-  vol = CLAMP(vol, 0, cm->range);
-  pa_volume_t pavol = pa_sw_volume_from_linear(vol);
+  if (!pulse_is_ready())
+    {
+      DBG("## NOTIFY: Pulse is not online");
+      return 0;
+    }
 
-  HASH_FOR_ALL(pulse_sink_input, s)
+  last_touch_time = main_get_now();
+  if (update_state == US_SLEEPING)
     {
-      if (s->noct_client_idx == i && s->volume != pavol)
-       {
-         DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
-         pa_cvolume cvol;
-         pa_cvolume_set(&cvol, s->channels, pavol);
-         PULSE_ASYNC_RUN(pa_context_set_sink_input_volume, pulse_ctx, s->idx, &cvol, pulse_success_cb);
-       }
+      DBG("## NOTIFY: Scheduling wakeup");
+      schedule_update();
     }
-  HASH_END_FOR;
+
+  return 1;
 }
 
 void notify_rotary(int rotary, int delta)
 {
-  if (pulse_state != PS_ONLINE)
-    {
-      DBG("## NOTIFY: Pulse is not online");
-      return;
-    }
+  if (!prepare_notify())
+    return;
 
   switch (rotary)
     {
     case 0:
-      update_sink_from_rotary(delta, "ursarium");
+      update_sink_from_rotary(delta, PCH_SINK);
       break;
     case 1:
-      update_sink_from_rotary(delta, "catarium");
+      update_sink_from_rotary(delta, BT_SINK);
+      break;
+    case 2:
+      update_lights_from_rotary(1, delta);
+      break;
+    case 3:
+      update_lights_from_rotary(0, delta);
       break;
     case 8:
-      update_sink_from_rotary(delta, "ursarium");
-      update_sink_from_rotary(delta, "catarium");
+      update_rainbow_from_rotary(delta);
+      break;
+    case 9:
+      update_lights_from_slider(delta);
       break;
     default:
-      update_client_from_rotary(rotary, delta);
+      update_group_from_rotary(rotary, delta);
     }
 }
 
-static void update_sink_mute_from_button(int on, const char *sink_name)
+void notify_button(int button, int on)
 {
-  if (!on)
-    return;
-
-  struct pulse_sink *s = pulse_sink_by_name(sink_name);
-  if (!s)
+  if (!prepare_notify())
     return;
 
-  DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
-  PULSE_ASYNC_RUN(pa_context_set_sink_mute_by_index, pulse_ctx, s->idx, !s->mute, pulse_success_cb);
+  switch (button)
+    {
+    case 0:
+      update_sink_mute_from_button(on, PCH_SINK);
+      break;
+    case 1:
+      update_sink_mute_from_button(on, BT_SINK);
+      break;
+    case 2:
+      update_source_mute_from_button(on, LOGI_SOURCE);
+      break;
+    case 8:
+    case 9:
+      update_default_sink_from_button(button, on);
+      break;
+    case 10:
+      update_lights_from_button(1, on);
+      break;
+    case 11:
+      update_lights_from_button(0, on);
+      break;
+    case 12:
+      update_mpd_from_button(button, on);
+      break;
+    case 13:
+      if (on)
+       mpd_stop();
+      break;
+    case 14:
+      if (on)
+       mpd_prev();
+      break;
+    case 15:
+      if (on)
+       mpd_next();
+      break;
+    default:
+      update_group_from_button(button, on);
+    }
 }
 
-static void update_client_from_button(int button, int on)
+void notify_touch(int rotary UNUSED, int on UNUSED)
 {
-  if (button >= 8 || !on)
+  if (!prepare_notify())
     return;
 
-  int i = find_client_by_rotary(button);
-  if (i < 0)
-    return;
-  struct client_state *cs = &client_state[i];
+  // Rotary touches switch meaning of LEDs, this is handled inside display updates
+  if (rotary >= 4 && rotary < 8)
+    schedule_update();
+}
+
+static void notify_ir(const char *key)
+{
+  DBG("Received IR key %s", key);
+
+  // Lights
+  if (!strcmp(key, "preset+"))
+    update_lights_from_ir(1, 1);
+  else if (!strcmp(key, "preset-"))
+    update_lights_from_ir(1, -1);
+  else if (!strcmp(key, "tuning-up"))
+    update_lights_from_ir(0, 1);
+  else if (!strcmp(key, "tuning-down"))
+    update_lights_from_ir(0, -1);
+  else if (!strcmp(key, "band"))
+    update_lights_on_off_ir(1);
+  else if (!strcmp(key, "fm-mode"))
+    update_lights_on_off_ir(0);
+  else if (!strcmp(key, "dimmer"))
+    update_lights_temp_ir();
+
+  // Player
+  else if (!strcmp(key, "play"))
+    mpd_play();
+  else if (!strcmp(key, "stop"))
+    mpd_stop();
+  else if (!strcmp(key, "pause"))
+    mpd_pause(1);
+  else if (!strcmp(key, "prev-song"))
+    mpd_prev();
+  else if (!strcmp(key, "next-song"))
+    mpd_next();
+  else if (!strcmp(key, "rewind"))
+    update_sink_from_rotary(-2, PCH_SINK);
+  else if (!strcmp(key, "ffwd"))
+    update_sink_from_rotary(2, PCH_SINK);
+}
+
+void notify_mqtt(const char *topic, const char *val)
+{
+  const char blc[] = "burrow/lights/catarium/";
+  if (str_has_prefix(topic, blc))
+    {
+      topic += strlen(blc);
+      int ch;
+      if (!strcmp(topic, "top"))
+       ch = 1;
+      else if (!strcmp(topic, "bottom"))
+       ch = 0;
+      else
+       return;
 
-  calc_clients();
-  if (!cs->have_muted[0] && !cs->have_muted[1])
-    return;
-  uns mute = !cs->have_muted[1];
+      double b, t;
+      if (sscanf(val, "%lf %lf", &b, &t) != 2)
+       return;
 
-  HASH_FOR_ALL(pulse_sink_input, s)
-    {
-      if (s->noct_client_idx == i)
+      timestamp_t now = main_get_now();
+      if (!lights_last_update[ch] || lights_last_update[ch] + 1000 < now)
        {
-         DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
-         PULSE_ASYNC_RUN(pa_context_set_sink_input_mute, pulse_ctx, s->idx, mute, pulse_success_cb);
+         DBG("Received foreign light settings");
+         if (!b)
+           lights_on[ch] = 0;
+         else
+           {
+             lights_on[ch] = 1;
+             lights_brightness[ch] = b;
+             lights_temperature[ch] = t;
+           }
+         update_lights();
        }
     }
-  HASH_END_FOR;
-}
 
-void notify_button(int button, int on)
-{
-  if (pulse_state != PS_ONLINE)
+  if (!strcmp(topic, "burrow/lights/rainbow/brightness"))
     {
-      DBG("## NOTIFY: Pulse is not online");
-      return;
+      double b;
+      if (sscanf(val, "%lf", &b) == 1 && b >= 0 && b <= 1)
+       {
+         timestamp_t now = main_get_now();
+         if (!rainbow_last_update || rainbow_last_update + 1000 < now)
+           {
+             DBG("Received foreign rainbow settings");
+             rainbow_brightness = b;
+             update_rainbow();
+           }
+       }
     }
 
-  switch (button)
-    {
-    case 0:
-      update_sink_mute_from_button(on, "ursarium");
-      break;
-    case 1:
-      update_sink_mute_from_button(on, "catarium");
-      break;
-    default:
-      update_client_from_button(button, on);
-    }
+  if (!strcmp(topic, "burrow/control/catarium-ir"))
+    notify_ir(val);
 }
 
-int main(int argc UNUSED, char **argv)
+/*** Main entry point ***/
+
+static int debug;
+static int no_fork;
+
+static struct opt_section options = {
+  OPT_ITEMS {
+    OPT_HELP("Control console for the Ursary"),
+    OPT_HELP(""),
+    OPT_HELP("Options:"),
+    OPT_HELP_OPTION,
+    OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"),
+    OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"),
+    OPT_END
+  }
+};
+
+static void sigterm_handler(struct main_signal *ms UNUSED)
+{
+  main_shut_down();
+}
+
+static void daemon_body(struct daemon_params *dp)
 {
-  log_init(argv[0]);
   main_init();
   update_timer.handler = do_update;
 
+  usb_init();
   noct_init();
-
-  msg(L_INFO, "Initializing PulseAudio");
   pulse_init();
+  mpd_init();
+  mqtt_init();
+
+  static struct main_signal term_sig = {
+    .signum = SIGTERM,
+    .handler = sigterm_handler,
+  };
+  signal_add(&term_sig);
 
-  msg(L_INFO, "Entering main loop");
+  msg(L_INFO, "Ursary daemon starting");
   main_loop();
+  msg(L_INFO, "Ursary daemon shut down");
+  daemon_exit(dp);
+}
+
+int main(int argc UNUSED, char **argv)
+{
+  opt_parse(&options, argv+1);
+  unsetenv("DISPLAY");
+  unsetenv("HOME");
+
+  struct daemon_params dp = {
+    .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0),
+    .pid_file = "/run/ursaryd.pid",
+    .run_as_user = "ursary",
+  };
+  daemon_init(&dp);
+
+  log_init(argv[0]);
+  if (!debug)
+    {
+      struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
+      ls->levels = ~(1U << L_DEBUG);
+      log_set_default_stream(ls);
+    }
 
+  daemon_run(&dp, daemon_body);
   return 0;
 }