Skip to content

Commit

Permalink
Add TheengsDecoder python module and examples/tests (#49)
Browse files Browse the repository at this point in the history
* Add TheengsDecoder python module and examples/tests
  • Loading branch information
h2zero authored Jan 11, 2022
1 parent 48eba19 commit 5998f2c
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
/CMakeFiles
/decoder.dir
/x64
/python/_skbuild
/python/dist
/python/TheengsDecoder.egg-info
.vscode
.pio
.CMakeFiles
Expand Down
41 changes: 41 additions & 0 deletions docs/use/python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Using with Python

## Dependencies
Building this module it requires scikit-build and cmake, if not already installed you will need to open a terminal and execute:
```
pip install scikit-build
apt-get install cmake
```

## Installation

From a terminal cd to `DECODER_FOLDER/python` folder and execute:
```
pip install .
```

## Using

`import TheengsDecoder`

The library includes a BLE decoder [example](./../../examples/python/ScanAndDecode.py). To run the example, open the folder [ScanAndDecode](./../../examples/python/ScanAndDecode.py) in a terminal and type 'python ScanAndDecode.py`

If Theengs Decoder recognized a device, it will print a message like the example below, otherwise None.
```
TheengsDecoder found device: {"brand":"Xiaomi","model":"LYWSD03MMC","model_id":"LYWSD03MMC_ATC","tempc":26.3,"tempf":79.34,"hum":49,"batt":29,"volt":2.487}
```

Additionally the example will print the properties of the device as well as the brand and model using the `getProperties` and `getAttributes` methods. The output of these looks like:
```
{"properties":{"volt":{"unit":"V","name":"voltage"},"x_axis":{"unit":"int","name":"x_axis"},"y_axis":{"unit":"int","name":"y_axis"},"z_axis":{"unit":"int","name":"z_axis"},"tempc":{"unit":"°C","name":"temperature"},"hum":{"unit":"%","name":"humidity"}}}
brand: Mokosmart , model: BeaconX Pro
```

These functions are useful for passing the data to HomeAssistant or other home automation/monitoring services.

## Methods

- `decodeBLE(string)` Returns a string with the decoded data in JSON format or None.
- `getProperties('model_id string')` Returns the properties (string) of the given model ID or None
- `getAttribute('model_id string', 'attribute string')` Return the value (string) of named attrubte of the model ID or None.
5 changes: 5 additions & 0 deletions examples/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Scan and decode demo

## Requirements
- bleak: `pip install bleak`
- TheengsDecoder: (not available on PyPy yet) see the README in REPO_FOLDER/python for installation.
50 changes: 50 additions & 0 deletions examples/python/ScanAndDecode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import json
import struct
from bleak import BleakScanner
from TheengsDecoder import decodeBLE as dble
from TheengsDecoder import getProperties, getAttribute

def detection_callback(device, advertisement_data):
print(device.address, "RSSI:", device.rssi, advertisement_data)
data_json = {}

if advertisement_data.service_data:
dstr = list(advertisement_data.service_data.keys())[0]
# TheengsDecoder only accepts 16 bit uuid's, this converts the 128 bit uuid to 16 bit.
data_json['servicedatauuid'] = dstr[4:8]
dstr = str(list(advertisement_data.service_data.values())[0].hex())
data_json['servicedata'] = dstr

if advertisement_data.manufacturer_data:
dstr = str(struct.pack('<H', list(advertisement_data.manufacturer_data.keys())[0]).hex())
dstr += str(list(advertisement_data.manufacturer_data.values())[0].hex())
data_json['manufacturerdata'] = dstr

if advertisement_data.local_name:
data_json['name'] = advertisement_data.local_name

if data_json:
print("data sent to decoder: ", json.dumps(data_json))
data_json = dble(json.dumps(data_json))
print("TheengsDecoder found device:", data_json)

if data_json:
dev = json.loads(data_json)
print(getProperties(dev['model_id']))
brand = getAttribute(dev['model_id'], 'brand')
model = getAttribute(dev['model_id'], 'model')
print("brand:", brand, ", model:", model)


async def main():
scanner = BleakScanner()
scanner.register_detection_callback(detection_callback)
await scanner.start()
await asyncio.sleep(5.0)
await scanner.stop()

for d in scanner.discovered_devices:
print(d)

asyncio.run(main())
18 changes: 18 additions & 0 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.3)

project(decoder VERSION 0.1.0)
find_package(PythonExtensions REQUIRED)

add_library(_decoder MODULE TheengsDecoder/_decoder.cpp ../src/decoder.cpp)
python_extension_module(_decoder)

target_include_directories(_decoder
PUBLIC
$<INSTALL_INTERFACE:../arduino_json>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../src/arduino_json/src>
${CMAKE_CURRENT_SOURCE_DIR}/../src
)

target_compile_features(_decoder PRIVATE cxx_std_11)

install(TARGETS _decoder LIBRARY DESTINATION TheengsDecoder)
25 changes: 25 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Theengs Decoder

## dependencies
Building this module it requires scikit-build and cmake, if not already installed you will need to open a terminal and execute:
```
pip install scikit-build
apt-get install cmake
```

## installation

From a terminal cd to this folder and execute:
```
python setup.py install --user
```

## using

`import TheengsDecoder`

## methods

- `decodeBLE(string)` Returns a new string with the decoded data in json format or None.
- `getProperties('model_id string')` Returns the properties (string) of the given model ID or None
- `getAttribute('model_id string', 'attribute string')` Return the value (string) of named attrubte of the model ID or None.
3 changes: 3 additions & 0 deletions python/TheengsDecoder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._decoder import decodeBLE # noqa: F401
from ._decoder import getAttribute # noqa: F401
from ._decoder import getProperties # noqa: F401
110 changes: 110 additions & 0 deletions python/TheengsDecoder/_decoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Python includes
#include <Python.h>

#include "decoder.h"

// STD includes
#include <stdio.h>

//-----------------------------------------------------------------------------
static PyObject *decode_BLE(PyObject *self, PyObject *args)
{
// Unpack a string from the arguments
const char *strArg;
if (!PyArg_ParseTuple(args, "s", &strArg))
return NULL;

StaticJsonDocument<1024> doc;
DeserializationError err = deserializeJson(doc, strArg);
if (!err) {
TheengsDecoder decoder;
JsonObject bleObject;
bleObject = doc.as<JsonObject>();

if (decoder.decodeBLEJson(bleObject)) {
std::string buf;
bleObject.remove("servicedata");
bleObject.remove("manufacturerdata");
bleObject.remove("servicedatauuid");
serializeJson(bleObject, buf);
return Py_BuildValue("s", buf.c_str());
}
}

Py_RETURN_NONE;
}

static PyObject *decode_getTheengProperties(PyObject *self, PyObject *args)
{
const char *strArg;
if (!PyArg_ParseTuple(args, "s", &strArg))
return NULL;

TheengsDecoder decoder;
std::string prop = decoder.getTheengProperties(strArg);
if (!prop.empty()) {
return Py_BuildValue("s", prop.c_str());
}

Py_RETURN_NONE;
}

static PyObject *decode_getTheengAttribute(PyObject *self, PyObject *args)
{
const char *model;
const char *att;
if (!PyArg_ParseTuple(args, "ss", &model, &att))
return NULL;

TheengsDecoder decoder;
std::string prop = decoder.getTheengAttribute(model, att);
if (!prop.empty()) {
return Py_BuildValue("s", prop.c_str());
}

Py_RETURN_NONE;
}

//-----------------------------------------------------------------------------
static PyMethodDef decoder_methods[] = {
{
"decodeBLE",
decode_BLE,
METH_VARARGS,
"Decodes a BLE advertisement packet into JSON data."
},
{
"getAttribute",
decode_getTheengAttribute,
METH_VARARGS,
"Decodes a BLE advertisement packet into JSON data."
},
{
"getProperties",
decode_getTheengProperties,
METH_VARARGS,
"Decodes a BLE advertisement packet into JSON data."
},
{NULL, NULL, 0, NULL} /* Sentinel */
};

//-----------------------------------------------------------------------------
#if PY_MAJOR_VERSION < 3
PyMODINIT_FUNC init_decoder(void)
{
(void) Py_InitModule("_decoder", decoder_methods);
}
#else /* PY_MAJOR_VERSION >= 3 */
static struct PyModuleDef decoder_module_def = {
PyModuleDef_HEAD_INIT,
"_decoder",
"Internal \"_decoder\" module",
-1,
decoder_methods
};

PyMODINIT_FUNC PyInit__decoder(void)
{
return PyModule_Create(&decoder_module_def);
}
#endif /* PY_MAJOR_VERSION >= 3 */
2 changes: 2 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build-system]
requires = ["setuptools", "wheel", "scikit-build", "cmake", "ninja"]
11 changes: 11 additions & 0 deletions python/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from skbuild import setup

setup(
name="TheengsDecoder",
version="0.1.0",
description="A message decoder for the Internet of Things",
author='Theengs',
license=" GPL-3.0 License",
packages=['TheengsDecoder'],
)

19 changes: 19 additions & 0 deletions tests/python_ble/decoder_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from TheengsDecoder import decodeBLE as dble
from TheengsDecoder import getProperties
from TheengsDecoder import getAttribute
import json

x = {"servicedata":"712098004a63b6658d7cc40d071003f32600"}
z = dble(json.dumps(x))
print(z, "\n")

p = json.loads(z)
print(getProperties(p['model_id']), "\n")
brand = getAttribute(p['model_id'], 'brand')
model = getAttribute(p['model_id'], 'model')
print("brand:", brand, ", model:", model)

y = {}
z = dble(json.dumps(y))
print("decoder result (None expected): ", z)
print("Done")

0 comments on commit 5998f2c

Please sign in to comment.