pax_global_header00006660000000000000000000000064141313643110014507gustar00rootroot0000000000000052 comment=28f2ba4fba602527d3369c9cfbce16b783916933 pycayennelpp-2.4.0/000077500000000000000000000000001413136431100142215ustar00rootroot00000000000000pycayennelpp-2.4.0/.codacy.yml000066400000000000000000000002231413136431100162610ustar00rootroot00000000000000--- exclude_paths: - '**/tests/**' - '*.md' - 'LICENSE' - 'MANIFEST.in' - 'setup.cfg' - 'setup.py' - 'usetup.py' - 'sdist_upip.py' pycayennelpp-2.4.0/.github/000077500000000000000000000000001413136431100155615ustar00rootroot00000000000000pycayennelpp-2.4.0/.github/workflows/000077500000000000000000000000001413136431100176165ustar00rootroot00000000000000pycayennelpp-2.4.0/.github/workflows/micropython-publish.yml000066400000000000000000000016101413136431100243560ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Micropython Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python usetup.py sdist bdist_wheel rm dist/*.orig twine upload dist/micropython* pycayennelpp-2.4.0/.github/workflows/python-publish.yml000066400000000000000000000015541413136431100233330ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/pycayennelpp* pycayennelpp-2.4.0/.gitignore000066400000000000000000000022641413136431100162150ustar00rootroot00000000000000# 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/ pycayennelpp-2.4.0/.pydocstyle000066400000000000000000000001031413136431100164130ustar00rootroot00000000000000[pydocstyle] inherit = false ignore = D100,D203,D405 match = .*\.pypycayennelpp-2.4.0/.travis.yml000066400000000000000000000011761413136431100163370ustar00rootroot00000000000000language: python python: - "3.6" - "3.7" - "3.8" - "3.9" # command to install dependencies install: - pip install flake8 pytest coverage # preparce codacy coverage report before_script: - bash <(curl -Ls https://coverage.codacy.com/get.sh) download # command to run tests script: - test -z "$(git log -E --grep='^(Merge|((chore|docs|feat|fix|refactor|style|tests)\:))' --invert-grep)" - python3 -m flake8 cayennelpp - python3 -m pytest - python3 -m coverage run -m pytest - python3 -m coverage xml # upload codacy results after_success: - bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage.xml pycayennelpp-2.4.0/CODE_OF_CONDUCT.md000066400000000000000000000054651413136431100170320ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at For answers to common questions about this code of conduct, see [homepage]: https://www.contributor-covenant.org pycayennelpp-2.4.0/CONTRIBUTING.md000066400000000000000000000015741413136431100164610ustar00rootroot00000000000000## Contributing to PyCayenneLPP As a free open source software project (FOSS), PyCayenneLPP welcomes contributions of many forms. Essentially it comes down to either open an Issue or Pull Request on Github. Issues are used (but not limited) to report bugs, make feature requests, or ask questions. In the same way Pull Request are used (but not limited) to provide patches, make improvements to documentation, or add tests. To ensure and maintain code quality and robustness pull requests must pass Travis CI before being merged into master. Further, this projects uses [semantic commit messages](https://seesparkbox.com/foundry/semantic_commit_messages) in the format: `: `, the `scope` can be omitted. ## Code of conduct Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. pycayennelpp-2.4.0/LICENSE000066400000000000000000000020671413136431100152330ustar00rootroot00000000000000MIT License Copyright (c) 2018-2021 Sebastian Meiling 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. pycayennelpp-2.4.0/MANIFEST.in000066400000000000000000000000421413136431100157530ustar00rootroot00000000000000include README.md include LICENSE pycayennelpp-2.4.0/README.md000066400000000000000000000150431413136431100155030ustar00rootroot00000000000000# PyCayenneLPP [![Travis-CI](https://travis-ci.com/smlng/pycayennelpp.svg?branch=master)](https://travis-ci.com/smlng/pycayennelpp) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a47d55068ce348c2a83497d2ab5f07bf)](https://www.codacy.com/gh/smlng/pycayennelpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=smlng/pycayennelpp&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/a47d55068ce348c2a83497d2ab5f07bf)](https://www.codacy.com/gh/smlng/pycayennelpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=smlng/pycayennelpp&utm_campaign=Badge_Coverage) [![PyPi](https://badge.fury.io/py/pycayennelpp.svg)](https://badge.fury.io/py/pycayennelpp) [![GitHub](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/smlng/pycayennelpp/blob/master/LICENSE) A Cayenne Low Power Payload (CayenneLPP) decoder and encoder written in Python. PyCayenneLPP offers a concise interface with proper encoding and decoding functionality for the CayenneLPP format, supporting many sensor types. The project aims for overall high code quality and good test coverage. See also [myDevicesIoT/CayenneLPP](https://github.com/myDevicesIoT/CayenneLPP) for more information on the format and a reference implementation in C++. The project is under active development. Releases will be published on the fly as soon as a certain number of new features and fixes have been made. ## Supported Data Types The following table lists the currently supported data types with the LPP code (which equals IPSO code - 3200), data size in bytes, dimensions, signedness, and data resolution. | Type Name | LPP | Size | Dim | Signed | Resolution | |----------------|-----|------|-----|--------|------------| | Digital Input | 0 | 1 | 1 | False | 1 | | Digital Output | 1 | 1 | 1 | False | 1 | | Analog Input | 2 | 2 | 1 | True | 0.01 | | Analog Output | 3 | 2 | 1 | True | 0.01 | | Generic Sensor | 100 | 4 | 1 | False | 1 | | Illuminance | 101 | 2 | 1 | False | 1 Lux | | Presence | 102 | 1 | 1 | False | 1   | | Temperature | 103 | 2 | 1 | True | 0.1°C | | Humidity | 104 | 1 | 1 | False | 0.5 % | | Accelerometer | 113 | 6 | 3 | True | 0.001 G | | Barometer | 115 | 2 | 1 | False | 0.1 hPa | | Voltage | 116 | 2 | 1 | False | 0.01 V | | Current | 117 | 2 | 1 | False | 0.001 A | | Frequency | 118 | 4 | 1 | False | 1 Hz | | Percentage | 120 | 1 | 1 | False | 1 % | | Altitude | 121 | 2 | 1 | True | 1 m | | Load | 122 | 3 | 1 | True | 0.001 kg | | Concentration | 125 | 2 | 1 | False | 1 | | Power | 128 | 2 | 1 | False | 1 | | Distance | 130 | 4 | 1 | False | 0.001 km | | Energy | 131 | 4 | 1 | False | 0.001 kJ | | Direction | 132 | 2 | 1 | False | 1 ° | | Time | 133 | 4 | 1 | False | 1 s | | Gyrometer | 134 | 6 | 3 | True | 0.01 °/s | | Colour | 135 | 3 | 3 | False | 1 RGB | | Location | 136 | 9 | 3 | True | 0.00001 lat| | | | | | | 0.00001 lon| | | | | | | 0.01 alt | | Switch | 142 | 1 | 1 | False | 1 on/off | ## Getting Started PyCayenneLPP does not have any external dependencies and only uses builtin functions and types of Python 3. It is compatible with all the latest and officially supported Python versions 3.6 and above, though even Python 3.4 will do. Since PyCayenneLPP 1.2.0 MicroPython is officially supported, and published as a separate package under `micropython-pycayennelpp`. ### Python 3 Prerequisites The PyCayenneLPP package is available via PyPi using `pip`. To install it run: ```Shell pip3 install pycayennelpp ``` ### MicroPython Prerequisites Using MicroPythons `upip` module PyCayenneLPP can be installed as follows within MicroPython: ```Python import upip upip.install("micropython-pycayennelpp") ``` Or alternatively run with in a shell: ```Shell micropython -m upip install micropython-pycayennelpp ``` ### Usage Examples The following show how to utilise PyCayenneLPP in your own application to encode and decode data into and from CayenneLPP. The code snippets work with standard Python 3 as well as MicroPython, assuming you have installed the PyCayenneLPP package as shown above. ***Encoding*** ```Python from cayennelpp import LppFrame # create empty frame frame = LppFrame() # add some sensor data frame.add_temperature(0, -1.2) frame.add_humidity(6, 34.5) # get byte buffer in CayenneLPP format buffer = bytes(frame) ``` **Note:** MicroPython does not support `bytes(frame)` utilising the internal method `LppFrame.__bytes__(self)` (yet). Hence, you need to use `LppFrame.to_bytes(self)` instead. ***Decoding*** ```Python from cayennelpp import LppFrame # byte buffer in CayenneLPP format with 1 data item # i.e. on channel 1, with a temperature of 25.5C buffer = bytearray([0x01, 0x67, 0x00, 0xff]) # create frame from bytes frame = LppFrame().from_bytes(buffer) # print the frame and its data print(frame) ``` ***JSON Encoding*** The LppUtil class provides helper function for proper JSON encoding of PyCayenneLpp types, i.e. LppFrame, LppData and LppType. ```python import json from cayennelpp import LppFrame, LppUtil # create empty frame frame = LppFrame() # add some sensor data frame.add_temperature(0, -1.2) frame.add_humidity(6, 34.5) # json encoding print(json.dumps(frame, default=LppUtil.json_encode, indent=2)) ``` There are two wrapper functions to explicitly encode the LPP type as a number or string, number being default for `LppUtil.json_encode` (see above): ```python # type as number print(json.dumps(frame, default=LppUtil.json_encode_type_int, indent=2)) # type as string print(json.dumps(frame, default=LppUtil.json_encode_type_str, indent=2)) ``` ## Contributing Contributing to a free open source software project can take place in many different ways. Feel free to open issues and create pull requests to help improving this project. Each pull request has to pass some automatic tests and checks run by Travis-CI before being merged into the master branch. Please take note of the [contributing guidelines](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). ## License This is a free open source software project published under the [MIT License](LICENSE). pycayennelpp-2.4.0/cayennelpp/000077500000000000000000000000001413136431100163575ustar00rootroot00000000000000pycayennelpp-2.4.0/cayennelpp/__init__.py000066400000000000000000000005501413136431100204700ustar00rootroot00000000000000"""PyCayenneLPP: A decoder and encoder for the CayenneLPP data format. PyCayenneLPP offers a concise interface with proper encoding and decoding functionality for the CayenneLPP format, supporting many sensor types. """ from .lpp_data import LppData from .lpp_frame import LppFrame from .lpp_util import LppUtil __all__ = ['LppData', 'LppFrame', 'LppUtil'] pycayennelpp-2.4.0/cayennelpp/lpp_data.py000066400000000000000000000045111413136431100205160ustar00rootroot00000000000000from .lpp_type import LppType class LppData(object): """A LPP data object. A LppData contains all information related to a single sensor value, i.e. its type, value, and the channel for multiplexing various sensor devices with values of the same type. Attributes: chn (int): data channel number type (LppType): data type value (tuple): data value(s) """ def __init__(self, chn, type_, value): """Create a LppData object with given attributes.""" self.channel = chn self.type = LppType.get_lpp_type(type_) if self.type is None: raise ValueError("Invalid LPP data type!") if value is None: raise ValueError("Empty value!") if not isinstance(value, tuple): value = (value,) if not len(value) == self.type.dimension: raise ValueError("Invalid number of data values!") for i in range(self.type.dimension): if not self.type.signs[i] and value[i] < 0: raise ValueError("Invalid value, must be positive!") self.value = value def __bytes__(self): """Return a byte string representation of this LppData object.""" hdr_buf = bytearray([self.channel, int(self.type)]) dat_buf = self.type.encode(self.value) buf = hdr_buf + dat_buf return bytes(buf) def __len__(self): """Return the length of the LppData byte string representation.""" return self.type.size + 2 def __str__(self): """Return a pretty string representation of the LppData object.""" return 'LppData(channel = {}, type = {}, value = {})'.format( self.channel, self.type.name, str(self.value)) def to_bytes(self): """Explicit wrapper for MicroPython support.""" return self.__bytes__() @classmethod def from_bytes(class_object, buf): """Parse a given byte string and return a LppData object.""" if len(buf) < 3: raise BufferError("Invalid buffer size!") chn = buf[0] type_ = buf[1] lpp_type = LppType.get_lpp_type(type_) size = lpp_type.size if len(buf) < size + 2: raise BufferError("Buffer too small!") value = lpp_type.decode(buf[2:(2 + size)]) return class_object(chn, type_, value) pycayennelpp-2.4.0/cayennelpp/lpp_frame.py000066400000000000000000000204311413136431100206760ustar00rootroot00000000000000from .lpp_data import LppData class LppFrame(object): """A LPP frame instance. A LppFrame can hold multiple LppData objects to be encoded or decoded at once. It also provides convenient helper functions to easily add sensor values of certain type. Attributes: data (list): a list of LppData objects maxsize (int): (optional) byte size limit """ def __init__(self, data=None, maxsize=0): """Create a LppFrame object with (optional) arguments.""" self._maxsize = maxsize self._data = [] if data: for d in data: self.__add_data_item(d) def __str__(self): """Return a pretty string representation of the LppFrame object.""" out = "LppFrame(data = [" if self._data: out = out + "\n" for d in self._data: out = out + " " + str(d) + "\n" out = out + "])" return out def __len__(self): """Return the number of LppData items in this LppFrame.""" return len(self._data) def __iter__(self): """Return an iterator over all LppData items in this LppFrame.""" count = 0 while count < len(self._data): yield self._data[count] count += 1 def __bytes__(self): """Return this LppFrame object as a byte string.""" buf = bytearray() for d in self._data: buf += bytes(d) return bytes(buf) def to_bytes(self): """Return this LppFrame object as a byte string (for MicroPython).""" buf = bytearray() for d in self._data: buf += d.to_bytes() return bytes(buf) @classmethod def from_bytes(cls, buf): """Parse a given byte string and return as a LppFrame object.""" i = 0 data = [] while i < len(buf): lppdata = LppData.from_bytes(buf[i:]) data.append(lppdata) i = i + len(lppdata) return cls(data) def __add_data_item(self, item): """Helper function to add an LppData item to this LppFrame.""" if not isinstance(item, LppData): raise TypeError() if self.maxsize > 0: if self.size + len(item) > self.maxsize: raise BufferError() self._data.append(item) @property def data(self): """Return list of data items.""" return self._data @property def maxsize(self): """Return max allowed byte size for this LppFrame.""" return self._maxsize @maxsize.setter def maxsize(self, value): """Set the max byte size for this LppFrame.""" if value < 0: raise ValueError("Maxsize must be positive integer.") if value > 0 and value < self.size: raise ValueError("Maxsize must be greater than current frame size") self._maxsize = value def reset(self): """Reset LppFrame by clearing the list of LppData items.""" self._data.clear() @property def size(self): """Return the length of the LppFrame byte string representation.""" size = 0 for d in self._data: size += len(d) return size def get_by_type(self, type_): """Return sub list of data with items matching given type.""" return [d for d in self.data if int(d.type) == type_] def get_by_name(self, name): """Return sub list of data with items matching given name.""" name = name.strip().lower() return [d for d in self.data if str(d.type).lower().startswith(name)] def add_by_type(self, type_, channel, value_tuple): """Generic helper to add LppDate to this LppFrame.""" if not isinstance(value_tuple, tuple): raise TypeError('Parameter (value_tuple) must be a tuple!') data = LppData(channel, type_, value_tuple) self.__add_data_item(data) def add_digital_input(self, channel, value): """Create and add a digital input LppData item.""" self.add_by_type(0, channel, (value, )) def add_digital_output(self, channel, value): """Create and add a digital output LppData item.""" self.add_by_type(1, channel, (value, )) def add_analog_input(self, channel, value): """Create and add an analog input LppData item.""" self.add_by_type(2, channel, (value, )) def add_analog_output(self, channel, value): """Create and add an analog output LppData item.""" self.add_by_type(3, channel, (value, )) def add_generic(self, channel, value): """Create and add a generic 4-byte unsigned integer LppData item.""" self.add_by_type(100, channel, (value, )) def add_luminosity(self, channel, value): """Create and add an illuminance sensor LppData item.""" self.add_by_type(101, channel, (value, )) def add_presence(self, channel, value): """Create and add a presence sensor LppData item.""" self.add_by_type(102, channel, (value, )) def add_temperature(self, channel, value): """Create and add a temperature sensor LppData item.""" self.add_by_type(103, channel, (value, )) def add_humidity(self, channel, value): """Create and add a humidity sensor LppData item.""" self.add_by_type(104, channel, (value, )) def add_accelerometer(self, channel, x, y, z): """Create and add a accelerometer sensor LppData item.""" self.add_by_type(113, channel, (x, y, z)) def add_pressure(self, channel, value): """Alias method for add_barometer().""" self.add_barometer(channel, value) def add_barometer(self, channel, value): """Create and add a barometer sensor LppData item.""" self.add_by_type(115, channel, (value, )) def add_voltage(self, channel, value): """Create and add a voltage sensor LppData item.""" self.add_by_type(116, channel, (value, )) def add_current(self, channel, value): """Create and add a current sensor LppData item.""" self.add_by_type(117, channel, (value, )) def add_frequency(self, channel, value): """Create and add a frequency sensor LppData item.""" self.add_by_type(118, channel, (value, )) def add_percentage(self, channel, value): """Create and add a percentage LppData item.""" self.add_by_type(120, channel, (value, )) def add_altitude(self, channel, value): """Create and add a altitude LppData item.""" self.add_by_type(121, channel, (value, )) def add_load(self, channel, value): """Create and add a load sensor LppData item.""" self.add_by_type(122, channel, (value, )) def add_concentration(self, channel, value): """Create and add a concentration LppData item.""" self.add_by_type(125, channel, (value, )) def add_power(self, channel, value): """Create and add a power sensor LppData item.""" self.add_by_type(128, channel, (value, )) def add_distance(self, channel, value): """Create and add a distance LppData item.""" self.add_by_type(130, channel, (value, )) def add_energy(self, channel, value): """Create and add a energy sensor LppData item.""" self.add_by_type(131, channel, (value, )) def add_direction(self, channel, value): """Create and add a direction LppData item.""" self.add_by_type(132, channel, (value, )) def add_unix_time(self, channel, value): """Create and add a unix timestamp LppData item.""" self.add_by_type(133, channel, (value, )) def add_gyrometer(self, channel, x, y, z): """Create and add a gyrometer sensor LppData item.""" self.add_by_type(134, channel, (x, y, z)) def add_colour(self, channel, red, green, blue): """Create and add a color sensor LppData item.""" self.add_by_type(135, channel, (red, green, blue)) def add_gps(self, channel, lat, lon, alt): """Alias method for add_location().""" self.add_location(channel, lat, lon, alt) def add_location(self, channel, lat, lon, alt): """Create and add a location LppData item.""" self.add_by_type(136, channel, (lat, lon, alt)) def add_switch(self, channel, value): """Create and add a switch LppData item.""" self.add_by_type(142, channel, (value, )) pycayennelpp-2.4.0/cayennelpp/lpp_type.py000066400000000000000000000147051413136431100205740ustar00rootroot00000000000000class LppType(object): """Cayenne LPP type object. The LppType provides a simple wrapper to all sensor types of the Cayenne LPP standard (and beyond). It ensures proper encoding and decoding with sensible checks. This class is for internal use only and thus is not (directly) exposed. Attributes: type (int): LPP type ID number name (str): human readable type description sizes (list): byte size of values scales (list): scaling of values signs (list): signess of values """ __lpp_types = { 0: ('Digital Input', [1], [1], [False]), 1: ('Digital Output', [1], [1], [False]), 2: ('Analog Input', [2], [100], [True]), 3: ('Analog Output', [2], [100], [True]), 100: ('Generic Sensor', [4], [1], [False]), 101: ('Illuminance', [2], [1], [False]), 102: ('Presence', [1], [1], [False]), 103: ('Temperature', [2], [10], [True]), 104: ('Humidity', [1], [2], [False]), 113: ('Accelerometer', [2, 2, 2], [1000, 1000, 1000], [True, True, True]), 115: ('Barometer', [2], [10], [False]), 116: ('Voltage', [2], [100], [False]), 117: ('Current', [2], [1000], [False]), 118: ('Frequency', [4], [1], [False]), 120: ('Percentage', [1], [1], [False]), 121: ('Altitude', [2], [1], [True]), 122: ('Load', [3], [1000], [True]), 125: ('Concentration', [2], [1], [False]), 128: ('Power', [2], [1], [False]), 130: ('Distance', [4], [1000], [False]), 131: ('Energy', [4], [1000], [False]), 132: ('Direction', [2], [1], [False]), 133: ('Time', [4], [1], [False]), 134: ('Gyrometer', [2, 2, 2], [100, 100, 100], [True, True, True]), 135: ('Colour', [1, 1, 1], [1, 1, 1], [False, False, False]), 136: ('Location', [3, 3, 3], [10000, 10000, 100], [True, True, True]), 142: ('Switch', [1], [1], [False]) } def __init__(self, type_, name, sizes, scales, signs): """Create a LppType object with given attributes.""" if not isinstance(type_, int): raise TypeError('Parameter (type_) must be an integer!') if not isinstance(name, str): raise TypeError('Parameter (name) must be a string!') if not isinstance(sizes, list): raise TypeError('Parameter (sizes) must be a list of integers!') if not isinstance(scales, list): raise TypeError('Parameter (scales) must be a list of integers!') if not isinstance(signs, list): raise TypeError('Parameter (signs) must be a list of integers!') if len(sizes) != len(scales) or len(scales) != len(signs): raise ValueError('Invalid parameter length: sizes, scales, signs!') self.type = type_ self.name = name self.sizes = sizes self.scales = scales self.signs = signs def __int__(self): """Return LppType as integer, i.e. its numeric type.""" return self.type def __str__(self): """Return LppType as string, i.e. its name.""" return self.name @staticmethod def __assert_data_tuple(data, num): """Internal helper to ensure data is a tuple of given `num` length.""" if not isinstance(data, tuple): data = (data,) if not len(data) == num: raise ValueError() return data @staticmethod def __from_bytes(buf): """Internal helper to parse a number from buffer.""" buflen = len(buf) val = 0 for i in range(buflen): shift = (buflen - i - 1) * 8 val |= buf[i] << shift return val @staticmethod def __to_bytes(val, buflen): """Internal helper to write a value to a buffer.""" buf = bytearray(buflen) val = int(val) for i in range(buflen): shift = (buflen - i - 1) * 8 buf[i] = (val >> shift) & 0xff return buf @staticmethod def __to_signed(val, size): """Internal helper to convert unsigned int to signed.""" mask = 0x00 for i in range(size): mask |= 0xff << (i * 8) if val >= (1 << ((size * 8) - 1)): val = -1 - (val ^ mask) return val @staticmethod def __to_unsigned(val): """Convert signed (two's complement) value to unsigned.""" if val < 0: val = ~(-val - 1) return val @classmethod def get_lpp_type(cls, type_): """Return LppType object for given type or `None` if not found.""" if not isinstance(type_, int): raise TypeError('Parameter (type_) must be an integer!') if type_ in cls.__lpp_types: return cls(type_, *cls.__lpp_types[type_]) return None @property def dimension(self): """Return number of value dimensions.""" return len(self.sizes) @property def size(self): """Return size of byte string representation.""" return sum(self.sizes) def decode(self, buf): """Parse LppType from a byte string.""" if len(buf) != sum(self.sizes): raise BufferError('Invalid buffer length!') data = [] pos = 0 for i in range(len(self.sizes)): size = self.sizes[i] value = self.__from_bytes(buf[pos:(pos + size)]) if self.signs[i]: value = self.__to_signed(value, size) value = value / self.scales[i] data.append(value) pos += size return tuple(data) def encode(self, data): """Convert LppType into a byte string.""" data = self.__assert_data_tuple(data, len(self.sizes)) buf = bytearray(sum(self.sizes)) pos = 0 for i in range(len(self.sizes)): size = self.sizes[i] value = data[i] if not self.signs[i] and value < 0: raise ValueError('Invalid data, must be non negative!') value = int(value * self.scales[i]) maxval = 1 << (size * 8) if value >= maxval: raise ValueError('Invalid data, exceed value range!') if self.signs[i]: value = self.__to_unsigned(value) buf[pos:(pos + size)] = self.__to_bytes(value, size) pos += size return buf pycayennelpp-2.4.0/cayennelpp/lpp_util.py000066400000000000000000000020551413136431100205630ustar00rootroot00000000000000from .lpp_data import LppData from .lpp_frame import LppFrame from .lpp_type import LppType class LppUtil(): """Cayenne LPP utility functions. This class provides helper functions and wrapper, e.g. for encoding and decoding LppType, LppData, and LppFrame into various data formats. """ @staticmethod def json_encode(obj, type2str=False): """Encode LppType, LppData, and LppFrame to JSON.""" if isinstance(obj, LppType): if type2str: return obj.name return obj.type if isinstance(obj, LppData): return obj.__dict__ if isinstance(obj, LppFrame): return obj.data raise TypeError(repr(obj) + " is not JSON serialized") @classmethod def json_encode_type_int(cls, obj): """Wrapper function encode type as int in JSON.""" return cls.json_encode(obj, False) @classmethod def json_encode_type_str(cls, obj): """Wrapper function encode type as string in JSON.""" return cls.json_encode(obj, True) pycayennelpp-2.4.0/cayennelpp/tests/000077500000000000000000000000001413136431100175215ustar00rootroot00000000000000pycayennelpp-2.4.0/cayennelpp/tests/__init__.py000066400000000000000000000000331413136431100216260ustar00rootroot00000000000000# intentionally left empty pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_data.py000066400000000000000000000046411413136431100227230ustar00rootroot00000000000000import pytest from cayennelpp.lpp_data import LppData def test_temperature_from_bytes(): # 01 67 FF D7 = -4.1C temp_buf = bytes([0x01, 0x67, 0xFF, 0xD7]) temp_dat = LppData.from_bytes(temp_buf) assert temp_buf == bytes(temp_dat) def test_accelerometer_from_bytes(): # 06 71 04 D2 FB 2E 00 00 acc_buf = bytes([0x06, 0x71, 0x04, 0xD2, 0xFB, 0x2E, 0x00, 0x00]) acc_dat = LppData.from_bytes(acc_buf) assert acc_buf == bytes(acc_dat) def test_generic_from_bytes(): buff = bytes([0x00, 0x64, 0xff, 0xff, 0xff, 0xfb]) data = LppData.from_bytes(buff) assert buff == bytes(data) assert buff == data.to_bytes() assert int(data.type) == 100 assert data.value == (4294967291,) def test_generic_from_bytes_invalid_size(): with pytest.raises(Exception): buf = bytes([0x00, 0x64, 0x00, 0x00, 0x00]) LppData.from_bytes(buf) def test_gps_from_bytes(): # 01 88 06 76 5f f2 96 0a 00 03 e8 gps_buf = bytes([0x01, 0x88, 0x06, 0x76, 0x5f, 0xf2, 0x96, 0x0a, 0x00, 0x03, 0xe8]) gps_dat = LppData.from_bytes(gps_buf) assert gps_buf == bytes(gps_dat) def test_voltage_from_bytes(): # 25V on channel 1 buff = bytes([0x01, 0x74, 0x9, 0xc4]) data = LppData.from_bytes(buff) assert buff == bytes(data) assert data.value == (25,) def test_load_from_bytes(): # 42.321kg on channel 0 buff = bytes([0x00, 0x7A, 0x00, 0xA5, 0x51]) data = LppData.from_bytes(buff) assert buff == bytes(data) def test_unix_time_from_bytes(): # 1970-01-01T08:00Z (ie unix time 0) buff = bytes([0x01, 0x85, 0x00, 0x00, 0x00, 0x00]) data = LppData.from_bytes(buff) assert buff == bytes(data) assert data.value == (0,) def test_init_invalid_type(): with pytest.raises(Exception): LppData(0, 4242, 0) def test_init_data_none(): with pytest.raises(Exception): LppData(0, 0, None) def test_init_invalid_dimension(): with pytest.raises(Exception): LppData(0, 136, 0) def test_any_from_bytes_invalid_size(): with pytest.raises(Exception): buf = bytes([0x00, 0x00]) LppData.from_bytes(buf) def test_gps_from_bytes_invalid_size(): with pytest.raises(Exception): buf = bytes([0x00, 0x88, 0x00]) LppData.from_bytes(buf) def test_lpp_data_size(): assert len(LppData(0, 0, 0)) == 3 def test_lpp_data_str(): print(LppData(0, 0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_frame.py000066400000000000000000000217071413136431100231060ustar00rootroot00000000000000import pytest import base64 from datetime import datetime from datetime import timezone from cayennelpp.lpp_frame import LppFrame @pytest.fixture def frame(): empty_frame = LppFrame() return empty_frame @pytest.fixture def frame_hlt(): hlt = LppFrame() hlt.add_humidity(3, 45.6) hlt.add_load(1, 160.987) hlt.add_temperature(2, 12.3) return hlt def test_empty_frame(frame): assert not frame.data assert len(frame) == 0 assert not bytes(frame) def test_init_invalid_data_nolist(): with pytest.raises(Exception): LppFrame(42) def test_init_invalid_data_item(): with pytest.raises(Exception): LppFrame([0]) def test_frame_reset(frame): frame.add_digital_input(0, 1) assert len(frame) == 1 frame.reset() assert len(frame) == 0 def test_frame_size(frame): assert frame.size == 0 frame.add_digital_input(0, 1) assert frame.size == 3 frame.add_digital_output(1, 42) assert frame.size == 6 def test_frame_maxsize(frame): frame.add_digital_input(0, 1) assert frame.maxsize == 0 frame.maxsize = 6 assert frame.maxsize == 6 def test_frame_maxsize_invalid(frame): frame.add_digital_input(0, 1) assert frame.maxsize == 0 with pytest.raises(Exception): frame.maxsize = 1 def test_frame_maxsize_negative(frame): with pytest.raises(Exception): frame.maxsize = -42 def test_frame_maxsize_exceeded(frame): frame.maxsize = 3 with pytest.raises(Exception): frame.add_generic(0, 42) def test_frame_from_bytes(): # 03 67 01 10 05 67 00 FF = 27.2C + 25.5C buf = bytes([0x03, 0x67, 0x01, 0x10, 0x05, 0x67, 0x00, 0xff]) frame = LppFrame.from_bytes(buf) assert buf == bytes(frame) assert buf == frame.to_bytes() assert len(frame) == 2 # 01 67 FF D7 buf = bytes([0x01, 0x67, 0xFF, 0xD7]) frame = LppFrame.from_bytes(buf) assert buf == bytes(frame) assert buf == frame.to_bytes() assert len(frame) == 1 # 06 71 04 D2 FB 2E 00 00 buf = bytes([0x06, 0x71, 0x04, 0xD2, 0xFB, 0x2E, 0x00, 0x00]) frame = LppFrame.from_bytes(buf) assert buf == bytes(frame) assert buf == frame.to_bytes() assert len(frame) == 1 # 01 88 06 76 5f f2 96 0a 00 03 e8 buf = bytes([0x01, 0x88, 0x06, 0x76, 0x5f, 0xf2, 0x96, 0x0a, 0x00, 0x03, 0xe8]) frame = LppFrame.from_bytes(buf) assert buf == bytes(frame) assert buf == frame.to_bytes() assert len(frame) == 1 def test_frame_from_bytes_base64(): base64_str = "AYgILMMBiIMAAAACAAY=" frame = LppFrame.from_bytes(base64.decodebytes(base64_str.encode('ascii'))) assert len(frame) == 2 def test_add_digital_io(frame): frame.add_digital_input(0, 21) frame.add_digital_output(1, 42) assert len(frame) == 2 assert int(frame.data[0].type) == 0 assert int(frame.data[1].type) == 1 def test_add_analog_io(frame): frame.add_analog_input(0, 12.34) frame.add_analog_input(1, -12.34) frame.add_analog_output(0, 56.78) frame.add_analog_output(1, -56.78) assert len(frame) == 4 assert int(frame.data[0].type) == 2 assert int(frame.data[1].type) == 2 assert int(frame.data[2].type) == 3 assert int(frame.data[3].type) == 3 def test_add_sensors(frame): frame.add_luminosity(2, 12345) frame.add_presence(3, 1) frame.add_accelerometer(5, 1.234, -1.234, 0.0) frame.add_pressure(6, 1005.5) frame.add_barometer(6, 999.0) frame.add_gyrometer(7, 1.234, -1.234, 0.0) frame.add_gps(8, 1.234, -1.234, 0.0) assert len(frame) == 7 assert int(frame.data[0].type) == 101 assert int(frame.data[1].type) == 102 assert int(frame.data[2].type) == 113 assert int(frame.data[3].type) == 115 assert int(frame.data[4].type) == 115 assert int(frame.data[5].type) == 134 assert int(frame.data[6].type) == 136 def test_add_voltage(frame): frame.add_voltage(0, 25.2) frame.add_voltage(1, 120.2) assert len(frame) == 2 assert int(frame.data[0].type) == 116 assert int(frame.data[1].type) == 116 with pytest.raises(Exception): frame.add_voltage(2, -25) def test_add_current(frame): frame.add_current(0, 16.2) frame.add_current(1, 32.3) assert len(frame) == 2 assert int(frame.data[0].type) == 117 assert int(frame.data[1].type) == 117 with pytest.raises(Exception): frame.add_current(2, -25) def test_add_frequency(frame): frame.add_frequency(0, 4294967295) frame.add_frequency(1, 1) assert len(frame) == 2 assert int(frame.data[0].type) == 118 assert int(frame.data[1].type) == 118 def test_add_load(frame): frame.add_load(0, -5.432) frame.add_load(1, 160.987) assert len(frame) == 2 assert int(frame.data[0].type) == 122 assert int(frame.data[1].type) == 122 def test_add_generic(frame): frame.add_generic(0, 4294967295) frame.add_generic(1, 1) assert len(frame) == 2 assert int(frame.data[0].type) == 100 assert int(frame.data[1].type) == 100 def test_add_unix_time(frame): frame.add_unix_time(0, datetime.now(timezone.utc).timestamp()) frame.add_unix_time(1, 0) assert len(frame) == 2 assert int(frame.data[0].type) == 133 assert int(frame.data[1].type) == 133 def test_add_temperature(frame): frame.add_temperature(2, 12.3) frame.add_temperature(3, -32.1) assert len(frame) == 2 assert int(frame.data[0].type) == 103 assert int(frame.data[1].type) == 103 def test_add_percentage(frame): frame.add_percentage(2, 10) frame.add_percentage(3, 100) assert len(frame) == 2 assert int(frame.data[0].type) == 120 assert int(frame.data[1].type) == 120 def test_add_altitude(frame): frame.add_altitude(2, 4242) frame.add_altitude(3, -4242) assert len(frame) == 2 assert int(frame.data[0].type) == 121 assert int(frame.data[1].type) == 121 def test_add_concentration(frame): frame.add_concentration(2, 10) frame.add_concentration(3, 10000) assert len(frame) == 2 assert int(frame.data[0].type) == 125 assert int(frame.data[1].type) == 125 def test_add_power(frame): frame.add_power(2, 10) frame.add_power(3, 10000) assert len(frame) == 2 assert int(frame.data[0].type) == 128 assert int(frame.data[1].type) == 128 def test_add_distance(frame): frame.add_distance(2, 1.2345) frame.add_distance(3, 1234.5) assert len(frame) == 2 assert int(frame.data[0].type) == 130 assert int(frame.data[1].type) == 130 def test_add_energy(frame): frame.add_energy(2, 1.2345) frame.add_energy(3, 1234.5) assert len(frame) == 2 assert int(frame.data[0].type) == 131 assert int(frame.data[1].type) == 131 def test_add_direction(frame): frame.add_direction(2, 30) frame.add_direction(2, 120) frame.add_direction(3, 210) frame.add_direction(3, 300) assert len(frame) == 4 assert int(frame.data[0].type) == 132 assert int(frame.data[1].type) == 132 assert int(frame.data[2].type) == 132 assert int(frame.data[3].type) == 132 def test_add_humidity(frame): frame.add_humidity(2, 12.3) frame.add_humidity(3, 45.6) frame.add_humidity(4, 78.9) assert len(frame) == 3 assert int(frame.data[0].type) == 104 assert int(frame.data[1].type) == 104 assert int(frame.data[2].type) == 104 def test_add_colour(frame): frame.add_colour(2, 42, 42, 42) assert len(frame) == 1 assert int(frame.data[0].type) == 135 def test_add_switch(frame): frame.add_switch(2, 0) frame.add_switch(2, 1) assert len(frame) == 2 assert int(frame.data[0].type) == 142 assert int(frame.data[1].type) == 142 def test_print_empty_frame(frame): print(frame) def test_print_data_frame(frame_hlt): print(frame_hlt) def test_iterator(frame_hlt): counter = 0 for val in frame_hlt: print(val) counter += 1 assert counter == len(frame_hlt) def test_add_by_type(frame): with pytest.raises(TypeError): frame.add_by_type(0, 1, 42) def test_get_by_type(frame_hlt): h_list = frame_hlt.get_by_type(104) assert len(h_list) == 1 assert int(h_list[0].type) == 104 l_list = frame_hlt.get_by_type(122) assert len(l_list) == 1 assert int(l_list[0].type) == 122 t_list = frame_hlt.get_by_type(103) assert len(t_list) == 1 assert int(t_list[0].type) == 103 b_list = frame_hlt.get_by_type(102) assert len(b_list) == 0 def test_get_by_name(frame_hlt): h_list = frame_hlt.get_by_name("Humidity") assert len(h_list) == 1 assert int(h_list[0].type) == 104 l_list = frame_hlt.get_by_name("Load") assert len(l_list) == 1 assert int(l_list[0].type) == 122 t_list = frame_hlt.get_by_name("Temperature") assert len(t_list) == 1 assert int(t_list[0].type) == 103 b_list = frame_hlt.get_by_name("Barometer") assert len(b_list) == 0 def test_get_by_type_invalid(frame_hlt): p_list = frame_hlt.get_by_type(102) assert len(p_list) == 0 i_list = frame_hlt.get_by_type(666) assert len(i_list) == 0 pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type.py000066400000000000000000000016501413136431100227700ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType def test_init_invalid_type(): with pytest.raises(TypeError): LppType("foobar", "foobar", [1], [1], [False]) def test_init_invalid_name(): with pytest.raises(TypeError): LppType(42, 42, [1], [1], [False]) def test_init_invalid_sizes(): with pytest.raises(TypeError): LppType(42, "foobar", 42, [1], [False]) def test_init_invalid_scales(): with pytest.raises(TypeError): LppType(42, "foobar", [1], 42, [False]) def test_init_invalid_signs(): with pytest.raises(TypeError): LppType(42, "foobar", [1], [1], 42) def test_init_invalid_sizes_len(): with pytest.raises(ValueError): LppType(42, "foobar", [1, 1], [1], [False]) def test_get_lpp_type_invalid(): with pytest.raises(TypeError): LppType.get_lpp_type("foo") def test_get_lpp_type_none(): assert not LppType.get_lpp_type(999) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_accelerometer.py000066400000000000000000000012661413136431100256650ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def acc(): return LppType.get_lpp_type(113) def test_accelerometer(acc): val = (12.345, -12.345, 0.0) accel_buf = acc.encode(val) assert acc.decode(accel_buf) == val val = (-12.345, 0.0, -12.345) accel_buf = acc.encode(val) assert acc.decode(accel_buf) == val def test_accelerometer_invalid_buf(acc): with pytest.raises(Exception): acc.decode(bytearray([0x00])) def test_accelerometer_invalid_val_type(acc): with pytest.raises(Exception): acc.encode([0x00]) def test_accelerometer_invalid_val(acc): with pytest.raises(Exception): acc.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_aio.py000066400000000000000000000012021413136431100236110ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def aio(): return LppType.get_lpp_type(2) def test_analog_io(aio): val = (123.45,) aio_buf = aio.encode(val) assert aio.decode(aio_buf) == val val = (-123.45,) aio_buf = aio.encode(val) assert aio.decode(aio_buf) == val def test_analog_io_invalid_buf(aio): with pytest.raises(Exception): aio.decode(bytearray([0x00])) def test_analog_io_invalid_val_type(aio): with pytest.raises(Exception): aio.encode([0, 1]) def test_analog_io_invalid_val(aio): with pytest.raises(Exception): aio.encode((0, 1)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_barometer.py000066400000000000000000000012651413136431100250320ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def baro(): return LppType.get_lpp_type(115) def test_barometer(baro): val = (1234.5,) baro_buf = baro.encode(val) assert baro.decode(baro_buf) == val def test_barometer_negative_val(baro): with pytest.raises(Exception): val = (-1234.5,) baro.encode(val) def test_barometer_invalid_buf(baro): with pytest.raises(Exception): baro.decode(bytearray([0x00])) def test_barometer_invalid_val_type(baro): with pytest.raises(Exception): baro.encode([0x00]) def test_barometer_invalid_val(baro): with pytest.raises(Exception): baro.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_current.py000066400000000000000000000010321413136431100245240ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def cur(): return LppType.get_lpp_type(117) def test_current(cur): val = (2,) cur_buf = cur.encode(val) assert cur.decode(cur_buf) == val def test_current_invalid_buf(cur): with pytest.raises(Exception): cur.decode(bytearray([0x00])) def test_current_invalid_val(cur): with pytest.raises(Exception): cur.encode((0, 1)) def test_current_negative_val(cur): with pytest.raises(Exception): cur.encode((-42,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_dio.py000066400000000000000000000015271413136431100236260ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def dio(): return LppType.get_lpp_type(0) def test_digital_io(dio): val = (0,) dio_buf = dio.encode(val) assert dio.decode(dio_buf) == val val = (1,) dio_buf = dio.encode(val) assert dio.decode(dio_buf) == val def test_digital_io_max_value(dio): val = (255,) dio_buf = dio.encode(val) assert dio.decode(dio_buf) == val with pytest.raises(ValueError): val = (256,) dio_buf = dio.encode(val) def test_digital_io_invalid_buf(dio): with pytest.raises(Exception): dio.decode(bytearray([0x00, 0x00])) def test_digital_io_invalid_val_type(dio): with pytest.raises(Exception): dio.encode([0, 1]) def test_digital_io_invalid_val(dio): with pytest.raises(Exception): dio.encode((0, 1)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_frequency.py000066400000000000000000000012701413136431100250470ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def freq(): return LppType.get_lpp_type(118) def test_generic(freq): val = 4294967295 freq_buf = freq.encode((val,)) assert freq.decode(freq_buf) == (val,) def test_generic_invalid_buf(freq): with pytest.raises(BufferError): freq.decode(bytearray([0x00, 0x00, 0x00])) def test_generic_invalid_val(freq): with pytest.raises(ValueError): freq.encode((0, 1)) with pytest.raises(ValueError): # val exceeds 4 bytes val = 4294967297 freq.encode((val,)) def test_generic_negative_val(freq): with pytest.raises(ValueError): freq.encode((-1,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_generic.py000066400000000000000000000012471413136431100244660ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def gen(): return LppType.get_lpp_type(100) def test_generic(gen): val = 4294967295 vol_buf = gen.encode((val,)) assert gen.decode(vol_buf) == (val,) def test_generic_invalid_buf(gen): with pytest.raises(Exception): gen.decode(bytearray([0x00, 0x00, 0x00])) def test_generic_invalid_val(gen): with pytest.raises(Exception): gen.encode((0, 1)) with pytest.raises(ValueError): # val exceeds 4 bytes val = 4294967297 gen.encode((val,)) def test_generic_negative_val(gen): with pytest.raises(Exception): gen.encode((-1,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_gps.py000066400000000000000000000012161413136431100236370ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def gps(): return LppType.get_lpp_type(136) def test_gps(gps): val = (42.3519, -87.9094, 10.00) gps_buf = gps.encode(val) assert gps.decode(gps_buf) == val val = (-42.3519, 87.9094, -10.00) gps_buf = gps.encode(val) assert gps.decode(gps_buf) == val def test_gps_invalid_buf(gps): with pytest.raises(Exception): gps.decode(bytearray([0x00])) def test_gps_invalid_val_type(gps): with pytest.raises(Exception): gps.encode([0x00]) def test_gps_invalid_val(gps): with pytest.raises(Exception): gps.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_gyrometer.py000066400000000000000000000012561413136431100250670ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def gyro(): return LppType.get_lpp_type(134) def test_gyrometer(gyro): val = (123.45, -123.45, 0.0) gyro_buf = gyro.encode(val) assert gyro.decode(gyro_buf) == val val = (-123.45, 0.0, -123.45) gyro_buf = gyro.encode(val) assert gyro.decode(gyro_buf) == val def test_gyrometer_invalid_buf(gyro): with pytest.raises(Exception): gyro.decode(bytearray([0x00])) def test_gyrometer_invalid_val_type(gyro): with pytest.raises(Exception): gyro.encode([0x00]) def test_gyrometer_invalid_val(gyro): with pytest.raises(Exception): gyro.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_humidity.py000066400000000000000000000016111413136431100247010ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def hum(): return LppType.get_lpp_type(104) def test_humidity(hum): val = (50.00,) hum_buf = hum.encode(val) assert hum.decode(hum_buf) == val hum_buf = hum.encode(50.25) assert hum.decode(hum_buf) == val val = (50.50,) hum_buf = hum.encode(val) assert hum.decode(hum_buf) == val hum_buf = hum.encode(50.75) assert hum.decode(hum_buf) == val def test_humidity_negative_val(hum): with pytest.raises(Exception): val = (-50.50,) hum.encode(val) def test_humidity_invalid_buf(hum): with pytest.raises(Exception): hum.decode(bytearray([0x00, 0x00])) def test_humidity_invalid_val_type(hum): with pytest.raises(Exception): hum.encode([0x00]) def test_humidity_invalid_val(hum): with pytest.raises(Exception): hum.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_illuminance.py000066400000000000000000000017071413136431100253530ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def lum(): return LppType.get_lpp_type(101) def test_illuminance(lum): val = (0,) lum_buf = lum.encode(val) assert lum.decode(lum_buf) == val val = (12345,) lum_buf = lum.encode(val) assert lum.decode(lum_buf) == val def test_illuminance_max_value(lum): val = (65535,) lum_buf = lum.encode(val) assert lum.decode(lum_buf) == val with pytest.raises(ValueError): val = (65536,) lum_buf = lum.encode(val) def test_illuminance_invalid_buf(lum): with pytest.raises(Exception): lum.decode(bytearray([0x00])) def test_illuminance_invalid_val_type(lum): with pytest.raises(Exception): lum.encode([0, 1]) def test_illuminance_invalid_val(lum): with pytest.raises(Exception): lum.encode((0, 1)) def test_illuminance_negative_val(lum): with pytest.raises(Exception): lum.encode((-1,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_load.py000066400000000000000000000011741413136431100237700ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def load(): return LppType.get_lpp_type(122) def test_load(load): val = (-5.432,) aio_buf = load.encode(val) assert load.decode(aio_buf) == val val = (160.987,) aio_buf = load.encode(val) assert load.decode(aio_buf) == val def test_load_invalid_buf(load): with pytest.raises(Exception): load.decode(bytearray([0x00])) def test_load_invalid_val_type(load): with pytest.raises(Exception): load.encode([0, 1]) def test_load_invalid_val(load): with pytest.raises(Exception): load.encode((0, 1)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_presence.py000066400000000000000000000013551413136431100246560ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def pres(): return LppType.get_lpp_type(102) def test_presence(pres): val = (0,) pre_buf = pres.encode(val) assert pres.decode(pre_buf) == val val = (1,) pre_buf = pres.encode(val) assert pres.decode(pre_buf) == val def test_presence_invalid_buf(pres): with pytest.raises(Exception): pres.decode(bytearray([0x00, 0x00])) def test_presence_invalid_val_type(pres): with pytest.raises(Exception): pres.encode([0, 1]) def test_presence_invalid_val(pres): with pytest.raises(Exception): pres.encode((0, 1)) def test_presence_negative_val(pres): with pytest.raises(Exception): pres.encode((-1,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_temperature.py000066400000000000000000000012271413136431100254050ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def temp(): return LppType.get_lpp_type(103) def test_temperature(temp): val = (32.1,) temp_buf = temp.encode(val) assert temp.decode(temp_buf) == val val = (-4.1,) temp_buf = temp.encode(val) assert temp.decode(temp_buf) == val def test_temperature_invalid_buf(temp): with pytest.raises(Exception): temp.decode(bytearray([0x00])) def test_temperature_invalid_val_type(temp): with pytest.raises(Exception): temp.encode([0x00]) def test_temperature_invalid_val(temp): with pytest.raises(Exception): temp.encode((0, 0)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_unixtime.py000066400000000000000000000013701413136431100247110ustar00rootroot00000000000000import pytest from datetime import datetime from datetime import timezone from cayennelpp.lpp_type import LppType @pytest.fixture def ts(): return LppType.get_lpp_type(133) def test_unix_time_datetime(ts): now = datetime.now(timezone.utc).timestamp() ts_buf = ts.encode((now,)) assert ts.decode(ts_buf) == (int(now),) def test_unix_time_int(ts): val = 5 ts_buf = ts.encode((val,)) assert ts.decode(ts_buf) == (val,) def test_unix_time_invalid_buf(ts): with pytest.raises(Exception): ts.decode(bytearray([0x00])) def test_unix_time_invalid_val(ts): with pytest.raises(Exception): ts.encode((0, 1)) def test_unix_time_negative_val(ts): with pytest.raises(Exception): ts.encode((-1,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_type_voltage.py000066400000000000000000000010321413136431100245030ustar00rootroot00000000000000import pytest from cayennelpp.lpp_type import LppType @pytest.fixture def vol(): return LppType.get_lpp_type(116) def test_voltage(vol): val = (2,) vol_buf = vol.encode(val) assert vol.decode(vol_buf) == val def test_voltage_invalid_buf(vol): with pytest.raises(Exception): vol.decode(bytearray([0x00])) def test_voltage_invalid_val(vol): with pytest.raises(Exception): vol.encode((0, 1)) def test_voltage_negative_val(vol): with pytest.raises(Exception): vol.encode((-42,)) pycayennelpp-2.4.0/cayennelpp/tests/test_lpp_util.py000066400000000000000000000015411413136431100227630ustar00rootroot00000000000000import pytest import json from cayennelpp.lpp_frame import LppFrame from cayennelpp.lpp_util import LppUtil @pytest.fixture def frame(): lf = LppFrame() lf.add_humidity(3, 45.6) lf.add_temperature(2, 12.3) return lf def test_json_encode_type_int(frame): dump_int = json.dumps(frame, default=LppUtil.json_encode_type_int) assert len(dump_int) > 0 load_int = json.loads(dump_int) for o in load_int: assert isinstance(o['type'], int) def test_json_encode_type_str(frame): dump_str = json.dumps(frame, default=LppUtil.json_encode_type_str) assert len(dump_str) > 0 load_str = json.loads(dump_str) for o in load_str: assert isinstance(o['type'], str) def test_json_encode_invalid(): with pytest.raises(TypeError): json.dumps(type("foobar", (object,), {}), default=LppUtil.json_encode) pycayennelpp-2.4.0/sdist_upip.py000066400000000000000000000074231413136431100167640ustar00rootroot00000000000000# # This module overrides distutils (also compatible with setuptools) "sdist" # command to perform pre- and post-processing as required for MicroPython's # upip package manager. # # Preprocessing steps: # * Creation of Python resource module (R.py) from each top-level package's # resources. # Postprocessing steps: # * Removing metadata files not used by upip (this includes setup.py) # * Recompressing gzip archive with 4K dictionary size so it can be # installed even on low-heap targets. # import sys import os import zlib from subprocess import Popen, PIPE import glob import tarfile import re import io from distutils.filelist import FileList from setuptools.command.sdist import sdist as _sdist def gzip_4k(inf, fname): comp = zlib.compressobj(level=9, wbits=16 + 12) with open(fname + ".out", "wb") as outf: while 1: data = inf.read(1024) if not data: break outf.write(comp.compress(data)) outf.write(comp.flush()) os.rename(fname, fname + ".orig") os.rename(fname + ".out", fname) FILTERS = [ # include, exclude, repeat (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), (r".+\.py$", r"[^/]+$"), (None, r".+\.egg-info/.+"), ] outbuf = io.BytesIO() def filter_tar(name): fin = tarfile.open(name, "r:gz") fout = tarfile.open(fileobj=outbuf, mode="w") for info in fin: # print(info) if not "/" in info.name: continue fname = info.name.split("/", 1)[1] include = None for inc_re, exc_re in FILTERS: if include is None and inc_re: if re.match(inc_re, fname): include = True if include is None and exc_re: if re.match(exc_re, fname): include = False if include is None: include = True if include: print("including:", fname) else: print("excluding:", fname) continue farch = fin.extractfile(info) fout.addfile(info, farch) fout.close() fin.close() def make_resource_module(manifest_files): resources = [] # Any non-python file included in manifest is resource for fname in manifest_files: ext = fname.rsplit(".", 1)[-1] if ext != "py": resources.append(fname) if resources: print("creating resource module R.py") resources.sort() last_pkg = None r_file = None for fname in resources: try: pkg, res_name = fname.split("/", 1) except ValueError: print("not treating %s as a resource" % fname) continue if last_pkg != pkg: last_pkg = pkg if r_file: r_file.write("}\n") r_file.close() r_file = open(pkg + "/R.py", "w") r_file.write("R = {\n") with open(fname, "rb") as f: r_file.write("%r: %r,\n" % (res_name, f.read())) if r_file: r_file.write("}\n") r_file.close() class sdist(_sdist): def run(self): self.filelist = FileList() self.get_file_list() make_resource_module(self.filelist.files) r = super().run() assert len(self.archive_files) == 1 print("filtering files and recompressing with 4K dictionary") filter_tar(self.archive_files[0]) outbuf.seek(0) gzip_4k(outbuf, self.archive_files[0]) return r # For testing only if __name__ == "__main__": filter_tar(sys.argv[1]) outbuf.seek(0) gzip_4k(outbuf, sys.argv[1]) pycayennelpp-2.4.0/setup.cfg000066400000000000000000000000261413136431100160400ustar00rootroot00000000000000[aliases] test=pytest pycayennelpp-2.4.0/setup.py000066400000000000000000000020771413136431100157410ustar00rootroot00000000000000from setuptools import setup def readme(): with open('README.md') as f: return f.read() setup( name='pycayennelpp', version='2.4.0', python_requires='>=3.6', description='Encoder and Decoder for CayenneLLP', long_description=readme(), long_description_content_type='text/markdown', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9' ], keywords='cayenne lpp iot lora lorawan ttn', url='http://github.com/smlng/pycayennelpp', author='smlng', author_email='s@mlng.net', license='MIT', packages=['cayennelpp'], setup_requires=["pytest-runner"], tests_require=['pytest'], include_package_data=True ) pycayennelpp-2.4.0/usetup.py000066400000000000000000000015101413136431100161150ustar00rootroot00000000000000from setuptools import setup import sdist_upip def readme(): with open('README.md') as f: return f.read() setup( name='micropython-pycayennelpp', version='2.4.0', description='Encoder and Decoder for CayenneLLP', long_description=readme(), long_description_content_type='text/markdown', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: Implementation :: MicroPython' ], keywords='cayenne lpp iot lora lorawan ttn', url='http://github.com/smlng/pycayennelpp', author='smlng', author_email='s@mlng.net', license='MIT', packages=['cayennelpp'], cmdclass={'sdist': sdist_upip.sdist}, include_package_data=True )