loading

Monitor room and outdoor temperature with a BLE thermometer connected to Raspberry Pi

Browsing the web, I found a myriad of temp sensors that you can interface easily with Raspberry Pi. And came across many tutos detailing how to interface these sensors wirelessly to RPi with a 433MHz RF transmitter/receiver kit.

Monitoring the temperature wit RPi was fun but I wanted also the ability to read the temperature at a glance, without having to turn on my computer and open a web page.

Moreover these 433MHz RF kits didn't sound very efficient (low range) and reliable reading users feedback.

Then I came upon Oregon Scientific Wireless Indoor / Outdoor Thermometer with Bluetooth Low Energy Connectivity (EMR211) and thought that it was the perfect match.

I can connect the Weather Station base to my Raspberry Pi3 through Bluetooth Low Energy. I can monitor up to 3 sensors connected to the base + temperature at the base.

Step 1: Oregon Scientific Weather Station BLE Protocol

An easy way to explore the BLE services exposed by the Weather Station is to use a BLE Scanner app on a smartphone or tablet. Here I'll be using Nordic Semiconductor's nRF Connect on Android.

For people that aren't familiar with BLE, you might want to start with this Bluetooth Low Energy introductory guide. This will help you understand terminology like GATT, service, and characteristic.

With the BLE Scanner app, you wilI find all BLE devices surrounding you. And you will see that the Oregon Scientific Weather Station is adverting itself as IDTW211R.

If you connect to the Weather Station, you'll get the list of services exposed by the device.

  • Generic Access UUID: 0x1800
  • Generic Attribute UUID: 0x1801
  • Proprietary Service UUID: 74e7fe00-c6a4-11e2-b7a9-0002a5d5c51b
  • Device Information UUID: 0x180A
  • Battery Service UUID: 0x180F

The service that matters to us is the Proprietary Service (UUID: 74e7fe00-c6a4-11e2-b7a9-0002a5d5c51b). So we click on this service and get the list of characteristics exposed by the service.

The relevant characteristic for our application is UUID: 74e78e10-c6a4-11e2-b7a9-0002a5d5c51b (INDOOR_AND_CH1_TO_3_TH_DATA).

As indicated in the properties, this characteristic isn't readable but offers an indication service. And we will have to enable the indication through the corresponding Client Characteristic Configuration Descriptor (UUID: 0x2902).

It turns out that indication/notification have to be enabled for all the characteristics of the device to start getting any indication.

Step 2: Connect the Raspberry Pi3 to the BLE Weather Station

Now that we have a better idea on how to retrieve temperature data through BLE, let's try to get our Raspberry Pi talking to the Weather Station.

BLE on Raspberry Pi

I'll be using the Raspberry Pi3 (which integrates a WLAN/BT controller) with Raspbian Jessie with Pixel 2016-09-23.

The version of bluez (5.23) in this Raspbian release is quite outdated and we will have to upgrade to later version of bluez to work with Bluetooth Low Energy.

Download bluez 5.43 and install required dependencies:

sudo apt-get update

sudo apt-get install -y libglib2.0-dev libdbus-1-dev libudev-dev libical-dev libreadline-dev

wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.43.tar.xz

tar xvf bluez-5.43.tar.xz

Configure, build and install bluez

./configure
make
sudo make install

And finally reboot the Raspberry Pi.

Now let's verify that the bluetooth stack is up and running with the cmd hciconfig dev

And then confirm that we can scan for BLE devices: sudo hcitool lescan

We should see our Weather Station adverting as IDTW211R.

Next step is to connect to the Weather Station:

sudo gatttool -b <BLE ADDRESS> -t random -I

You should get a new command prompt with the BLE address in between brackets.

connect

You should get a "Connection successful" and the prompt gets colored

Once connected, we can run some commands to get more details on the device. For instance, the primary command will list the services exposed by the device.

Collecting data from the Weather Station

As explained earlier to start getting indications on the INDOOR_AND_CH1_TO_3_TH_DATA (UUID: 74e78e10-c6a4-11e2-b7a9-0002a5d5c51b), we have to enable Indication/Notification on all characteristics.

We enable indication by writting 0x0002 (0x0001 for notification) in Client Characteristic Configuration Descriptor (UUID: 0x2902) of each characteristic. Write shall be placed in little endian format so: char-write-req 0200

In return, you should start getting indications/notifications. The relevant ones for us are INDOOR_AND_CH1_TO_3_TH_DATA indications (so handle 0x0017).

Indication handle = 0x0017 value: 01 05 01 15 01 ff 7f ff 7f 7f 7f 7f 7f ff ff 7f 7f 7f 7f 7f
Indication handle = 0x0017 value: 82 7f 7f 7f 21 01 f8 00 24 01 ba 00 ff 7f ff 7f ff 7f ff 7f

For each round of indications, we get two data packets of 20 bytes each. The most significant byte indicates the type of data (Type 0 or Type 1). See last picture for more details on the data packets.

Step 3: Python Script to Retrieve Weather Station Data

Now that we succeeded to get the data out of the Weather Station, let's automate the whole process and make some sense of the data.

Here is a python script that connect to the Weather Station, retrieve the data and extract the temperature of the Weather Station base and wireless sensor coming with it.

This script makes use of bluepy library providing an API to allow access to Bluetooth Low Energy devices from Python. So you will have to install this module before executing the script: https://github.com/IanHarvey/bluepy#installation

Usage

The script can be executed with the MAC address passed as argument or without argument.

In this latter case, the script will perform a scan and look for a device adverting as IDTW211R. It has to be executed with root privilege because bluez requires root privilege for scan operations.

python bleWeatherStation.py [mac-address]
sudo python bleWeatherStation.py

bleWeatherStation.py

#!/usr/bin/python
# -*- coding: utf-8 -*- # Connect to Oregon Scientific BLE Weather Station # Copyright (c) 2016 Arnaud Balmelle # # This script will connect to Oregon Scientific BLE Weather Station # and retrieve the temperature of the base and sensors attached to it. # If no mac-address is passed as argument, it will scan for an Oregon Scientific BLE Weather Station. # # Supported Oregon Scientific Weather Station: EMR211 and RAR218HG (and probably BAR218HG) # # Usage: python bleWeatherStation.py [mac-address] # # Dependencies: # - Bluetooth 4.1 and bluez installed # - bluepy library (https://github.com/IanHarvey/bluepy) # # License: Released under an MIT license: http://opensource.org/licenses/MIT

import sys import logging import time import sqlite3 from bluepy.btle import *

# uncomment the following line to get debug information logging.basicConfig(format='%(asctime)s: %(message)s', level=logging.DEBUG)

WEATHERSTATION_NAME = "IDTW211R" # IDTW213R for RAR218HG

class WeatherStation: def __init__(self, mac): self._data = {} try: self.p = Peripheral(mac, ADDR_TYPE_RANDOM) self.p.setDelegate(NotificationDelegate()) logging.debug('WeatherStation connected !') except BTLEException: self.p = 0 logging.debug('Connection to WeatherStation failed !') raise def _enableNotification(self): try: # Enable all notification or indication self.p.writeCharacteristic(0x000c, "\x02\x00") self.p.writeCharacteristic(0x000f, "\x02\x00") self.p.writeCharacteristic(0x0012, "\x02\x00") self.p.writeCharacteristic(0x0015, "\x01\x00") self.p.writeCharacteristic(0x0018, "\x02\x00") self.p.writeCharacteristic(0x001b, "\x02\x00") self.p.writeCharacteristic(0x001e, "\x02\x00") self.p.writeCharacteristic(0x0021, "\x02\x00") self.p.writeCharacteristic(0x0032, "\x01\x00") logging.debug('Notifications enabled') except BTLEException as err: print(err) self.p.disconnect() def monitorWeatherStation(self): try: # Enable notification self._enableNotification() # Wait for notifications while self.p.waitForNotifications(1.0): # handleNotification() was called continue logging.debug('Notification timeout') except: return None regs = self.p.delegate.getData() if regs is not None: # expand INDOOR_AND_CH1_TO_3_TH_DATA_TYPE0 self._data['index0_temperature'] = ''.join(regs['data_type0'][4:6] + regs['data_type0'][2:4]) self._data['index1_temperature'] = ''.join(regs['data_type0'][8:10] + regs['data_type0'][6:8]) self._data['index2_temperature'] = ''.join(regs['data_type0'][12:14] + regs['data_type0'][10:12]) self._data['index3_temperature'] = ''.join(regs['data_type0'][16:18] + regs['data_type0'][14:16]) self._data['index0_humidity'] = regs['data_type0'][18:20] self._data['index1_humidity'] = regs['data_type0'][20:22] self._data['index2_humidity'] = regs['data_type0'][22:24] self._data['index3_humidity'] = regs['data_type0'][24:26] self._data['temperature_trend'] = regs['data_type0'][26:28] self._data['humidity_trend'] = regs['data_type0'][28:30] self._data['index0_humidity_max'] = regs['data_type0'][30:32] self._data['index0_humidity_min'] = regs['data_type0'][32:34] self._data['index1_humidity_max'] = regs['data_type0'][34:36] self._data['index1_humidity_min'] = regs['data_type0'][36:38] self._data['index2_humidity_max'] = regs['data_type0'][38:40] # expand INDOOR_AND_CH1_TO_3_TH_DATA_TYPE1 self._data['index2_humidity_min'] = regs['data_type1'][2:4] self._data['index3_humidity_max'] = regs['data_type1'][4:6] self._data['index3_humidity_min'] = regs['data_type1'][6:8] self._data['index0_temperature_max'] = ''.join(regs['data_type1'][10:12] + regs['data_type1'][8:10]) self._data['index0_temperature_min'] = ''.join(regs['data_type1'][14:16] + regs['data_type1'][12:14]) self._data['index1_temperature_max'] = ''.join(regs['data_type1'][18:20] + regs['data_type1'][16:18]) self._data['index1_temperature_min'] = ''.join(regs['data_type1'][22:24] + regs['data_type1'][20:22]) self._data['index2_temperature_max'] = ''.join(regs['data_type1'][26:28] + regs['data_type1'][24:26]) self._data['index2_temperature_min'] = ''.join(regs['data_type1'][30:32] + regs['data_type1'][28:30]) self._data['index3_temperature_max'] = ''.join(regs['data_type1'][34:36] + regs['data_type1'][32:34]) self._data['index3_temperature_min'] = ''.join(regs['data_type1'][38:40] + regs['data_type1'][36:38]) return True else: return None def getValue(self, indexstr): val = int(self._data[indexstr], 16) if val >= 0x8000: val = ((val + 0x8000) & 0xFFFF) - 0x8000 return val def getIndoorTemp(self): if 'index0_temperature' in self._data: temp = self.getValue('index0_temperature') / 10.0 max = self.getValue('index0_temperature_max') / 10.0 min = self.getValue('index0_temperature_min') / 10.0 logging.debug('Indoor temp : %.1f°C, max : %.1f°C, min : %.1f°C', temp, max, min) return temp else: return None def getOutdoorTemp(self): if 'index1_temperature' in self._data: temp = self.getValue('index1_temperature') / 10.0 max = self.getValue('index1_temperature_max') / 10.0 min = self.getValue('index1_temperature_min') / 10.0 logging.debug('Outdoor temp : %.1f°C, max : %.1f°C, min : %.1f°C', temp, max, min) return temp else: return None def disconnect(self): self.p.disconnect() class NotificationDelegate(DefaultDelegate): def __init__(self): DefaultDelegate.__init__(self) self._indoorAndOutdoorTemp_type0 = None self._indoorAndOutdoorTemp_type1 = None def handleNotification(self, cHandle, data): formatedData = binascii.b2a_hex(data) if cHandle == 0x0017: # indoorAndOutdoorTemp indication received if formatedData[0] == '8': # Type1 data packet received self._indoorAndOutdoorTemp_type1 = formatedData logging.debug('indoorAndOutdoorTemp_type1 = %s', formatedData) else: # Type0 data packet received self._indoorAndOutdoorTemp_type0 = formatedData logging.debug('indoorAndOutdoorTemp_type0 = %s', formatedData) else: # skip other indications/notifications logging.debug('handle %x = %s', cHandle, formatedData) def getData(self): if self._indoorAndOutdoorTemp_type0 is not None: # return sensors data return {'data_type0':self._indoorAndOutdoorTemp_type0, 'data_type1':self._indoorAndOutdoorTemp_type1} else: return None

class ScanDelegate(DefaultDelegate): def __init__(self): DefaultDelegate.__init__(self) def handleDiscovery(self, dev, isNewDev, isNewData): global weatherStationMacAddr if dev.getValueText(9) == WEATHERSTATION_NAME: # Weather Station in range, saving Mac address for future connection logging.debug('WeatherStation found') weatherStationMacAddr = dev.addr

if __name__=="__main__":

weatherStationMacAddr = None if len(sys.argv) < 2: # No MAC address passed as argument try: # Scanning to see if Weather Station in range scanner = Scanner().withDelegate(ScanDelegate()) devices = scanner.scan(2.0) except BTLEException as err: print(err) print('Scanning required root privilege, so do not forget to run the script with sudo.') else: # Weather Station MAC address passed as argument, will attempt to connect with this address weatherStationMacAddr = sys.argv[1] if weatherStationMacAddr is None: logging.debug('No WeatherStation in range !') else: try: # Attempting to connect to device with MAC address "weatherStationMacAddr" weatherStation = WeatherStation(weatherStationMacAddr) if weatherStation.monitorWeatherStation() is not None: # WeatherStation data received indoor = weatherStation.getIndoorTemp() outdoor = weatherStation.getOutdoorTemp() else: logging.debug('No data received from WeatherStation') weatherStation.disconnect() except KeyboardInterrupt: logging.debug('Program stopped by user')

<p>The Android APP has some nice insights into all the other attributes...</p><p>BLE_SVC_DEVICE_INFORMATION, &quot;Device Info Primiary Service&quot;</p><p>BLE_SVC_BATTERY_SERVICE, &quot;Battery level service&quot;</p><p>BLE_ATTR_BATTERY_LEVEL, &quot;Battery level characteristic&quot;</p><p>BLE_ATTR_FIRMWARE_REVISION, &quot;Firmware Revision String characteristic&quot;</p><p>BLE_ATTR_HARDWARE_REVISION, &quot;Hardware Revision String characteristic&quot;</p><p>BLEWS_SVC_BLE_WEATHER_STATION, &quot;Primiary Service&quot;</p><p>BLEWS_ATTR_DEVICE_INFORMATION, &quot;Device info&quot;</p><p>BLEWS_ATTR_COMMAND, &quot;Command or Reply&quot;</p><p>BLEWS_ATTR_LOGGER_DATA, &quot;Logger data&quot;</p><p>BLEWS_ATTR_INDOOR_AND_CH1_TO_3_TH_DATA, &quot;INDOOR_AND_CH1_TO_3 Basic TH data&quot;</p><p>BLEWS_ATTR_INDOOR_AND_CH1_TO_3_EXTRA_TEMPERATURE_DATA, &quot;INDOOR_AND_CH1_TO_3 Extra temperature&quot;</p><p>BLEWS_ATTR_CH4_TO_7_TH_DATA, &quot;CH4-7 TH&quot;</p><p>BLEWS_ATTR_CH4_TO_7_EXTRA_TEMPERATURE_DATA, &quot;CH4-7 extra temperature&quot;</p><p>BLEWS_ATTR_CH8_TO_11_TH_DATA, &quot;CH8-11 TH data&quot;</p><p>BLEWS_ATTR_CH8_TO_11_EXTRA_TEMPERATURE_DATA, &quot;CH8-11 extra temperature &quot;</p><p>BLEWS_ATTR_PRESSURE_DATA, &quot;Pressure data&quot;</p><p>BLEWS_ATTR_RAINFALL_DATA, &quot;Rain data&quot;</p><p>BLEWS_ATTR_SETTINGS_DATA, &quot;Time data&quot;</p><p>BLEWS_ATTR_WIND_DATA, &quot;Wind data&quot;</p><p>BLEWS_ATTR_SOIL_DATA, &quot;Soil data&quot;</p><p>MANUFACTURER_NAME_CHAR, &quot;Manufacturer Name String characteristic&quot;</p><p>MODEL_NAME_CHAR, &quot;Model Number String characteristic&quot;</p><p>BLE_SVC_DEVICE_INFORMATION = &quot;0000180a-0000-1000-8000-00805f9b34fb&quot;;</p><p>BLE_SVC_BATTERY_SERVICE = &quot;0000180f-0000-1000-8000-00805f9b34fb&quot;;</p><p>BLE_ATTR_BATTERY_LEVEL = &quot;00002a19-0000-1000-8000-00805f9b34fb&quot;;</p><p>BLE_ATTR_FIRMWARE_REVISION = &quot;00002a26-0000-1000-8000-00805f9b34fb&quot;;</p><p>BLE_ATTR_HARDWARE_REVISION = &quot;00002a27-0000-1000-8000-00805f9b34fb&quot;;</p><p>BLEWS_SVC_BLE_WEATHER_STATION = &quot;74e7fe00-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_DEVICE_INFORMATION = &quot;74e78e02-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_COMMAND = &quot;74e78e03-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_LOGGER_DATA = &quot;74e78e04-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_INDOOR_AND_CH1_TO_3_TH_DATA = &quot;74e78e10-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_INDOOR_AND_CH1_TO_3_EXTRA_TEMPERATURE_DATA = &quot;74e78e11-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_CH4_TO_7_TH_DATA = &quot;74e78e14-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_CH4_TO_7_EXTRA_TEMPERATURE_DATA = &quot;74e78e15-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_CH8_TO_11_TH_DATA = &quot;74e78e18-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_CH8_TO_11_EXTRA_TEMPERATURE_DATA = &quot;74e78e19-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_PRESSURE_DATA = &quot;74e78e20-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_RAINFALL_DATA = &quot;74e78e28-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_SETTINGS_DATA = &quot;74e78e2c-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_SOIL_DATA = &quot;74e78e30-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>BLEWS_ATTR_WIND_DATA = &quot;74e78e24-c6a4-11e2-b7a9-0002a5d5c51b&quot;;</p><p>MODEL_NAME_CHAR = &quot;00002a24-0000-1000-8000-00805f9b34fb&quot;;</p><p>MANUFACTURER_NAME_CHAR = &quot;00002a29-0000-1000-8000-00805f9b34fb&quot;;</p>
<p>Updated the Instructable for Raspbian Jessie with Pixel (2016-09-23) and bluez 5.43 (GATT D-Bus API is no longer marked as experimental with this release)</p>
<p>Fantastic! I looked for this information when I got the BAR218HG last year, and didn't find it then, but now its all here. I've modified the script to read the temperature and humidity of the internal and my 3 external sensors, and am working on adding it to a database along with temperatures of my Raspberry Pi's, and getting some nice web based graphs of it all.</p>
<p>I'm keen to learn how you did this. My Python skills are abysmal and it'd be great to be able to use my 2 sensors and read humidity.</p>
<p>Yes, handle 0x001A is the indication of PRESSURE DATA characteristic.</p><p>EMR211 doesn't support pressure. Hence I am not getting any indication on handle 0x001a.</p><p>Apparently the following data can be reported in the indication of PRESSURE DATA characteristic:</p><p>bytes B1+B0: pressure</p><p>bytes B3+B2: altitude_pressure</p><p>bytes B5+B4: altitude</p><p>byte B6: weather</p>
<p>Looking at the notifications coming back I've spotted that on the BAR218HG bytes [2:4]+[0:2] of the data from handle 0x001A is the pressure measured by the main until, in *10 mb.</p>
<p>With the weather turning cold around here, I've noticed that the code handle negative temperatures correctly. The numbers are encoded as signed 16 bit values, so I've replaced lines of the form:-</p><p> temp = int(self._data['index{0}_temperature'.format(index)], 16) / 10.0</p><p>with:-</p><p> temp = self.getValue('index{0}_temperature'.format(index)) / 10.0</p><p>Where getValue is a new method defined as:-</p><p> def getValue(self, indexstr):<br> val = int(self._data[indexstr], 16)<br> if val &gt;= 0x8000:<br> val = ((val+0x8000)&amp;0xFFFF) - 0x8000<br> return val</p>
<p>Good catch ! Thanks for sharing.</p><p>I haven't been able to test negative temperature yet ;</p><p>I integrated your fix in the code shared on the Instructable.</p>
<p>Somehow the connection is very unreliable for me, so far I only managed to get 2 successful runs. Any suggestions why?</p>
<p>Hello Voogel,</p><p>Have you upgraded to bluez 5.29 as suggested in this Instructable? Which Raspbian version are you running?</p><p>I have already faced some hangs of the Weather Station base where Rpi can connect over BLE and enable indication/notification but the base wouldn't throw any indication.</p><p>Increasing notification timeout to 5s doesn't help in this situation. You have to reset the base to get it back to work.</p><p>Please note also that only one single device can be connect to the Weather Station base at a time. If you're already connected to the base with the Weather@Home mobile app, then you won't be able to connect your Rpi.</p><p>Hope you will be able to solve your problem.</p><p>Cheers,</p><p>Arn&oslash;</p>
<p>Voogel: increase the timeout value in self.p.waitForNotifications, with the default of 1 I didn't get any data, I've got mine set to 5 which picks up all my sensors.</p>
<p>Hello,</p><p>thanks for sharing. However the python script seems to have lost somethong in the web page. can you post a raw version of it ? </p><p>thanks</p>
Indeed, some formatting flaws got introduced mysteriously in the code. All is fixed now!
<p>thanks, it works fine now :)</p>
<p>Thanks a lot,</p><p>I've been able to get the data from a BAR218HG station on RPI Zero with IoT pHat using your instructable.</p><p>I'd like to retrieve the past data (24h or 1week) like Weather@Home android app.</p><p>Do you have any hint on how to query the main station to get that data ?</p><p>Thanks again</p>
You're welcome. Glad to hear that you were able to get the code to work on Rpi Zero.<br><br>No, I wasn't able to figure out how to retrieve historical data.<br>So I am running the python script every hour thanks to cron utility and storing the results in a SQLite database on my RPi.
<p>Excellent tutorial Arn&oslash;! For me it worked on first trial with a RAR218HG Oregon scientific weather station. I only had to implement slight changes to report humidity - though all framework was there and ready for that. Awsome.</p><p>Thanks a lot for sharing!</p>
<p>Thanks for sharing :)</p>

About This Instructable

3,178views

17favorites

More by Arnø:Connect Raspberry Pi to Oregon Scientific BLE Weather Station 
Add instructable to: