Added Bosch Sensortec I2C BME280/BMP280 sensor
Added support for additional measurements in addition to the temperature Fixed handling of default I2C address Updated documentation for the new I2C sensors (and fixed a few errors)
This commit is contained in:
@@ -33,6 +33,25 @@ PROG_VERSION=PROG_NAME + ' ' + VERSION
|
||||
|
||||
CPU_SENSOR_DEV = '/sys/class/thermal/thermal_zone0/temp'
|
||||
CPU_SENSOR_SCALE=1000
|
||||
|
||||
I2C_BMX280_DEFAULT_ADDR=0x77
|
||||
I2C_BMX280_CALIBRATE_ADDR=0x88
|
||||
I2C_BMX280_CALIBRATE_LEN=24
|
||||
I2C_BMX280_CAL_HUM0_ADDR=0xA1
|
||||
I2C_BMX280_CAL_HUM_ADDR=0xE1
|
||||
I2C_BMX280_CAL_HUM_LEN=7
|
||||
I2C_BMX280_CONFIG_ADDR=0xF5
|
||||
I2C_BMX280_CONFIG = 0xA0 # Stand_by time = 1000 ms
|
||||
I2C_BMX280_CTRL_MEAS_ADDR=0xF4
|
||||
I2C_BMX280_CTRL_MEAS=0x27 # Normal mode, Pressure
|
||||
# and Temperature Oversampling rate = 1
|
||||
I2C_BMX280_CTRL_HUM_ADDR=0xF2
|
||||
I2C_BMX280_CTRL_HUM=1 # Humidity Oversampling rate = 1
|
||||
I2C_BMX280_MEAS_ADDR=0xF7
|
||||
I2C_BMX280_MEAS_LEN=8
|
||||
I2C_BMX280_SENSOR_SCALE=5120.0
|
||||
|
||||
I2C_MCP9808_DEFAULT_ADDR=0x18
|
||||
I2C_MCP9808_CONFIG_ADDR=0x1
|
||||
I2C_MCP9808_CONFIG = [ 0x00, 0x00 ] # continuous conversion (power-up default)
|
||||
I2C_MCP9808_PRECISION_ADDR=0x08
|
||||
@@ -89,6 +108,11 @@ class SetLogFile(StoreAction):
|
||||
|
||||
###############################################################################
|
||||
|
||||
def hex_int(string):
|
||||
"""Use int()'s auto-detection to parse 10-base and 16-base (0x..) numbers"""
|
||||
return int(string, 0);
|
||||
|
||||
|
||||
def convert_celcius(temp_read, scale = 1):
|
||||
"""Converts raw temperature sensore value to degrees Celcius"""
|
||||
return float(temp_read) / float(scale)
|
||||
@@ -101,18 +125,9 @@ def convert_farenheit(temp_read, scale = 1):
|
||||
CONVERT_FARENHEIT = ( convert_farenheit, 'F', 'Farenheit' )
|
||||
|
||||
|
||||
def isempty(string):
|
||||
"""Checks whether string 'str' provided is unset or empty"""
|
||||
return string is None or len(string) == 0
|
||||
|
||||
|
||||
def hex_int(string):
|
||||
"""Use int()'s auto-detection to parse 10-base and 16-base (0x..) numbers"""
|
||||
return int(string, 0);
|
||||
|
||||
|
||||
####################[ Get CPU temperature of Raspberry Pi ]####################
|
||||
def read_rpi_cpu_temp(args):
|
||||
"""Reads CPU temperature and converts it to desired unit, returns temperature"""
|
||||
"""Reads CPU temperature ands returns it converted to desired unit"""
|
||||
with open(args.file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
logger.debug('Temperature sensor data read from %s: %s', f.name, lines)
|
||||
@@ -124,9 +139,8 @@ def read_rpi_cpu_temp(args):
|
||||
return temp, 1
|
||||
|
||||
|
||||
def read_i2c_mcp9808_temp(args):
|
||||
"""Returns temperature from I2C MCP9808 sensor in desired unit"""
|
||||
|
||||
###############[ Get I2C (sm)bus object and I2C device address ]###############
|
||||
def i2c_get_smbus_devaddr(args):
|
||||
try:
|
||||
import smbus
|
||||
except ImportError:
|
||||
@@ -134,31 +148,149 @@ def read_i2c_mcp9808_temp(args):
|
||||
import smbus2 as smbus
|
||||
except ImportError:
|
||||
logger.critical("Unable to import either smbus or smbus2 library");
|
||||
raise ImportError("missing I2C library, please install python-smbus or smbus2");
|
||||
|
||||
raise ImportError("missing I2C library, please install smbus2 "
|
||||
"or Debian python-smbus package ");
|
||||
try:
|
||||
bus = smbus.SMBus(args.i2cbus) # Get I2C bus
|
||||
return (smbus.SMBus(args.i2cbus), # get i2c bus
|
||||
args.default_address if args.address is None else args.address)
|
||||
except OSError as e:
|
||||
logger.critical(e)
|
||||
raise IOError("Invalid I2C bus: %d" % args.i2cbus)
|
||||
|
||||
####################[ Get I2C BME280/BMP280 Sensor values ]####################
|
||||
# Inspired by https://github.com/ControlEverythingCommunity/BME280
|
||||
def read_i2c_bmX280(args):
|
||||
"""Returns temperature from I2C BME280/BMP280 sensor in desired unit"""
|
||||
|
||||
i2c_bus, i2c_addr = i2c_get_smbus_devaddr(args)
|
||||
|
||||
def convertLE16(data, offset, signed=False):
|
||||
result = (data[offset + 1] << 8) | data[offset]
|
||||
if signed and result > 32767:
|
||||
result -= 65536
|
||||
return result
|
||||
|
||||
try:
|
||||
bus.write_i2c_block_data(args.address, I2C_MCP9808_CONFIG_ADDR,
|
||||
# Get Temperature and Pressure Calibration Data
|
||||
data = i2c_bus.read_i2c_block_data(i2c_addr, I2C_BMX280_CALIBRATE_ADDR,
|
||||
I2C_BMX280_CALIBRATE_LEN)
|
||||
dig_T1 = convertLE16(data, 0)
|
||||
dig_T2 = convertLE16(data, 2, True)
|
||||
dig_T3 = convertLE16(data, 4, True)
|
||||
dig_P1 = convertLE16(data, 6)
|
||||
dig_P2 = convertLE16(data, 8, True)
|
||||
dig_P3 = convertLE16(data, 10, True)
|
||||
dig_P4 = convertLE16(data, 12, True)
|
||||
dig_P5 = convertLE16(data, 14, True)
|
||||
dig_P6 = convertLE16(data, 16, True)
|
||||
dig_P7 = convertLE16(data, 18, True)
|
||||
dig_P8 = convertLE16(data, 20, True)
|
||||
dig_P9 = convertLE16(data, 22, True)
|
||||
|
||||
if args.read_humidity:
|
||||
# Get Humidity Calibration Data
|
||||
dig_H1 = i2c_bus.read_byte_data(i2c_addr,I2C_BMX280_CAL_HUM0_ADDR)
|
||||
data = i2c_bus.read_i2c_block_data(i2c_addr,I2C_BMX280_CAL_HUM_ADDR,
|
||||
I2C_BMX280_CAL_HUM_LEN)
|
||||
dig_H2 = convertLE16(data, 0, True)
|
||||
dig_H3 = data[2]
|
||||
dig_H4 = (data[3] << 4) | (data[4] & 0x0f)
|
||||
dig_H5 = (data[5] << 4) | (data[4] >> 4)
|
||||
dig_H6 = data[6]
|
||||
if dig_H6 > 127:
|
||||
dig_H6 -= 256
|
||||
|
||||
i2c_bus.write_byte_data(i2c_addr, I2C_BMX280_CTRL_HUM_ADDR,
|
||||
I2C_BMX280_CTRL_HUM)
|
||||
|
||||
# Setup BMP280/BME280 configuration and wait 0.5 seconds for things to settle
|
||||
i2c_bus.write_byte_data(i2c_addr, I2C_BMX280_CTRL_MEAS_ADDR,
|
||||
I2C_BMX280_CTRL_MEAS)
|
||||
i2c_bus.write_byte_data(i2c_addr, I2C_BMX280_CONFIG_ADDR,
|
||||
I2C_BMX280_CONFIG)
|
||||
sleep(0.5)
|
||||
|
||||
# Read sensor data and and convert using calibration data
|
||||
data = i2c_bus.read_i2c_block_data(i2c_addr, I2C_BMX280_MEAS_ADDR,
|
||||
I2C_BMX280_MEAS_LEN)
|
||||
|
||||
# Convert 20-bits Temperature and calculate value with calibration data
|
||||
adc_t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
|
||||
var1 = ((adc_t) / 16384.0 - (dig_T1) / 1024.0) * (dig_T2)
|
||||
var2 = (((adc_t) / 131072.0 - (dig_T1) / 8192.0) * ((adc_t)/131072.0 - (dig_T1)/8192.0)) * (dig_T3)
|
||||
t_fine = (var1 + var2)
|
||||
|
||||
# Convert 20-bits Pressure and calculate value with calibration data
|
||||
adc_p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
|
||||
var1 = (t_fine / 2.0) - 64000.0
|
||||
var2 = var1 * var1 * (dig_P6) / 32768.0
|
||||
var2 = var2 + var1 * (dig_P5) * 2.0
|
||||
var2 = (var2 / 4.0) + ((dig_P4) * 65536.0)
|
||||
var1 = ((dig_P3) * var1 * var1 / 524288.0 + ( dig_P2) * var1) / 524288.0
|
||||
var1 = (1.0 + var1 / 32768.0) * (dig_P1)
|
||||
p = 1048576.0 - adc_p
|
||||
p = (p - (var2 / 4096.0)) * 6250.0 / var1
|
||||
var1 = (dig_P9) * p * p / 2147483648.0
|
||||
var2 = p * (dig_P8) / 32768.0
|
||||
pressure = (p + (var1 + var2 + (dig_P7)) / 16.0) / 100
|
||||
|
||||
extra_readings = [ ('pressure', pressure, 'hPa'), ]
|
||||
|
||||
if args.read_humidity:
|
||||
# Convert 16-byte Humidity and calculate value with calibration data
|
||||
adc_h = (data[6] << 8) | data[7]
|
||||
var_H = ((t_fine) - 76800.0)
|
||||
var_H = (adc_h - (dig_H4 * 64.0 + dig_H5 / 16384.0 * var_H)) * (dig_H2 / 65536.0 * (1.0 + dig_H6 / 67108864.0 * var_H * (1.0 + dig_H3 / 67108864.0 * var_H)))
|
||||
humidity = var_H * (1.0 - dig_H1 * var_H / 524288.0)
|
||||
if humidity > 100.0 :
|
||||
humidity = 100.0
|
||||
elif humidity < 0.0 :
|
||||
humidity = 0.0
|
||||
extra_readings += ('humidity', humidity, '%', 0, 100),
|
||||
logger.debug('BME280 sensor data: temperature %#02x: %#x = %d, '
|
||||
'pressure: %#x, humidity: %#x', i2c_addr, adc_t,
|
||||
t_fine, adc_p, adc_h)
|
||||
logger.debug('Sensor Humidity value is %.2f %%', humidity)
|
||||
else:
|
||||
logger.debug('BMP280 sensor data: temperature %#02x: %#x = %d, '
|
||||
'pressure: %#x',i2c_addr, adc_t, t_fine, adc_p)
|
||||
|
||||
# convert temperature to right units and scale and return value
|
||||
temp = args.converter[0](t_fine, I2C_BMX280_SENSOR_SCALE)
|
||||
logger.debug('Sensor Temperature value %d is %.2f%s',
|
||||
t_fine, temp, args.converter[1])
|
||||
logger.debug('Sensor Pressure value is %.2f hPa', pressure)
|
||||
return (temp, extra_readings), 1
|
||||
except IOError as e:
|
||||
logger.critical(e)
|
||||
raise IOError("Error while communicating with I2C device %#02x" %
|
||||
i2c_addr)
|
||||
|
||||
|
||||
########################[ Get I2C MCP9808 Temperature ]########################
|
||||
# Inspired by https://github.com/ControlEverythingCommunity/MCP9808
|
||||
def read_i2c_mcp9808(args):
|
||||
"""Returns temperature from I2C mcp9808 sensor in desired unit"""
|
||||
|
||||
i2c_bus, i2c_addr = i2c_get_smbus_devaddr(args)
|
||||
|
||||
try:
|
||||
# Setup MCP9808 configuration and wait 0.5 seconds for things to settle
|
||||
i2c_bus.write_i2c_block_data(i2c_addr, I2C_MCP9808_CONFIG_ADDR,
|
||||
I2C_MCP9808_CONFIG)
|
||||
bus.write_byte_data(args.address, I2C_MCP9808_PRECISION_ADDR,
|
||||
i2c_bus.write_byte_data(i2c_addr, I2C_MCP9808_PRECISION_ADDR,
|
||||
I2C_MCP9808_PRECISION)
|
||||
sleep(0.5)
|
||||
|
||||
# Read temperature (0x05), 2 bytes (MSB, LSB)
|
||||
data = bus.read_i2c_block_data(args.address, I2C_MCP9808_TEMP_ADDR, 2)
|
||||
logger.debug('Temperature sensor data from MCP9808 %#02x: 0x%02x%02x',
|
||||
args.address, data[0], data[1])
|
||||
|
||||
# Convert the data to 13-bits
|
||||
temp_read = ((data[0] & 0x1F) * 256) + data[1]
|
||||
if temp_read > 4095 :
|
||||
temp_read -= 8192
|
||||
# Read temperature (2 bytes - MSB,LSB) and convert to 13-bit signed int
|
||||
data = i2c_bus.read_i2c_block_data(i2c_addr,I2C_MCP9808_TEMP_ADDR,2)
|
||||
temp_read = ((data[0] & 0xF) << 8) | data[1]
|
||||
if (data[0] & 0x10):
|
||||
temp_read = -temp_read
|
||||
logger.debug('MCP9808 sensor data %#02x: 0x%02x%02x = %d',
|
||||
i2c_addr, data[0], data[1], temp_read)
|
||||
|
||||
# convert temperatur to right units and scale and return value
|
||||
temp = args.converter[0](temp_read, I2C_MCP9808_SENSOR_SCALE)
|
||||
logger.debug('Temperature sensor value %d is %.2f%s',
|
||||
temp_read, temp, args.converter[1])
|
||||
@@ -166,15 +298,16 @@ def read_i2c_mcp9808_temp(args):
|
||||
except IOError as e:
|
||||
logger.critical(e)
|
||||
raise IOError("Error while communicating with I2C device %#02x" %
|
||||
args.address)
|
||||
i2c_addr)
|
||||
|
||||
|
||||
#####################[ Get 1-Wire sensor device filename ]#####################
|
||||
def get_w1_sensor_device_filename(args, dev_dir=W1_SENSOR_DEV_DIR,
|
||||
prefix=W1_SENSOR_DEV_PREFIX, suffix=W1_SENSOR_DEV_SUFFIX):
|
||||
prefix=W1_SENSOR_DEV_PREFIX, suffix=W1_SENSOR_DEV_SUFFIX):
|
||||
"""Auto-determine sensor datafile name (unless args.file is set)"""
|
||||
if isempty(args.file):
|
||||
if not args.file:
|
||||
search_pat = dev_dir + ('/' if dev_dir[-1]!='/' else '')
|
||||
search_pat+= prefix + '*' if isempty(args.serial) else '*' + args.serial
|
||||
search_pat+= prefix + ('*' + args.serial if args.serial else '*')
|
||||
logger.debug('looking for sensors with search pattern %s', search_pat)
|
||||
|
||||
device_folders = glob(search_pat)
|
||||
@@ -196,7 +329,8 @@ def get_w1_sensor_device_filename(args, dev_dir=W1_SENSOR_DEV_DIR,
|
||||
return filename
|
||||
|
||||
|
||||
def read_w1_ds18b20_temp(args):
|
||||
###################[ Get 1-Wire DS18b20 sensor temperature ]###################
|
||||
def read_w1_ds18b20(args):
|
||||
"""Returns temperature from 1-wire ds18b20 sensor in desired unit"""
|
||||
device_file = get_w1_sensor_device_filename(args)
|
||||
lines=[ '' ]
|
||||
@@ -227,6 +361,7 @@ def read_w1_ds18b20_temp(args):
|
||||
raise ValueError(errmsg)
|
||||
|
||||
|
||||
#######################[ Parse Command Line parameters ]#######################
|
||||
def parse_args():
|
||||
"""Parse command line and get parameters from environment, if present"""
|
||||
|
||||
@@ -269,18 +404,35 @@ def parse_args():
|
||||
help='input file (or device) to obtain data from'
|
||||
' (defaults to %s)' % CPU_SENSOR_DEV)
|
||||
cmdparser = subparser.add_parser('rpi_cpu', parents=[cpuparser],
|
||||
help='read built-in Raspberry Pi CPU temperature')
|
||||
cmdparser.set_defaults(func=read_rpi_cpu_temp, cmdparser=cmdparser, retries=0)
|
||||
help='read built-in Raspberry Pi CPU temperature')
|
||||
cmdparser.set_defaults(func=read_rpi_cpu_temp,cmdparser=cmdparser,retries=0)
|
||||
|
||||
i2cparser = ArgumentParser(add_help=False)
|
||||
i2cparser.add_argument('-a', '--address', type=hex_int,
|
||||
help='I2C Address of sensor, use 0x.. for hex (*)')
|
||||
i2cparser.add_argument('-b', '--i2cbus', default=1, type=int,
|
||||
help='I2C Bus to use (defaults to 1)')
|
||||
|
||||
cmdparser = subparser.add_parser('i2c_bme280', parents=[i2cparser],
|
||||
help='read I2C connected BME280 sensor',
|
||||
epilog='(*) default I2C address for an BME280 is %#x' %
|
||||
I2C_BMX280_DEFAULT_ADDR)
|
||||
cmdparser.set_defaults(func=read_i2c_bmX280, cmdparser=cmdparser, retries=0,
|
||||
read_humidity=True, default_address=I2C_BMX280_DEFAULT_ADDR)
|
||||
|
||||
cmdparser = subparser.add_parser('i2c_bmp280', parents=[i2cparser],
|
||||
help='read I2C connected BMP280 sensor',
|
||||
epilog='(*) default I2C address for an BMP280 is %#x' %
|
||||
I2C_BMX280_DEFAULT_ADDR)
|
||||
cmdparser.set_defaults(func=read_i2c_bmX280, cmdparser=cmdparser, retries=0,
|
||||
read_humidity=False, default_address=I2C_BMX280_DEFAULT_ADDR)
|
||||
|
||||
cmdparser = subparser.add_parser('i2c_mcp9808', parents=[i2cparser],
|
||||
help='read I2C connected MCP9808 sensor',
|
||||
epilog='(*) default I2C address for an MCP9808 is 0x18')
|
||||
cmdparser.set_defaults(func=read_i2c_mcp9808_temp, cmdparser=cmdparser, retries=0, address=0x18)
|
||||
epilog='(*) default I2C address for an MCP9808 is %#x' %
|
||||
I2C_MCP9808_DEFAULT_ADDR)
|
||||
cmdparser.set_defaults(func=read_i2c_mcp9808, cmdparser=cmdparser,
|
||||
retries=0, default_address=I2C_MCP9808_DEFAULT_ADDR)
|
||||
|
||||
w1parser = ArgumentParser(add_help=False)
|
||||
pgroup = w1parser.add_mutually_exclusive_group(required=False)
|
||||
@@ -288,16 +440,17 @@ def parse_args():
|
||||
help='(unique part of) temperature sensor serial (*)')
|
||||
pgroup.add_argument('-f', '--file',
|
||||
help='input file (or device) to obtain data from (*)')
|
||||
w1parser.add_argument('-r', '--retries', type=int,default=W1_SENSOR_READ_RETRIES,
|
||||
help='number of times to retry reading sensor data when'
|
||||
' unstable (defaults to %d)' % W1_SENSOR_READ_RETRIES)
|
||||
w1parser.add_argument('-r', '--retries', type=int,
|
||||
default=W1_SENSOR_READ_RETRIES,
|
||||
help='number of times to retry reading sensor data when'
|
||||
' unstable (defaults to %d)' % W1_SENSOR_READ_RETRIES)
|
||||
cmdparser = subparser.add_parser('w1_ds18b20', parents=[w1parser],
|
||||
help='read 1-wire connected DS18b20 sensor',
|
||||
epilog='(*) by default the script will look for the first device that '
|
||||
'matches %s* in %s, if multiple entries are found -s or -f must '
|
||||
'be used to specify which sensor to read.' %
|
||||
(W1_SENSOR_DEV_PREFIX, W1_SENSOR_DEV_DIR))
|
||||
cmdparser.set_defaults(func=read_w1_ds18b20_temp, cmdparser=cmdparser)
|
||||
cmdparser.set_defaults(func=read_w1_ds18b20, cmdparser=cmdparser)
|
||||
|
||||
# parse arguments and post-process command line options
|
||||
args = parser.parse_args()
|
||||
@@ -306,6 +459,7 @@ def parse_args():
|
||||
return args
|
||||
|
||||
|
||||
############################[ Exit the Nagios way ]############################
|
||||
def nagios_exit(status, message, data=None):
|
||||
"""exit 'nagios-style', print status and message followed by perf. data"""
|
||||
if logger.isEnabledFor(logging.CRITICAL):
|
||||
@@ -319,6 +473,7 @@ def nagios_exit(status, message, data=None):
|
||||
exit(status[1])
|
||||
|
||||
|
||||
#################################[ Main logic ]#################################
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
args = parse_args()
|
||||
@@ -333,11 +488,16 @@ if __name__ == '__main__':
|
||||
temperature, tries = args.func(args)
|
||||
endtime = time()
|
||||
|
||||
if isinstance(temperature, tuple):
|
||||
temperature, extra_data = temperature
|
||||
else:
|
||||
extra_data = ()
|
||||
|
||||
except (KeyboardInterrupt) as e:
|
||||
nagios_exit(NAGIOS_UNKNOWN,'temperature sensor read aborted by user')
|
||||
nagios_exit(NAGIOS_UNKNOWN,'sensor read aborted by user')
|
||||
|
||||
except (IOError, ValueError, ImportError) as e:
|
||||
nagios_exit(NAGIOS_UNKNOWN,'temperature sensor read failed: %s' % e)
|
||||
nagios_exit(NAGIOS_UNKNOWN,'sensor read failed: %s' % e)
|
||||
|
||||
elapse = endtime-starttime
|
||||
logger.info('Got temperature reading of %.2f degrees %s in %fs',
|
||||
@@ -345,6 +505,10 @@ if __name__ == '__main__':
|
||||
|
||||
unit = args.converter[1]
|
||||
message = 'current temperature is %.2f%s' % (temperature, unit)
|
||||
data = [ ('temperature', [ '%f%s' % (temperature, unit),
|
||||
args.warn, args.critical, None, None]),
|
||||
('retries', [ tries-1, None, args.retries, 0, None ]),
|
||||
('checktime', [ '%fs' % elapse, None, None, 0, None]) ]
|
||||
if args.critical is not None and temperature > args.critical:
|
||||
nagiosresult = NAGIOS_CRITICAL
|
||||
message+= ' and exceeds critical threshold %.2f%s' %(args.critical,unit)
|
||||
@@ -354,10 +518,10 @@ if __name__ == '__main__':
|
||||
else:
|
||||
nagiosresult = NAGIOS_OK
|
||||
|
||||
nagios_exit(nagiosresult, message, [
|
||||
('temperature', [ '%f%s' % (temperature, unit),
|
||||
args.warn, args.critical, None, None]),
|
||||
('retries', [ tries-1, None, args.retries, 0, None ]),
|
||||
('checktime', [ '%fs' % elapse, None, None, 0, None])
|
||||
])
|
||||
for e in extra_data:
|
||||
message += ', %s is %.2f%s' % (e[0], e[1], e[2])
|
||||
data += (e[0], [ '%f%s' % (e[1], e[2]), None, None,
|
||||
e[3] if len(e)>3 else None, e[4] if len(e)>4 else None ]),
|
||||
|
||||
nagios_exit(nagiosresult, message, data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user