From 8146e336d7057d2064ed43e6684a11f601c84f47 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Fri, 22 Jan 2016 08:56:34 +0100
Subject: Add fancontrol-ng
---
CMakeLists.txt | 15 +-
README.adoc | 5 +-
fancontrol-ng.c | 680 +++++++++++++++++++++++++++++++++++++++++++++
fancontrol-ng.conf.example | 15 +
fancontrol-ng.service.in | 9 +
liberty | 2 +-
6 files changed, 722 insertions(+), 4 deletions(-)
create mode 100644 fancontrol-ng.c
create mode 100644 fancontrol-ng.conf.example
create mode 100644 fancontrol-ng.service.in
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7f6614b..75fdd8a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,11 +40,22 @@ add_threads (dwmstatus)
add_executable (brightness brightness.c)
target_link_libraries (brightness ${project_libraries})
-add_threads (brightness)
+
+add_executable (fancontrol-ng fancontrol-ng.c)
+target_link_libraries (fancontrol-ng ${project_libraries})
# The files to be installed
include (GNUInstallDirs)
-install (TARGETS dwmstatus brightness DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+configure_file (${PROJECT_SOURCE_DIR}/fancontrol-ng.service.in
+ ${PROJECT_BINARY_DIR}/fancontrol-ng.service @ONLY)
+install (FILES ${PROJECT_BINARY_DIR}/fancontrol-ng.service
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system)
+install (FILES fancontrol-ng.conf.example
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/fancontrol-ng)
+
+install (TARGETS dwmstatus brightness fancontrol-ng
+ DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# CPack
diff --git a/README.adoc b/README.adoc
index b0f884b..26c99c1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -7,8 +7,11 @@ to other people as well.
- 'dwmstatus' does literally everything my dwm doesn't but I'd like it to. It
includes PulseAudio volume management and hand-written NUT and MPD clients,
- all in the name of liberation from GPL-licensed software of course
+ all in the name of liberation from GPL-licensed software of course.
- 'brightness' allows me to change the brightness of w/e display device I have.
+ - 'fancontrol-ng' is a clone of fancontrol that can handle errors on resume
+ from suspend instead of setting fans to maximum speed and quitting;
+ in general it doesn't handle everything the original does
Don't expect them to work under any OS that isn't Linux.
diff --git a/fancontrol-ng.c b/fancontrol-ng.c
new file mode 100644
index 0000000..d68f2f0
--- /dev/null
+++ b/fancontrol-ng.c
@@ -0,0 +1,680 @@
+/*
+ * fancontrol-ng.c: clone of fancontrol from lm_sensors
+ *
+ * Copyright (c) 2016, Přemysl Janouch
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#define LIBERTY_WANT_POLLER
+
+#include "config.h"
+#undef PROGRAM_NAME
+#define PROGRAM_NAME "fancontrol-ng"
+#include "liberty/liberty.c"
+
+/// Shorthand to set an error and return failure from the function
+#define FAIL(...) \
+ BLOCK_START \
+ error_set (e, __VA_ARGS__); \
+ return false; \
+ BLOCK_END
+
+// --- Main program ------------------------------------------------------------
+
+struct device
+{
+ LIST_HEADER (struct device)
+
+ struct app_context *ctx; ///< Application context
+ struct config_item *config; ///< Configuration root for the device
+ char *path; ///< Base path
+
+ struct poller_timer timer; ///< Refresh timer
+};
+
+struct app_context
+{
+ struct poller poller; ///< Poller
+ bool polling; ///< The event loop is running
+
+ struct config_item *config; ///< Program configuration
+ struct device *devices; ///< All devices
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+log_message_custom (void *user_data, const char *quote, const char *fmt,
+ va_list ap)
+{
+ (void) user_data;
+ FILE *stream = stdout;
+
+ // TODO: sd-daemon.h log level prefixes?
+ fputs (quote, stream);
+ vfprintf (stream, fmt, ap);
+ fputs ("\n", stream);
+}
+
+static char *
+read_file_cstr (const char *path, struct error **e)
+{
+ struct str s;
+ str_init (&s);
+ if (read_file (path, &s, e))
+ return str_steal (&s);
+ str_free (&s);
+ return NULL;
+}
+
+static int64_t
+read_file_unsigned (const char *path, struct error **e)
+{
+ char *s, *end;
+ if (!(s = read_file_cstr (path, e)))
+ return -1;
+ if ((end = strpbrk (s, "\r\n")))
+ *end = 0;
+
+ unsigned long num;
+ bool ok = xstrtoul (&num, s, 10);
+ free (s);
+
+ if (!ok || num > INT64_MAX)
+ {
+ error_set (e, "error reading `%s': %s", path, "invalid integer value");
+ return -1;
+ }
+ return num;
+}
+
+static bool
+write_file_printf (const char *path, struct error **e, const char *format, ...)
+ ATTRIBUTE_PRINTF (3, 4);
+
+static bool
+write_file_printf (const char *path, struct error **e, const char *format, ...)
+{
+ struct str s;
+ str_init (&s);
+
+ va_list ap;
+ va_start (ap, format);
+ str_append_vprintf (&s, format, ap);
+ va_end (ap);
+
+ bool success = write_file (path, s.str, s.len, e);
+ str_free (&s);
+ return success;
+}
+
+// --- Configuration -----------------------------------------------------------
+
+static bool
+config_validate_nonnegative (const struct config_item *item, struct error **e)
+{
+ if (item->type == CONFIG_ITEM_NULL)
+ return true;
+
+ hard_assert (item->type == CONFIG_ITEM_INTEGER);
+ if (item->value.integer >= 0)
+ return true;
+
+ error_set (e, "must be non-negative");
+ return false;
+}
+
+static struct config_schema g_config_device[] =
+{
+ { .name = "name",
+ .comment = "Device identifier",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "interval",
+ .comment = "Temperature checking interval",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "5" },
+ {}
+};
+
+static struct config_schema g_config_pwm[] =
+{
+ { .name = "temp",
+ .comment = "Path to temperature sensor output",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "min_temp",
+ .comment = "Temperature for no fan operation",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "40" },
+ { .name = "max_temp",
+ .comment = "Temperature for maximum fan operation",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "80" },
+ { .name = "min_start",
+ .comment = "Minimum value for the fan to start spinning",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "0" },
+ { .name = "min_stop",
+ .comment = "Mimimum value for the fan to stop spinning",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative,
+ .default_ = "0" },
+ { .name = "pwm_min",
+ .comment = "Minimum PWM value to use",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative },
+ { .name = "pwm_max",
+ .comment = "Maximum PWM value to use",
+ .type = CONFIG_ITEM_INTEGER,
+ .validate = config_validate_nonnegative },
+ {}
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int64_t
+get_config_integer (struct config_item *root, const char *key)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
+ return item->value.integer;
+}
+
+static const char *
+get_config_string (struct config_item *root, const char *key)
+{
+ struct config_item *item = config_item_get (root, key, NULL);
+ hard_assert (item);
+ if (item->type == CONFIG_ITEM_NULL)
+ return NULL;
+ hard_assert (config_item_type_is_string (item->type));
+ return item->value.string.str;
+}
+
+// --- Fan control -------------------------------------------------------------
+
+// Consider this a failed attempt to avoid creating special PWM objects
+// based on the configuration. The complexity just moved somewhere else.
+
+struct paths
+{
+ char *temp; ///< Current temperature
+ char *pwm; ///< Current PWM value
+ char *pwm_enable; ///< PWM control state
+ char *pwm_min; ///< Minimum PWM value
+ char *pwm_max; ///< Maximum PWM value
+};
+
+static struct paths *
+paths_new (const char *device_path, const char *path, struct config_item *pwm)
+{
+ struct paths *self = xcalloc (1, sizeof *self);
+ self->temp = xstrdup_printf
+ ("%s/%s", device_path, get_config_string (pwm, "temp"));
+
+ self->pwm = xstrdup_printf ("%s/%s", device_path, path);
+ self->pwm_enable = xstrdup_printf ("%s/%s_enable", device_path, path);
+ self->pwm_min = xstrdup_printf ("%s/%s_min", device_path, path);
+ self->pwm_max = xstrdup_printf ("%s/%s_max", device_path, path);
+ return self;
+}
+
+static void
+paths_destroy (struct paths *self)
+{
+ free (self->temp);
+
+ free (self->pwm);
+ free (self->pwm_enable);
+ free (self->pwm_min);
+ free (self->pwm_max);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+pwm_update (struct paths *paths, struct config_item *pwm, struct error **e)
+{
+ int64_t cur_enable, cur_temp, cur_pwm, pwm_min, pwm_max;
+ if ((cur_enable = read_file_unsigned (paths->pwm_enable, e)) < 0
+ || (cur_temp = read_file_unsigned (paths->temp, e)) < 0
+ || (cur_pwm = read_file_unsigned (paths->pwm, e)) < 0)
+ return false;
+
+ struct config_item *pwm_min_item = config_item_get (pwm, "pwm_min", NULL);
+ if (pwm_min_item->type == CONFIG_ITEM_INTEGER)
+ pwm_min = pwm_min_item->value.integer;
+ else if ((pwm_min = read_file_unsigned (paths->pwm_min, NULL)) < 0)
+ pwm_min = 0;
+
+ struct config_item *pwm_max_item = config_item_get (pwm, "pwm_max", NULL);
+ if (pwm_max_item->type == CONFIG_ITEM_INTEGER)
+ pwm_max = pwm_max_item->value.integer;
+ else if ((pwm_max = read_file_unsigned (paths->pwm_max, NULL)) < 0)
+ pwm_max = 255;
+
+ int64_t min_temp = get_config_integer (pwm, "min_temp");
+ int64_t max_temp = get_config_integer (pwm, "max_temp");
+ int64_t min_start = get_config_integer (pwm, "min_start");
+ int64_t min_stop = get_config_integer (pwm, "min_stop");
+
+ if (min_temp >= max_temp) FAIL ("min_temp must be less than max_temp");
+ if (pwm_max > 255) FAIL ("pwm_max must be at most 255");
+ if (min_stop >= pwm_max) FAIL ("min_stop must be less than pwm_max");
+ if (min_stop < pwm_min) FAIL ("min_stop must be at least pwm_min");
+
+ // I'm not sure if this strangely complicated computation is justifiable
+ double where
+ = ((double) cur_temp / 1000 - min_temp)
+ / ((double) max_temp - min_temp);
+
+ int64_t new_pwm;
+ if (where <= 0) new_pwm = pwm_min;
+ else if (where >= 1) new_pwm = pwm_max;
+ else
+ {
+ new_pwm = min_stop + where * (pwm_max - min_stop);
+
+ // If needed, we start the fan until next iteration
+ if (cur_pwm <= min_stop)
+ new_pwm = MAX (new_pwm, min_start);
+ }
+
+ new_pwm = MAX (new_pwm, pwm_min);
+ new_pwm = MIN (new_pwm, pwm_max);
+
+ if (cur_enable != 1 && !write_file_printf (paths->pwm_enable, e, "1"))
+ return false;
+ if (!write_file_printf (paths->pwm, e, "%" PRId64, new_pwm))
+ return false;
+ return true;
+}
+
+static bool
+pwm_set_enable (struct paths *paths, char value)
+{
+ struct error *e = NULL;
+ if (write_file (paths->pwm_enable, &value, 1, &e))
+ return true;
+
+ print_error ("failed to change PWM mode to %c: %s",
+ value, e->message);
+ error_free (e);
+ return false;
+}
+
+static bool
+pwm_give_up (struct paths *paths)
+{
+ // Try automatic control, and if that fails, go full speed
+ return pwm_set_enable (paths, '2') || pwm_set_enable (paths, '0');
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct pwm_iter
+{
+ struct str_map_iter object_iter; ///< Configuration iterator
+ struct device *device; ///< Device
+
+ struct config_item *pwm; ///< PWM
+ const char *pwm_path; ///< PWM path
+ struct paths *paths; ///< Paths
+};
+
+static void
+pwm_iter_init (struct pwm_iter *self, struct device *device)
+{
+ str_map_iter_init (&self->object_iter,
+ &config_item_get (device->config, "pwms", NULL)->value.object);
+ self->device = device;
+ self->paths = NULL;
+}
+
+static void
+pwm_iter_free (struct pwm_iter *self)
+{
+ if (self->paths)
+ {
+ paths_destroy (self->paths);
+ self->paths = NULL;
+ }
+}
+
+static bool
+pwm_iter_next (struct pwm_iter *self)
+{
+ pwm_iter_free (self);
+ if (!(self->pwm = str_map_iter_next (&self->object_iter)))
+ return false;
+
+ self->pwm_path = self->object_iter.link->key;
+ self->paths = paths_new (self->device->path, self->pwm_path, self->pwm);
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+device_run (struct device *self)
+{
+ struct pwm_iter iter;
+ pwm_iter_init (&iter, self);
+
+ while (pwm_iter_next (&iter))
+ {
+ struct error *e = NULL;
+ if (pwm_update (iter.paths, iter.pwm, &e))
+ continue;
+
+ print_error ("pwm `%s': %s", iter.pwm_path, e->message);
+ error_free (e);
+ pwm_give_up (iter.paths);
+ }
+
+ pwm_iter_free (&iter);
+
+ poller_timer_set (&self->timer,
+ 1000 * get_config_integer (self->config, "interval"));
+}
+
+static void
+device_stop (struct device *self)
+{
+ struct pwm_iter iter;
+ pwm_iter_init (&iter, self);
+
+ while (pwm_iter_next (&iter))
+ pwm_give_up (iter.paths);
+
+ pwm_iter_free (&iter);
+}
+
+static void
+device_create (struct app_context *ctx, const char *path,
+ struct config_item *root)
+{
+ struct device *self = xcalloc (1, sizeof *self);
+ self->config = root;
+ self->path = xstrdup (path);
+
+ poller_timer_init (&self->timer, &ctx->poller);
+ self->timer.dispatcher = (poller_timer_fn) device_run;
+ self->timer.user_data = self;
+
+ LIST_PREPEND (ctx->devices, self);
+}
+
+// --- Configuration -----------------------------------------------------------
+
+// TODO: consider moving to liberty,
+// degesch and json-rpc-shell have exactly the same function
+static struct config_item *
+load_configuration_file (const char *filename, struct error **e)
+{
+ struct config_item *root = NULL;
+
+ struct str data;
+ str_init (&data);
+ if (!read_file (filename, &data, e))
+ goto end;
+
+ struct error *error = NULL;
+ if (!(root = config_item_parse (data.str, data.len, false, &error)))
+ {
+ error_set (e, "parse error: %s", error->message);
+ error_free (error);
+ }
+end:
+ str_free (&data);
+ return root;
+}
+
+// There is no room for errors in the configuration, everything must be valid.
+// Thus the reset to defaults on invalid values is effectively disabled here.
+static bool
+apply_schema (struct config_schema *schema, struct config_item *object,
+ struct error **e)
+{
+ struct error *warning = NULL, *error = NULL;
+ config_schema_initialize_item (schema, object, NULL, &warning, &error);
+
+ if (error && warning)
+ {
+ error_free (warning);
+ error_propagate (e, error);
+ return false;
+ }
+ if (error)
+ {
+ error_propagate (e, error);
+ return false;
+ }
+ if (warning)
+ {
+ // The standard warning is inappropriate here
+ error_free (warning);
+ FAIL ("invalid item `%s'", schema->name);
+ }
+ return true;
+}
+
+static bool
+check_device_configuration (struct config_item *subtree, struct error **e)
+{
+ // Check regular fields in the device object
+ for (struct config_schema *s = g_config_device; s->name; s++)
+ if (!apply_schema (s, subtree, e))
+ return false;
+
+ // Check for a subobject with PWMs to control
+ struct config_item *pwms;
+ if (!(pwms = config_item_get (subtree, "pwms", e)))
+ return false;
+ if (pwms->type != CONFIG_ITEM_OBJECT)
+ FAIL ("`%s' is not an object", "pwms");
+ if (!pwms->value.object.len)
+ FAIL ("no PWMs defined");
+
+ // Check regular fields in all PWM subobjects
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &pwms->value.object);
+
+ struct config_item *pwm;
+ struct error *error = NULL;
+ while ((pwm = str_map_iter_next (&iter)))
+ {
+ const char *subpath = iter.link->key;
+ for (struct config_schema *s = g_config_pwm; s->name; s++)
+ if (!apply_schema (s, pwm, &error))
+ {
+ error_set (e, "PWM `%s': %s", subpath, error->message);
+ error_free (error);
+ return false;
+ }
+ if (!get_config_string (pwm, "temp"))
+ FAIL ("PWM `%s': %s", subpath, "`temp' cannot be null");
+ }
+ return true;
+}
+
+static void
+load_configuration (struct app_context *ctx, const char *config_path)
+{
+ struct error *e = NULL;
+ struct config_item *root = load_configuration_file (config_path, &e);
+
+ if (e)
+ {
+ print_error ("error loading configuration: %s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &(ctx->config = root)->value.object);
+
+ struct config_item *subtree;
+ while ((subtree = str_map_iter_next (&iter)))
+ {
+ const char *path = iter.link->key;
+ if (subtree->type != CONFIG_ITEM_OBJECT)
+ print_fatal ("device `%s' in configuration is not an object", path);
+ else if (!check_device_configuration (subtree, &e))
+ print_fatal ("device `%s': %s", path, e->message);
+ else
+ device_create (ctx, path, subtree);
+ }
+}
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+static void
+sigterm_handler (int signum)
+{
+ (void) signum;
+
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], "", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+setup_signal_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ (void) signal (SIGPIPE, SIG_IGN);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = sigterm_handler;
+ sigemptyset (&sa.sa_mask);
+
+ if (sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+}
+
+// --- Main program ------------------------------------------------------------
+
+static void
+on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
+{
+ char id = 0;
+ (void) read (fd->fd, &id, 1);
+
+ ctx->polling = false;
+}
+
+static const char *
+parse_program_arguments (int argc, char **argv)
+{
+ static const struct opt opts[] =
+ {
+ { 'd', "debug", NULL, 0, "run in debug mode" },
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh;
+ opt_handler_init (&oh, argc, argv, opts, "CONFIG", "Fan controller.");
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'd':
+ g_debug_mode = true;
+ break;
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 1)
+ {
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ opt_handler_free (&oh);
+ return argv[0];
+}
+
+int
+main (int argc, char *argv[])
+{
+ g_log_message_real = log_message_custom;
+ const char *config_path = parse_program_arguments (argc, argv);
+
+ struct app_context ctx;
+ memset (&ctx, 0, sizeof ctx);
+ poller_init (&ctx.poller);
+
+ setup_signal_handlers ();
+
+ struct poller_fd signal_event;
+ poller_fd_init (&signal_event, &ctx.poller, g_signal_pipe[0]);
+ signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
+ signal_event.user_data = &ctx;
+ poller_fd_set (&signal_event, POLLIN);
+
+ load_configuration (&ctx, config_path);
+
+ if (!ctx.devices)
+ exit_fatal ("no devices present in configuration");
+ LIST_FOR_EACH (struct device, iter, ctx.devices)
+ device_run (iter);
+
+ ctx.polling = true;
+ while (ctx.polling)
+ poller_run (&ctx.poller);
+
+ LIST_FOR_EACH (struct device, iter, ctx.devices)
+ device_stop (iter);
+
+ config_item_destroy (ctx.config);
+ poller_free (&ctx.poller);
+ return EXIT_SUCCESS;
+}
diff --git a/fancontrol-ng.conf.example b/fancontrol-ng.conf.example
new file mode 100644
index 0000000..c225669
--- /dev/null
+++ b/fancontrol-ng.conf.example
@@ -0,0 +1,15 @@
+"/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0" = {
+ name = "radeon"
+ interval = 10
+ pwms = {
+ "hwmon/hwmon2/pwm1" = {
+ temp = "hwmon/hwmon2/temp1_input"
+ min_temp = 40
+ max_temp = 80
+ min_start = 50
+ min_stop = 40
+# min_pwm = 0
+# max_pwm = 255
+ }
+ }
+}
diff --git a/fancontrol-ng.service.in b/fancontrol-ng.service.in
new file mode 100644
index 0000000..1a093a4
--- /dev/null
+++ b/fancontrol-ng.service.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=fancontrol-ng
+
+[Service]
+ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/fancontrol-ng @CMAKE_INSTALL_FULL_SYSCONFDIR@/fancontrol-ng.conf
+Restart=on-abort
+
+[Install]
+WantedBy=default.target
diff --git a/liberty b/liberty
index 9e3cb2b..052d2ff 160000
--- a/liberty
+++ b/liberty
@@ -1 +1 @@
-Subproject commit 9e3cb2b6aa2db3ca0ea6a854fb3f89a163c84235
+Subproject commit 052d2ffc9a3141ef2bb771f70190ed7a0bb9da44
--
cgit v1.2.3-70-g09d2