• Stars
    star
    194
  • Rank 200,219 (Top 4 %)
  • Language
    Python
  • License
    MIT License
  • Created about 8 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Python package for communicating with RuuviTag BLE Sensor and for decoding sensor data from broadcasted data

RuuviTag Sensor Python Package

Build Status License PyPI version PyPI downloads Python versions

ruuvitag-sensor is a Python package for communicating with RuuviTag BLE Sensor and for decoding measurement data from broadcasted BLE data.

Requirements

NOTE: Version 2.0 contains method renames. When using a version prior to 2.0, check the documentation and examples from PyPI or in GitHub, switch to the correct release tag from switch branches/tags.

Installation

Install the latest released version

$ python -m pip install ruuvitag-sensor

Install the latest development version

$ python -m venv .venv
$ source .venv/bin/activate
$ python -m pip install git+https://github.com/ttu/ruuvitag-sensor

# For development, clone this repository and install for development in editable mode
$ python -m pip install -e .[dev]

Full installation guide for Raspberry PI & Raspbian

Usage

The package provides 3 ways to fetch data from sensors:

  1. Synchronously with callback
  2. Asynchronously with async/await (BETA)
  3. Observable streams with ReactiveX

RuuviTag sensors can be identified using MAC addresses. Methods return a tuple with MAC and sensor data payload.

('D2:A3:6E:C8:E0:25', {'data_format': 5, 'humidity': 47.62, 'temperature': 23.58, 'pressure': 1023.68, 'acceleration': 993.2331045630729, 'acceleration_x': -48, 'acceleration_y': -12, 'acceleration_z': 992, 'tx_power': 4, 'battery': 2197, 'movement_counter': 0, 'measurement_sequence_number': 88, 'mac': 'd2a36ec8e025', 'rssi': -80})

1. Get sensor data synchronously with callback

get_data calls the callback whenever a RuuviTag sensor broadcasts data. This method is the preferred way to use the library.

from ruuvitag_sensor.ruuvi import RuuviTagSensor


def handle_data(found_data):
    print(f"MAC {found_data[0]}")
    print(f"Data {found_data[1]}")

if __name__ == "__main__":
    RuuviTagSensor.get_data(handle_data)

The line if __name__ == "__main__": is required on Windows and macOS due to the way the multiprocessing library works. It is not required on Linux, but it is recommended. It is omitted from the rest of the examples below.

The optional list of MACs and run flag can be passed to the get_data function. The callback is called only for MACs in the list and setting the run flag to false will stop execution. If the run flag is not passed, the function will execute forever.

from ruuvitag_sensor.ruuvi import RuuviTagSensor, RunFlag

counter = 10
# RunFlag for stopping execution at desired time
run_flag = RunFlag()

def handle_data(found_data):
    print(f"MAC: {found_data[0]}")
    print(f"Data: {found_data[1]}")

    global counter
    counter = counter - 1
    if counter < 0:
        run_flag.running = False

# List of MACs of sensors which will execute callback function
macs = ["AA:2C:6A:1E:59:3D", "CC:2C:6A:1E:59:3D"]

RuuviTagSensor.get_data(handle_data, macs, run_flag)

2. Get sensor data asynchronously

NOTE: Asynchronous functionality is currently in beta state and works only with Bleak-adapter.

import asyncio
from ruuvitag_sensor.ruuvi import RuuviTagSensor


async def main():
    async for found_data in RuuviTagSensor.get_data_async():
        print(f"MAC: {found_data[0]}")
        print(f"Data: {found_data[1]}")

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())

The optional list of MACs and run flag can be passed to the get_data_async function.

3. Get sensor data with observable streams (ReactiveX / RxPY)

RuuviTagReactive is a reactive wrapper and background process for RuuviTagSensor get_data. An optional MAC address list can be passed on the initializer and execution can be stopped with the stop function.

from ruuvitag_sensor.ruuvi_rx import RuuviTagReactive
from reactivex import operators as ops

ruuvi_rx = RuuviTagReactive()

# Print all notifications
ruuvi_rx.get_subject().\
    subscribe(print)

# Print only last data every 10 seconds for F4:A5:74:89:16:57
ruuvi_rx.get_subject().pipe(
      ops.filter(lambda x: x[0] == "F4:A5:74:89:16:57"),
      ops.buffer_with_time(10.0)
    ).subscribe(lambda data: print(data[len(data) - 1]))

# Execute only every time when temperature changes for F4:A5:74:89:16:57
ruuvi_rx.get_subject().pipe(
      ops.filter(lambda x: x[0] == "F4:A5:74:89:16:57"),
      ops.map(lambda x: x[1]["temperature"]),
      ops.distinct_until_changed()
    ).subscribe(lambda x: print(f"Temperature changed: {x}"))

# Close all connections and stop bluetooth communication
ruuvi_rx.stop()

More samples and a simple HTTP server under the examples directory.

Check the official documentation of ReactiveX and the list of operators.

Other helper methods

Get data for specified sensors for a specific duration

get_data_for_sensors will collect the latest data from sensors for a specified duration.

from ruuvitag_sensor.ruuvi import RuuviTagSensor

# List of MACs of sensors which data will be collected
# If list is empty, data will be collected for all found sensors
macs = ["AA:2C:6A:1E:59:3D", "CC:2C:6A:1E:59:3D"]
# get_data_for_sensors will look data for the duration of timeout_in_sec
timeout_in_sec = 4

data = RuuviTagSensor.get_data_for_sensors(macs, timeout_in_sec)

# Dictionary will have latest data for each sensor
print(data["AA:2C:6A:1E:59:3D"])
print(data["CC:2C:6A:1E:59:3D"])

NOTE: This method shouldn't be used for a long duration with a short timeout. get_data_for_sensors will start and stop a new BLE scanning process with every method call. For long-running processes, it is recommended to use the get_data-method.

Get data from a sensor

NOTE: For a single sensor it is recommended to use RuuviTagSensor.get_data or RuuviTagSensor.get_data_for_sensors methods instead of RuuviTag-class.

from ruuvitag_sensor.ruuvitag import RuuviTag

sensor = RuuviTag("AA:2C:6A:1E:59:3D")

# update state from the device
state = sensor.update()

# get latest state (does not get it from the device)
state = sensor.state

print(state)
Find sensors

RuuviTagSensor.find_ruuvitags and RuuviTagSensor.find_ruuvitags_async methods will execute forever and when a new RuuviTag sensor is found, it will print its MAC address and state at that moment. This function can be used with command-line applications. Logging must be enabled and set to print to the console.

from ruuvitag_sensor.ruuvi import RuuviTagSensor
import ruuvitag_sensor.log

ruuvitag_sensor.log.enable_console()

RuuviTagSensor.find_ruuvitags()

Using different Bluetooth device

If you have multiple Bluetooth devices installed, a device to be used might not be the default (Linux: hci0). The device can be passed with a bt_device-parameter.

from ruuvitag_sensor.ruuvi import RuuviTagSensor
from ruuvitag_sensor.ruuvitag import RuuviTag

sensor = RuuviTag("F4:A5:74:89:16:57", "hci1")

RuuviTagSensor.find_ruuvitags("hci1")

data = RuuviTagSensor.get_data_for_sensors(bt_device="hci1")

RuuviTagSensor.get_data(lambda x: print(f"{x[0]} - {x[1]}"), bt_device=device))

Parse data

from ruuvitag_sensor.data_formats import DataFormats
from ruuvitag_sensor.decoder import get_decoder

full_data = "043E2A0201030157168974A51F0201060303AAFE1716AAFE10F9037275752E76692F23416A5558314D417730C3"
data = full_data[26:]

# convert_data returns tuple which has Data Format type and encoded data
(data_format, encoded) = DataFormats.convert_data(data)

sensor_data = get_decoder(data_format).decode_data(encoded)

print(sensor_data)
# {'temperature': 25.12, 'identifier': '0', 'humidity': 26.5, 'pressure': 992.0}

Data Formats

Example data has data from 4 sensors with different firmware.

  • 1st is Data Format 2 (URL), the identifier is None as the sensor doesn't broadcast any identifier data
  • 2nd is Data Format 4 (URL) and it has an identifier character
  • 3rd is Data Format 3 (RAW)
  • 4th is Data Format 5 (RAW v2)
{
'CA:F7:44:DE:EB:E1': { 'data_format': 2, 'temperature': 22.0, 'humidity': 28.0, 'pressure': 991.0, 'identifier': None, 'rssi': None },
'F4:A5:74:89:16:57': { 'data_format': 4, 'temperature': 23.24, 'humidity': 29.0, 'pressure': 991.0, 'identifier': '0', 'rssi': None },
'A3:GE:2D:91:A4:1F': { 'data_format': 3, 'battery': 2899, 'pressure': 1027.66, 'humidity': 20.5, 'acceleration': 63818.215675463696, 'acceleration_x': 200.34, 'acceleration_y': 0.512, 'acceleration_z': -200.42, 'temperature': 26.3, 'rssi': None },
'CB:B8:33:4C:88:4F': { 'data_format': 5, 'battery': 2.995, 'pressure': 1000.43, 'mac': 'cbb8334c884f', 'measurement_sequence_number': 2467, 'acceleration_z': 1028, 'acceleration': 1028.0389097694697, 'temperature': 22.14, 'acceleration_y': -8, 'acceleration_x': 4, 'humidity': 53.97, 'tx_power': 4, 'movement_counter': 70, 'rssi': -65 }
}

Note on Data Format 2 and 4

There is no reason to use Data Format 2 or 4.

The original reason to use the URL-encoded data was to use Google's Nearby notifications to let users view tags without the need to install any app. Since Google's Nearby has been discontinued, there isn't any benefit in using the Eddystone format anymore.

Logging and Printing to the console

Logging can be enabled by importing ruuvitag_sensor.log. Console print can be enabled by calling ruuvitag_sensor.log.enable_console(). The command line application has console logging enabled by default.

from ruuvitag_sensor.ruuvi import RuuviTagSensor
import ruuvitag_sensor.log

ruuvitag_sensor.log.enable_console()

data = RuuviTagSensor.get_data_for_sensors()

print(data)

Log all events to log-file

By default only errors are logged to ruuvitag_sensor.log-file. The level can be changed by changing FileHandler's log level.

import logging
from ruuvitag_sensor.log import log
from ruuvitag_sensor.ruuvi import RuuviTagSensor

for handler in log.handlers:
    if isinstance(handler, logging.FileHandler):
        handler.setLevel(logging.DEBUG)

data = RuuviTagSensor.get_data_for_sensors()

A custom event handler for a specific log event

If custom functionality is required when a specific event happens, e.g. exit when a specific sensor is blacklisted, logging event handlers can be utilized for this functionality.

from logging import StreamHandler
from ruuvitag_sensor.log import log
from ruuvitag_sensor.ruuvi import RuuviTagSensor


class ExitHandler(StreamHandler):

    def emit(self, record):
        if (record.levelname != "DEBUG"):
            return
        msg = self.format(record)
        if "Blacklisting MAC F4:A5:74:89:16:57E" in msg:
            exit(1)


exit_handler = ExitHandler()
log.addHandler(exit_handler)

data = RuuviTagSensor.get_data_for_sensors()

Command line application

$ python ruuvitag_sensor -h

usage: ruuvitag_sensor [-h] [-g MAC_ADDRESS] [-d BT_DEVICE] [-f] [-l] [-s] [--version]

optional arguments:
  -h, --help            show this help message and exit
  -g MAC_ADDRESS, --get MAC_ADDRESS
                        Get data
  -d BT_DEVICE, --device BT_DEVICE
                        Set Bluetooth device id (default hci0)
  -f, --find            Find broadcasting RuuviTags
  -l, --latest          Get latest data for found RuuviTags
  -s, --stream          Stream broadcasts from all RuuviTags
  --version             show program's version number and exit

BlueZ

BlueZ works only on Linux. When using BlueZ, Windows and macOS support is only for testing with hard-coded data and for data decoding.

BlueZ tools require superuser rights.

Install BlueZ.

$ sudo apt-get install bluez bluez-hcidump

ruuvitag-sensor package uses internally hciconfig, hcitool and hcidump. These tools are deprecated. In case tools are missing, an older version of BlueZ is required (Issue)

BlueZ limitations

ruuvitag-sensor package uses BlueZ to listen to broadcasted BL information (uses hciconf, hcitool, hcidump). Implementation does not handle well all unexpected errors or changes, e.g. when the adapter is busy, rebooted or powered down.

In case of errors, the application tries to exit immediately, so it can be automatically restarted.

Bleak

Bleak is not installed automatically with ruuvitag-sensor package. Install it manually from PyPI.

$ python -m pip install bleak

Add environment variable RUUVI_BLE_ADAPTER with value Bleak. E.g.

$ export RUUVI_BLE_ADAPTER="Bleak"

Bleak supports only async methods.

import asyncio
from ruuvitag_sensor.ruuvi import RuuviTagSensor


async def main():
    async for data in RuuviTagSensor.get_data_async():
        print(data)

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())

Check get_async_bleak and other async examples from examples directory.

Bleak dummy BLE data

Bleak-adapter has a development-time generator for dummy data, which can be useful during the development, if no sensors are available. Set RUUVI_BLE_ADAPTER environment variable to bleak_dev.

Bleson

Current state and known bugs in issue #78.

Bleson works with Linux, macOS and partially with Windows.

Bleson is not installed automatically with ruuvitag-sensor package. Install it manually from GitHub.

$ pip install git+https://github.com/TheCellule/python-bleson

Add environment variable RUUVI_BLE_ADAPTER with value Bleson. E.g.

$ export RUUVI_BLE_ADAPTER="Bleson"

NOTE: On macOS, only Data Format 5 works, as macOS doesn't advertise MAC address and only DF5 has MAC in sensor payload. RuuviTag-class doesn't work with macOS.

NOTE: On Windows, Bleson requires Python 3.6. Unfortunately on Windows, Bleson doesn't send any payload for the advertised package, so it is still unusable.

Python 2.x and 3.6 and below

Last version of ruuvitag-sensor with Python 2.x and <3.7 support is 1.2.1.

Branch / Tag / commit

$ git checkout release/1.2.1

Install from PyPI

$ python -m pip install ruuvitag-sensor==1.2.1

Examples

Examples are in examples directory, e.g.

Changelog

Changelog

Developer notes

Notes for developers who are interested in developing ruuvitag-sensor package or are interested in its internal functionality.

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

Licensed under the MIT License.

More Repositories

1

json-flatfile-datastore

Simple JSON flat file data store with support for typed and dynamic data.
C#
430
star
2

dotnet-fake-json-server

Fake JSON Server is a Fake REST API that can be used as a Back End for prototyping or as a template for a CRUD Back End.
C#
385
star
3

kotlin-is-like-csharp

Compare the syntax of Kotlin vs C# through short code examples
HTML
67
star
4

lego-boost-app

React Application for controlling Lego Boost from the browser with Web Bluetooth API
TypeScript
66
star
5

csharp-tutorial

Code examples for C# language tutorial
C#
50
star
6

lego-boost-browser

Library for controlling Lego Boost with Web Bluetooth API
TypeScript
29
star
7

node-movehub-async

Simple to use asynchronous methods for the Lego Boost Move Hub
JavaScript
16
star
8

office-slack-bot

BotKit Slack bot. Show free meeting rooms, book rooms (Google Calendar API) etc.
JavaScript
12
star
9

map-service

Leaflet map and .NET Core backend. Web API, Websockets, Marten, Postgres
C#
9
star
10

lego-boost-ai

Lego Boost AI and manual control
JavaScript
9
star
11

node-yamaha-avr

Node.js module for controlling Yamaha RX-series receiver
JavaScript
6
star
12

patterns-and-principles

Code examples for Patterns and Principles - are these important anymore? training
C#
6
star
13

chat-server

Small implementation of a server for a chat application for testing Docker
C#
5
star
14

csharp-rx-koans

C# Reactive Extensions (Rx) Koans forked from https://archive.codeplex.com/?p=rxkoans and ported to .NET 5
C#
4
star
15

godot-dino-run

Small game inspired by Chrome Dino Run
GDScript
4
star
16

Strategy-Game

Game architecture and engine project
C#
3
star
17

News-Service

News web site with voting (in progress)
Python
3
star
18

sensordata-node-restapi

Node Express REST API (ES2015 & ESnext) for cubesensors-iot-azure (https://github.com/ttu/cubesensors-iot-azure)
JavaScript
3
star
19

mono-raspberry-app

Flighdata service and Raspberry PI test. C# (Mono), NancyFx, SignalR, Redis, ZeroMQ, ProtoBuf, Raspberry GPIO
JavaScript
3
star
20

Match-Rate

Match rating web site and Windows Phone 7 application
C#
2
star
21

strava-segment-explorer

TypeScript
2
star
22

todomvc-fake-server

Redux TodoMVC example converted to use Fake JSON Server as a Back End
JavaScript
2
star
23

Email-Statistics

Creates statistics of mailbox content
C#
2
star
24

godot-platformer

Project for learning Godot
GDScript
1
star
25

hwo-2014

Hello World Open 2014 source code for Mono bot
C#
1
star
26

dummy-sensor-server

REST API and Socket.io server that generates random data in sensor-like format
JavaScript
1
star
27

rabbitmq-zeromq-experiments

Testing RabbitMQ and ZeroMQ with .NET
C#
1
star
28

cubesensors-iot-azure

Processing for (Cube)Sensor data. Python processing, F# REST API, optional Azure Streaming.
F#
1
star
29

multi-camera-system

System for controlling multiple cameras
Python
1
star
30

steward-vagrant

Development environment for The Thing System Steward with Vagrant
Shell
1
star
31

rest-api-presentation

HTML
1
star
32

scratchpad

Random code snippets etc.
JavaScript
1
star
33

Uptime

QT desktop widget for N900 that shows current uptime
1
star
34

miniWiki

Create read-only wiki from text files
JavaScript
1
star
35

Product-Getter

N900 application for finding cheap products from play.com. Play.com's web page has changed, so html processing needs to be fixed.
C++
1
star