Skip to content

Commit

Permalink
Merge pull request #1 from ruuvi-friends/better-structure
Browse files Browse the repository at this point in the history
Iteration on structure and base interface
  • Loading branch information
sergioisidoro authored Apr 10, 2020
2 parents 48127e7 + 02f01ee commit e510a04
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 94 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
[![Build Status](https://travis-ci.org/ruuvi-friends/simple-ruuvitag.svg?branch=master)](https://travis-ci.org/ruuvi-friends/simple-ruuvitag)

# Simple Ruuvitag 🔧

Simple Ruuvitag is a hard frok and ***HEAVY*** simplification of [ttu work](https://github.com/ttu) [ruuvitag-sensor](https://github.com/ttu/ruuvitag-sensor)

It uses bleson python lib, which leverages Python 3 native support for Bluetooth sockets.
However, in order for python to have access to AF_SOCKET family, python needs to be compiled with lib-bluetooth or bluez.

**⚠️ This is a recent library with no guarantee of stability. There might be breaking changes so use the release tags to pull specific version**


# Usage

## Simplest usage
Expand All @@ -13,8 +18,8 @@ The client will keep the latest state of all sensors that have been polled.

```python
macs = ['CC:2C:6A:1E:59:3D', 'DD:2C:6A:1E:59:3D']
ruuvi_client = RuuviTagClient()
ruuvi_client.listen(mac_addresses=macs)
ruuvi_client = RuuviTagClient(mac_addresses=macs)
ruuvi_client.start()

last_datas = ruuvi_client.get_current_datas()
print(last_datas)
Expand All @@ -36,16 +41,16 @@ def handle_callback(data):
print("Data from %s: %s" % (data[0], data[1]))

macs = ['CC:2C:6A:1E:59:3D', 'DD:2C:6A:1E:59:3D']
ruuvi_client = RuuviTagClient()
ruuvi_client.listen(callback=handle_callback, mac_addresses=macs)
ruuvi_client = RuuviTagClient(callback=handle_callback, mac_addresses=macs)
ruuvi_client.start()
```

## Continous use:
Although the bluetooth observer is running, data might stop coming through.
For this we have `ruuvi_client.rescan()` witch will restart the observer, and data
should start flowing again.
```
ruuvi_client.listen()
ruuvi_client.start()
time.sleep(5)
last_datas = ruuvi_client.get_current_datas()
print(last_datas)
Expand Down
11 changes: 11 additions & 0 deletions simple_ruuvitag/adaptors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

class BluetoothAdaptor(object):

def __init__(self, callback, bt_device=''):
'''
Arguments:
callback: Function that receives the data from BLE
device (string): BLE device (default 0)
'''

self.callback = callback
50 changes: 24 additions & 26 deletions simple_ruuvitag/adaptors/bleson.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,41 @@
import time
import logging
from simple_ruuvitag.adaptors import BluetoothAdaptor
from bleson import get_provider, Observer

log = logging.getLogger(__name__)

class BlesonClient(object):
class BlesonClient(BluetoothAdaptor):
'''Bluetooth LE communication with Bleson'''

def __init__(self):
self.observer = None
self.callback = None


def handle_callback(self, advertisment):
def handle_callback(self, advertisement):

if not advertisment.mfg_data:
if not advertisement.mfg_data:
# No data to return
return

processed_data = {
"address": advertisment.address.address,
"raw_data": "FF" + advertisment.mfg_data.hex(),
"address": advertisement.address.address,
"raw_data": advertisement.mfg_data.hex(),
# these are documented but don't work
# "tx_power": data.tx_power,
# "rssi": data.rssi,
# "name": data.name,
# "tx_power": advertisement.tx_power,
# "rssi": advertisement.rssi,
# "name": advertisement.name,
}
self.callback(processed_data)

def start(self):
if not self.observer:
log.info('Cannot start a client that has not been setup')
return
self.observer.start()
if processed_data["raw_data"][0:2] != 'FF':
processed_data["raw_data"] = 'FF' + processed_data["raw_data"]

def setup(self, callback, bt_device=''):
self.callback(processed_data)

def __init__(self, callback, bt_device=''):
'''
Attributes:
callback: Function that recieves the data from BLE
Arguments:
callback: Function that receives the data from BLE
device (string): BLE device (default 0)
'''
super().__init__(callback, bt_device)

# set callback
self.callback = callback
self.observer = None

if not bt_device:
bt_device = 0
Expand All @@ -55,7 +48,12 @@ def setup(self, callback, bt_device=''):
adapter = get_provider().get_adapter(int(bt_device))
self.observer = Observer(adapter)
self.observer.on_advertising_data = self.handle_callback
return self.observer

def start(self):
if not self.observer:
log.info('Cannot start a client that has not been setup')
return
self.observer.start()

def stop(self):
self.observer.stop()
33 changes: 21 additions & 12 deletions simple_ruuvitag/adaptors/dummy.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import logging
from simple_ruuvitag.adaptors import BluetoothAdaptor

log = logging.getLogger(__name__)

class DummyBle(object):
class DummyBle(BluetoothAdaptor):
'''Bluetooth LE communication with Bleson'''

def mock_datas(self, callback):

callback(('DU:MM:YD:AT:A9:3D',
'1E0201060303AAFE1616AAFE10EE037275752E76692F23416A7759414D4663CD'))

callback(('NO:TS:UP:PO:RT:ED',
'1E0201060303AAFE1616AAFE10EE037275752E76692F23416A7759414D4663CD'))

def __init__(self):
self.observer = None
callback(
{
"address": 'DU:MM:YD:AT:A9:3D',
"raw_data": '1E0201060303AAFE1616AAFE10EE037275752E76692F23416A7759414D4663CD'
}
)

def start(self, callback, bt_device=''):
callback(
{
"address": 'NO:TS:UP:PO:RT:ED',
"raw_data": '1E0201060303AAFE1616AAFE10EE037275752E76692F23416A7759414D4663CD'
}
)

def start(self):

# Simulates the call to the callback
self.mock_datas(callback)
self.mock_datas(self.callback)

return None

def stop(self):
pass
# dummy BLE cannot stop.
pass
72 changes: 72 additions & 0 deletions simple_ruuvitag/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ def get_decoder(data_type):
Returns:
object: Data decoder
"""
if data_type == 2:
log.warning("DATA TYPE 2 IS OBSOLETE. UPDATE YOUR TAG")
# https://github.com/ruuvi/ruuvi-sensor-protocols/blob/master/dataformat_04.md
return UrlDecoder()
if data_type == 4:
log.warning("DATA TYPE 4 IS OBSOLETE. UPDATE YOUR TAG")
# https://github.com/ruuvi/ruuvi-sensor-protocols/blob/master/dataformat_04.md
return UrlDecoder()
if data_type == 3:
log.warning("DATA TYPE 3 IS DEPRECATED - UPDATE YOUR TAG")
# https://github.com/ruuvi/ruuvi-sensor-protocols/blob/master/dataformat_03.md
return Df3Decoder()
return Df5Decoder()

Expand All @@ -34,6 +44,68 @@ def rshift(val, n):
"""
return (val % 0x100000000) >> n


class UrlDecoder(object):
"""
Decodes data from RuuviTag url
Protocol specification:
https://github.com/ruuvi/ruuvi-sensor-protocols
Decoder operations are ported from:
https://github.com/ruuvi/sensor-protocol-for-eddystone-url/blob/master/index.html
0: uint8_t format; // (0x02 = realtime sensor readings)
1: uint8_t humidity; // one lsb is 0.5%
2-3: uint16_t temperature; // Signed 8.8 fixed-point notation.
4-5: uint16_t pressure; // (-50kPa)
6-7: uint16_t time; // seconds (now from reset)
The bytes for temperature, pressure and time are swaped during the encoding
"""

def _get_temperature(self, decoded):
"""Return temperature in celsius"""
temp = (decoded[2] & 127) + decoded[3] / 100
sign = (decoded[2] >> 7) & 1
if sign == 0:
return round(temp, 2)
return round(-1 * temp, 2)

def _get_humidity(self, decoded):
"""Return humidity %"""
return decoded[1] * 0.5

def _get_pressure(self, decoded):
"""Return air pressure hPa"""
pres = ((decoded[4] << 8) + decoded[5]) + 50000
return pres / 100

def decode_data(self, encoded):
"""
Decode sensor data.
Returns:
dict: Sensor values
"""
try:
identifier = None
data_format = 2
if len(encoded) > 8:
data_format = 4
identifier = encoded[8:]
encoded = encoded[:8]
decoded = bytearray(base64.b64decode(encoded, '-_'))
return {
'data_format': data_format,
'temperature': self._get_temperature(decoded),
'humidity': self._get_humidity(decoded),
'pressure': self._get_pressure(decoded),
'identifier': identifier
}
except:
log.exception('Encoded value: %s not valid', encoded)
return None

class Df3Decoder(object):
"""
Decodes data from RuuviTag with Data Format 3
Expand Down
53 changes: 23 additions & 30 deletions simple_ruuvitag/ruuvi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import datetime
from datetime import datetime
import logging

from simple_ruuvitag.data_formats import DataFormats
Expand All @@ -14,43 +14,37 @@ class RuuviTagClient(object):
"""
RuuviTag communication functionality
"""

def __init__(self, adapter='bleson'):
def __init__(self, callback=log.info, mac_addresses=None,
bt_device=None, adapter='bleson'):

if os.environ.get('CI') == 'True':
log.warn("Adapter override to Dummy due to CI env variable")
self.ble = DummyBle()
log.info("Adapter override to Dummy due to CI env variable")
self.ble_adaptor = DummyBle

if adapter == 'dummy':
self.ble = DummyBle()
self.ble_adaptor = DummyBle

elif adapter == 'bleson':
self.ble = BlesonClient()
self.ble_adaptor = BlesonClient
else:
raise RuntimeError("Unsupported adapter %s" % adapter)

# Setup defaults
self.mac_blacklist = []
self.callback = print
self.mac_addresses = None
self.latest_data = {}

# Use this if you want to start listening right away
# example: bt_device='hci0' (defaults to device 0)
def listen(self, callback=log.info, mac_addresses=None, bt_device=None):
self.setup(callback=log.info, mac_addresses=mac_addresses, bt_device=bt_device)
self.start()

# Use this if you want to setup but wait before you start scanning
# example: bt_device='hci0' (defaults to device 0)
def setup(self, callback=log.info, mac_addresses=None, bt_device=None):
self.latest_data = {}
if mac_addresses:
if isinstance(mac_addresses, list):
self.mac_addresses = [x.upper() for x in mac_addresses]
else:
self.mac_addresses = mac_addresses.upper()

self.callback = callback
self.ble.setup(self.convert_data_and_callback, bt_device=bt_device)
self.ble = self.ble_adaptor(
self.convert_data_and_callback, bt_device=bt_device
)

def resume(self):
self.ble.start()
Expand All @@ -59,7 +53,7 @@ def start(self):
self.ble.start()

def rescan(self):
self.ble.stop()
self.ble.stop()
self.ble.start()

def stop(self):
Expand All @@ -71,7 +65,7 @@ def pause(self):
def get_current_datas(self, consume=False):
"""
Get current data gets the current state of the known tags.
If consume=True it will delete the current data so that old
If consume=True it will delete the current data so that old
readings don't get interpreted as current readings.
"""
return_data = self.latest_data.copy()
Expand All @@ -88,32 +82,31 @@ def convert_data_and_callback(self, data):

# {
# "address": "MAC ADDRESS IN UPPERCASE"
# "raw_data":
# "rssi":
# "tx_power"
# "name": st
# "raw_data": xxxx
# }

mac_address = data["address"]
raw_data = data["raw_data"]

if mac_address in self.mac_blacklist:
log.debug("Skipping blacklisted mac %s" % mac_address)
log.debug("Skipping blacklisted mac %s", mac_address)
return

if self.mac_addresses and mac_address not in self.mac_addresses:
log.debug("Skipping non selected mac %s" % mac_address)
log.debug("Skipping non selected mac %s", mac_address)
return

(data_format, data) = DataFormats.convert_data(raw_data)

print (data_format)
if data is not None:
state = get_decoder(data_format).decode_data(data)
if state is not None:
self.latest_data[mac_address] = state
self.latest_data[mac_address]['_updated_at'] = datetime.datetime.now()
self.latest_data[mac_address]['_updated_at'] = datetime.now()
self.callback(mac_address, state)
else:
log.error('Decoded data is null. MAC: %s - Raw: %s', mac_address, raw_data)
log.error(
'Null decoded data. %s - raw: %s', mac_address, raw_data
)
else:
self.mac_blacklist.append(mac_address)
self.mac_blacklist.append(mac_address)
Loading

0 comments on commit e510a04

Please sign in to comment.