TOC
- What’s new
- Introduction
- Hardware requirements
- Software requirements
- Enable I2C bus
- Connect I2C hardware
- Check installation
- Lua script for BMP085
- OH integration via Exec Binding
- OH integration via REST API
- OH integration via MQTT
- Calibrating the sensor
- Further reading
1. What’s new
- 2021-12-09: lua script successfully tested with BMP180
- 2021-11-21: MQTT implemented, compatible sensors added
- 2021-11-19: REST API implemented
- 2021-11-19: Calibrating the sensor
2. Introduction
By following this tutorial you will learn how to:
- setup your Raspberry Pi 4 for I2C devices,
- connect I2C devices to your Raspberry Pi 4,
- interface to I2C devices via Lua and lua-periphery,
- publish the sensor measurements to OH by using different methods (Exec Binding, REST API, MQTT),
- calibrate the sensor by using publicly available data (scraping data from websites via HTTP Binding).
3. Hardware requirements
- Raspberry Pi 4
- I2C sensor, e.g. BMP085 (hard to come by nowadays - BMP180 is compatible with the Lua script used in this tutorial, but please note that BMP280 and BMP390 are not compatible; the BMP180 can be had for less than 2 EUR from Aliexpress)
4. Software requirements
- openhabian snapshot 32bit (3.2.0-SNAPSHOT - Build #2583; earlier versions may work, not tested)
- Exec Binding or REST API or MQTT Binding; for calibration: HTTP Binding
- lua-periphery:
sudo apt-get install luarocks
sudo luarocks install lua-periphery
export LUA_CPATH=/usr/local/lib/lua/5.1/?.so
5. Enable I2C bus
6. Connect I2C hardware
- PIN 1 - 3.3V
- PIN 3 - SDA
- PIN 5 - SCL
- PIN 6 - GND
7. Check installation
openhabian@openhabian:~ $ i2cdetect -F 1
Functionalities implemented by /dev/i2c-1:
I2C yes
SMBus Quick Command yes
SMBus Send Byte yes
SMBus Receive Byte yes
SMBus Write Byte yes
SMBus Read Byte yes
SMBus Write Word yes
SMBus Read Word yes
SMBus Process Call yes
SMBus Block Write yes
SMBus Block Read no
SMBus Block Process Call no
SMBus PEC yes
I2C Block Write yes
I2C Block Read yes
openhabian@openhabian:~ $ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77
8. Lua script for BMP085
Configure
i2c_device_address
(see output ofi2cdetect -y 1
)oversampling_setting
(0, 1, 2, 3 - see datasheet)temperature_decimal_places
pressure_decimal_places
altitude_decimal_places
true_altitude
(altitude of the BMP085 - m above sea level)
No command line options, output format (numbers only, units are °C, hPa, m):
- for Exec Binding: items are separated by a space,
<temperature> <true air pressure> <absolute altitude> <air pressure at sea level>
- for REST API: no output, REST API calls
- for MQTT: no output, publish to MQTT broker
--
-- BMP085
--
-- return value:
-- 1. temperature (°C)
-- 2. true pressure (hPa)
-- 3. absolute altitude (m)
-- 4. pressure at sea level (hPa)
--
-- by Ap15e
--
local I2C = require('periphery').I2C
local socket = require 'socket'
local i2c_device_address = 0x77
-- 0: 1 sample , conversion time 4.5 ms
-- 1: 2 samples, conversion time 7.5 ms
-- 2: 4 samples, conversion time 13.5 ms
-- 3: 8 samples, conversion time 25.5 ms
local oversampling_setting = 4
local temperature_decimal_places = 2 -- °C
local pressure_decimal_places = 2 -- hPa
local altitude_decimal_places = 1 -- m
local true_altitude = 58 -- altitude of sensor, m above sea level
-- 0: Exec Binding
-- 1: REST API
-- 2: MQTT
local publishing_method = 0
local publishing_interval = 60 -- seconds
-- open i2c-1 controller
local i2c = I2C( '/dev/i2c-1' )
function get_short( addr, signed )
local msgs = { { addr }, { 0x00, 0x00, flags = I2C.I2C_M_RD } }
i2c:transfer( i2c_device_address, msgs )
if signed and ( msgs[ 2 ][ 1 ] > 127 )
then
return msgs[ 2 ][ 1 ] * 256 + msgs[ 2 ][ 2 ] - 65536
else
return msgs[ 2 ][ 1 ] * 256 + msgs[ 2 ][ 2 ]
end
end -- get_short
function round( value, decimal_places )
return math.floor( value * math.pow( 10, decimal_places ) + 0.5 ) / math.pow( 10, decimal_places )
end -- round
function get_temperature_and_pressure()
-- read uncompensated temperature value
-- write 0x2E into reg 0xF4
local msgs = { { 0xF4, 0x2E } }
i2c:transfer( i2c_device_address, msgs )
-- wait 5 ms
socket.sleep( 0.005 )
-- uncompensated temperature
local UT = get_short( 0xF6, true )
-- read uncompensated pressure value
-- write 0x34+(oversampling_setting<<6) into reg 0xF4
local msgs = { { 0xF4, 0x34 + oversampling_setting * 64 } }
i2c:transfer( i2c_device_address, msgs )
-- wait
socket.sleep( 8 / 1000 * ( oversampling_setting + 1 ) )
local msgs = { { 0xF6 }, { 0x00, 0x00, 0x00, flags = I2C.I2C_M_RD } }
i2c:transfer( i2c_device_address, msgs )
-- uncompensated pressure
local UP = ( msgs[ 2 ][ 1 ] * 65536 + msgs[ 2 ][ 2 ] * 256 + msgs[ 2 ][ 3 ] ) / math.pow( 2, 8 - oversampling_setting )
-- calculate true temperature
local X1 = ( UT - coeffs.AC6 ) * coeffs.AC5 / 32768
local X2 = coeffs.MC * 2048 / ( X1 + coeffs.MD )
local B5 = X1 + X2
local T = round( ( B5 + 8 ) / 16 / 10, temperature_decimal_places )
--[[
-- test values from datasheet
oversampling_setting = 0
B5 = 2399
UT = 27898
UP = 23843
coeffs.B1 = 6190
coeffs.B2 = 4
coeffs.AC1 = 408
coeffs.AC2 = -72
coeffs.AC3 = -14383
coeffs.AC4 = 32741
--]]
-- calculate true pressure
local B6 = B5 - 4000
X1 = coeffs.B2 * ( B6 * B6 / 4096 ) / 2048
X2 = coeffs.AC2 * B6 / 2048
local X3 = X1 + X2
local B3 = ( ( coeffs.AC1 * 4 + X3 ) * math.pow( 2, oversampling_setting ) + 2 ) / 4
X1 = coeffs.AC3 * B6 / 8192
X2 = ( coeffs.B1 * ( B6 * B6 / 4096 ) ) / 65536
X3 = ( X1 + X2 + 2 ) / 4
local B4 = coeffs.AC4 * ( X3 + 32768 ) / 32768
local B7 = ( UP - B3 ) * ( 50000 / math.pow( 2, oversampling_setting ) )
local p = B7 / B4 * 2
X1 = math.pow( p / 256, 2 )
X1 = ( X1 * 3038 ) / 65536
X2 = ( -7357 * p ) / 65536
p = p + ( X1 + X2 + 3791 )/ 16
local p = round( p / 100, pressure_decimal_places )
-- calculate absolute altitude
local p_0 = 1013.25 -- standard atmosphere (1 atm = 1013.25 kPa): Earth's average atmospheric pressure at sea level
local a = round( 288.15 / 0.0065 * ( 1 - math.pow( p / p_0, 1 / 5.255 ) ), altitude_decimal_places ) -- barometric formula
-- calculate pressure at sea level
local p_sea_level = round( p / math.pow( 1 - true_altitude / ( 288.15 / 0.0065 ), 5.255 ), altitude_decimal_places )
return T, p, a, p_sea_level
end -- get_temperature_and_pressure
coeffs = {}
coeffs.AC1 = get_short( 0xAA, true )
coeffs.AC2 = get_short( 0xAC, true )
coeffs.AC3 = get_short( 0xAE, true )
coeffs.AC4 = get_short( 0xB0, false )
coeffs.AC5 = get_short( 0xB2, false )
coeffs.AC6 = get_short( 0xB4, false )
coeffs.B1 = get_short( 0xB6, true )
coeffs.B2 = get_short( 0xB8, true )
coeffs.MB = get_short( 0xBA, true )
coeffs.MC = get_short( 0xBC, true )
coeffs.MD = get_short( 0xBE, true )
if publishing_method == 0 -- Exec Binding / print values
then -- 0: Exec Binding
local temperature, local_air_pressure, absolute_altitude, air_pressure_at_sea_level = get_temperature_and_pressure()
print( tostring( temperature ) .. ' ' ..
tostring( local_air_pressure ) .. ' ' ..
tostring( absolute_altitude ) .. ' ' ..
tostring( air_pressure_at_sea_level ) )
else if publishing_method == 1 -- REST API / POST values
then
repeat
local temperature, local_air_pressure, absolute_altitude, air_pressure_at_sea_level = get_temperature_and_pressure()
os.execute( 'curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d "' ..
tostring( temperature ) .. '" "http://openhabian:8080/rest/items/BMP085_temperature"' )
os.execute( 'curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d "' ..
tostring( local_air_pressure ) .. '" "http://openhabian:8080/rest/items/BMP085_local_air_pressure"' )
os.execute( 'curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d "' ..
tostring( absolute_altitude ) .. '" "http://openhabian:8080/rest/items/BMP085_absolute_altitude"' )
os.execute( 'curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d "' ..
tostring( air_pressure_at_sea_level ) .. '" "http://openhabian:8080/rest/items/BMP085_air_pressure_at_sea_level"' )
socket.sleep( publishing_interval )
until false
else if publishing_method == 2 -- MQTT
then
repeat
local temperature, local_air_pressure, absolute_altitude, air_pressure_at_sea_level = get_temperature_and_pressure()
os.execute( 'mosquitto_pub -t /sensors/i2c/BMP085/1/temperature -m ' .. tostring( temperature ) )
os.execute( 'mosquitto_pub -t /sensors/i2c/BMP085/1/local_air_pressure -m ' .. tostring( local_air_pressure ) )
os.execute( 'mosquitto_pub -t /sensors/i2c/BMP085/1/absolute_altitude -m ' .. tostring( absolute_altitude ) )
os.execute( 'mosquitto_pub -t /sensors/i2c/BMP085/1/air_pressure_at_sea_level -m ' .. tostring( air_pressure_at_sea_level ) )
socket.sleep( publishing_interval )
until false
else
print( 'invalid publishing method' )
end end end
-- close i2c-1 controller
i2c:close()
9. OH integration via Exec Binding
-
Install Exec Binding and RegEx Transformation (Settings → Other Add-ons → Transformation Add-ons).
-
Add a new Exec Binding Thing.
-
Configure the new Thing:
Command:lua <your_path>/bmp085_WIP.lua
Transformation:REGEX(.*\s(.*)\s.*\s.*)
- shift the parentheses to get the other measurements.
Adjust Interval.
Click ‘Create Thing’. -
Whitelist the Command
lua <your_path>/bmp085_WIP.lua
inmisc/exec.whitelist
. -
Add the new Thing to your Model (‘Create Equipment from Thing’, ‘Select All’, ‘Add to Model’).
-
Change the type of the new Item from ‘String’ to ‘Number’ (the Item Analyzer doesn’t like strings …):
-
Add the new item to your favourite UI:
-
In case of problems use
log:set TRACE org.openhab.binding.exec
to get an idea what is going on.
10. OH integration via REST API
What would be needed is a Virtual Thing - but AFAICT OH doesn’t support such a Design Pattern (a ‘Virtual Thing Binding’ would be required to create a Virtual Thing …):
BMP085
- Channel_1: temperature
- Channel_2: local air pressure
- Channel_3: absolute altitude
- Channel_4: air pressure at sea level
Further reading re ‘Virtual Thing’:
IMHO a Virtual Thing should be part of OH. But IIUC, this won’t happen anytime soon … So let’s stick to Virtual Items:
Create four Virtual Items (Settings → Items → Add Item → … → Create):
Name: BMP085_temperature
Label: BMP085 - temperature
Type: Number
Name: BMP085_local_air_pressure
Label: BMP085 - local air pressure
Type: Number
Name: BMP085_absolute_altitude
Label: BMP085 - absolute altitude
Type: Number
Name: BMP085_air_pressure_at_sea_level
Label: BMP085 - air pressure at sea level
Type: Number
You might want to use ‘WeatherService’ as ‘Semantic Class’.
Check your new Virtual Items:
http://<openhabian>:8080/rest/items
Test your new Virtual Items (it might be necessary to refresh the Item UI page):
curl -X POST --header "Content-Type: text/plain" --header "Accept: application/json" -d "123456" "http://<openhabian>:8080/rest/items/BMP085_temperature"
Modify the Lua script:
-- 0: Exec Binding
-- 1: REST API
-- 2: MQTT
publishing_method = 1
Start the Lua script:
lua BMP085.lua
11. OH integration via MQTT
Install mosquitto:
sudo openhabian-config
Select menu item
20 Optional Components
and then
23 Mosquitto
Create a MQTT Broker Bridge:
Things -> Add -> MQTT Binding -> MQTT Broker Bridge
Create a Generic MQTT Thing as a container for the sensor data:
Things -> Add -> MQTT Binding -> Generic MQTT Thing
and Label it BMP085 MQTT Thing
, set the Parent bridge to the MQTT Broker Brige
Add the following channels (Add Number Value
) to the BMP085 MQTT Thing
:
- Channel Identifier:
BMP085_MQTT_local_air_pressure
Label:BMP085 - MQTT - local air pressure
MQTT State Topic:/sensors/i2c/BMP085/1/local_air_pressure
- Channel Identifier:
BMP085_MQTT_air_pressure_at_sea_level
Label:BMP085 - MQTT - air pressure at sea level
MQTT State Topic:/sensors/i2c/BMP085/1/air_pressure_at_sea_level
- Channel Identifier:
BMP085_MQTT_temperature
Label:BMP085 - MQTT - temperature
MQTT State Topic:/sensors/i2c/BMP085/1/temperature
- Channel Identifier:
BMP085_MQTT_absolute_altitude
Label:BMP085 - MQTT - absolute altitude
MQTT State Topic:/sensors/i2c/BMP085/1/absolute_altitude
Result:
Add the BMP085 MQTT Thing
to your Model (Select all channels → Add to Model)
Test setup:
mosquitto_pub -t /sensors/i2c/BMP085/1/local_air_pressure -m 1002
Modify the Lua script:
-- 0: Exec Binding
-- 1: REST API
-- 2: MQTT
publishing_method = 2
Start the Lua script:
lua BMP085.lua
Further reading:
12. Calibrating the sensor
Use the HTTP Binding to scrape the air pressure from the website of your local airport. Use the Item Analyzer to compare the readings:
Height above sea level for BMP085 is (difference between absolute altitudes for 1026.3 hPa and 1019.39 hPa): 57.1 m
Height above sea level for airport nearby is (difference between absolute altitudes for 1027.8 hPa and 1021.9 hPa): 48.7 m - this matches the height of the runway as published by official bodies.
function absolute_altitude( p )
local p0 = 1013.25
return 288.15 / 0.0065 * ( 1 - math.pow( p / p0, 1 / 5.255 ) )
end
-- BMP085
local p1 = 1026.3
local p2 = 1019.39
print( math.abs( absolute_altitude( p1 ) - absolute_altitude( p2 ) ) )
-- airport nearby
local p1 = 1027.8
local p2 = 1021.9
print( math.abs( absolute_altitude( p1 ) - absolute_altitude( p2 ) ) )
The air pressure at sea level should be identical for BMP085 and aiport nearby (distance between BMP085 and airport: 20 km), but there is a difference of about 1.5 hPa corresponding to a height difference of 1.5 * 8.43 m = 12.6 m (at sea level).
Two possibilites:
- The airport sensor is 12.6 m above the runway (not unplausible …).
- The BMP085 is off by about 1.5 hPa (not unplausible either - the sensor is quite old).
I’ll check the BMP085 against a BMP180, BMP280 and BMP390. The sensors will arrive in a few weeks - stay tuned.
13. Further reading
- Detailed discussion of BMP085:
https://www.sparkfun.com/tutorials/253 - A lua-periphery I²C primer:
https://devcenter.nubix.io/articles/i2c