Add sensors plugin
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 25 May 2015 20:56:09 +0000 (20:56 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 25 May 2015 20:56:09 +0000 (20:56 +0000)
This plugin uses the lm_sensors library to monitor
all sorts of internal sensors of a system. Those
could be temperature sensors, fan sensors or
voltage sensors.

Makefile.am
configure.ac
src/_collectymodule.c
src/collecty/plugins/__init__.py
src/collecty/plugins/sensors.py [new file with mode: 0644]

index b9aa841..98ac26d 100644 (file)
@@ -93,7 +93,8 @@ collectyplugins_PYTHON = \
        src/collecty/plugins/interface.py \
        src/collecty/plugins/latency.py \
        src/collecty/plugins/loadavg.py \
-       src/collecty/plugins/memory.py
+       src/collecty/plugins/memory.py \
+       src/collecty/plugins/sensors.py
 
 collectypluginsdir = $(collectydir)/plugins
 
@@ -116,7 +117,8 @@ _collecty_la_LDFLAGS = \
 
 _collecty_la_LIBADD = \
        $(LIBATASMART_LIBS) \
-       $(PYTHON_DEVEL_LIBS)
+       $(PYTHON_DEVEL_LIBS) \
+       $(SENSORS_LIBS)
 
 dist_dbuspolicy_DATA = \
        src/dbus/org.ipfire.collecty1.conf
index 53616a0..8980c58 100644 (file)
@@ -70,6 +70,16 @@ PKG_CHECK_MODULES([LIBATASMART], [libatasmart >= 0.19])
 
 save_LIBS="$LIBS"
 
+# lm-sensors
+AC_CHECK_HEADERS([sensors/sensors.h sensors/errors.h])
+
+LIBS=
+AC_CHECK_LIB(sensors, sensors_init, [], [AC_MSG_ERROR([*** sensors library not found])])
+SENSORS_LIBS="$LIBS"
+AC_SUBST(SENSORS_LIBS)
+
+LIBS="$save_LIBS"
+
 # pkg-config
 PKG_PROG_PKG_CONFIG
 # This makes sure pkg.m4 is available.
index eefe871..38ade7d 100644 (file)
@@ -22,6 +22,8 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/hdreg.h>
+#include <sensors/error.h>
+#include <sensors/sensors.h>
 #include <stdbool.h>
 #include <string.h>
 #include <sys/ioctl.h>
@@ -311,7 +313,418 @@ static PyTypeObject BlockDeviceType = {
        BlockDevice_new,                    /* tp_new */
 };
 
+typedef struct {
+       PyObject_HEAD
+       const sensors_chip_name* chip;
+       const sensors_feature* feature;
+} SensorObject;
+
+static void Sensor_dealloc(SensorObject* self) {
+       self->ob_type->tp_free((PyObject*)self);
+}
+
+static PyObject* Sensor_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
+       SensorObject* self = (SensorObject*)type->tp_alloc(type, 0);
+
+       return (PyObject *)self;
+}
+
+static int Sensor_init(SensorObject* self, PyObject* args, PyObject* kwds) {
+       return 0;
+}
+
+static PyObject* Sensor_get_label(SensorObject* self) {
+       char* label = sensors_get_label(self->chip, self->feature);
+
+       if (label) {
+               PyObject* string = PyString_FromString(label);
+               free(label);
+
+               return string;
+       }
+
+       Py_RETURN_NONE;
+}
+
+static PyObject* Sensor_get_name(SensorObject* self) {
+       char chip_name[512];
+
+       int r = sensors_snprintf_chip_name(chip_name, sizeof(chip_name), self->chip);
+       if (r < 0) {
+               PyErr_Format(PyExc_RuntimeError, "Could not print chip name");
+               return NULL;
+       }
+
+       return PyString_FromString(chip_name);
+}
+
+static PyObject* Sensor_get_type(SensorObject* self) {
+       const char* type = NULL;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_IN:
+                       type = "voltage";
+                       break;
+
+               case SENSORS_FEATURE_FAN:
+                       type = "fan";
+                       break;
+
+               case SENSORS_FEATURE_TEMP:
+                       type = "temperature";
+                       break;
+
+               case SENSORS_FEATURE_POWER:
+                       type = "power";
+                       break;
+
+               default:
+                       break;
+       }
+
+       if (type)
+               return PyString_FromString(type);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject* Sensor_get_bus(SensorObject* self) {
+       const char* type = NULL;
+
+       switch (self->chip->bus.type) {
+               case SENSORS_BUS_TYPE_I2C:
+                       type = "i2c";
+                       break;
+
+               case SENSORS_BUS_TYPE_ISA:
+                       type = "isa";
+                       break;
+
+               case SENSORS_BUS_TYPE_PCI:
+                       type = "pci";
+                       break;
+
+               case SENSORS_BUS_TYPE_SPI:
+                       type = "spi";
+                       break;
+
+               case SENSORS_BUS_TYPE_VIRTUAL:
+                       type = "virtual";
+                       break;
+
+               case SENSORS_BUS_TYPE_ACPI:
+                       type = "acpi";
+                       break;
+
+               case SENSORS_BUS_TYPE_HID:
+                       type = "hid";
+                       break;
+
+               default:
+                       break;
+       }
+
+       if (type)
+               return PyString_FromString(type);
+
+       Py_RETURN_NONE;
+}
+
+static const sensors_subfeature* Sensor_get_subfeature(SensorObject* sensor, sensors_subfeature_type type) {
+       const sensors_subfeature* subfeature;
+       int subfeature_num = 0;
+
+       while ((subfeature = sensors_get_all_subfeatures(sensor->chip, sensor->feature, &subfeature_num))) {
+               if (subfeature->type == type)
+                       break;
+       }
+
+       return subfeature;
+}
+
+static PyObject* Sensor_return_value(SensorObject* sensor, sensors_subfeature_type subfeature_type) {
+       double value;
+
+       const sensors_subfeature* subfeature = Sensor_get_subfeature(sensor, subfeature_type);
+       if (!subfeature) {
+               PyErr_Format(PyExc_AttributeError, "Could not find sensor of requested type");
+               return NULL;
+       }
+
+       // Fetch value from the sensor
+       int r = sensors_get_value(sensor->chip, subfeature->number, &value);
+       if (r < 0) {
+               PyErr_Format(PyExc_ValueError, "Error retrieving value from sensor: %s",
+                       sensors_strerror(errno));
+               return NULL;
+       }
+
+       // Convert all temperature values from Celcius to Kelvon
+       if (sensor->feature->type == SENSORS_FEATURE_TEMP)
+               value += 273.15;
+
+       return PyFloat_FromDouble(value);
+}
+
+static PyObject* Sensor_no_value() {
+       PyErr_Format(PyExc_ValueError, "Value not supported for this sensor type");
+       return NULL;
+}
+
+static PyObject* Sensor_get_value(SensorObject* self) {
+       sensors_subfeature_type subfeature_type;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_IN:
+                       subfeature_type = SENSORS_SUBFEATURE_IN_INPUT;
+                       break;
+
+               case SENSORS_FEATURE_FAN:
+                       subfeature_type = SENSORS_SUBFEATURE_FAN_INPUT;
+                       break;
+
+               case SENSORS_FEATURE_TEMP:
+                       subfeature_type = SENSORS_SUBFEATURE_TEMP_INPUT;
+                       break;
+
+               case SENSORS_FEATURE_POWER:
+                       subfeature_type = SENSORS_SUBFEATURE_POWER_INPUT;
+                       break;
+
+               default:
+                       return Sensor_no_value();
+       }
+
+       return Sensor_return_value(self, subfeature_type);
+}
+
+static PyObject* Sensor_get_critical(SensorObject* self) {
+       sensors_subfeature_type subfeature_type;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_IN:
+                       subfeature_type = SENSORS_SUBFEATURE_IN_CRIT;
+                       break;
+
+               case SENSORS_FEATURE_TEMP:
+                       subfeature_type = SENSORS_SUBFEATURE_TEMP_CRIT;
+                       break;
+
+               case SENSORS_FEATURE_POWER:
+                       subfeature_type = SENSORS_SUBFEATURE_POWER_CRIT;
+                       break;
+
+               default:
+                       return Sensor_no_value();
+       }
+
+       return Sensor_return_value(self, subfeature_type);
+}
+
+static PyObject* Sensor_get_maximum(SensorObject* self) {
+       sensors_subfeature_type subfeature_type;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_IN:
+                       subfeature_type = SENSORS_SUBFEATURE_IN_MAX;
+                       break;
+
+               case SENSORS_FEATURE_FAN:
+                       subfeature_type = SENSORS_SUBFEATURE_FAN_MAX;
+                       break;
+
+               case SENSORS_FEATURE_TEMP:
+                       subfeature_type = SENSORS_SUBFEATURE_TEMP_MAX;
+                       break;
+
+               case SENSORS_FEATURE_POWER:
+                       subfeature_type = SENSORS_SUBFEATURE_POWER_MAX;
+                       break;
+
+               default:
+                       return Sensor_no_value();
+       }
+
+       return Sensor_return_value(self, subfeature_type);
+}
+
+static PyObject* Sensor_get_minimum(SensorObject* self) {
+       sensors_subfeature_type subfeature_type;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_IN:
+                       subfeature_type = SENSORS_SUBFEATURE_IN_MIN;
+                       break;
+
+               case SENSORS_FEATURE_FAN:
+                       subfeature_type = SENSORS_SUBFEATURE_FAN_MIN;
+                       break;
+
+               case SENSORS_FEATURE_TEMP:
+                       subfeature_type = SENSORS_SUBFEATURE_TEMP_MIN;
+                       break;
+
+               default:
+                       return Sensor_no_value();
+       }
+
+       return Sensor_return_value(self, subfeature_type);
+}
+
+static PyObject* Sensor_get_high(SensorObject* self) {
+       sensors_subfeature_type subfeature_type;
+
+       switch (self->feature->type) {
+               case SENSORS_FEATURE_TEMP:
+                       subfeature_type = SENSORS_SUBFEATURE_TEMP_MAX;
+                       break;
+
+               default:
+                       return Sensor_no_value();
+       }
+
+       return Sensor_return_value(self, subfeature_type);
+}
+
+static PyGetSetDef Sensor_getsetters[] = {
+       {"bus", (getter)Sensor_get_bus, NULL, NULL, NULL},
+       {"critical", (getter)Sensor_get_critical, NULL, NULL, NULL},
+       {"high", (getter)Sensor_get_high, NULL, NULL, NULL},
+       {"label", (getter)Sensor_get_label, NULL, NULL, NULL},
+       {"maximum", (getter)Sensor_get_maximum, NULL, NULL, NULL},
+       {"minumum", (getter)Sensor_get_minimum, NULL, NULL, NULL},
+       {"name", (getter)Sensor_get_name, NULL, NULL, NULL},
+       {"type", (getter)Sensor_get_type, NULL, NULL, NULL},
+       {"value", (getter)Sensor_get_value, NULL, NULL, NULL},
+       {NULL},
+};
+
+static PyTypeObject SensorType = {
+       PyObject_HEAD_INIT(NULL)
+       0,                                  /*ob_size*/
+       "_collecty.Sensor",                 /*tp_name*/
+       sizeof(SensorObject),               /*tp_basicsize*/
+       0,                                  /*tp_itemsize*/
+       (destructor)Sensor_dealloc,         /*tp_dealloc*/
+       0,                                  /*tp_print*/
+       0,                                  /*tp_getattr*/
+       0,                                  /*tp_setattr*/
+       0,                                  /*tp_compare*/
+       0,                                  /*tp_repr*/
+       0,                                  /*tp_as_number*/
+       0,                                  /*tp_as_sequence*/
+       0,                                  /*tp_as_mapping*/
+       0,                                  /*tp_hash */
+       0,                                  /*tp_call*/
+       0,                                  /*tp_str*/
+       0,                                  /*tp_getattro*/
+       0,                                  /*tp_setattro*/
+       0,                                  /*tp_as_buffer*/
+       Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+       "Sensor objects",                   /* tp_doc */
+       0,                                  /* tp_traverse */
+       0,                                  /* tp_clear */
+       0,                                  /* tp_richcompare */
+       0,                                  /* tp_weaklistoffset */
+       0,                                  /* tp_iter */
+       0,                                  /* tp_iternext */
+       0,                                  /* tp_methods */
+       0,                                  /* tp_members */
+       Sensor_getsetters,                  /* tp_getset */
+       0,                                  /* tp_base */
+       0,                                  /* tp_dict */
+       0,                                  /* tp_descr_get */
+       0,                                  /* tp_descr_set */
+       0,                                  /* tp_dictoffset */
+       (initproc)Sensor_init,              /* tp_init */
+       0,                                  /* tp_alloc */
+       Sensor_new,                         /* tp_new */
+};
+
+static SensorObject* make_sensor_object(const sensors_chip_name* chip, const sensors_feature* feature) {
+       SensorObject* sensor = PyObject_New(SensorObject, &SensorType);
+       if (!sensor)
+               return NULL;
+
+       if (!PyObject_Init((PyObject*)sensor, &SensorType)) {
+               Py_DECREF(sensor);
+               return NULL;
+       }
+
+       sensor->chip = chip;
+       sensor->feature = feature;
+
+       return sensor;
+}
+
+static PyObject* _collecty_sensors_init() {
+       // Clean up everything first in case sensors_init was called earlier
+       sensors_cleanup();
+
+       int r = sensors_init(NULL);
+       if (r) {
+               PyErr_Format(PyExc_OSError, "Could not initialise sensors: %s",
+                       sensors_strerror(errno));
+               return NULL;
+       }
+
+       Py_RETURN_NONE;
+}
+
+static PyObject* _collecty_sensors_cleanup() {
+       sensors_cleanup();
+       Py_RETURN_NONE;
+}
+
+static PyObject* _collecty_get_detected_sensors(PyObject* o, PyObject* args) {
+       const char* name = NULL;
+       sensors_chip_name chip_name;
+
+       if (!PyArg_ParseTuple(args, "|z", &name))
+               return NULL;
+
+       if (name) {
+               int r = sensors_parse_chip_name(name, &chip_name);
+               if (r < 0) {
+                       PyErr_Format(PyExc_ValueError, "Could not parse chip name: %s", name);
+                       return NULL;
+               }
+       }
+
+       PyObject* list = PyList_New(0);
+
+       const sensors_chip_name* chip;
+       int chip_num = 0;
+
+       while ((chip = sensors_get_detected_chips((name) ? &chip_name : NULL, &chip_num))) {
+               const sensors_feature* feature;
+               int feature_num = 0;
+
+               while ((feature = sensors_get_features(chip, &feature_num))) {
+                       // Skip sensors we do not want to support
+                       switch (feature->type) {
+                               case SENSORS_FEATURE_IN:
+                               case SENSORS_FEATURE_FAN:
+                               case SENSORS_FEATURE_TEMP:
+                               case SENSORS_FEATURE_POWER:
+                                       break;
+
+                               default:
+                                       continue;
+                       }
+
+                       SensorObject* sensor = make_sensor_object(chip, feature);
+                       PyList_Append(list, (PyObject*)sensor);
+               }
+       }
+
+       return list;
+}
+
 static PyMethodDef collecty_module_methods[] = {
+       {"get_detected_sensors", (PyCFunction)_collecty_get_detected_sensors, METH_VARARGS, NULL},
+       {"sensors_cleanup", (PyCFunction)_collecty_sensors_cleanup, METH_NOARGS, NULL},
+       {"sensors_init", (PyCFunction)_collecty_sensors_init, METH_NOARGS, NULL},
        {NULL},
 };
 
@@ -319,7 +732,11 @@ void init_collecty(void) {
        if (PyType_Ready(&BlockDeviceType) < 0)
                return;
 
+       if (PyType_Ready(&SensorType) < 0)
+               return;
+
        PyObject* m = Py_InitModule("_collecty", collecty_module_methods);
 
        PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType);
+       PyModule_AddObject(m, "Sensor", (PyObject*)&SensorType);
 }
index 5f843a8..faa9348 100644 (file)
@@ -30,3 +30,4 @@ import interface
 import latency
 import loadavg
 import memory
+import sensors
diff --git a/src/collecty/plugins/sensors.py b/src/collecty/plugins/sensors.py
new file mode 100644 (file)
index 0000000..e4c247e
--- /dev/null
@@ -0,0 +1,343 @@
+#!/usr/bin/python
+# encoding: utf-8
+###############################################################################
+#                                                                             #
+# collecty - A system statistics collection daemon for IPFire                 #
+# Copyright (C) 2015 IPFire development team                                  #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+from collecty import _collecty
+import os
+import re
+
+import base
+
+from ..i18n import _
+
+class GraphTemplateSensorsTemperature(base.GraphTemplate):
+       name = "sensors-temperature"
+
+       rrd_graph = [
+               "DEF:value_kelvin=%(file)s:value:AVERAGE",
+               "DEF:critical_kelvin=%(file)s:critical:AVERAGE",
+               "DEF:high_kelvin=%(file)s:high:AVERAGE",
+               "DEF:low_kelvin=%(file)s:low:AVERAGE",
+
+               # Convert everything to celsius
+               "CDEF:value=value_kelvin,273.15,-",
+               "CDEF:critical=critical_kelvin,273.15,-",
+               "CDEF:high=high_kelvin,273.15,-",
+               "CDEF:low=low_kelvin,273.15,-",
+
+               # Change colour when the value gets above high
+               "CDEF:value_high=value,high,GT,value,UNKN,IF",
+               "CDEF:value_normal=value,high,GT,UNKN,value,IF",
+
+               "VDEF:value_cur=value,LAST",
+               "VDEF:value_avg=value,AVERAGE",
+               "VDEF:value_max=value,MAXIMUM",
+               "VDEF:value_min=value,MINIMUM",
+
+               # Get data points for the threshold lines
+               "VDEF:critical_line=critical,MINIMUM",
+               "VDEF:low_line=low,MAXIMUM",
+
+               # Draw the temperature value
+               "LINE3:value_high#ff0000",
+               "LINE2:value_normal#00ff00:%-15s" % _("Temperature"),
+
+               # Draw the legend
+               "GPRINT:value_cur:%%10.2lf °C\l",
+               "GPRINT:value_avg:  %-15s %%6.2lf °C\l" % _("Average"),
+               "GPRINT:value_max:  %-15s %%6.2lf °C\l" % _("Maximum"),
+               "GPRINT:value_min:  %-15s %%6.2lf °C\l" % _("Minimum"),
+
+               # Empty line
+               "COMMENT: \\n",
+
+               # Draw boundary lines
+               "COMMENT:%s\:" % _("Temperature Thresholds"),
+               "HRULE:critical_line#000000:%-15s" % _("Critical"),
+               "GPRINT:critical_line:%%6.2lf °C\\r",
+               "HRULE:low_line#0000ff:%-15s" % _("Low"),
+               "GPRINT:low_line:%%6.2lf °C\\r",
+       ]
+
+       @property
+       def graph_title(self):
+               return _("Temperature (%s)") % self.object.sensor.name
+
+       @property
+       def graph_vertical_label(self):
+               return _("° Celsius")
+
+
+class GraphTemplateSensorsProcessorTemperature(base.GraphTemplate):
+       name = "processor-temperature"
+
+       core_colours = {
+               "core0" : "#ff000033",
+               "core1" : "#0000ff33",
+               "core2" : "#00ff0033",
+               "core3" : "#0000ff33",
+       }
+
+       def get_temperature_sensors(self):
+               return self.plugin.get_detected_sensor_objects("coretemp-*")
+
+       def get_object_table(self):
+               objects_table = {}
+
+               counter = 0
+               for object in self.get_temperature_sensors():
+                       objects_table["core%s" % counter] = object
+                       counter += 1
+
+               return objects_table
+
+       @property
+       def rrd_graph(self):
+               rrd_graph = []
+
+               cores = sorted(self.object_table.keys())
+
+               for core in cores:
+                       i = {
+                               "core" : core,
+                       }
+
+                       core_graph = [
+                               "DEF:value_kelvin_%(core)s=%%(%(core)s)s:value:AVERAGE",
+                               "DEF:critical_kelvin_%(core)s=%%(%(core)s)s:critical:AVERAGE",
+                               "DEF:high_kelvin_%(core)s=%%(%(core)s)s:high:AVERAGE",
+
+                               # Convert everything to celsius
+                               "CDEF:value_%(core)s=value_kelvin_%(core)s,273.15,-",
+                               "CDEF:critical_%(core)s=critical_kelvin_%(core)s,273.15,-",
+                               "CDEF:high_%(core)s=high_kelvin_%(core)s,273.15,-",
+                       ]
+
+                       rrd_graph += [line % i for line in core_graph]
+
+               all_core_values = ("value_%s" % c for c in cores)
+               all_core_highs  = ("high_%s"  % c for c in cores)
+
+               rrd_graph += [
+                       # Compute the temperature of the processor
+                       # by taking the average of all cores
+                       "CDEF:value=%s,%s,AVG" % (",".join(all_core_values), len(cores)),
+                       "CDEF:high=%s,MIN" % ",".join(all_core_highs),
+
+                       # Change colour when the value gets above high
+                       "CDEF:value_high=value,high,GT,value,UNKN,IF",
+                       "CDEF:value_normal=value,high,GT,UNKN,value,IF",
+
+                       "VDEF:value_avg=value,AVERAGE",
+                       "VDEF:value_max=value,MAXIMUM",
+                       "VDEF:value_min=value,MINIMUM",
+
+                       "LINE3:value_high#FF0000",
+                       "LINE3:value_normal#000000:%-15s\l" % _("Temperature"),
+
+                       "GPRINT:value_avg:    %-15s %%6.2lf °C\l" % _("Average"),
+                       "GPRINT:value_max:    %-15s %%6.2lf °C\l" % _("Maximum"),
+                       "GPRINT:value_min:    %-15s %%6.2lf °C\l" % _("Minimum"),
+               ]
+
+               for core in cores:
+                       object = self.object_table.get(core)
+
+                       i = {
+                               "colour" : self.core_colours.get(core, "#000000"),
+                               "core"   : core,
+                               "label"  : object.sensor.label,
+                       }
+
+                       core_graph = [
+                               # TODO these lines were supposed to be dashed, but that
+                               # didn't really work here
+                               "LINE2:value_%(core)s%(colour)s:%(label)-10s",
+                       ]
+
+                       rrd_graph += [line % i for line in core_graph]
+
+               # Draw the critical line
+               rrd_graph += [
+                       "VDEF:critical_line=critical_core0,MINIMUM",
+                       "HRULE:critical_line#000000:%-15s" % _("Critical"),
+                       "GPRINT:critical_line:%%6.2lf °C\\r",
+               ]
+
+               return rrd_graph
+
+       @property
+       def graph_title(self):
+               return _("Processor")
+
+       @property
+       def graph_vertical_label(self):
+               return _("Temperature")
+
+
+class SensorBaseObject(base.Object):
+       def init(self, sensor):
+               self.sensor = sensor
+
+       def __repr__(self):
+               return "<%s %s (%s)>" % (self.__class__.__name__, self.sensor.name, self.sensor.label)
+
+       @property
+       def id(self):
+               return "-".join((self.sensor.name, self.sensor.label))
+
+       @property
+       def type(self):
+               return self.sensor.type
+
+
+class SensorTemperatureObject(SensorBaseObject):
+       rrd_schema = [
+               "DS:value:GAUGE:0:U",
+               "DS:critical:GAUGE:0:U",
+               "DS:low:GAUGE:0:U",
+               "DS:high:GAUGE:0:U",
+       ]
+
+       def collect(self):
+               assert self.type == "temperature"
+
+               return (
+                       self.sensor.value,
+                       self.critical,
+                       self.low,
+                       self.high,
+               )
+
+       @property
+       def critical(self):
+               try:
+                       return self.sensor.critical
+               except AttributeError:
+                       return "NaN"
+
+       @property
+       def low(self):
+               try:
+                       return self.sensor.minimum
+               except AttributeError:
+                       return "NaN"
+
+       @property
+       def high(self):
+               try:
+                       return self.sensor.high
+               except AttributeError:
+                       return "NaN"
+
+
+class SensorVoltageObject(SensorBaseObject):
+       rrd_schema = [
+               "DS:value:GAUGE:0:U",
+               "DS:minimum:GAUGE:0:U",
+               "DS:maximum:GAUGE:0:U",
+       ]
+
+       def collect(self):
+               assert self.type == "voltage"
+
+               return (
+                       self.sensor.value,
+                       self.minimum,
+                       self.maximum,
+               )
+
+       @property
+       def minimum(self):
+               try:
+                       return self.sensor.minimum
+               except AttributeError:
+                       return "NaN"
+
+       @property
+       def maximum(self):
+               try:
+                       return self.sensor.maximum
+               except AttributeError:
+                       return "NaN"
+
+
+class SensorFanObject(SensorBaseObject):
+       rrd_schema = [
+               "DS:value:GAUGE:0:U",
+               "DS:minimum:GAUGE:0:U",
+               "DS:maximum:GAUGE:0:U",
+       ]
+
+       def collect(self):
+               assert self.type == "fan"
+
+               return (
+                       self.sensor.value,
+                       self.minimum,
+                       self.maximum,
+               )
+
+       @property
+       def mimimum(self):
+               try:
+                       return self.sensor.minimum
+               except AttributeError:
+                       return "NaN"
+
+       @property
+       def maximum(self):
+               try:
+                       return self.sensor.maximum
+               except AttributeError:
+                       return "NaN"
+
+
+class SensorsPlugin(base.Plugin):
+       name = "sensors"
+       description = "Sensors Plugin"
+
+       templates = [
+               GraphTemplateSensorsProcessorTemperature,
+               GraphTemplateSensorsTemperature,
+       ]
+
+       def init(self):
+               # Initialise the sensors library.
+               _collecty.sensors_init()
+
+       def __del__(self):
+               _collecty.sensors_cleanup()
+
+       @property
+       def objects(self):
+               return self.get_detected_sensor_objects()
+
+       def get_detected_sensor_objects(self, what=None):
+               for sensor in _collecty.get_detected_sensors(what):
+                       if sensor.type == "temperature":
+                               yield SensorTemperatureObject(self, sensor)
+
+                       elif sensor.type == "voltage":
+                               yield SensorVoltageObject(self, sensor)
+
+                       elif sensor.type == "fan":
+                               yield SensorFanObject(self, sensor)