├── .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 |
--------------------------------------------------------------------------------