├── .gitignore ├── LICENSE ├── README.md ├── cpu_temp.py ├── mcp3425.py ├── sht21.py └── shtc3.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Danilo Bargen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collectd Python Plugins 2 | 3 | This is a collections of Python plugin for Collectd. 4 | 5 | - `cpu_temp.py`: Report the CPU temperature. Tested on a Raspberry Pi 3. 6 | - `sht21.py`: Measure temperature and relative humidity from a Sensirion SHT21 7 | sensor connected via I²C. Calculate dew point and absolute humidity. Tested 8 | on a Raspberry Pi 3. 9 | - `shtc3.py`: Measure temperature and relative humidity from a Sensirion SHTC3 10 | sensor connected via I²C. Calculate dew point and absolute humidity. Tested 11 | on a Raspberry Pi 3. 12 | - `mcp3425.py`: Measure voltage using an MCP3425 analog-digital converter. 13 | 14 | For more information, please refer to [my 15 | blogpost](https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/). 16 | 17 | ## Configuration 18 | 19 | Copy the desired Python files to your target system. Then add the module to 20 | your `collectd.conf`. Make sure to adjust the `ModulePath` value. The following 21 | example assumes the plugins were copied to `/opt/collectd_plugins`. 22 | 23 | ### cpu_temp 24 | 25 | If your CPU temperature cannot be read from 26 | `/sys/class/thermal/thermal_zone0/temp`, make sure to adjust that variable too. 27 | 28 | LoadPlugin python 29 | 30 | ModulePath "/opt/collectd_plugins" 31 | Import "cpu_temp" 32 | 33 | Path "/sys/class/thermal/thermal_zone0/temp" 34 | 35 | 36 | 37 | ### sht21 38 | 39 | For this plugin to work, the `sht21` kernel module must be loaded: 40 | 41 | echo "sht21" > /etc/modules-load.d/sht21.conf 42 | 43 | There are currently no configuration options available. 44 | 45 | LoadPlugin python 46 | 47 | ModulePath "/opt/collectd_plugins" 48 | Import "sht21" 49 | 50 | 51 | ### shtc3 52 | 53 | For this plugin to work, the `shtc1` kernel module must be loaded: 54 | 55 | echo "shtc1" > /etc/modules-load.d/shtc1.conf 56 | modprobe shtc1 57 | 58 | Default config: 59 | 60 | LoadPlugin python 61 | 62 | ModulePath "/opt/collectd_plugins" 63 | Import "shtc3" 64 | 65 | 66 | Optionally, the hwmon device (hwmon0 by default) can be configured: 67 | 68 | LoadPlugin python 69 | 70 | ModulePath "/opt/collectd_plugins" 71 | Import "shtc3" 72 | 73 | Hwmon "hwmon2" 74 | 75 | 76 | 77 | ### mcp3425 78 | 79 | The plugin assumes that you're using three voltage divider resistors to bring 80 | the voltage into a measurable range. You can configure them in the Python 81 | script. 82 | 83 | This plugin requires the python-smbus package to be installed. 84 | 85 | There are currently no configuration options available. 86 | 87 | LoadPlugin python 88 | 89 | ModulePath "/opt/collectd_plugins" 90 | Import "mcp3425" 91 | 92 | 93 | ## Other Plugins 94 | 95 | This is my personal collection of plugins. If you also created a Collectd 96 | plugin that's great! I won't accept pull requests for now though since I cannot 97 | test and maintain plugins for which I don't have any matching hardware. 98 | 99 | Instead, feel free to create a pull request to add your plugin to the list 100 | below! 101 | 102 | 103 | 104 | - [`arris_modem.py`](https://github.com/jakup/collectd-python-plugins): Report 105 | the upstream/downstream channels of an Arris DOCSIS3 cable modem. 106 | 107 | ## License 108 | 109 | MIT License, see LICENSE file. 110 | -------------------------------------------------------------------------------- /cpu_temp.py: -------------------------------------------------------------------------------- 1 | import collectd 2 | 3 | PATH = '/sys/class/thermal/thermal_zone0/temp' 4 | 5 | 6 | def config_func(config): 7 | path_set = False 8 | 9 | for node in config.children: 10 | key = node.key.lower() 11 | val = node.values[0] 12 | 13 | if key == 'path': 14 | global PATH 15 | PATH = val 16 | path_set = True 17 | else: 18 | collectd.info('cpu_temp plugin: Unknown config key "%s"' % key) 19 | 20 | if path_set: 21 | collectd.info('cpu_temp plugin: Using overridden path %s' % PATH) 22 | else: 23 | collectd.info('cpu_temp plugin: Using default path %s' % PATH) 24 | 25 | 26 | def read_func(): 27 | # Read raw value 28 | with open(PATH, 'rb') as f: 29 | temp = f.read().strip() 30 | 31 | # Convert to degrees celsius 32 | deg = float(int(temp)) / 1000 33 | 34 | # Dispatch value to collectd 35 | val = collectd.Values(type='temperature') 36 | val.plugin = 'cpu_temp' 37 | val.dispatch(values=[deg]) 38 | 39 | 40 | collectd.register_config(config_func) 41 | collectd.register_read(read_func) 42 | -------------------------------------------------------------------------------- /mcp3425.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | MCP3425 ADC Plugin. 4 | 5 | Return supply voltage (in mV) from ADC readings. 6 | 7 | This plugin requires the python-smbus package to be installed. 8 | 9 | Configure your three voltage divider resistors below. 10 | Default values are 6.8K, 3.6K and 470. 11 | 12 | """ 13 | from __future__ import print_function, division, absolute_import, unicode_literals 14 | 15 | import time 16 | 17 | import collectd 18 | import smbus 19 | 20 | 21 | # I2C address 22 | DEVICE_ADDRESS = 0x68 23 | 24 | # Voltage dividers 25 | DIVIDER_R1 = 6800 26 | DIVIDER_R2 = 3600 27 | DIVIDER_R3 = 470 28 | 29 | # ADC reference 30 | ADC_REF = 2048 # mV 31 | 32 | # Config register bits 33 | BIT_G0 = 0 34 | BIT_G1 = 1 35 | BIT_S0 = 2 36 | BIT_S1 = 3 37 | BIT_OC = 4 38 | BIT_C0 = 5 39 | BIT_C1 = 6 40 | BIT_RDY = 7 41 | 42 | # Conversion mode 43 | CONVERSION_MODE_ONESHOT = 0 44 | CONVERSION_MODE_CONT = 1 << BIT_OC 45 | 46 | # Sample rate / accuracy 47 | SAMPLE_RATE_240SPS = 0 # 12 bits 48 | SAMPLE_RATE_60SPS = 1 << BIT_S0 # 14 bits 49 | SAMPLE_RATE_15SPS = 1 << BIT_S1 # 16 bits 50 | 51 | # PGA gain selection 52 | PGA_GAIN_1 = 0 53 | PGA_GAIN_2 = 1 << BIT_G0 54 | PGA_GAIN_4 = 1 << BIT_G1 55 | PGA_GAIN_8 = (1 << BIT_G1) | (1 << BIT_G0) 56 | 57 | # Start a conversion in one shot mode 58 | START_CONVERSION = 1 << BIT_RDY 59 | 60 | 61 | def get_voltage(measurement, bit): 62 | """ 63 | Calculate voltage for the specified bit of accuracy. 64 | """ 65 | v2 = measurement * ADC_REF / (2**bit) 66 | total_r = DIVIDER_R1 + DIVIDER_R2 + DIVIDER_R3 67 | return v2 / (DIVIDER_R2 / total_r) * 2 68 | 69 | 70 | def init(): 71 | collectd.info('mcp3425 plugin: Initialized') 72 | 73 | 74 | def read(): 75 | # Connect to I2C 76 | bus = smbus.SMBus(1) 77 | 78 | # Write configuration 79 | config = START_CONVERSION | CONVERSION_MODE_ONESHOT | SAMPLE_RATE_15SPS | PGA_GAIN_1 80 | bus.write_byte(DEVICE_ADDRESS, config) 81 | 82 | # Wait a bit for the measurement to finish 83 | time.sleep(0.15) 84 | 85 | # Read measurement 86 | data = bus.read_i2c_block_data(DEVICE_ADDRESS, 0x00, 3) 87 | value = (data[0] << 8) + data[1] 88 | 89 | # Convert to mV 90 | millivolts = get_voltage(value, 16) 91 | 92 | # Dispatch values 93 | v_voltage = collectd.Values(plugin='mcp3425', type='voltage', type_instance='supply_voltage') 94 | v_voltage.dispatch(values=[millivolts]) 95 | 96 | 97 | collectd.register_init(init) 98 | collectd.register_read(read) 99 | -------------------------------------------------------------------------------- /sht21.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | SHT21 Sensor Plugin. 4 | 5 | Return temperature and relative humidity from sensor readings. 6 | 7 | Calculcate and return absolute humidity and dew point. 8 | 9 | Source for calculations: 10 | http://www.vaisala.com/Vaisala%20Documents/Application%20notes/Humidity_Conversion_Formulas_B210973EN-F.pdf 11 | """ 12 | from __future__ import print_function, division, absolute_import, unicode_literals 13 | 14 | import os.path 15 | import math 16 | 17 | import collectd 18 | 19 | 20 | DEV_REG = '/sys/bus/i2c/devices/i2c-1/new_device' 21 | DEV_REG_PARAM = 'sht21 0x40' 22 | DEV_TMP = '/sys/class/hwmon/hwmon0/temp1_input' 23 | DEV_HUM = '/sys/class/hwmon/hwmon0/humidity1_input' 24 | 25 | 26 | def pws_constants(t): 27 | """ 28 | Lookup-table for water vapor saturation pressure constants (A, m, Tn). 29 | """ 30 | if t < -20: 31 | raise ValueError('Temperature out of range (-20 - 350°C') 32 | if t < 50: 33 | return (6.116441, 7.591386, 240.7263) 34 | if t < 100: 35 | return (6.004918, 7.337936, 229.3975) 36 | if t < 150: 37 | return (5.856548, 7.27731, 225.1033) 38 | if t < 200: 39 | return (6.002859, 7.290361, 227.1704) 40 | return (9.980622, 7.388931, 263.1239) 41 | 42 | 43 | def pws(t): 44 | r""" 45 | Calculate water vapor saturation pressure based on temperature (in hPa). 46 | 47 | P_{WS} = A \cdot 10^{\frac{m \cdot T}{T + T_n}} 48 | 49 | """ 50 | A, m, Tn = pws_constants(t) 51 | power = (m * t) / (t + Tn) 52 | return A * 10 ** power 53 | 54 | 55 | def pw(t, rh): 56 | r""" 57 | Calculate Pw (in hPa). 58 | 59 | P_W = P_{WS} \cdot RH / 100 60 | 61 | """ 62 | return pws(t) * rh / 100 63 | 64 | 65 | def td(t, rh): 66 | r""" 67 | Calculate the dew point (in °C). 68 | 69 | T_d = \frac{T_n}{\frac{m}{log_{10}\left(\frac{P_w}{A}\right)} - 1} 70 | 71 | """ 72 | A, m, Tn = pws_constants(t) 73 | Pw = pw(t, rh) 74 | return Tn / ((m / math.log(Pw / A, 10)) - 1) 75 | 76 | 77 | def celsius_to_kelvin(celsius): 78 | return celsius + 273.15 79 | 80 | 81 | def ah(t, rh): 82 | r""" 83 | Calculate the absolute humidity (in g/m³). 84 | 85 | A = C \cdot P_w / T 86 | 87 | """ 88 | C = 2.16679 89 | Pw = pw(t, rh) 90 | T = celsius_to_kelvin(t) 91 | return C * (Pw * 100) / T 92 | 93 | 94 | def init(): 95 | if os.path.isfile(DEV_TMP) and os.path.isfile(DEV_HUM): 96 | collectd.info('sht21 plugin: Sensor already registered in sysfs') 97 | else: 98 | with open(DEV_REG, 'wb') as f: 99 | f.write(DEV_REG_PARAM) 100 | collectd.info('sht21 plugin: Sensor successfully registered in sysfs') 101 | 102 | 103 | def read(): 104 | # Read values 105 | with open(DEV_TMP, 'rb') as f: 106 | val = f.read().strip() 107 | temperature = float(int(val)) / 1000 108 | with open(DEV_HUM, 'rb') as f: 109 | val = f.read().strip() 110 | humidity = float(int(val)) / 1000 111 | 112 | # Calculate values 113 | try: 114 | dewpoint = td(temperature, humidity) 115 | except ValueError as e: 116 | collectd.error('sht21 plugin: Could not calculate dew point: %s' % e) 117 | dewpoint = 0 118 | absolute_humidity = ah(temperature, humidity) 119 | 120 | # Dispatch values 121 | v_tmp = collectd.Values(plugin='sht21', type='temperature', type_instance='temperature') 122 | v_tmp.dispatch(values=[temperature]) 123 | v_hum = collectd.Values(plugin='sht21', type='humidity', type_instance='relative_humidity') 124 | v_hum.dispatch(values=[humidity]) 125 | v_abs = collectd.Values(plugin='sht21', type='gauge', type_instance='absolute_humidity') 126 | v_abs.dispatch(values=[absolute_humidity]) 127 | v_dew = collectd.Values(plugin='sht21', type='temperature', type_instance='dewpoint') 128 | v_dew.dispatch(values=[dewpoint]) 129 | 130 | 131 | collectd.register_init(init) 132 | collectd.register_read(read) 133 | -------------------------------------------------------------------------------- /shtc3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | SHTC3 Sensor Plugin. 4 | 5 | Return temperature and relative humidity from sensor readings. 6 | 7 | Calculcate and return absolute humidity and dew point. 8 | 9 | Source for calculations: 10 | http://www.vaisala.com/Vaisala%20Documents/Application%20notes/Humidity_Conversion_Formulas_B210973EN-F.pdf 11 | """ 12 | from __future__ import print_function, division, absolute_import, unicode_literals 13 | 14 | import os.path 15 | import math 16 | 17 | import collectd 18 | 19 | 20 | DEV_REG = '/sys/bus/i2c/devices/i2c-1/new_device' 21 | DEV_REG_PARAM = 'shtc1 0x70' 22 | DEV_TMP = '/sys/class/hwmon/{}/temp1_input' 23 | DEV_HUM = '/sys/class/hwmon/{}/humidity1_input' 24 | HWMON = 'hwmon0' 25 | 26 | 27 | def pws_constants(t): 28 | """ 29 | Lookup-table for water vapor saturation pressure constants (A, m, Tn). 30 | """ 31 | if t < -20: 32 | raise ValueError('Temperature out of range (-20 - 350°C') 33 | if t < 50: 34 | return (6.116441, 7.591386, 240.7263) 35 | if t < 100: 36 | return (6.004918, 7.337936, 229.3975) 37 | if t < 150: 38 | return (5.856548, 7.27731, 225.1033) 39 | if t < 200: 40 | return (6.002859, 7.290361, 227.1704) 41 | return (9.980622, 7.388931, 263.1239) 42 | 43 | 44 | def pws(t): 45 | r""" 46 | Calculate water vapor saturation pressure based on temperature (in hPa). 47 | 48 | P_{WS} = A \cdot 10^{\frac{m \cdot T}{T + T_n}} 49 | 50 | """ 51 | A, m, Tn = pws_constants(t) 52 | power = (m * t) / (t + Tn) 53 | return A * 10 ** power 54 | 55 | 56 | def pw(t, rh): 57 | r""" 58 | Calculate Pw (in hPa). 59 | 60 | P_W = P_{WS} \cdot RH / 100 61 | 62 | """ 63 | return pws(t) * rh / 100 64 | 65 | 66 | def td(t, rh): 67 | r""" 68 | Calculate the dew point (in °C). 69 | 70 | T_d = \frac{T_n}{\frac{m}{log_{10}\left(\frac{P_w}{A}\right)} - 1} 71 | 72 | """ 73 | A, m, Tn = pws_constants(t) 74 | Pw = pw(t, rh) 75 | return Tn / ((m / math.log(Pw / A, 10)) - 1) 76 | 77 | 78 | def celsius_to_kelvin(celsius): 79 | return celsius + 273.15 80 | 81 | 82 | def ah(t, rh): 83 | r""" 84 | Calculate the absolute humidity (in g/m³). 85 | 86 | A = C \cdot P_w / T 87 | 88 | """ 89 | C = 2.16679 90 | Pw = pw(t, rh) 91 | T = celsius_to_kelvin(t) 92 | return C * (Pw * 100) / T 93 | 94 | 95 | def init(): 96 | if os.path.isfile(DEV_TMP.format(HWMON)) and os.path.isfile(DEV_HUM.format(HWMON)): 97 | collectd.info('shtc3 plugin: Sensor already registered in sysfs') 98 | else: 99 | with open(DEV_REG, 'wb') as f: 100 | f.write(DEV_REG_PARAM) 101 | collectd.info('shtc3 plugin: Sensor successfully registered in sysfs') 102 | 103 | 104 | def config(config): 105 | hwmon_set = False 106 | 107 | for node in config.children: 108 | key = node.key.lower() 109 | val = node.values[0] 110 | 111 | if key == 'hwmon': 112 | global HWMON 113 | HWMON = val 114 | hwmon_set = True 115 | else: 116 | collectd.info('shtc3 plugin: Unknown config key "%s"' % key) 117 | 118 | if hwmon_set: 119 | collectd.info('shtc3 plugin: Using overridden hwmon: %s' % HWMON) 120 | else: 121 | collectd.info('shtc3 plugin: Using default hwmon: %s' % HWMON) 122 | 123 | 124 | def read(): 125 | # Read values 126 | with open(DEV_TMP.format(HWMON), 'rb') as f: 127 | val = f.read().strip() 128 | temperature = float(int(val)) / 1000 129 | with open(DEV_HUM.format(HWMON), 'rb') as f: 130 | val = f.read().strip() 131 | humidity = float(int(val)) / 1000 132 | 133 | # Calculate values 134 | try: 135 | dewpoint = td(temperature, humidity) 136 | except ValueError as e: 137 | collectd.error('shtc3 plugin: Could not calculate dew point: %s' % e) 138 | dewpoint = 0 139 | absolute_humidity = ah(temperature, humidity) 140 | 141 | # Dispatch values 142 | v_tmp = collectd.Values(plugin='shtc3', type='temperature', type_instance='temperature') 143 | v_tmp.dispatch(values=[temperature]) 144 | v_hum = collectd.Values(plugin='shtc3', type='humidity', type_instance='relative_humidity') 145 | v_hum.dispatch(values=[humidity]) 146 | v_abs = collectd.Values(plugin='shtc3', type='gauge', type_instance='absolute_humidity') 147 | v_abs.dispatch(values=[absolute_humidity]) 148 | v_dew = collectd.Values(plugin='shtc3', type='temperature', type_instance='dewpoint') 149 | v_dew.dispatch(values=[dewpoint]) 150 | 151 | 152 | collectd.register_init(init) 153 | collectd.register_config(config) 154 | collectd.register_read(read) 155 | --------------------------------------------------------------------------------