Integration of I2C sensors (raspberrypi4/BMP085; openhabian snapshot, Lua 5.1, lua-periphery, Exec Binding/REST API/MQTT)

TOC

  1. What’s new
  2. Introduction
  3. Hardware requirements
  4. Software requirements
  5. Enable I2C bus
  6. Connect I2C hardware
  7. Check installation
  8. Lua script for BMP085
  9. OH integration via Exec Binding
  10. OH integration via REST API
  11. OH integration via MQTT
  12. Calibrating the sensor
  13. Further reading

1. What’s new

  • 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 - judging from the datasheets, BMP180 should be 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 of i2cdetect -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

  1. Install Exec Binding and RegEx Transformation (Settings → Other Add-ons → Transformation Add-ons).

  2. Add a new Exec Binding Thing.

  3. 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’.

  4. Whitelist the Command lua <your_path>/bmp085_WIP.lua in misc/exec.whitelist.

  5. Add the new Thing to your Model (‘Create Equipment from Thing’, ‘Select All’, ‘Add to Model’).

  6. Change the type of the new Item from ‘String’ to ‘Number’ (the Item Analyzer doesn’t like strings …):

  7. Add the new item to your favourite UI:


  8. 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 - not implemented yet
publishing_method = 1

Start the Lua script:
lua BMP085.lua

grafik

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 - not implemented yet
publishing_method = 2

Start the Lua script:
lua BMP085.lua

grafik

Further reading:

https://www.hivemq.com/mqtt-essentials/

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

1 Like