Introduction: Connect Raspberry Pi to Oregon Scientific BLE Weather Station

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')