Initial commit
This commit is contained in:
commit
19900291f6
104
.gitignore
vendored
Normal file
104
.gitignore
vendored
Normal file
@ -0,0 +1,104 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Michael Smith
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# py-cayenne-lpp-decoder
|
||||
Cayenne Low Power Payload decoder written in Python.
|
||||
See [Cayenne Low Power Payload Documentation](https://mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload) for more information.
|
||||
|
||||
The decoder expects a base64 encoded payload string, commonly used in LoRaWAN implementations.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
>>> import cayennelpp
|
||||
>>> print(cayennelpp.lppdecode('A2cBEAVnAP8='))
|
||||
[{'channel': 3, 'type': 'Temperature Sensor', 'value': 27.2}, {'channel': 5, 'type': 'Temperature Sensor',
|
||||
'value': 25.5}]
|
||||
|
||||
```
|
||||
139
cayennelpp.py
Normal file
139
cayennelpp.py
Normal file
@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
|
||||
def one_per_bit_unsigned(data):
|
||||
return int.from_bytes(data, byteorder='big', signed=False)
|
||||
|
||||
|
||||
def point_zero_one_per_bit_signed(data):
|
||||
return int.from_bytes(data, byteorder='big', signed=True) / 100
|
||||
|
||||
|
||||
def temperature(data):
|
||||
return int.from_bytes(data, byteorder='big', signed=True) / 10
|
||||
|
||||
|
||||
def humidity(data):
|
||||
return int.from_bytes(data, byteorder='big', signed=False) / 2
|
||||
|
||||
|
||||
def accelerometer(data):
|
||||
x = int.from_bytes(data[:2], byteorder='big', signed=True) / 1000
|
||||
y = int.from_bytes(data[2:4], byteorder='big', signed=True) / 1000
|
||||
z = int.from_bytes(data[4:6], byteorder='big', signed=True) / 1000
|
||||
|
||||
return {'x': x, 'y': y, 'z': z}
|
||||
|
||||
|
||||
def barometer(data):
|
||||
return int.from_bytes(data, byteorder='big', signed=False) / 10
|
||||
|
||||
|
||||
def gyrometer(data):
|
||||
x = int.from_bytes(data[:2], byteorder='big', signed=True) / 100
|
||||
y = int.from_bytes(data[2:4], byteorder='big', signed=True) / 100
|
||||
z = int.from_bytes(data[4:6], byteorder='big', signed=True) / 100
|
||||
|
||||
return {'x': x, 'y': y, 'z': z}
|
||||
|
||||
|
||||
def gps_location(data):
|
||||
lat = int.from_bytes(data[:3], byteorder='big', signed=True) / 10000
|
||||
lon = int.from_bytes(data[3:6], byteorder='big', signed=True) / 10000
|
||||
alt = int.from_bytes(data[6:], byteorder='big', signed=True) / 100
|
||||
|
||||
return {'lat': lat, 'lon': lon, 'alt': alt}
|
||||
|
||||
|
||||
DATA_TYPES = {
|
||||
b'\x00': {
|
||||
"name": "Digital Input",
|
||||
"size": 1,
|
||||
"decoder": one_per_bit_unsigned
|
||||
},
|
||||
b'\x01': {
|
||||
"name": "Digital Output",
|
||||
"size": 1,
|
||||
"decoder": one_per_bit_unsigned
|
||||
},
|
||||
b'\x02': {
|
||||
"name": "Analog Input",
|
||||
"size": 2,
|
||||
"decoder": point_zero_one_per_bit_signed
|
||||
},
|
||||
b'\x03': {
|
||||
"name": "Analog Output",
|
||||
"size": 2,
|
||||
"decoder": point_zero_one_per_bit_signed
|
||||
},
|
||||
b'\x65': {
|
||||
"name": "Illuminance Sensor",
|
||||
"size": 2,
|
||||
"decoder": one_per_bit_unsigned
|
||||
},
|
||||
b'\x66': {
|
||||
"name": "Presence Sensor",
|
||||
"size": 1,
|
||||
"decoder": one_per_bit_unsigned
|
||||
},
|
||||
b'\x67': {
|
||||
"name": "Temperature Sensor",
|
||||
"size": 2,
|
||||
"decoder": temperature
|
||||
},
|
||||
b'\x68': {
|
||||
"name": "Humidity Sensor",
|
||||
"size": 1,
|
||||
"decoder": humidity
|
||||
},
|
||||
b'\x71': {
|
||||
"name": "Accelerometer",
|
||||
"size": 6,
|
||||
"decoder": accelerometer
|
||||
},
|
||||
b'\x73': {
|
||||
"name": "Barometer",
|
||||
"size": 2,
|
||||
"decoder": barometer
|
||||
},
|
||||
b'\x86': {
|
||||
"name": "Gyrometer",
|
||||
"size": 6,
|
||||
"decoder": gyrometer
|
||||
},
|
||||
b'\x88': {
|
||||
"name": "GPS Location",
|
||||
"size": 9,
|
||||
"decoder": gps_location
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def lppdecode(base64_string):
|
||||
cursor = 0
|
||||
result = []
|
||||
payload = base64.b64decode(base64_string)
|
||||
|
||||
while cursor < len(payload):
|
||||
data_channel = int.from_bytes(payload[cursor:cursor + 1],
|
||||
byteorder='big')
|
||||
cursor += 1
|
||||
data_type = payload[cursor:cursor + 1]
|
||||
if data_type in DATA_TYPES:
|
||||
cursor += 1
|
||||
name = DATA_TYPES[data_type]['name']
|
||||
size = DATA_TYPES[data_type]['size']
|
||||
decoder = DATA_TYPES[data_type]['decoder']
|
||||
result.append(
|
||||
{"channel": data_channel,
|
||||
"type": name,
|
||||
"value": decoder(payload[cursor:cursor + size])})
|
||||
cursor += size
|
||||
else:
|
||||
# Unknown LPP data type
|
||||
result = None
|
||||
break
|
||||
|
||||
return result
|
||||
66
test_decoder.py
Normal file
66
test_decoder.py
Normal file
@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import cayennelpp
|
||||
|
||||
|
||||
class DecoderTestCase(unittest.TestCase):
|
||||
"""Unit tests for Cayenne LPP decoder."""
|
||||
|
||||
def test_two_temperature_sensors(self):
|
||||
result = cayennelpp.lppdecode('A2cBEAVnAP8=')
|
||||
self.assertEqual(result,
|
||||
[{'channel': 3,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': 27.2},
|
||||
{'channel': 5,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': 25.5}])
|
||||
|
||||
def test_temperature_and_acceleration_sensors(self):
|
||||
result = cayennelpp.lppdecode('AWf/1wZxBNL7LgAA')
|
||||
self.assertEqual(result,
|
||||
[{'channel': 1,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': -4.1},
|
||||
{'channel': 6,
|
||||
'type': 'Accelerometer',
|
||||
'value': {'x': 1.234, 'y': -1.234, 'z': 0.0}}])
|
||||
|
||||
def test_decode_gps(self):
|
||||
result = cayennelpp.lppdecode('AYgGdl/ylgoAA+g=')
|
||||
self.assertEqual(result,
|
||||
[{'channel': 1,
|
||||
'type': 'GPS Location',
|
||||
'value': {'lat': 42.3519,
|
||||
'lon': -87.9094,
|
||||
'alt': 10.0}}])
|
||||
|
||||
def test_decode_all_previous_tests_combined(self):
|
||||
result = cayennelpp.lppdecode(
|
||||
'AWcBEAVnAP8CZ//XA3EE0vsuAAAEiAZ2X/KWCgAD6A==')
|
||||
self.assertEqual(result,
|
||||
[{'channel': 1,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': 27.2},
|
||||
{'channel': 5,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': 25.5},
|
||||
{'channel': 2,
|
||||
'type': 'Temperature Sensor',
|
||||
'value': -4.1},
|
||||
{'channel': 3,
|
||||
'type': 'Accelerometer',
|
||||
'value': {'x': 1.234, 'y': -1.234, 'z': 0.0}},
|
||||
{'channel': 4,
|
||||
'type': 'GPS Location',
|
||||
'value': {'lat': 42.3519,
|
||||
'lon': -87.9094,
|
||||
'alt': 10.0}}])
|
||||
|
||||
def test_decode_unknown_lpp_data_type(self):
|
||||
result = cayennelpp.lppdecode('AQQBEA==')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user