New initial version
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 getcom
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[run]
|
||||||
|
relative_files = True
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
exclude = __init__.py
|
||||||
|
max_line_length = 120
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: Python package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install flake8 nose coverage
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
# stop the test if there are Python syntax errors or undefined names
|
||||||
|
flake8 enocean --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 enocean --count --exit-zero --max-complexity=15 --max-line-length=127 --statistics
|
||||||
|
- name: Test with nose
|
||||||
|
run: |
|
||||||
|
nosetests -s -q --with-coverage --cover-package=enocean
|
||||||
|
- name: Coveralls
|
||||||
|
uses: AndreMiras/coveralls-python-action@develop
|
||||||
|
with:
|
||||||
|
parallel: true
|
||||||
|
flag-name: run-${{ matrix.test_number }}
|
||||||
|
|
||||||
|
finish:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Coveralls Finished
|
||||||
|
uses: AndreMiras/coveralls-python-action@develop
|
||||||
|
with:
|
||||||
|
parallel-finished: true
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# This workflows 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/*
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
bin
|
||||||
|
include
|
||||||
|
lib
|
||||||
|
local
|
||||||
|
share
|
||||||
|
lib-python
|
||||||
|
lib_pypy
|
||||||
|
site-packages
|
||||||
|
venv
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
*.egg-info
|
||||||
|
build
|
||||||
|
man
|
||||||
|
MANIFEST
|
||||||
|
dist
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2016 Kimmo Huoman
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Python EnOcean #
|
||||||
|
|
||||||
|
[](https://travis-ci.org/kipe/enocean)
|
||||||
|
[](https://coveralls.io/github/kipe/enocean?branch=master)
|
||||||
|
|
||||||
|
A Python library for reading and controlling [EnOcean](http://www.enocean.com/) devices.
|
||||||
|
|
||||||
|
Started as a part of [Forget Me Not](http://www.element14.com/community/community/design-challenges/forget-me-not)
|
||||||
|
design challenge @ [element14](http://www.element14.com/).
|
||||||
|
|
||||||
|
## Install ##
|
||||||
|
|
||||||
|
If not installed already, install [pip](https://pypi.python.org/pypi/pip) by running
|
||||||
|
|
||||||
|
`sudo apt-get install python-pip`
|
||||||
|
|
||||||
|
After pip is installed, install the module by running
|
||||||
|
|
||||||
|
`sudo pip install enocean` (or `sudo pip install git+https://github.com/kipe/enocean.git` if you want the "bleeding edge").
|
||||||
|
|
||||||
|
After this, it's just a matter of running `enocean_example.py` and pressing the
|
||||||
|
learn button on magnetic contact or temperature switch or pressing the rocker switch.
|
||||||
|
|
||||||
|
You should be displayed with a log of the presses, as well as parsed values
|
||||||
|
(assuming the sensors are the ones provided in the [EnOcean Starter Kit](https://www.enocean.com/en/enocean_modules/esk-300)).
|
||||||
|
|
||||||
|
The example script can be stopped by pressing `CTRL+C`
|
||||||
@@ -0,0 +1,723 @@
|
|||||||
|
# Supported profiles
|
||||||
|
All profiles (should) correspond to the official [EEP](http://www.enocean-alliance.org/eep/) by EnOcean.
|
||||||
|
|
||||||
|
### RPS Telegram (0xF6)
|
||||||
|
##### RORG 0xF6 - FUNC 0x01 - TYPE 0x01 - Push Button
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|PB |Status of the push button |enum |0 - Released |
|
||||||
|
| | | |1 - Pressed |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xF6 - FUNC 0x02 - TYPE 0x01 - Light and Blind Control - Application Style 1
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|R1 |Rocker 1st action |enum |0 - Button AI |
|
||||||
|
| | | |1 - Button AO |
|
||||||
|
| | | |2 - Button BI |
|
||||||
|
| | | |3 - Button BO |
|
||||||
|
|EB |Energy bow |enum |0 - released |
|
||||||
|
| | | |1 - pressed |
|
||||||
|
|R2 |Rocker 2nd action |enum |0 - Button AI |
|
||||||
|
| | | |1 - Button AO |
|
||||||
|
| | | |2 - Button BI |
|
||||||
|
| | | |3 - Button BO |
|
||||||
|
|SA |2nd action |enum |0 - No 2nd action |
|
||||||
|
| | | |1 - 2nd action valid |
|
||||||
|
|T21 |T21 |status | |
|
||||||
|
|NU |NU |status | |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xF6 - FUNC 0x02 - TYPE 0x02 - Light and Blind Control - Application Style 2
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|R1 |Rocker 1st action |enum |0 - Button AI |
|
||||||
|
| | | |1 - Button AO |
|
||||||
|
| | | |2 - Button BI |
|
||||||
|
| | | |3 - Button BO |
|
||||||
|
|EB |Energy bow |enum |0 - released |
|
||||||
|
| | | |1 - pressed |
|
||||||
|
|R2 |Rocker 2nd action |enum |0 - Button AI |
|
||||||
|
| | | |1 - Button AO |
|
||||||
|
| | | |2 - Button BI |
|
||||||
|
| | | |3 - Button BO |
|
||||||
|
|SA |2nd action |enum |0 - No 2nd action |
|
||||||
|
| | | |1 - 2nd action valid |
|
||||||
|
|T21 |T21 |status | |
|
||||||
|
|NU |NU |status | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xF6 - FUNC 0x05 - TYPE 0x01 - Liquid Leakage Sensor (mechanic harvester)
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|WAS |Water Sensor |enum |0-16 - not specified |
|
||||||
|
| | | |17 - Water detected |
|
||||||
|
| | | |18-255 - not specified |
|
||||||
|
|T21 |T21 |status | |
|
||||||
|
|NU |NU |status | |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xF6 - FUNC 0x05 - TYPE 0x02 - Smoke Detector
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SMO |Status of detection and battery |enum |0 - Smoke Alarm OFF |
|
||||||
|
| | | |16 - Smoke Alarm ON |
|
||||||
|
| | | |48 - Energy LOW |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xF6 - FUNC 0x10 - TYPE 0x00 - Window Handle
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|WIN |Window handle |enum |0 - Moved from up to vertical |
|
||||||
|
| | | |1 - Moved from vertical to up |
|
||||||
|
| | | |2 - Moved from down to vertical |
|
||||||
|
| | | |3 - Moved from vertical to down |
|
||||||
|
|T21 |T21 |status | |
|
||||||
|
|NU |NU |status | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1BS Telegram (0xD5)
|
||||||
|
##### RORG 0xD5 - FUNC 0x00 - TYPE 0x01 - Single Input Contact
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CO |Contact |enum |0 - open |
|
||||||
|
| | | |1 - closed |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 4BS Telegram (0xA5)
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x01 - Temperature Sensor Range -40°C to 0°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -40.0-0.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x02 - Temperature Sensor Range -30°C to +10°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -30.0-10.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x03 - Temperature Sensor Range -20°C to +20°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -20.0-20.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x04 - Temperature Sensor Range -10°C to +30°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -10.0-30.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x05 - Temperature Sensor Range 0°C to +40°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-40.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x06 - Temperature Sensor Range +10°C to +50°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 10.0-50.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x07 - Temperature Sensor Range +20°C to +60°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 20.0-60.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x08 - Temperature Sensor Range +30°C to +70°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 30.0-70.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x09 - Temperature Sensor Range +40°C to +80°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 40.0-80.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x0A - Temperature Sensor Range +50°C to +90°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 50.0-90.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x0B - Temperature Sensor Range +60°C to +100°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 60.0-100.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x10 - Temperature Sensor Range -60°C to +20°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -60.0-20.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x11 - Temperature Sensor Range -50°C to +30°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -50.0-30.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x12 - Temperature Sensor Range -40°C to +40°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -40.0-40.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x13 - Temperature Sensor Range -30°C to +50°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -30.0-50.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x14 - Temperature Sensor Range -20°C to +60°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -20.0-60.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x15 - Temperature Sensor Range -10°C to +70°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ -10.0-70.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x16 - Temperature Sensor Range 0°C to +80°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-80.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x17 - Temperature Sensor Range +10°C to +90°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 10.0-90.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x18 - Temperature Sensor Range +20°C to +100°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 20.0-100.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x19 - Temperature Sensor Range +30°C to +110°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 30.0-110.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x1A - Temperature Sensor Range +40°C to +120°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 40.0-120.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x1B - Temperature Sensor Range +50°C to +130°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 50.0-130.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x20 - 10 Bit Temperature Sensor Range -10°C to +41.2°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |1023.0-0.0 ↔ -10.0-41.2 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x02 - TYPE 0x30 - 10 Bit Temperature Sensor Range -40°C to +62.3°C
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |1023.0-0.0 ↔ -40.0-62.3 °C |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x04 - TYPE 0x01 - Range 0°C to +40°C and 0% to 100%
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|HUM |Rel. Humidity (linear) |value |0.0-250.0 ↔ 0.0-100.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-250.0 ↔ 0.0-40.0 °C |
|
||||||
|
|TSN |Availability of the Temperature Sensor |enum |0 - not available |
|
||||||
|
| | | |1 - available |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x04 - TYPE 0x03 - Range -20°C to +60°C 10bit-measurement and 0% to 100%
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|HUM |Rel. Humidity (linear) |value |0.0-255.0 ↔ 0.0-100.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-1023.0 ↔ -20.0-60.0 °C |
|
||||||
|
|TTP |Telegram Type |enum |0 - Heartbeat |
|
||||||
|
| | | |1 - Event triggered |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x06 - TYPE 0x01 - Range 300lx to 60.000lx
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SVC |Supply voltage (linear) |value |0.0-255.0 ↔ 0.0-5.1 V |
|
||||||
|
|ILL2 |Illumination 2 (linear) |value |0.0-255.0 ↔ 300.0-30000.0 lx |
|
||||||
|
|ILL1 |Illumination 1 (linear) |value |0.0-255.0 ↔ 600.0-60000.0 lx |
|
||||||
|
|RS |Range select |enum |0 - Range acc. to DB_1 (ILL1) |
|
||||||
|
| | | |1 - Range acc. to DB_2 (ILL2) |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x06 - TYPE 0x02 - Range 0lx to 1.020lx
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SVC |Supply voltage (linear) |value |0.0-255.0 ↔ 0.0-5.1 V |
|
||||||
|
|ILL2 |Illumination 2 (linear) |value |0.0-255.0 ↔ 0.0-510.0 lx |
|
||||||
|
|ILL1 |Illumination 1 (linear) |value |0.0-255.0 ↔ 0.0-1020.0 lx |
|
||||||
|
|RS |Range select |enum |0 - Range acc. to DB_1 (ILL1) |
|
||||||
|
| | | |1 - Range acc. to DB_2 (ILL2) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x07 - TYPE 0x01 - Occupancy with Supply voltage monitor
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SVC |Supply voltage (OPTIONAL) |value |0.0-250.0 ↔ 0.0-5.0 V |
|
||||||
|
|PIR |PIR Status |enum |0 - off |
|
||||||
|
| | | |1 - on |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x08 - TYPE 0x01 - Range 0lx to 510lx, 0°C to +51°C and Occupancy Button
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SVC |Supply voltage (linear) |value |0.0-255.0 ↔ 0.0-5.1 V |
|
||||||
|
|ILL |Illumination (linear) |value |0.0-255.0 ↔ 0.0-510.0 lx |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-255.0 ↔ 0.0-51.0 °C |
|
||||||
|
|PIRS |PIR Status |enum |0 - PIR on |
|
||||||
|
| | | |1 - PIR off |
|
||||||
|
|OCC |Occupancy Button |enum |0 - Button pressed |
|
||||||
|
| | | |1 - Button released |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x09 - TYPE 0x04 - CO2 Sensor
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|HUM |Rel. Humidity (linear) |value |0.0-200.0 ↔ 0.0-100.0 % |
|
||||||
|
|Conc |Concentration (linear) |value |0.0-255.0 ↔ 0.0-2550.0 ppm |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-255.0 ↔ 0.0-51.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x09 - TYPE 0x05 - VOC Sensor
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|Conc |VOC Concentration |value |0.0-65535.0 ↔ 0.0-65535.0 ppb |
|
||||||
|
|VOC_ID |VOC Identification |enum |0 - VOCT (total) |
|
||||||
|
| | | |1 - Formaldehyde |
|
||||||
|
| | | |2 - Benzene |
|
||||||
|
| | | |3 - Styrene |
|
||||||
|
| | | |4 - Toluene |
|
||||||
|
| | | |5 - Tetrachloroethylene |
|
||||||
|
| | | |6 - Xylene |
|
||||||
|
| | | |7 - n-Hexane |
|
||||||
|
| | | |8 - n-Octane |
|
||||||
|
| | | |9 - Cyclopentane |
|
||||||
|
| | | |10 - Methanol |
|
||||||
|
| | | |11 - Ethanol |
|
||||||
|
| | | |12 - 1-Pentanol |
|
||||||
|
| | | |13 - Acetone |
|
||||||
|
| | | |14 - ethylene Oxide |
|
||||||
|
| | | |15 - Acetaldehyde ue |
|
||||||
|
| | | |16 - Acetic Acid |
|
||||||
|
| | | |17 - Propionice Acid |
|
||||||
|
| | | |18 - ValericAcid |
|
||||||
|
| | | |19 - ButyricAcid |
|
||||||
|
| | | |20 - Ammoniac |
|
||||||
|
| | | |22 - Hydrogen Sulfide |
|
||||||
|
| | | |23 - Dimethylsulfide |
|
||||||
|
| | | |24 - 2-Butanol (butyl Alcohol) |
|
||||||
|
| | | |25 - 2-Methylpropanol |
|
||||||
|
| | | |26 - Diethyl ether |
|
||||||
|
| | | |255 - ozone |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x10 - TYPE 0x03 - Temperature Sensor and Set Point
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Set Point (linear) |value |0.0-255.0 ↔ 0.0-255.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-40.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x10 - TYPE 0x05 - Temperature Sensor, Set Point and Occupancy Control
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Set Point (linear) |value |0.0-255.0 ↔ 0.0-255.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-40.0 °C |
|
||||||
|
|OCC |Occupancy Button |enum |0 - Button pressed |
|
||||||
|
| | | |1 - Button released |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x10 - TYPE 0x06 - Temperature Sensor, Set Point and Day/Night Control
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Set Point (linear) |value |0.0-255.0 ↔ 0.0-255.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-40.0 °C |
|
||||||
|
|SLSW |Slide switch |enum |0 - Position I / Night / Off |
|
||||||
|
| | | |1 - Position O / Day / On |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x10 - TYPE 0x10 - Temperature and Humidity Sensor, Set Point and Occupancy Control
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Set Point (linear) |value |0.0-255.0 ↔ 0.0-255.0 |
|
||||||
|
|HUM |Rel. Humidity (linear) |value |0.0-250.0 ↔ 0.0-100.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-250.0 ↔ 0.0-40.0 °C |
|
||||||
|
|OCC |Occupancy Button |enum |0 - Button pressed |
|
||||||
|
| | | |1 - Button released |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x10 - TYPE 0x12 - Temperature and Humidity Sensor and Set Point
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Set Point (linear) |value |0.0-255.0 ↔ 0.0-255.0 |
|
||||||
|
|HUM |Rel. Humidity (linear) |value |0.0-250.0 ↔ 0.0-100.0 % |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-250.0 ↔ 0.0-40.0 °C |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x11 - TYPE 0x02 - Temperature Controller Output
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CVAR |Actual value of controller |value |0.0-255.0 ↔ 0.0-100.0 % |
|
||||||
|
|FAN |Actual value of fan |enum |0 - State 0 Manual |
|
||||||
|
| | | |1 - State 1 Manual |
|
||||||
|
| | | |2 - State 2 Manual |
|
||||||
|
| | | |3 - State 3 Manual |
|
||||||
|
| | | |16 - State 0 Automatic |
|
||||||
|
| | | |17 - State 1 Automatic |
|
||||||
|
| | | |18 - State 2 Automatic |
|
||||||
|
| | | |19 - State 3 Automatic |
|
||||||
|
| | | |255 - Not Available |
|
||||||
|
|ASP |Actual Setpoint |value |0.0-255.0 ↔ 0.0-51.2 C |
|
||||||
|
|ALR |Alarm |enum |0 - No alarm |
|
||||||
|
| | | |1 - Alarm |
|
||||||
|
|CTM |Controller mode |enum |1 - Heating |
|
||||||
|
| | | |2 - Cooling |
|
||||||
|
| | | |3 - Off |
|
||||||
|
|CTS |Controller state |enum |0 - Automatic |
|
||||||
|
| | | |1 - Override |
|
||||||
|
|ERH |Energy hold-off |enum |0 - Normal |
|
||||||
|
| | | |1 - Energy hold-off / Dew point |
|
||||||
|
|RO |Room occupancy |enum |0 - Occupied |
|
||||||
|
| | | |1 - Unoccupied |
|
||||||
|
| | | |2 - StandBy |
|
||||||
|
| | | |3 - Frost |
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x11 - TYPE 0x03 - Blind Status
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|BSP |Blind/shutter position |value |0.0-100.0 ↔ 0.0-100.0 % |
|
||||||
|
|AS |Angle sign |enum |0 - Positive sign |
|
||||||
|
| | | |1 - Negative sign |
|
||||||
|
|AN |Angle in 2 degrees steps |value |0.0-90.0 ↔ 0.0-180.0 degrees |
|
||||||
|
|PVF |Position value flag |enum |0 - No position value available |
|
||||||
|
| | | |1 - Position value available |
|
||||||
|
|AVF |Angle value flag |enum |0 - No Angle value available |
|
||||||
|
| | | |1 - Angle value available |
|
||||||
|
|ES |Error state |enum |0 - No error present |
|
||||||
|
| | | |1 - End-positions are not configured |
|
||||||
|
| | | |2 - Internal failure |
|
||||||
|
| | | |3 - Not used |
|
||||||
|
|EP |End position |enum |0 - No End-position available |
|
||||||
|
| | | |1 - No End-position reached |
|
||||||
|
| | | |2 - Blind fully open |
|
||||||
|
| | | |3 - Blind fully closed |
|
||||||
|
|ST |Status |enum |0 - No status available |
|
||||||
|
| | | |1 - Blind is stopped |
|
||||||
|
| | | |2 - Blind opens |
|
||||||
|
| | | |3 - Blind closes |
|
||||||
|
|SM |Service mode |enum |0 - Normal mode |
|
||||||
|
| | | |1 - Service mode activated |
|
||||||
|
|MOTP |Mode of the position |enum |0 - Normal mode |
|
||||||
|
| | | |1 - Inverse ode |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x14 - TYPE 0x01 - Single Input Contact (Window/Door), Supply voltage monitor
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SVC |Supply voltage / super cap. (linear); 251 - 255 reserved for error code|value |0.0-250.0 ↔ 0.0-5.0 V |
|
||||||
|
|CT |Contact |enum |1 - open |
|
||||||
|
| | | |0 - closed |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x20 - TYPE 0x01 - Battery Powered Actuator (BI-DIR)
|
||||||
|
|
||||||
|
###### direction: 1
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CV |Current Value |value |0.0-100.0 ↔ 0.0-100.0 % |
|
||||||
|
|SO |Service On |enum |0 - off |
|
||||||
|
| | | |1 - on |
|
||||||
|
|ENIE |Energy input enabled |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|ES |Energy storage sufficiently charged |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|BCAP |Battery capacity; change battery next days |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|CCO |Contact, cover open |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|FTS |Failure Temperature sensor, out of range |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|DWO |Detection, window open |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|ACO |Actuator obstructed |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|TMP |Temperature (linear) |value |0.0-255.0 ↔ 0.0-40.0 °C |
|
||||||
|
|
||||||
|
###### direction: 2
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|SP |Valve Position or Temperature Setpoint |value |0.0-100.0 ↔ 0.0-100.0 % |
|
||||||
|
|TMP |Temperature from RCU |value |0.0-255.0 ↔ 0.0-40.0 °C |
|
||||||
|
|RIN |Run init sequence |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|LFS |Lift set |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|VO |Valve open / maintenance |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|VC |Valve closed |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|SB |Summer bit, Reduction of energy consumption |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|SPS |Set point selection |enum |0 - Valve position |
|
||||||
|
| | | |1 - Temperature set point |
|
||||||
|
|SPN |Set point inverse |enum |0 - false |
|
||||||
|
| | | |1 - true |
|
||||||
|
|RCU |Select function |enum |0 - RCU |
|
||||||
|
| | | |1 - service on |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x12 - TYPE 0x01 - Electricity
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|MR |current value in W or cumulative value in kWh |value |0.0-16777215.0 ↔ 0.0-16777215.0 |
|
||||||
|
|TI |Tariff info |value |0.0-15.0 ↔ 0.0-15.0 |
|
||||||
|
|DT |Current value or cumulative value |enum |0 - kWh |
|
||||||
|
| | | |1 - W |
|
||||||
|
|DIV |Divisor for value |enum |0 - x/1 |
|
||||||
|
| | | |1 - x/10 |
|
||||||
|
| | | |2 - x/100 |
|
||||||
|
| | | |3 - x/1000 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x30 - TYPE 0x03 - Digital Inputs, Wake and Temperature
|
||||||
|
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|TMP |Temperature (linear) |value |255.0-0.0 ↔ 0.0-40.0 °C |
|
||||||
|
|WA0 |Value of wake signal |enum |0 - Low |
|
||||||
|
| | | |1 - High |
|
||||||
|
|DI3 |Digital Input 3 |enum |0 - Low |
|
||||||
|
| | | |1 - High |
|
||||||
|
|DI2 |Digital Input 2 |enum |0 - Low |
|
||||||
|
| | | |1 - High |
|
||||||
|
|DI1 |Digital Input 1 |enum |0 - Low |
|
||||||
|
| | | |1 - High |
|
||||||
|
|DI0 |Digital Input 0 |enum |0 - Low |
|
||||||
|
| | | |1 - High |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xA5 - FUNC 0x38 - TYPE 0x08 - Gateway
|
||||||
|
|
||||||
|
###### command: 1
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|COM |Command ID |enum |0-13 - Command ID {value} |
|
||||||
|
|TIM |Time in 1/10 seconds. 0 = no time specifed |value |1.0-65535.0 ↔ 0.1-6553.5 s |
|
||||||
|
|LCK |Lock for duration time if time >0, unlimited time of no time specified. Locking may be cleared with "unlock". During lock phase no other commands will be accepted or executed|enum |0 - Unlock |
|
||||||
|
| | | |1 - Lock |
|
||||||
|
|DEL |Delay or duration (if Time > 0); 0 = Duration (Execute switching command immediately and switch back after duration) 1 = Delay (Execute switching command after delay)|enum |0 - Duration |
|
||||||
|
| | | |1 - Delay |
|
||||||
|
|SW |Switching command ON/OFF |enum |0 - Off |
|
||||||
|
| | | |1 - On |
|
||||||
|
|
||||||
|
###### command: 2
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|COM |Command ID |enum |0-13 - Command ID {value} |
|
||||||
|
|EDIM |Dimming value (absolute [0...255] or relative [0...100])|value |0.0-255.0 ↔ 0.0-255.0 % |
|
||||||
|
|RMP |Ramping time in seconds, 0 = no ramping, 1...255 = seconds to 100%|value |0.0-255.0 ↔ 0.0-255.0 s |
|
||||||
|
|EDIMR |Dimming Range |enum |0 - Absolute value |
|
||||||
|
| | | |1 - Relative value |
|
||||||
|
|STR |Store final value |enum |0 - No |
|
||||||
|
| | | |1 - Yes |
|
||||||
|
|SW |Switching command |enum |0 - Off |
|
||||||
|
| | | |1 - On |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### VLD Telegram (0xD2)
|
||||||
|
##### RORG 0xD2 - FUNC 0x01 - TYPE 0x01 - Electronic switch with Local Control
|
||||||
|
|
||||||
|
###### command: 4
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|PF |Power Failure |enum |0 - Power Failure Detection disabled/not supported |
|
||||||
|
| | | |1 - Power Failure Detection enabled |
|
||||||
|
|PFD |Power Failure Detection |enum |0 - Power Failure Detection not detected/not supported/disabled |
|
||||||
|
| | | |1 - Power Failure Detection Detected |
|
||||||
|
|CMD |Command indentifier |enum |0-13 - Command ID {value} |
|
||||||
|
|OC |Over current switch off |enum |0 - Over current switch off: ready / not supported |
|
||||||
|
| | | |1 - Over current switch off: executed |
|
||||||
|
|EL |Error level |enum |0 - Error level 0: hardware OK |
|
||||||
|
| | | |1 - Error level 1: hardware warning |
|
||||||
|
| | | |2 - Error level 2: hardware failure |
|
||||||
|
| | | |3 - Error level not supported |
|
||||||
|
|IO |I/O channel |enum |0-29 - Output channel {value} (to load) |
|
||||||
|
| | | |30 - Not applicable, do not use |
|
||||||
|
| | | |31 - Input channel (from mains supply) |
|
||||||
|
|LC |Local control |enum |0 - Local control disabled / not supported |
|
||||||
|
| | | |1 - Local control enabled |
|
||||||
|
|OV |Output value |enum |0 - Output value 0% or OFF |
|
||||||
|
| | | |1-100 - Output value {value}% or ON |
|
||||||
|
| | | |101-126 - Not used |
|
||||||
|
| | | |127 - output value not valid / not set |
|
||||||
|
|
||||||
|
###### command: 1
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CMD |Command indentifier |enum |0-13 - Command ID {value} |
|
||||||
|
|DV |Dim value |enum |0 - Switch to new output value |
|
||||||
|
| | | |1 - Dim to new output level - dim timer 1 |
|
||||||
|
| | | |2 - Dim to new output level - dim timer 2 |
|
||||||
|
| | | |3 - Dim to new output level - dim timer 3 |
|
||||||
|
| | | |4 - Stop dimming |
|
||||||
|
|IO |I/O channel |enum |0-29 - Output channel {value} (to load) |
|
||||||
|
| | | |30 - All output channels supported by the device |
|
||||||
|
| | | |31 - Input channel (from mains supply) |
|
||||||
|
|OV |Output value |enum |0 - Output value 0% or OFF |
|
||||||
|
| | | |1-100 - Output value {value}% or ON |
|
||||||
|
| | | |101-126 - Not used |
|
||||||
|
| | | |127 - output value not valid / not set |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### RORG 0xD2 - FUNC 0x05 - TYPE 0x00 - Type 0x00
|
||||||
|
|
||||||
|
###### command: 1
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|POS |Vertical position |enum |0-100 - Output position {value}% |
|
||||||
|
| | | |127 - Do not change |
|
||||||
|
|ANG |Rotation angle |enum |0-100 - Output angle {value}% |
|
||||||
|
| | | |127 - Do not change |
|
||||||
|
|REPO |Repositioning |enum |0 - Go directly to POS/ANG |
|
||||||
|
| | | |1 - Go up (0%), then to POS/ANG |
|
||||||
|
| | | |2 - Go down (100%), then to POS/ANG |
|
||||||
|
| | | |3 - Reserved |
|
||||||
|
|LOCK |Locking modes |enum |0 - Do not change |
|
||||||
|
| | | |1 - Set blockage mode |
|
||||||
|
| | | |2 - Set alarm mode |
|
||||||
|
| | | |3 - Reserved |
|
||||||
|
| | | |4 - Reserved |
|
||||||
|
| | | |5 - Reserved |
|
||||||
|
| | | |6 - Reserved |
|
||||||
|
| | | |7 - Deblockage |
|
||||||
|
|CHN |Channel |enum |0 - Channel 1 |
|
||||||
|
|CMD |Command Id |enum |0-5 - Command ID {value} |
|
||||||
|
|
||||||
|
###### command: 2
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CHN |Channel |enum |0 - Channel 1 |
|
||||||
|
|CMD |Command Id |enum |0-5 - Command ID {value} |
|
||||||
|
|
||||||
|
###### command: 3
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|CHN |Channel |enum |0 - Channel 1 |
|
||||||
|
|CMD |Command Id |enum |0-5 - Command ID {value} |
|
||||||
|
|
||||||
|
###### command: 4
|
||||||
|
|shortcut|description |type |values |
|
||||||
|
|--------|--------------------------------------------------|--------|---- |
|
||||||
|
|POS |Vertical position |enum |0-100 - Output position {value}% |
|
||||||
|
| | | |127 - Do not change |
|
||||||
|
|ANG |Rotation angle |enum |0-100 - Output angle {value}% |
|
||||||
|
| | | |127 - Do not change |
|
||||||
|
|REPO |Repositioning |enum |0 - Go directly to POS/ANG |
|
||||||
|
| | | |1 - Go up (0%), then to POS/ANG |
|
||||||
|
| | | |2 - Go down (100%), then to POS/ANG |
|
||||||
|
| | | |3 - Reserved |
|
||||||
|
|LOCK |Locking modes |enum |0 - Do not change |
|
||||||
|
| | | |1 - Set blockage mode |
|
||||||
|
| | | |2 - Set alarm mode |
|
||||||
|
| | | |3 - Reserved |
|
||||||
|
| | | |4 - Reserved |
|
||||||
|
| | | |5 - Reserved |
|
||||||
|
| | | |6 - Reserved |
|
||||||
|
| | | |7 - Deblockage |
|
||||||
|
|CHN |Channel |enum |0 - Channel 1 |
|
||||||
|
|CMD |Command Id |enum |0-5 - Command ID {value} |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
''' Provider for different Communicator -classes for EnOcean. '''
|
||||||
|
from enocean.communicators.communicator import Communicator
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
from enocean.communicators.tcpcommunicator import TCPCommunicator
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import threading
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
from enocean.protocol.packet import Packet, UTETeachInPacket
|
||||||
|
from enocean.protocol.constants import PACKET, PARSE_RESULT, RETURN_CODE
|
||||||
|
|
||||||
|
|
||||||
|
class Communicator(threading.Thread):
|
||||||
|
'''
|
||||||
|
Communicator base-class for EnOcean.
|
||||||
|
Not to be used directly, only serves as base class for SerialCommunicator etc.
|
||||||
|
'''
|
||||||
|
logger = logging.getLogger('enocean.communicators.Communicator')
|
||||||
|
|
||||||
|
def __init__(self, callback=None, teach_in=True):
|
||||||
|
super(Communicator, self).__init__()
|
||||||
|
# Create an event to stop the thread
|
||||||
|
self._stop_flag = threading.Event()
|
||||||
|
# Input buffer
|
||||||
|
self._buffer = []
|
||||||
|
# Setup packet queues
|
||||||
|
self.transmit = queue.Queue()
|
||||||
|
self.receive = queue.Queue()
|
||||||
|
# Set the callback method
|
||||||
|
self.__callback = callback
|
||||||
|
# Internal variable for the Base ID of the module.
|
||||||
|
self._base_id = None
|
||||||
|
# Should new messages be learned automatically? Defaults to True.
|
||||||
|
# TODO: Not sure if we should use CO_WR_LEARNMODE??
|
||||||
|
self.teach_in = teach_in
|
||||||
|
|
||||||
|
def _get_from_send_queue(self):
|
||||||
|
''' Get message from send queue, if one exists '''
|
||||||
|
try:
|
||||||
|
packet = self.transmit.get(block=False)
|
||||||
|
self.logger.info('Sending packet')
|
||||||
|
self.logger.debug(packet)
|
||||||
|
return packet
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send(self, packet):
|
||||||
|
if not isinstance(packet, Packet):
|
||||||
|
self.logger.error('Object to send must be an instance of Packet')
|
||||||
|
return False
|
||||||
|
self.transmit.put(packet)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_flag.set()
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
''' Parses messages and puts them to receive queue '''
|
||||||
|
# Loop while we get new messages
|
||||||
|
while True:
|
||||||
|
status, self._buffer, packet = Packet.parse_msg(self._buffer)
|
||||||
|
# If message is incomplete -> break the loop
|
||||||
|
if status == PARSE_RESULT.INCOMPLETE:
|
||||||
|
return status
|
||||||
|
|
||||||
|
# If message is OK, add it to receive queue or send to the callback method
|
||||||
|
if status == PARSE_RESULT.OK and packet:
|
||||||
|
packet.received = datetime.datetime.now()
|
||||||
|
|
||||||
|
if isinstance(packet, UTETeachInPacket) and self.teach_in:
|
||||||
|
response_packet = packet.create_response_packet(self.base_id)
|
||||||
|
self.logger.info('Sending response to UTE teach-in.')
|
||||||
|
self.send(response_packet)
|
||||||
|
|
||||||
|
if self.__callback is None:
|
||||||
|
self.receive.put(packet)
|
||||||
|
else:
|
||||||
|
self.__callback(packet)
|
||||||
|
self.logger.debug(packet)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_id(self):
|
||||||
|
''' Fetches Base ID from the transmitter, if required. Otherwise returns the currently set Base ID. '''
|
||||||
|
# If base id is already set, return it.
|
||||||
|
if self._base_id is not None:
|
||||||
|
return self._base_id
|
||||||
|
|
||||||
|
# Send COMMON_COMMAND 0x08, CO_RD_IDBASE request to the module
|
||||||
|
self.send(Packet(PACKET.COMMON_COMMAND, data=[0x08]))
|
||||||
|
# Loop over 10 times, to make sure we catch the response.
|
||||||
|
# Thanks to timeout, shouldn't take more than a second.
|
||||||
|
# Unfortunately, all other messages received during this time are ignored.
|
||||||
|
for i in range(0, 10):
|
||||||
|
try:
|
||||||
|
packet = self.receive.get(block=True, timeout=0.1)
|
||||||
|
# We're only interested in responses to the request in question.
|
||||||
|
if packet.packet_type == PACKET.RESPONSE and packet.response == RETURN_CODE.OK and len(packet.response_data) == 4: # noqa: E501
|
||||||
|
# Base ID is set in the response data.
|
||||||
|
self._base_id = packet.response_data
|
||||||
|
# Put packet back to the Queue, so the user can also react to it if required...
|
||||||
|
self.receive.put(packet)
|
||||||
|
break
|
||||||
|
# Put other packets back to the Queue.
|
||||||
|
self.receive.put(packet)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
# Return the current Base ID (might be None).
|
||||||
|
return self._base_id
|
||||||
|
|
||||||
|
@base_id.setter
|
||||||
|
def base_id(self, base_id):
|
||||||
|
''' Sets the Base ID manually, only for testing purposes. '''
|
||||||
|
self._base_id = base_id
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import logging
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
|
||||||
|
from enocean.communicators.communicator import Communicator
|
||||||
|
|
||||||
|
|
||||||
|
class SerialCommunicator(Communicator):
|
||||||
|
''' Serial port communicator class for EnOcean radio '''
|
||||||
|
logger = logging.getLogger('enocean.communicators.SerialCommunicator')
|
||||||
|
|
||||||
|
def __init__(self, port='/dev/ttyS4', callback=None):
|
||||||
|
super(SerialCommunicator, self).__init__(callback)
|
||||||
|
# Initialize serial port
|
||||||
|
self.__ser = serial.Serial(port, 57600, timeout=0.1)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.logger.info('SerialCommunicator started')
|
||||||
|
while not self._stop_flag.is_set():
|
||||||
|
# If there's messages in transmit queue
|
||||||
|
# send them
|
||||||
|
while True:
|
||||||
|
packet = self._get_from_send_queue()
|
||||||
|
if not packet:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
self.__ser.write(bytearray(packet.build()))
|
||||||
|
except serial.SerialException:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
# Read chars from serial port as hex numbers
|
||||||
|
try:
|
||||||
|
self._buffer.extend(bytearray(self.__ser.read(16)))
|
||||||
|
except serial.SerialException:
|
||||||
|
self.logger.error('Serial port exception! (device disconnected or multiple access on port?)')
|
||||||
|
self.stop()
|
||||||
|
self.parse()
|
||||||
|
time.sleep(0)
|
||||||
|
|
||||||
|
self.__ser.close()
|
||||||
|
self.logger.info('SerialCommunicator stopped')
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from enocean.communicators.communicator import Communicator
|
||||||
|
|
||||||
|
|
||||||
|
class TCPCommunicator(Communicator):
|
||||||
|
''' Socket communicator class for EnOcean radio '''
|
||||||
|
logger = logging.getLogger('enocean.communicators.TCPCommunicator')
|
||||||
|
|
||||||
|
def __init__(self, host='', port=9637):
|
||||||
|
super(TCPCommunicator, self).__init__()
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.logger.info('TCPCommunicator started')
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.bind((self.host, self.port))
|
||||||
|
sock.listen(5)
|
||||||
|
sock.settimeout(0.5)
|
||||||
|
|
||||||
|
while not self._stop_flag.is_set():
|
||||||
|
try:
|
||||||
|
(client, addr) = sock.accept()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
self.logger.debug('Client "%s" connected' % (addr))
|
||||||
|
client.settimeout(0.5)
|
||||||
|
while True and not self._stop_flag.is_set():
|
||||||
|
try:
|
||||||
|
data = client.recv(2048)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self._buffer.extend(bytearray(data))
|
||||||
|
self.parse()
|
||||||
|
client.close()
|
||||||
|
self.logger.debug('Client disconnected')
|
||||||
|
sock.close()
|
||||||
|
self.logger.info('TCPCommunicator stopped')
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
from enocean.communicators.communicator import Communicator
|
||||||
|
from enocean.protocol.packet import Packet, RadioPacket
|
||||||
|
from enocean.protocol.constants import PACKET
|
||||||
|
from enocean.decorators import timing
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_buffer():
|
||||||
|
''' Test buffer parsing for Communicator '''
|
||||||
|
data = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x00,
|
||||||
|
0x75
|
||||||
|
])
|
||||||
|
com = Communicator()
|
||||||
|
com._buffer.extend(data[0:5])
|
||||||
|
com.parse()
|
||||||
|
assert com.receive.qsize() == 0
|
||||||
|
com._buffer.extend(data[5:])
|
||||||
|
com.parse()
|
||||||
|
assert com.receive.qsize() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_send():
|
||||||
|
''' Test sending packets to Communicator '''
|
||||||
|
com = Communicator()
|
||||||
|
assert com.send('AJSNDJASNDJANSD') is False
|
||||||
|
assert com.transmit.qsize() == 0
|
||||||
|
assert com._get_from_send_queue() is None
|
||||||
|
assert com.send(Packet(PACKET.COMMON_COMMAND, [0x08])) is True
|
||||||
|
assert com.transmit.qsize() == 1
|
||||||
|
assert isinstance(com._get_from_send_queue(), Packet)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop():
|
||||||
|
com = Communicator()
|
||||||
|
com.stop()
|
||||||
|
assert com._stop_flag.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_callback():
|
||||||
|
def callback(packet):
|
||||||
|
assert isinstance(packet, RadioPacket)
|
||||||
|
|
||||||
|
data = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x00,
|
||||||
|
0x75
|
||||||
|
])
|
||||||
|
|
||||||
|
com = Communicator(callback=callback)
|
||||||
|
com._buffer.extend(data)
|
||||||
|
com.parse()
|
||||||
|
assert com.receive.qsize() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_id():
|
||||||
|
com = Communicator()
|
||||||
|
assert com.base_id is None
|
||||||
|
|
||||||
|
other_data = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x00,
|
||||||
|
0x75
|
||||||
|
])
|
||||||
|
|
||||||
|
response_data = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x05, 0x00, 0x02,
|
||||||
|
0xCE,
|
||||||
|
0x00, 0xFF, 0x87, 0xCA, 0x00,
|
||||||
|
0xA3
|
||||||
|
])
|
||||||
|
|
||||||
|
com._buffer.extend(other_data)
|
||||||
|
com._buffer.extend(response_data)
|
||||||
|
com.parse()
|
||||||
|
assert com.base_id == [0xFF, 0x87, 0xCA, 0x00]
|
||||||
|
assert com.receive.qsize() == 2
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def send_to_tcp_socket(host, port, packet):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect((host, port))
|
||||||
|
sock.send(str(bytearray(packet.build())))
|
||||||
|
sock.close()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging(level=logging.DEBUG, log_to_file=False, logsize=1024, logcount=5):
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
logger = logging.getLogger('enocean')
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setLevel(level)
|
||||||
|
stream_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(stream_handler)
|
||||||
|
|
||||||
|
if log_to_file:
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler('enocean.log', 'a', logsize*1000, logcount)
|
||||||
|
file_handler.setLevel(level)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division
|
||||||
|
import time
|
||||||
|
import functools
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
|
||||||
|
def timing(rounds=1, limit=None):
|
||||||
|
'''
|
||||||
|
Wrapper to implement simple timing of tests.
|
||||||
|
Allows running multiple rounds to calculate average time.
|
||||||
|
Limit (in milliseconds) can be set to assert, if (average) duration is too high.
|
||||||
|
'''
|
||||||
|
def decorator(method):
|
||||||
|
@functools.wraps(method)
|
||||||
|
def f():
|
||||||
|
if rounds == 1:
|
||||||
|
start = time.time()
|
||||||
|
method()
|
||||||
|
duration = time.time() - start
|
||||||
|
else:
|
||||||
|
start = time.time()
|
||||||
|
for i in range(rounds):
|
||||||
|
method()
|
||||||
|
duration = (time.time() - start) / rounds
|
||||||
|
# Use milliseconds for duration counter
|
||||||
|
duration = duration * 1e3
|
||||||
|
|
||||||
|
print('Test "%s.%s" took %.06f ms.' % (method.__module__, method.__name__, duration))
|
||||||
|
if limit is not None:
|
||||||
|
assert limit > duration, 'Timing failure: %.06f > %.06f' % (duration, limit)
|
||||||
|
|
||||||
|
# Run tests with timings, only if WITH_TIMINGS environment variable is set.
|
||||||
|
# This is because tests with multiple rounds can take long to process.
|
||||||
|
if environ.get('WITH_TIMINGS', None) == '1':
|
||||||
|
return f
|
||||||
|
return method
|
||||||
|
return decorator
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
# EnOceanSerialProtocol3.pdf / 12
|
||||||
|
class PACKET(IntEnum):
|
||||||
|
RESERVED = 0x00
|
||||||
|
# RADIO == RADIO_ERP1
|
||||||
|
# Kept for backwards compatibility reasons, for example custom packet
|
||||||
|
# generation shouldn't be affected...
|
||||||
|
RADIO = 0x01
|
||||||
|
RADIO_ERP1 = 0x01
|
||||||
|
RESPONSE = 0x02
|
||||||
|
RADIO_SUB_TEL = 0x03
|
||||||
|
EVENT = 0x04
|
||||||
|
COMMON_COMMAND = 0x05
|
||||||
|
SMART_ACK_COMMAND = 0x06
|
||||||
|
REMOTE_MAN_COMMAND = 0x07
|
||||||
|
RADIO_MESSAGE = 0x09
|
||||||
|
# RADIO_ADVANCED == RADIO_ERP2
|
||||||
|
# Kept for backwards compatibility reasons
|
||||||
|
RADIO_ADVANCED = 0x0A
|
||||||
|
RADIO_ERP2 = 0x0A
|
||||||
|
RADIO_802_15_4 = 0x10
|
||||||
|
COMMAND_2_4 = 0x11
|
||||||
|
|
||||||
|
|
||||||
|
# EnOceanSerialProtocol3.pdf / 18
|
||||||
|
class RETURN_CODE(IntEnum):
|
||||||
|
OK = 0x00
|
||||||
|
ERROR = 0x01
|
||||||
|
NOT_SUPPORTED = 0x02
|
||||||
|
WRONG_PARAM = 0x03
|
||||||
|
OPERATION_DENIED = 0x04
|
||||||
|
|
||||||
|
|
||||||
|
# EnOceanSerialProtocol3.pdf / 20
|
||||||
|
class EVENT_CODE(IntEnum):
|
||||||
|
SA_RECLAIM_NOT_SUCCESFUL = 0x01
|
||||||
|
SA_CONFIRM_LEARN = 0x02
|
||||||
|
SA_LEARN_ACK = 0x03
|
||||||
|
CO_READY = 0x04
|
||||||
|
CO_EVENT_SECUREDEVICES = 0x05
|
||||||
|
|
||||||
|
|
||||||
|
# EnOcean_Equipment_Profiles_EEP_V2.61_public.pdf / 8
|
||||||
|
class RORG(IntEnum):
|
||||||
|
UNDEFINED = 0x00
|
||||||
|
RPS = 0xF6
|
||||||
|
BS1 = 0xD5
|
||||||
|
BS4 = 0xA5
|
||||||
|
VLD = 0xD2
|
||||||
|
MSC = 0xD1
|
||||||
|
ADT = 0xA6
|
||||||
|
SM_LRN_REQ = 0xC6
|
||||||
|
SM_LRN_ANS = 0xC7
|
||||||
|
SM_REC = 0xA7
|
||||||
|
SYS_EX = 0xC5
|
||||||
|
SEC = 0x30
|
||||||
|
SEC_ENCAPS = 0x31
|
||||||
|
UTE = 0xD4
|
||||||
|
|
||||||
|
|
||||||
|
# Results for message parsing
|
||||||
|
class PARSE_RESULT(IntEnum):
|
||||||
|
OK = 0x00
|
||||||
|
INCOMPLETE = 0x01
|
||||||
|
CRC_MISMATCH = 0x03
|
||||||
|
|
||||||
|
|
||||||
|
# Data byte indexing
|
||||||
|
# Starts from the end, so works on messages of all length.
|
||||||
|
class DB0(object):
|
||||||
|
BIT_0 = -1
|
||||||
|
BIT_1 = -2
|
||||||
|
BIT_2 = -3
|
||||||
|
BIT_3 = -4
|
||||||
|
BIT_4 = -5
|
||||||
|
BIT_5 = -6
|
||||||
|
BIT_6 = -7
|
||||||
|
BIT_7 = -8
|
||||||
|
|
||||||
|
|
||||||
|
class DB1(object):
|
||||||
|
BIT_0 = -9
|
||||||
|
BIT_1 = -10
|
||||||
|
BIT_2 = -11
|
||||||
|
BIT_3 = -12
|
||||||
|
BIT_4 = -13
|
||||||
|
BIT_5 = -14
|
||||||
|
BIT_6 = -15
|
||||||
|
BIT_7 = -16
|
||||||
|
|
||||||
|
|
||||||
|
class DB2(object):
|
||||||
|
BIT_0 = -17
|
||||||
|
BIT_1 = -18
|
||||||
|
BIT_2 = -19
|
||||||
|
BIT_3 = -20
|
||||||
|
BIT_4 = -21
|
||||||
|
BIT_5 = -22
|
||||||
|
BIT_6 = -23
|
||||||
|
BIT_7 = -24
|
||||||
|
|
||||||
|
|
||||||
|
class DB3(object):
|
||||||
|
BIT_0 = -25
|
||||||
|
BIT_1 = -26
|
||||||
|
BIT_2 = -27
|
||||||
|
BIT_3 = -28
|
||||||
|
BIT_4 = -29
|
||||||
|
BIT_5 = -30
|
||||||
|
BIT_6 = -31
|
||||||
|
BIT_7 = -32
|
||||||
|
|
||||||
|
|
||||||
|
class DB4(object):
|
||||||
|
BIT_0 = -33
|
||||||
|
BIT_1 = -34
|
||||||
|
BIT_2 = -35
|
||||||
|
BIT_3 = -36
|
||||||
|
BIT_4 = -37
|
||||||
|
BIT_5 = -38
|
||||||
|
BIT_6 = -39
|
||||||
|
BIT_7 = -40
|
||||||
|
|
||||||
|
|
||||||
|
class DB5(object):
|
||||||
|
BIT_0 = -41
|
||||||
|
BIT_1 = -42
|
||||||
|
BIT_2 = -43
|
||||||
|
BIT_3 = -44
|
||||||
|
BIT_4 = -45
|
||||||
|
BIT_5 = -46
|
||||||
|
BIT_6 = -47
|
||||||
|
BIT_7 = -48
|
||||||
|
|
||||||
|
|
||||||
|
class DB6(object):
|
||||||
|
BIT_0 = -49
|
||||||
|
BIT_1 = -50
|
||||||
|
BIT_2 = -51
|
||||||
|
BIT_3 = -52
|
||||||
|
BIT_4 = -53
|
||||||
|
BIT_5 = -54
|
||||||
|
BIT_6 = -55
|
||||||
|
BIT_7 = -56
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
# https://gist.github.com/hypebeast/3833758
|
||||||
|
CRC_TABLE = (
|
||||||
|
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38,
|
||||||
|
0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77,
|
||||||
|
0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, 0x48, 0x4f, 0x46,
|
||||||
|
0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9,
|
||||||
|
0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4,
|
||||||
|
0xc3, 0xca, 0xcd, 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b,
|
||||||
|
0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba,
|
||||||
|
0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
|
||||||
|
0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7,
|
||||||
|
0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88,
|
||||||
|
0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, 0x27, 0x20, 0x29,
|
||||||
|
0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16,
|
||||||
|
0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b,
|
||||||
|
0x4c, 0x45, 0x42, 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74,
|
||||||
|
0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b,
|
||||||
|
0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
|
||||||
|
0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1,
|
||||||
|
0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e,
|
||||||
|
0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, 0x51, 0x56, 0x5f,
|
||||||
|
0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10,
|
||||||
|
0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d,
|
||||||
|
0x3a, 0x33, 0x34, 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55,
|
||||||
|
0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f, 0x6a, 0x6d, 0x64,
|
||||||
|
0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
|
||||||
|
0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae,
|
||||||
|
0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91,
|
||||||
|
0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, 0xde, 0xd9, 0xd0,
|
||||||
|
0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef,
|
||||||
|
0xfa, 0xfd, 0xf4, 0xf3)
|
||||||
|
|
||||||
|
|
||||||
|
def calc(msg):
|
||||||
|
checksum = 0
|
||||||
|
for byte in msg:
|
||||||
|
checksum = CRC_TABLE[checksum & 0xFF ^ byte & 0xFF]
|
||||||
|
return checksum
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from sys import version_info
|
||||||
|
from collections import OrderedDict
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from bs4 import XMLParsedAsHTMLWarning
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
|
||||||
|
import enocean.utils
|
||||||
|
# Left as a helper
|
||||||
|
from enocean.protocol.constants import RORG # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
class EEP(object):
|
||||||
|
logger = logging.getLogger('enocean.protocol.eep')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.init_ok = False
|
||||||
|
self.telegrams = {}
|
||||||
|
|
||||||
|
eep_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'EEP.xml')
|
||||||
|
try:
|
||||||
|
if version_info[0] > 2:
|
||||||
|
with open(eep_path, 'r', encoding='UTF-8') as xml_file:
|
||||||
|
self.soup = BeautifulSoup(xml_file.read(), "html.parser")
|
||||||
|
else:
|
||||||
|
with open(eep_path, 'r') as xml_file:
|
||||||
|
self.soup = BeautifulSoup(xml_file.read(), "html.parser")
|
||||||
|
self.init_ok = True
|
||||||
|
self.__load_xml()
|
||||||
|
except IOError:
|
||||||
|
# Impossible to test with the current structure?
|
||||||
|
# To be honest, as the XML is included with the library,
|
||||||
|
# there should be no possibility of ever reaching this...
|
||||||
|
self.logger.warn('Cannot load protocol file!')
|
||||||
|
self.init_ok = False
|
||||||
|
|
||||||
|
def __load_xml(self):
|
||||||
|
self.telegrams = {
|
||||||
|
enocean.utils.from_hex_string(telegram['rorg']): {
|
||||||
|
enocean.utils.from_hex_string(function['func']): {
|
||||||
|
enocean.utils.from_hex_string(type['type'], ): type
|
||||||
|
for type in function.find_all('profile')
|
||||||
|
}
|
||||||
|
for function in telegram.find_all('profiles')
|
||||||
|
}
|
||||||
|
for telegram in self.soup.find_all('telegram')
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_raw(source, bitarray):
|
||||||
|
''' Get raw data as integer, based on offset and size '''
|
||||||
|
offset = int(source['offset'])
|
||||||
|
size = int(source['size'])
|
||||||
|
return int(''.join(['1' if digit else '0' for digit in bitarray[offset:offset + size]]), 2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_raw(target, raw_value, bitarray):
|
||||||
|
''' put value into bit array '''
|
||||||
|
offset = int(target['offset'])
|
||||||
|
size = int(target['size'])
|
||||||
|
for digit in range(size):
|
||||||
|
bitarray[offset+digit] = (raw_value >> (size-digit-1)) & 0x01 != 0
|
||||||
|
return bitarray
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_rangeitem(source, raw_value):
|
||||||
|
for rangeitem in source.find_all('rangeitem'):
|
||||||
|
if raw_value in range(int(rangeitem.get('start', -1)), int(rangeitem.get('end', -1)) + 1):
|
||||||
|
return rangeitem
|
||||||
|
|
||||||
|
def _get_value(self, source, bitarray):
|
||||||
|
''' Get value, based on the data in XML '''
|
||||||
|
raw_value = self._get_raw(source, bitarray)
|
||||||
|
|
||||||
|
rng = source.find('range')
|
||||||
|
rng_min = float(rng.find('min').text)
|
||||||
|
rng_max = float(rng.find('max').text)
|
||||||
|
|
||||||
|
scl = source.find('scale')
|
||||||
|
scl_min = float(scl.find('min').text)
|
||||||
|
scl_max = float(scl.find('max').text)
|
||||||
|
|
||||||
|
return {
|
||||||
|
source['shortcut']: {
|
||||||
|
'description': source.get('description'),
|
||||||
|
'unit': source['unit'],
|
||||||
|
'value': (scl_max - scl_min) / (rng_max - rng_min) * (raw_value - rng_min) + scl_min,
|
||||||
|
'raw_value': raw_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_enum(self, source, bitarray):
|
||||||
|
''' Get enum value, based on the data in XML '''
|
||||||
|
raw_value = self._get_raw(source, bitarray)
|
||||||
|
|
||||||
|
# Find value description.
|
||||||
|
value_desc = source.find('item', {'value': str(raw_value)}) or self._get_rangeitem(source, raw_value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
source['shortcut']: {
|
||||||
|
'description': source.get('description'),
|
||||||
|
'unit': source.get('unit', ''),
|
||||||
|
'value': value_desc['description'].format(value=raw_value),
|
||||||
|
'raw_value': raw_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_boolean(self, source, bitarray):
|
||||||
|
''' Get boolean value, based on the data in XML '''
|
||||||
|
raw_value = self._get_raw(source, bitarray)
|
||||||
|
return {
|
||||||
|
source['shortcut']: {
|
||||||
|
'description': source.get('description'),
|
||||||
|
'unit': source.get('unit', ''),
|
||||||
|
'value': True if raw_value else False,
|
||||||
|
'raw_value': raw_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _set_value(self, target, value, bitarray):
|
||||||
|
''' set given numeric value to target field in bitarray '''
|
||||||
|
# derive raw value
|
||||||
|
rng = target.find('range')
|
||||||
|
rng_min = float(rng.find('min').text)
|
||||||
|
rng_max = float(rng.find('max').text)
|
||||||
|
scl = target.find('scale')
|
||||||
|
scl_min = float(scl.find('min').text)
|
||||||
|
scl_max = float(scl.find('max').text)
|
||||||
|
raw_value = (value - scl_min) * (rng_max - rng_min) / (scl_max - scl_min) + rng_min
|
||||||
|
# store value in bitfield
|
||||||
|
return self._set_raw(target, int(raw_value), bitarray)
|
||||||
|
|
||||||
|
def _set_enum(self, target, value, bitarray):
|
||||||
|
''' set given enum value (by string or integer value) to target field in bitarray '''
|
||||||
|
# derive raw value
|
||||||
|
if isinstance(value, int):
|
||||||
|
# check whether this value exists
|
||||||
|
if target.find('item', {'value': value}) or self._get_rangeitem(target, value):
|
||||||
|
# set integer values directly
|
||||||
|
raw_value = value
|
||||||
|
else:
|
||||||
|
raise ValueError('Enum value "%s" not found in EEP.' % (value))
|
||||||
|
else:
|
||||||
|
value_item = target.find('item', {'description': value})
|
||||||
|
if value_item is None:
|
||||||
|
raise ValueError('Enum description for value "%s" not found in EEP.' % (value))
|
||||||
|
raw_value = int(value_item['value'])
|
||||||
|
return self._set_raw(target, raw_value, bitarray)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_boolean(target, data, bitarray):
|
||||||
|
''' set given value to target bit in bitarray '''
|
||||||
|
bitarray[int(target['offset'])] = data
|
||||||
|
return bitarray
|
||||||
|
|
||||||
|
def find_profile(self, bitarray, eep_rorg, rorg_func, rorg_type, direction=None, command=None):
|
||||||
|
''' Find profile and data description, matching RORG, FUNC and TYPE '''
|
||||||
|
if not self.init_ok:
|
||||||
|
self.logger.warn('EEP.xml not loaded!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if eep_rorg not in self.telegrams.keys():
|
||||||
|
self.logger.warn('Cannot find rorg %s in EEP!', hex(eep_rorg))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if rorg_func not in self.telegrams[eep_rorg].keys():
|
||||||
|
self.logger.warn('Cannot find rorg %s func %s in EEP!', hex(eep_rorg), hex(rorg_func))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if rorg_type not in self.telegrams[eep_rorg][rorg_func].keys():
|
||||||
|
self.logger.warn('Cannot find rorg %s func %s type %s in EEP!', hex(eep_rorg), hex(rorg_func), hex(rorg_type))
|
||||||
|
return None
|
||||||
|
|
||||||
|
profile = self.telegrams[eep_rorg][rorg_func][rorg_type]
|
||||||
|
|
||||||
|
if command:
|
||||||
|
# multiple commands can be defined, with the command id always in same location (per RORG-FUNC-TYPE).
|
||||||
|
eep_command = profile.find('command', recursive=False)
|
||||||
|
# If commands are not set in EEP, or command is None,
|
||||||
|
# get the first data as a "best guess".
|
||||||
|
if not eep_command:
|
||||||
|
return profile.find('data', recursive=False)
|
||||||
|
|
||||||
|
# If eep_command is defined, so should be data.command
|
||||||
|
return profile.find('data', {'command': str(command)}, recursive=False)
|
||||||
|
|
||||||
|
# extract data description
|
||||||
|
# the direction tag is optional
|
||||||
|
if direction is None:
|
||||||
|
return profile.find('data', recursive=False)
|
||||||
|
return profile.find('data', {'direction': direction}, recursive=False)
|
||||||
|
|
||||||
|
def get_values(self, profile, bitarray, status):
|
||||||
|
''' Get keys and values from bitarray '''
|
||||||
|
if not self.init_ok or profile is None:
|
||||||
|
return [], {}
|
||||||
|
|
||||||
|
output = OrderedDict({})
|
||||||
|
for source in profile.contents:
|
||||||
|
if not source.name:
|
||||||
|
continue
|
||||||
|
if source.name == 'value':
|
||||||
|
output.update(self._get_value(source, bitarray))
|
||||||
|
if source.name == 'enum':
|
||||||
|
output.update(self._get_enum(source, bitarray))
|
||||||
|
if source.name == 'status':
|
||||||
|
output.update(self._get_boolean(source, status))
|
||||||
|
return output.keys(), output
|
||||||
|
|
||||||
|
def set_values(self, profile, data, status, properties):
|
||||||
|
''' Update data based on data contained in properties '''
|
||||||
|
if not self.init_ok or profile is None:
|
||||||
|
return data, status
|
||||||
|
|
||||||
|
for shortcut, value in properties.items():
|
||||||
|
# find the given property from EEP
|
||||||
|
target = profile.find(shortcut=shortcut)
|
||||||
|
if not target:
|
||||||
|
# TODO: Should we raise an error?
|
||||||
|
self.logger.warning('Cannot find data description for shortcut %s', shortcut)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# update bit_data
|
||||||
|
if target.name == 'value':
|
||||||
|
data = self._set_value(target, value, data)
|
||||||
|
if target.name == 'enum':
|
||||||
|
data = self._set_enum(target, value, data)
|
||||||
|
if target.name == 'status':
|
||||||
|
status = self._set_boolean(target, value, status)
|
||||||
|
return data, status
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import enocean.utils
|
||||||
|
from enocean.protocol import crc8
|
||||||
|
from enocean.protocol.eep import EEP
|
||||||
|
from enocean.protocol.constants import PACKET, RORG, PARSE_RESULT, DB0, DB2, DB3, DB4, DB6
|
||||||
|
|
||||||
|
|
||||||
|
class Packet(object):
|
||||||
|
'''
|
||||||
|
Base class for Packet.
|
||||||
|
Mainly used for for packet generation and
|
||||||
|
Packet.parse_msg(buf) for parsing message.
|
||||||
|
parse_msg() returns subclass, if one is defined for the data type.
|
||||||
|
'''
|
||||||
|
eep = EEP()
|
||||||
|
logger = logging.getLogger('enocean.protocol.packet')
|
||||||
|
|
||||||
|
def __init__(self, packet_type, data=None, optional=None):
|
||||||
|
self.packet_type = packet_type
|
||||||
|
self.rorg = RORG.UNDEFINED
|
||||||
|
self.rorg_func = None
|
||||||
|
self.rorg_type = None
|
||||||
|
self.rorg_manufacturer = None
|
||||||
|
|
||||||
|
self.received = None
|
||||||
|
|
||||||
|
if not isinstance(data, list) or data is None:
|
||||||
|
self.logger.warning('Replacing Packet.data with default value.')
|
||||||
|
self.data = []
|
||||||
|
else:
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
if not isinstance(optional, list) or optional is None:
|
||||||
|
self.logger.warning('Replacing Packet.optional with default value.')
|
||||||
|
self.optional = []
|
||||||
|
else:
|
||||||
|
self.optional = optional
|
||||||
|
|
||||||
|
self.status = 0
|
||||||
|
self.parsed = OrderedDict({})
|
||||||
|
self.repeater_count = 0
|
||||||
|
self._profile = None
|
||||||
|
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '0x%02X %s %s %s' % (
|
||||||
|
self.packet_type,
|
||||||
|
[hex(o) for o in self.data],
|
||||||
|
[hex(o) for o in self.optional],
|
||||||
|
self.parsed)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.packet_type == other.packet_type and self.rorg == other.rorg \
|
||||||
|
and self.data == other.data and self.optional == other.optional
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _bit_data(self):
|
||||||
|
# First and last 5 bits are always defined, so the data we're modifying is between them...
|
||||||
|
# TODO: This is valid for the packets we're currently manipulating.
|
||||||
|
# Needs the redefinition of Packet.data -> Packet.message.
|
||||||
|
# Packet.data would then only have the actual, documented data-bytes.
|
||||||
|
# Packet.message would contain the whole message.
|
||||||
|
# See discussion in issue #14
|
||||||
|
return enocean.utils.to_bitarray(self.data[1:len(self.data) - 5], (len(self.data) - 6) * 8)
|
||||||
|
|
||||||
|
@_bit_data.setter
|
||||||
|
def _bit_data(self, value):
|
||||||
|
# The same as getting the data, first and last 5 bits are ommitted, as they are defined...
|
||||||
|
for byte in range(len(self.data) - 6):
|
||||||
|
self.data[byte+1] = enocean.utils.from_bitarray(value[byte*8:(byte+1)*8])
|
||||||
|
|
||||||
|
# # COMMENTED OUT, AS NOTHING TOUCHES _bit_optional FOR NOW.
|
||||||
|
# # Thus, this is also untested.
|
||||||
|
# @property
|
||||||
|
# def _bit_optional(self):
|
||||||
|
# return enocean.utils.to_bitarray(self.optional, 8 * len(self.optional))
|
||||||
|
|
||||||
|
# @_bit_optional.setter
|
||||||
|
# def _bit_optional(self, value):
|
||||||
|
# if self.rorg in [RORG.RPS, RORG.BS1]:
|
||||||
|
# self.data[1] = enocean.utils.from_bitarray(value)
|
||||||
|
# if self.rorg == RORG.BS4:
|
||||||
|
# for byte in range(4):
|
||||||
|
# self.data[byte+1] = enocean.utils.from_bitarray(value[byte*8:(byte+1)*8])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _bit_status(self):
|
||||||
|
return enocean.utils.to_bitarray(self.status)
|
||||||
|
|
||||||
|
@_bit_status.setter
|
||||||
|
def _bit_status(self, value):
|
||||||
|
self.status = enocean.utils.from_bitarray(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_msg(buf):
|
||||||
|
'''
|
||||||
|
Parses message from buffer.
|
||||||
|
returns:
|
||||||
|
- PARSE_RESULT
|
||||||
|
- remaining buffer
|
||||||
|
- Packet -object (if message was valid, else None)
|
||||||
|
'''
|
||||||
|
# If the buffer doesn't contain 0x55 (start char)
|
||||||
|
# the message isn't needed -> ignore
|
||||||
|
if 0x55 not in buf:
|
||||||
|
return PARSE_RESULT.INCOMPLETE, [], None
|
||||||
|
|
||||||
|
# Valid buffer starts from 0x55
|
||||||
|
# Convert to list, as index -method isn't defined for bytearray
|
||||||
|
buf = [ord(x) if not isinstance(x, int) else x for x in buf[list(buf).index(0x55):]]
|
||||||
|
try:
|
||||||
|
data_len = (buf[1] << 8) | buf[2]
|
||||||
|
opt_len = buf[3]
|
||||||
|
except IndexError:
|
||||||
|
# If the fields don't exist, message is incomplete
|
||||||
|
return PARSE_RESULT.INCOMPLETE, buf, None
|
||||||
|
|
||||||
|
# Header: 6 bytes, data, optional data and data checksum
|
||||||
|
msg_len = 6 + data_len + opt_len + 1
|
||||||
|
if len(buf) < msg_len:
|
||||||
|
# If buffer isn't long enough, the message is incomplete
|
||||||
|
return PARSE_RESULT.INCOMPLETE, buf, None
|
||||||
|
|
||||||
|
msg = buf[0:msg_len]
|
||||||
|
buf = buf[msg_len:]
|
||||||
|
|
||||||
|
packet_type = msg[4]
|
||||||
|
data = msg[6:6 + data_len]
|
||||||
|
opt_data = msg[6 + data_len:6 + data_len + opt_len]
|
||||||
|
|
||||||
|
# Check CRCs for header and data
|
||||||
|
if msg[5] != crc8.calc(msg[1:5]):
|
||||||
|
# Fail if doesn't match message
|
||||||
|
Packet.logger.error('Header CRC error!')
|
||||||
|
# Return CRC_MISMATCH
|
||||||
|
return PARSE_RESULT.CRC_MISMATCH, buf, None
|
||||||
|
if msg[6 + data_len + opt_len] != crc8.calc(msg[6:6 + data_len + opt_len]):
|
||||||
|
# Fail if doesn't match message
|
||||||
|
Packet.logger.error('Data CRC error!')
|
||||||
|
# Return CRC_MISMATCH
|
||||||
|
return PARSE_RESULT.CRC_MISMATCH, buf, None
|
||||||
|
|
||||||
|
# If we got this far, everything went ok (?)
|
||||||
|
if packet_type == PACKET.RADIO_ERP1:
|
||||||
|
# Need to handle UTE Teach-in here, as it's a separate packet type...
|
||||||
|
if data[0] == RORG.UTE:
|
||||||
|
packet = UTETeachInPacket(packet_type, data, opt_data)
|
||||||
|
else:
|
||||||
|
packet = RadioPacket(packet_type, data, opt_data)
|
||||||
|
elif packet_type == PACKET.RESPONSE:
|
||||||
|
packet = ResponsePacket(packet_type, data, opt_data)
|
||||||
|
elif packet_type == PACKET.EVENT:
|
||||||
|
packet = EventPacket(packet_type, data, opt_data)
|
||||||
|
else:
|
||||||
|
packet = Packet(packet_type, data, opt_data)
|
||||||
|
|
||||||
|
return PARSE_RESULT.OK, buf, packet
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None,
|
||||||
|
destination=None,
|
||||||
|
sender=None,
|
||||||
|
learn=False, **kwargs):
|
||||||
|
'''
|
||||||
|
Creates an packet ready for sending.
|
||||||
|
Uses rorg, rorg_func and rorg_type to determine the values set based on EEP.
|
||||||
|
Additional arguments (**kwargs) are used for setting the values.
|
||||||
|
|
||||||
|
Currently only supports:
|
||||||
|
- PACKET.RADIO_ERP1
|
||||||
|
- RORGs RPS, BS1, BS4, VLD.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- Require sender to be set? Would force the "correct" sender to be set.
|
||||||
|
- Do we need to set telegram control bits?
|
||||||
|
Might be useful for acting as a repeater?
|
||||||
|
'''
|
||||||
|
|
||||||
|
if packet_type != PACKET.RADIO_ERP1:
|
||||||
|
# At least for now, only support PACKET.RADIO_ERP1.
|
||||||
|
raise ValueError('Packet type not supported by this function.')
|
||||||
|
|
||||||
|
if rorg not in [RORG.RPS, RORG.BS1, RORG.BS4, RORG.VLD]:
|
||||||
|
# At least for now, only support these RORGS.
|
||||||
|
raise ValueError('RORG not supported by this function.')
|
||||||
|
|
||||||
|
if destination is None:
|
||||||
|
Packet.logger.warning('Replacing destination with broadcast address.')
|
||||||
|
destination = [0xFF, 0xFF, 0xFF, 0xFF]
|
||||||
|
|
||||||
|
# TODO: Should use the correct Base ID as default.
|
||||||
|
# Might want to change the sender to be an offset from the actual address?
|
||||||
|
if sender is None:
|
||||||
|
Packet.logger.warning('Replacing sender with default address.')
|
||||||
|
sender = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||||
|
|
||||||
|
if not isinstance(destination, list) or len(destination) != 4:
|
||||||
|
raise ValueError('Destination must a list containing 4 (numeric) values.')
|
||||||
|
|
||||||
|
if not isinstance(sender, list) or len(sender) != 4:
|
||||||
|
raise ValueError('Sender must a list containing 4 (numeric) values.')
|
||||||
|
|
||||||
|
packet = Packet(packet_type, data=[], optional=[])
|
||||||
|
packet.rorg = rorg
|
||||||
|
packet.data = [packet.rorg]
|
||||||
|
# Select EEP at this point, so we know how many bits we're dealing with (for VLD).
|
||||||
|
packet.select_eep(rorg_func, rorg_type, direction, command)
|
||||||
|
|
||||||
|
# Initialize data depending on the profile.
|
||||||
|
if rorg in [RORG.RPS, RORG.BS1]:
|
||||||
|
packet.data.extend([0])
|
||||||
|
elif rorg == RORG.BS4:
|
||||||
|
packet.data.extend([0, 0, 0, 0])
|
||||||
|
else:
|
||||||
|
packet.data.extend([0] * int(packet._profile.get('bits', '1')))
|
||||||
|
packet.data.extend(sender)
|
||||||
|
packet.data.extend([0])
|
||||||
|
# Always use sub-telegram 3, maximum dbm (as per spec, when sending),
|
||||||
|
# and no security (security not supported as per EnOcean Serial Protocol).
|
||||||
|
packet.optional = [3] + destination + [0xFF] + [0]
|
||||||
|
|
||||||
|
if command:
|
||||||
|
# Set CMD to command, if applicable.. Helps with VLD.
|
||||||
|
kwargs['CMD'] = command
|
||||||
|
|
||||||
|
packet.set_eep(kwargs)
|
||||||
|
if rorg in [RORG.BS1, RORG.BS4] and not learn:
|
||||||
|
if rorg == RORG.BS1:
|
||||||
|
packet.data[1] |= (1 << 3)
|
||||||
|
if rorg == RORG.BS4:
|
||||||
|
packet.data[4] |= (1 << 3)
|
||||||
|
packet.data[-1] = packet.status
|
||||||
|
|
||||||
|
# Parse the built packet, so it corresponds to the received packages
|
||||||
|
# For example, stuff like RadioPacket.learn should be set.
|
||||||
|
packet = Packet.parse_msg(packet.build())[2]
|
||||||
|
packet.rorg = rorg
|
||||||
|
packet.parse_eep(rorg_func, rorg_type, direction, command)
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
''' Parse data from Packet '''
|
||||||
|
# Parse status from messages
|
||||||
|
if self.rorg in [RORG.RPS, RORG.BS1, RORG.BS4]:
|
||||||
|
self.status = self.data[-1]
|
||||||
|
if self.rorg == RORG.VLD:
|
||||||
|
self.status = self.optional[-1]
|
||||||
|
|
||||||
|
if self.rorg in [RORG.RPS, RORG.BS1, RORG.BS4]:
|
||||||
|
# These message types should have repeater count in the last for bits of status.
|
||||||
|
self.repeater_count = enocean.utils.from_bitarray(self._bit_status[4:])
|
||||||
|
return self.parsed
|
||||||
|
|
||||||
|
def select_eep(self, rorg_func, rorg_type, direction=None, command=None):
|
||||||
|
''' Set EEP based on FUNC and TYPE '''
|
||||||
|
# set EEP profile
|
||||||
|
self.rorg_func = rorg_func
|
||||||
|
self.rorg_type = rorg_type
|
||||||
|
self._profile = self.eep.find_profile(self._bit_data, self.rorg, rorg_func, rorg_type, direction, command)
|
||||||
|
return self._profile is not None
|
||||||
|
|
||||||
|
def parse_eep(self, rorg_func=None, rorg_type=None, direction=None, command=None):
|
||||||
|
''' Parse EEP based on FUNC and TYPE '''
|
||||||
|
# set EEP profile, if demanded
|
||||||
|
if rorg_func is not None and rorg_type is not None:
|
||||||
|
self.select_eep(rorg_func, rorg_type, direction, command)
|
||||||
|
# parse data
|
||||||
|
provides, values = self.eep.get_values(self._profile, self._bit_data, self._bit_status)
|
||||||
|
self.parsed.update(values)
|
||||||
|
return list(provides)
|
||||||
|
|
||||||
|
def set_eep(self, data):
|
||||||
|
''' Update packet data based on EEP. Input data is a dictionary with keys corresponding to the EEP. '''
|
||||||
|
self._bit_data, self._bit_status = self.eep.set_values(self._profile, self._bit_data, self._bit_status, data)
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
''' Build Packet for sending to EnOcean controller '''
|
||||||
|
data_length = len(self.data)
|
||||||
|
ords = [0x55, (data_length >> 8) & 0xFF, data_length & 0xFF, len(self.optional), int(self.packet_type)]
|
||||||
|
ords.append(crc8.calc(ords[1:5]))
|
||||||
|
ords.extend(self.data)
|
||||||
|
ords.extend(self.optional)
|
||||||
|
ords.append(crc8.calc(ords[6:]))
|
||||||
|
return ords
|
||||||
|
|
||||||
|
|
||||||
|
class RadioPacket(Packet):
|
||||||
|
destination = [0xFF, 0xFF, 0xFF, 0xFF]
|
||||||
|
dBm = 0
|
||||||
|
sender = [0xFF, 0xFF, 0xFF, 0xFF]
|
||||||
|
learn = True
|
||||||
|
contains_eep = False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
packet_str = super(RadioPacket, self).__str__()
|
||||||
|
return '%s->%s (%d dBm): %s' % (self.sender_hex, self.destination_hex, self.dBm, packet_str)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(rorg, rorg_func, rorg_type, direction=None, command=None,
|
||||||
|
destination=None, sender=None, learn=False, **kwargs):
|
||||||
|
return Packet.create(PACKET.RADIO_ERP1, rorg, rorg_func, rorg_type,
|
||||||
|
direction, command, destination, sender, learn, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sender_int(self):
|
||||||
|
return enocean.utils.combine_hex(self.sender)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sender_hex(self):
|
||||||
|
return enocean.utils.to_hex_string(self.sender)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination_int(self):
|
||||||
|
return enocean.utils.combine_hex(self.destination)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination_hex(self):
|
||||||
|
return enocean.utils.to_hex_string(self.destination)
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
self.destination = self.optional[1:5]
|
||||||
|
self.dBm = -self.optional[5]
|
||||||
|
self.sender = self.data[-5:-1]
|
||||||
|
# Default to learn == True, as some devices don't have a learn button
|
||||||
|
self.learn = True
|
||||||
|
|
||||||
|
self.rorg = self.data[0]
|
||||||
|
|
||||||
|
# parse learn bit and FUNC/TYPE, if applicable
|
||||||
|
if self.rorg == RORG.BS1:
|
||||||
|
self.learn = not self._bit_data[DB0.BIT_3]
|
||||||
|
if self.rorg == RORG.BS4:
|
||||||
|
self.learn = not self._bit_data[DB0.BIT_3]
|
||||||
|
if self.learn:
|
||||||
|
self.contains_eep = self._bit_data[DB0.BIT_7]
|
||||||
|
if self.contains_eep:
|
||||||
|
# Get rorg_func and rorg_type from an unidirectional learn packet
|
||||||
|
self.rorg_func = enocean.utils.from_bitarray(self._bit_data[DB3.BIT_7:DB3.BIT_1])
|
||||||
|
self.rorg_type = enocean.utils.from_bitarray(self._bit_data[DB3.BIT_1:DB2.BIT_2])
|
||||||
|
self.rorg_manufacturer = enocean.utils.from_bitarray(self._bit_data[DB2.BIT_2:DB0.BIT_7])
|
||||||
|
self.logger.debug('learn received, EEP detected, RORG: 0x%02X, FUNC: 0x%02X, TYPE: 0x%02X, Manufacturer: 0x%02X' % (self.rorg, self.rorg_func, self.rorg_type, self.rorg_manufacturer)) # noqa: E501
|
||||||
|
|
||||||
|
return super(RadioPacket, self).parse()
|
||||||
|
|
||||||
|
|
||||||
|
class UTETeachInPacket(RadioPacket):
|
||||||
|
# Request types
|
||||||
|
TEACH_IN = 0b00
|
||||||
|
DELETE = 0b01
|
||||||
|
NOT_SPECIFIC = 0b10
|
||||||
|
|
||||||
|
# Response types
|
||||||
|
NOT_ACCEPTED = [False, False]
|
||||||
|
TEACHIN_ACCEPTED = [False, True]
|
||||||
|
DELETE_ACCEPTED = [True, False]
|
||||||
|
EEP_NOT_SUPPORTED = [True, True]
|
||||||
|
|
||||||
|
unidirectional = False
|
||||||
|
response_expected = False
|
||||||
|
number_of_channels = 0xFF
|
||||||
|
rorg_of_eep = RORG.UNDEFINED
|
||||||
|
request_type = NOT_SPECIFIC
|
||||||
|
channel = None
|
||||||
|
|
||||||
|
contains_eep = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bidirectional(self):
|
||||||
|
return not self.unidirectional
|
||||||
|
|
||||||
|
@property
|
||||||
|
def teach_in(self):
|
||||||
|
return self.request_type != self.DELETE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delete(self):
|
||||||
|
return self.request_type == self.DELETE
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
super(UTETeachInPacket, self).parse()
|
||||||
|
self.unidirectional = not self._bit_data[DB6.BIT_7]
|
||||||
|
self.response_expected = not self._bit_data[DB6.BIT_6]
|
||||||
|
self.request_type = enocean.utils.from_bitarray(self._bit_data[DB6.BIT_5:DB6.BIT_3])
|
||||||
|
self.rorg_manufacturer = enocean.utils.from_bitarray(self._bit_data[DB3.BIT_2:DB2.BIT_7] + self._bit_data[DB4.BIT_7:DB3.BIT_7]) # noqa: E501
|
||||||
|
self.channel = self.data[2]
|
||||||
|
self.rorg_type = self.data[5]
|
||||||
|
self.rorg_func = self.data[6]
|
||||||
|
self.rorg_of_eep = self.data[7]
|
||||||
|
if self.teach_in:
|
||||||
|
self.learn = True
|
||||||
|
return self.parsed
|
||||||
|
|
||||||
|
def create_response_packet(self, sender_id, response=TEACHIN_ACCEPTED):
|
||||||
|
# Create data:
|
||||||
|
# - Respond with same RORG (UTE Teach-in)
|
||||||
|
# - Always use bidirectional communication, set response code, set command identifier.
|
||||||
|
# - Databytes 5 to 0 are copied from the original message
|
||||||
|
# - Set sender id and status
|
||||||
|
data = [self.rorg] + \
|
||||||
|
[enocean.utils.from_bitarray([True, False] + response + [False, False, False, True])] + \
|
||||||
|
self.data[2:8] + \
|
||||||
|
sender_id + [0]
|
||||||
|
|
||||||
|
# Always use 0x03 to indicate sending, attach sender ID, dBm, and security level
|
||||||
|
optional = [0x03] + self.sender + [0xFF, 0x00]
|
||||||
|
|
||||||
|
return RadioPacket(PACKET.RADIO_ERP1, data=data, optional=optional)
|
||||||
|
|
||||||
|
|
||||||
|
class ResponsePacket(Packet):
|
||||||
|
response = 0
|
||||||
|
response_data = []
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
self.response = self.data[0]
|
||||||
|
self.response_data = self.data[1:]
|
||||||
|
return super(ResponsePacket, self).parse()
|
||||||
|
|
||||||
|
|
||||||
|
class EventPacket(Packet):
|
||||||
|
event = 0
|
||||||
|
event_data = []
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
self.event = self.data[0]
|
||||||
|
self.event_data = self.data[1:]
|
||||||
|
return super(EventPacket, self).parse()
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
from enocean.protocol.packet import Packet
|
||||||
|
from enocean.protocol.eep import EEP
|
||||||
|
from enocean.protocol.constants import RORG
|
||||||
|
from enocean.decorators import timing
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_temperature():
|
||||||
|
''' Tests RADIO message for EEP -profile 0xA5 0x02 0x05 '''
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x00,
|
||||||
|
0x75
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x02, 0x05) == ['TMP']
|
||||||
|
assert round(packet.parsed['TMP']['value'], 1) == 26.7
|
||||||
|
assert packet.parsed['TMP']['raw_value'] == 85
|
||||||
|
assert packet.learn is False
|
||||||
|
assert packet.contains_eep is False
|
||||||
|
assert packet.rorg == 0xA5
|
||||||
|
assert packet.rorg == int(RORG.BS4)
|
||||||
|
assert packet.rorg_func == 0x02
|
||||||
|
assert packet.rorg_type == 0x05
|
||||||
|
assert packet.status == 0x00
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
assert packet.sender == [0x01, 0x81, 0xB7, 0x44]
|
||||||
|
assert packet.sender_hex == '01:81:B7:44'
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_magnetic_switch():
|
||||||
|
''' Tests RADIO message for EEP -profile 0xD5 0x00 0x01 '''
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x08, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x36, 0x00,
|
||||||
|
0x53
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x00, 0x01) == ['CO']
|
||||||
|
assert packet.parsed['CO']['value'] == 'open'
|
||||||
|
assert packet.parsed['CO']['raw_value'] == 0
|
||||||
|
assert packet.status == 0x00
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x09, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x36, 0x00,
|
||||||
|
0xC7
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x00, 0x01) == ['CO']
|
||||||
|
assert packet.parsed['CO']['value'] == 'closed'
|
||||||
|
assert packet.parsed['CO']['raw_value'] == 1
|
||||||
|
assert packet.learn is False
|
||||||
|
assert packet.status == 0x00
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_switch():
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xF6, 0x50, 0x00, 0x29, 0x89, 0x79, 0x30,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x37, 0x00,
|
||||||
|
0x9D
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x02, 0x02) == ['R1', 'EB', 'R2', 'SA', 'T21', 'NU']
|
||||||
|
assert packet.parsed['SA']['value'] == 'No 2nd action'
|
||||||
|
assert packet.parsed['EB']['value'] == 'pressed'
|
||||||
|
assert packet.parsed['R1']['value'] == 'Button BI'
|
||||||
|
assert packet.parsed['T21']['value'] is True
|
||||||
|
assert packet.parsed['NU']['value'] is True
|
||||||
|
assert packet.learn is True
|
||||||
|
assert packet.status == 0x30
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xF6, 0x00, 0x00, 0x29, 0x89, 0x79, 0x20,
|
||||||
|
0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0x4A, 0x00,
|
||||||
|
0x03
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x02, 0x02) == ['R1', 'EB', 'R2', 'SA', 'T21', 'NU']
|
||||||
|
assert packet.parsed['SA']['value'] == 'No 2nd action'
|
||||||
|
assert packet.parsed['EB']['value'] == 'released'
|
||||||
|
assert packet.parsed['T21']['value'] is True
|
||||||
|
assert packet.parsed['NU']['value'] is False
|
||||||
|
assert packet.learn is True
|
||||||
|
assert packet.status == 0x20
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_eep_parsing():
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x08, 0x28, 0x46, 0x80, 0x01, 0x8A, 0x7B, 0x30, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x49, 0x00,
|
||||||
|
0x26
|
||||||
|
]))
|
||||||
|
assert packet.learn is True
|
||||||
|
assert packet.contains_eep is True
|
||||||
|
assert packet.rorg_func == 0x02
|
||||||
|
assert packet.rorg_type == 0x05
|
||||||
|
assert packet.status == 0x00
|
||||||
|
assert packet.repeater_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_eep_remaining():
|
||||||
|
# Magnetic switch -example
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x08, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x36, 0x00,
|
||||||
|
0x53
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x00, 0x01) == ['CO']
|
||||||
|
|
||||||
|
# Temperature-example
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x00,
|
||||||
|
0x75
|
||||||
|
]))
|
||||||
|
# If this fails, the data is retained from the last Packet parsing!
|
||||||
|
assert packet.parse_eep(0x00, 0x01) == []
|
||||||
|
# Once we have parse with the correct func and type, this should pass.
|
||||||
|
assert packet.parse_eep(0x02, 0x05) == ['TMP']
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_eep_direction():
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x32, 0x20, 0x89, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x43
|
||||||
|
]))
|
||||||
|
assert packet.parse_eep(0x20, 0x01, 1) == ['CV', 'SO', 'ENIE', 'ES', 'BCAP', 'CCO', 'FTS', 'DWO', 'ACO', 'TMP']
|
||||||
|
assert packet.parsed['CV']['value'] == 50
|
||||||
|
assert packet.parse_eep(0x20, 0x01, 2) == ['SP', 'TMP', 'RIN', 'LFS', 'VO', 'VC', 'SB', 'SPS', 'SPN', 'RCU']
|
||||||
|
assert packet.parsed['SP']['value'] == 50
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_vld():
|
||||||
|
status, buf, p = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x09, 0x07, 0x01,
|
||||||
|
0x56,
|
||||||
|
0xD2, 0x04, 0x00, 0x64, 0x01, 0x94, 0xE3, 0xB9, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00,
|
||||||
|
0xE4
|
||||||
|
]))
|
||||||
|
assert p.rorg == RORG.VLD
|
||||||
|
assert p.parse_eep(0x01, 0x01) == ['PF', 'PFD', 'CMD', 'OC', 'EL', 'IO', 'LC', 'OV']
|
||||||
|
|
||||||
|
assert p.parsed['EL']['raw_value'] == 0
|
||||||
|
assert p.parsed['EL']['value'] == 'Error level 0: hardware OK'
|
||||||
|
|
||||||
|
assert p.parsed['PF']['raw_value'] == 0
|
||||||
|
assert p.parsed['PF']['value'] == 'Power Failure Detection disabled/not supported'
|
||||||
|
|
||||||
|
assert p.parsed['PFD']['raw_value'] == 0
|
||||||
|
assert p.parsed['PFD']['value'] == 'Power Failure Detection not detected/not supported/disabled'
|
||||||
|
|
||||||
|
assert p.parsed['IO']['raw_value'] == 0
|
||||||
|
assert p.parsed['IO']['value'] == 'Output channel 0 (to load)'
|
||||||
|
|
||||||
|
assert p.parsed['OV']['raw_value'] == 100
|
||||||
|
assert p.parsed['OV']['value'] == 'Output value 100% or ON'
|
||||||
|
|
||||||
|
assert p.parsed['OC']['raw_value'] == 0
|
||||||
|
assert p.parsed['OC']['value'] == 'Over current switch off: ready / not supported'
|
||||||
|
|
||||||
|
assert p.parsed['LC']['raw_value'] == 0
|
||||||
|
assert p.parsed['LC']['value'] == 'Local control disabled / not supported'
|
||||||
|
|
||||||
|
status, buf, p = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x09, 0x07, 0x01,
|
||||||
|
0x56,
|
||||||
|
0xD2, 0x04, 0x00, 0x00, 0x01, 0x94, 0xE3, 0xB9, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00,
|
||||||
|
0xBF
|
||||||
|
]))
|
||||||
|
assert p.rorg == RORG.VLD
|
||||||
|
assert p.parse_eep(0x01, 0x01) == ['PF', 'PFD', 'CMD', 'OC', 'EL', 'IO', 'LC', 'OV']
|
||||||
|
|
||||||
|
assert p.parsed['EL']['raw_value'] == 0
|
||||||
|
assert p.parsed['EL']['value'] == 'Error level 0: hardware OK'
|
||||||
|
|
||||||
|
assert p.parsed['PF']['raw_value'] == 0
|
||||||
|
assert p.parsed['PF']['value'] == 'Power Failure Detection disabled/not supported'
|
||||||
|
|
||||||
|
assert p.parsed['PFD']['raw_value'] == 0
|
||||||
|
assert p.parsed['PFD']['value'] == 'Power Failure Detection not detected/not supported/disabled'
|
||||||
|
|
||||||
|
assert p.parsed['IO']['raw_value'] == 0
|
||||||
|
assert p.parsed['IO']['value'] == 'Output channel 0 (to load)'
|
||||||
|
|
||||||
|
assert p.parsed['OV']['raw_value'] == 0
|
||||||
|
assert p.parsed['OV']['value'] == 'Output value 0% or OFF'
|
||||||
|
|
||||||
|
assert p.parsed['OC']['raw_value'] == 0
|
||||||
|
assert p.parsed['OC']['value'] == 'Over current switch off: ready / not supported'
|
||||||
|
|
||||||
|
assert p.parsed['LC']['raw_value'] == 0
|
||||||
|
assert p.parsed['LC']['value'] == 'Local control disabled / not supported'
|
||||||
|
|
||||||
|
|
||||||
|
def test_fails():
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x08, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x36, 0x00,
|
||||||
|
0x53
|
||||||
|
]))
|
||||||
|
eep = EEP()
|
||||||
|
# Mock initialization failure
|
||||||
|
eep.init_ok = False
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xD5, 0x00, 0x01) is None
|
||||||
|
# TODO: Needs better test. A much better.
|
||||||
|
assert eep.set_values(profile=None, data=[True], status=[False, False], properties={'CV': False})
|
||||||
|
eep.init_ok = True
|
||||||
|
profile = eep.find_profile(packet._bit_data, 0xD5, 0x00, 0x01)
|
||||||
|
assert eep.set_values(profile, packet._bit_data, packet.status, {'ASD': 1})
|
||||||
|
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xFF, 0x00, 0x01) is None
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xD5, 0xFF, 0x01) is None
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xD5, 0x00, 0xFF) is None
|
||||||
|
|
||||||
|
status, buf, packet = Packet.parse_msg(bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x09, 0x07, 0x01,
|
||||||
|
0x56,
|
||||||
|
0xD2, 0x04, 0x00, 0x00, 0x01, 0x94, 0xE3, 0xB9, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00,
|
||||||
|
0xBF
|
||||||
|
]))
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xD2, 0x01, 0x01) is not None
|
||||||
|
assert eep.find_profile(packet._bit_data, 0xD2, 0x01, 0x01, command=-1) is None
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
from enocean.protocol.packet import Packet, EventPacket
|
||||||
|
from enocean.protocol.constants import PACKET, PARSE_RESULT, EVENT_CODE
|
||||||
|
from enocean.decorators import timing
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_packet_examples():
|
||||||
|
''' Tests examples found at EnOceanSerialProtocol3.pdf / 74 '''
|
||||||
|
telegram_examples = {
|
||||||
|
# Radio VLD
|
||||||
|
PACKET.RADIO_ERP1: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0F, 0x07, 0x01,
|
||||||
|
0x2B,
|
||||||
|
0xD2, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x80, 0x35, 0xC4, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0x00,
|
||||||
|
0x36]),
|
||||||
|
'data_len': 15,
|
||||||
|
'opt_len': 7,
|
||||||
|
},
|
||||||
|
# CO_WR_SLEEP
|
||||||
|
PACKET.COMMON_COMMAND: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x05, 0x00, 0x05,
|
||||||
|
0xDB,
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0x0A,
|
||||||
|
0x54]),
|
||||||
|
'data_len': 5,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# CO_WR_RESET
|
||||||
|
PACKET.COMMON_COMMAND: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x05,
|
||||||
|
0x70,
|
||||||
|
0x02,
|
||||||
|
0x0E]),
|
||||||
|
'data_len': 1,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# CO_RD_IDBASE
|
||||||
|
PACKET.COMMON_COMMAND: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x05,
|
||||||
|
0x70,
|
||||||
|
0x08,
|
||||||
|
0x38]),
|
||||||
|
'data_len': 1,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# Response RET_OK
|
||||||
|
PACKET.RESPONSE: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x05, 0x00, 0x02,
|
||||||
|
0xCE,
|
||||||
|
0x00, 0xFF, 0x80, 0x00, 0x00,
|
||||||
|
0xDA]),
|
||||||
|
'data_len': 5,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# REMOTE_MAN_COMMAND
|
||||||
|
PACKET.REMOTE_MAN_COMMAND: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x19, 0x00, 0x07, 0x8D,
|
||||||
|
0x12, 0x12, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||||
|
0xDA]),
|
||||||
|
'data_len': 25,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# QueryID
|
||||||
|
PACKET.REMOTE_MAN_COMMAND: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0C, 0x00, 0x07,
|
||||||
|
0xEF,
|
||||||
|
0x00, 0x04, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x65]),
|
||||||
|
'data_len': 12,
|
||||||
|
'opt_len': 0,
|
||||||
|
},
|
||||||
|
# Custom test, containing 0x55 in message
|
||||||
|
PACKET.RESPONSE: {
|
||||||
|
'msg': bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x05, 0x01, 0x02,
|
||||||
|
0xDB,
|
||||||
|
0x00, 0xFF, 0x9E, 0x55, 0x00,
|
||||||
|
0x0A,
|
||||||
|
0x79,
|
||||||
|
# unnecessary data, to check for message length checking
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||||
|
]),
|
||||||
|
'data_len': 5,
|
||||||
|
'opt_len': 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for packet, values in telegram_examples.items():
|
||||||
|
status, remainder, pack = Packet.parse_msg(values['msg'])
|
||||||
|
assert status == PARSE_RESULT.OK
|
||||||
|
assert pack.packet_type != 0x00
|
||||||
|
assert pack.packet_type == packet
|
||||||
|
assert len(pack.data) == values['data_len']
|
||||||
|
assert len(pack.optional) == values['opt_len']
|
||||||
|
assert pack.status == 0x00
|
||||||
|
assert pack.repeater_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_packet_fails():
|
||||||
|
'''
|
||||||
|
Tests designed to fail.
|
||||||
|
These include changes to checksum, data length or something like that.
|
||||||
|
'''
|
||||||
|
fail_examples = (
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0F, 0x07, 0x01,
|
||||||
|
0x2B,
|
||||||
|
0xD2, 0xDD, 0xDC, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x80, 0x35, 0xC4, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0x00,
|
||||||
|
0x36
|
||||||
|
]),
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0F, 0x07, 0x01,
|
||||||
|
0x2B,
|
||||||
|
0xD2, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x80, 0x35, 0xC4, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0x00,
|
||||||
|
0x37
|
||||||
|
]),
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0F, 0x07, 0x01,
|
||||||
|
0x1B,
|
||||||
|
0xD2, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x80, 0x35, 0xC4, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0x00,
|
||||||
|
0x36
|
||||||
|
]),
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x05,
|
||||||
|
0x70,
|
||||||
|
0x38
|
||||||
|
]),
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
for msg in fail_examples:
|
||||||
|
status, remainder, packet = Packet.parse_msg(msg)
|
||||||
|
assert status in [PARSE_RESULT.INCOMPLETE, PARSE_RESULT.CRC_MISMATCH]
|
||||||
|
|
||||||
|
|
||||||
|
def test_packet_equals():
|
||||||
|
data_1 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x05,
|
||||||
|
0x70,
|
||||||
|
0x08,
|
||||||
|
0x38
|
||||||
|
])
|
||||||
|
data_2 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x05,
|
||||||
|
0x70,
|
||||||
|
0x08,
|
||||||
|
0x38
|
||||||
|
])
|
||||||
|
_, _, packet_1 = Packet.parse_msg(data_1)
|
||||||
|
_, _, packet_2 = Packet.parse_msg(data_2)
|
||||||
|
|
||||||
|
assert str(packet_1) == '0x%02X %s %s %s' % (packet_1.packet_type,
|
||||||
|
[hex(o) for o in packet_1.data],
|
||||||
|
[hex(o) for o in packet_1.optional],
|
||||||
|
packet_1.parsed)
|
||||||
|
assert str(packet_1) == str(packet_2)
|
||||||
|
assert packet_1 == packet_2
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_packet():
|
||||||
|
data = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x01, 0x00, 0x04,
|
||||||
|
0x77,
|
||||||
|
0x01,
|
||||||
|
0x07
|
||||||
|
])
|
||||||
|
|
||||||
|
_, _, packet = Packet.parse_msg(data)
|
||||||
|
assert isinstance(packet, EventPacket)
|
||||||
|
assert packet.event == EVENT_CODE.SA_RECLAIM_NOT_SUCCESFUL
|
||||||
|
assert packet.event_data == []
|
||||||
|
assert packet.optional == []
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
from nose.tools import raises
|
||||||
|
|
||||||
|
from enocean.protocol.packet import Packet, RadioPacket
|
||||||
|
from enocean.protocol.constants import PACKET, RORG
|
||||||
|
from enocean.decorators import timing
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_packet_assembly():
|
||||||
|
PACKET_CONTENT_1 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x00, 0x01,
|
||||||
|
0x80,
|
||||||
|
0xA5, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x18
|
||||||
|
])
|
||||||
|
PACKET_CONTENT_2 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0xE4
|
||||||
|
])
|
||||||
|
PACKET_CONTENT_3 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x32, 0x20, 0x89, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x43
|
||||||
|
])
|
||||||
|
PACKET_CONTENT_4 = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x32, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x80
|
||||||
|
])
|
||||||
|
|
||||||
|
# manually assemble packet
|
||||||
|
packet = Packet(PACKET.RADIO_ERP1)
|
||||||
|
packet.rorg = RORG.BS4
|
||||||
|
sender_bytes = [(0xdeadbeef >> i & 0xff) for i in (24, 16, 8, 0)]
|
||||||
|
data = [0, 0, 0, 0]
|
||||||
|
packet.data = [packet.rorg] + data + sender_bytes + [0]
|
||||||
|
|
||||||
|
# test content
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(PACKET_CONTENT_1)
|
||||||
|
assert list(packet_serialized) == list(PACKET_CONTENT_1)
|
||||||
|
|
||||||
|
# set optional data
|
||||||
|
sub_tel_num = 3
|
||||||
|
destination = [255, 255, 255, 255] # broadcast
|
||||||
|
dbm = 0xff
|
||||||
|
security = 0
|
||||||
|
packet.optional = [sub_tel_num] + destination + [dbm] + [security]
|
||||||
|
|
||||||
|
# test content
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(PACKET_CONTENT_2)
|
||||||
|
assert list(packet_serialized) == list(PACKET_CONTENT_2)
|
||||||
|
|
||||||
|
# update data based on EEP
|
||||||
|
packet.select_eep(0x20, 0x01, 1)
|
||||||
|
prop = {
|
||||||
|
'CV': 50,
|
||||||
|
'TMP': 21.5,
|
||||||
|
'ES': 'true',
|
||||||
|
}
|
||||||
|
packet.set_eep(prop)
|
||||||
|
|
||||||
|
# test content
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(PACKET_CONTENT_3)
|
||||||
|
assert list(packet_serialized) == list(PACKET_CONTENT_3)
|
||||||
|
assert packet.rorg_func == 0x20
|
||||||
|
assert packet.rorg_type == 0x01
|
||||||
|
|
||||||
|
# Test the easier method of sending packets.
|
||||||
|
packet = Packet.create(PACKET.RADIO_ERP1, rorg=RORG.BS4, rorg_func=0x20, rorg_type=0x01,
|
||||||
|
learn=True, direction=1, **prop)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(PACKET_CONTENT_3)
|
||||||
|
assert list(packet_serialized) == list(PACKET_CONTENT_3)
|
||||||
|
assert packet.rorg_func == 0x20
|
||||||
|
assert packet.rorg_type == 0x01
|
||||||
|
|
||||||
|
# Test creating RadioPacket directly.
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS4, rorg_func=0x20, rorg_type=0x01, learn=True, direction=2, SP=50)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(PACKET_CONTENT_4)
|
||||||
|
assert list(packet_serialized) == list(PACKET_CONTENT_4)
|
||||||
|
assert packet.rorg_func == 0x20
|
||||||
|
assert packet.rorg_type == 0x01
|
||||||
|
|
||||||
|
|
||||||
|
# Corresponds to the tests done in test_eep
|
||||||
|
@timing(1000)
|
||||||
|
def test_temperature():
|
||||||
|
TEMPERATURE = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x5C
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS4, rorg_func=0x02, rorg_type=0x05,
|
||||||
|
sender=[0x01, 0x81, 0xB7, 0x44], TMP=26.66666666666666666666666666666666666666666667)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(TEMPERATURE)
|
||||||
|
assert list(packet_serialized) == list(TEMPERATURE)
|
||||||
|
assert packet.learn is False
|
||||||
|
|
||||||
|
TEMPERATURE = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x00, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0xE0
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS4, rorg_func=0x02, rorg_type=0x05, sender=[0x01, 0x81, 0xB7, 0x44],
|
||||||
|
learn=True, TMP=26.66666666666666666666666666666666666666666667)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(TEMPERATURE)
|
||||||
|
assert packet.learn is True
|
||||||
|
|
||||||
|
|
||||||
|
# Corresponds to the tests done in test_eep
|
||||||
|
@timing(1000)
|
||||||
|
def test_magnetic_switch():
|
||||||
|
MAGNETIC_SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x08, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0xBA
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS1, rorg_func=0x00, rorg_type=0x01, sender=[0x01, 0x82, 0x5D, 0xAB],
|
||||||
|
CO='open')
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(MAGNETIC_SWITCH)
|
||||||
|
assert list(packet_serialized) == list(MAGNETIC_SWITCH)
|
||||||
|
assert packet.learn is False
|
||||||
|
|
||||||
|
MAGNETIC_SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x00, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x06
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS1, rorg_func=0x00, rorg_type=0x01, sender=[0x01, 0x82, 0x5D, 0xAB],
|
||||||
|
learn=True, CO='open')
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(MAGNETIC_SWITCH)
|
||||||
|
assert list(packet_serialized) == list(MAGNETIC_SWITCH)
|
||||||
|
assert packet.learn is True
|
||||||
|
|
||||||
|
MAGNETIC_SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x09, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x2E
|
||||||
|
])
|
||||||
|
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS1, rorg_func=0x00, rorg_type=0x01, sender=[0x01, 0x82, 0x5D, 0xAB],
|
||||||
|
CO='closed')
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(MAGNETIC_SWITCH)
|
||||||
|
assert list(packet_serialized) == list(MAGNETIC_SWITCH)
|
||||||
|
assert packet.learn is False
|
||||||
|
|
||||||
|
MAGNETIC_SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x01, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x92
|
||||||
|
])
|
||||||
|
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS1, rorg_func=0x00, rorg_type=0x01, sender=[0x01, 0x82, 0x5D, 0xAB],
|
||||||
|
learn=True, CO='closed')
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(MAGNETIC_SWITCH)
|
||||||
|
assert list(packet_serialized) == list(MAGNETIC_SWITCH)
|
||||||
|
assert packet.learn is True
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_switch():
|
||||||
|
SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xF6, 0x50, 0x00, 0x29, 0x89, 0x79, 0x30,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x61
|
||||||
|
])
|
||||||
|
|
||||||
|
# test also enum setting by integer value with EB0
|
||||||
|
packet = RadioPacket.create(rorg=RORG.RPS, rorg_func=0x02, rorg_type=0x02, sender=[0x00, 0x29, 0x89, 0x79],
|
||||||
|
SA='No 2nd action',
|
||||||
|
EB=1,
|
||||||
|
R1='Button BI',
|
||||||
|
T21=True,
|
||||||
|
NU=True,
|
||||||
|
)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(SWITCH)
|
||||||
|
assert list(packet_serialized) == list(SWITCH)
|
||||||
|
|
||||||
|
SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xF6, 0x00, 0x00, 0x29, 0x89, 0x79, 0x20,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0xD2
|
||||||
|
])
|
||||||
|
|
||||||
|
packet = RadioPacket.create(rorg=RORG.RPS, rorg_func=0x02, rorg_type=0x02, sender=[0x00, 0x29, 0x89, 0x79],
|
||||||
|
SA='No 2nd action',
|
||||||
|
EB='released',
|
||||||
|
T21=True,
|
||||||
|
NU=False,
|
||||||
|
)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(SWITCH)
|
||||||
|
assert list(packet_serialized) == list(SWITCH)
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
@raises(ValueError)
|
||||||
|
def test_illegal_eep_enum1():
|
||||||
|
RadioPacket.create(rorg=RORG.RPS, rorg_func=0x02, rorg_type=0x02, sender=[0x00, 0x29, 0x89, 0x79], EB='inexisting')
|
||||||
|
|
||||||
|
|
||||||
|
@raises(ValueError)
|
||||||
|
@timing(1000)
|
||||||
|
def test_illegal_eep_enum2():
|
||||||
|
RadioPacket.create(rorg=RORG.RPS, rorg_func=0x02, rorg_type=0x02, sender=[0x00, 0x29, 0x89, 0x79], EB=2)
|
||||||
|
|
||||||
|
|
||||||
|
# Corresponds to the tests done in test_eep
|
||||||
|
@timing(1000)
|
||||||
|
def test_packets_with_destination():
|
||||||
|
TEMPERATURE = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0A, 0x07, 0x01,
|
||||||
|
0xEB,
|
||||||
|
0xA5, 0x00, 0x00, 0x55, 0x08, 0x01, 0x81, 0xB7, 0x44, 0x00,
|
||||||
|
0x03, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0x00,
|
||||||
|
0x5F
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS4, rorg_func=0x02, rorg_type=0x05, sender=[0x01, 0x81, 0xB7, 0x44],
|
||||||
|
destination=[0xDE, 0xAD, 0xBE, 0xEF],
|
||||||
|
TMP=26.66666666666666666666666666666666666666666667)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(TEMPERATURE)
|
||||||
|
assert list(packet_serialized) == list(TEMPERATURE)
|
||||||
|
assert packet.learn is False
|
||||||
|
assert packet.sender_int == 25278276
|
||||||
|
assert packet.destination_int == 3735928559
|
||||||
|
|
||||||
|
MAGNETIC_SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x07, 0x07, 0x01,
|
||||||
|
0x7A,
|
||||||
|
0xD5, 0x08, 0x01, 0x82, 0x5D, 0xAB, 0x00,
|
||||||
|
0x03, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0x00,
|
||||||
|
0xB9
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.BS1, rorg_func=0x00, rorg_type=0x01, sender=[0x01, 0x82, 0x5D, 0xAB],
|
||||||
|
destination=[0xDE, 0xAD, 0xBE, 0xEF], CO='open')
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
assert len(packet_serialized) == len(MAGNETIC_SWITCH)
|
||||||
|
assert list(packet_serialized) == list(MAGNETIC_SWITCH)
|
||||||
|
assert packet.learn is False
|
||||||
|
|
||||||
|
|
||||||
|
@timing(1000)
|
||||||
|
def test_vld():
|
||||||
|
SWITCH = bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x09, 0x07, 0x01,
|
||||||
|
0x56,
|
||||||
|
0xD2, 0x01, 0x1E, 0x64, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
|
||||||
|
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
|
||||||
|
0x5A
|
||||||
|
])
|
||||||
|
packet = RadioPacket.create(rorg=RORG.VLD, rorg_func=0x01, rorg_type=0x01, command=1, DV=0, IO=0x1E, OV=0x64)
|
||||||
|
packet_serialized = packet.build()
|
||||||
|
|
||||||
|
assert len(packet_serialized) == len(SWITCH)
|
||||||
|
assert list(packet_serialized) == list(SWITCH)
|
||||||
|
assert packet.parsed['CMD']['raw_value'] == 0x01
|
||||||
|
assert packet.parsed['IO']['raw_value'] == 0x1E
|
||||||
|
assert packet.parsed['IO']['value'] == 'All output channels supported by the device'
|
||||||
|
assert packet.parsed['DV']['value'] == 'Switch to new output value'
|
||||||
|
assert packet.parsed['OV']['value'] == 'Output value 100% or ON'
|
||||||
|
|
||||||
|
|
||||||
|
def test_fails():
|
||||||
|
try:
|
||||||
|
Packet.create(PACKET.RESPONSE, 0xA5, 0x01, 0x01)
|
||||||
|
assert False
|
||||||
|
except ValueError:
|
||||||
|
assert True
|
||||||
|
try:
|
||||||
|
Packet.create(PACKET.RADIO_ERP1, 0xA6, 0x01, 0x01)
|
||||||
|
assert False
|
||||||
|
except ValueError:
|
||||||
|
assert True
|
||||||
|
try:
|
||||||
|
Packet.create(PACKET.RADIO_ERP1, 0xA5, 0x01, 0x01, destination='ASDASDASD')
|
||||||
|
assert False
|
||||||
|
except ValueError:
|
||||||
|
assert True
|
||||||
|
try:
|
||||||
|
Packet.create(PACKET.RADIO_ERP1, 0xA5, 0x01, 0x01, sender='ASDASDASD')
|
||||||
|
assert False
|
||||||
|
except ValueError:
|
||||||
|
assert True
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
from enocean.communicators import Communicator
|
||||||
|
from enocean.protocol.packet import Packet
|
||||||
|
from enocean.protocol.constants import RORG, DB6
|
||||||
|
from enocean.decorators import timing
|
||||||
|
|
||||||
|
|
||||||
|
@timing(rounds=100, limit=750)
|
||||||
|
def test_ute_in():
|
||||||
|
communicator = Communicator()
|
||||||
|
communicator.base_id = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||||
|
|
||||||
|
status, buf, packet = Packet.parse_msg(
|
||||||
|
bytearray([
|
||||||
|
0x55,
|
||||||
|
0x00, 0x0D, 0x07, 0x01,
|
||||||
|
0xFD,
|
||||||
|
0xD4, 0xA0, 0xFF, 0x3E, 0x00, 0x01, 0x01, 0xD2, 0x01, 0x94, 0xE3, 0xB9, 0x00,
|
||||||
|
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00,
|
||||||
|
0xAB
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert packet.sender_hex == '01:94:E3:B9'
|
||||||
|
assert packet.unidirectional is False
|
||||||
|
assert packet.bidirectional is True
|
||||||
|
assert packet.response_expected is True
|
||||||
|
assert packet.number_of_channels == 0xFF
|
||||||
|
assert packet.rorg_manufacturer == 0x3E
|
||||||
|
assert packet.rorg_of_eep == RORG.VLD
|
||||||
|
assert packet.rorg_func == 0x01
|
||||||
|
assert packet.rorg_type == 0x01
|
||||||
|
assert packet.teach_in is True
|
||||||
|
assert packet.delete is False
|
||||||
|
assert packet.learn is True
|
||||||
|
assert packet.contains_eep is True
|
||||||
|
|
||||||
|
response_packet = packet.create_response_packet(communicator.base_id)
|
||||||
|
assert response_packet.sender_hex == 'DE:AD:BE:EF'
|
||||||
|
assert response_packet.destination_hex == '01:94:E3:B9'
|
||||||
|
assert response_packet._bit_data[DB6.BIT_5:DB6.BIT_3] == [False, True]
|
||||||
|
assert response_packet.data[2:7] == packet.data[2:7]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
from enocean.protocol.eep import EEP
|
||||||
|
eep = EEP()
|
||||||
|
# profiles = eep.
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_range():
|
||||||
|
offset = -40
|
||||||
|
values = range(0x01, 0x0C)
|
||||||
|
for i in range(len(values)):
|
||||||
|
minimum = float(i * 10 + offset)
|
||||||
|
maximum = minimum + 40
|
||||||
|
profile = eep.find_profile([], 0xA5, 0x02, values[i])
|
||||||
|
|
||||||
|
assert minimum == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('min').text)
|
||||||
|
assert maximum == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('max').text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_range():
|
||||||
|
offset = -60
|
||||||
|
values = range(0x10, 0x1C)
|
||||||
|
for i in range(len(values)):
|
||||||
|
minimum = float(i * 10 + offset)
|
||||||
|
maximum = minimum + 80
|
||||||
|
profile = eep.find_profile([], 0xA5, 0x02, values[i])
|
||||||
|
|
||||||
|
assert minimum == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('min').text)
|
||||||
|
assert maximum == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('max').text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest():
|
||||||
|
profile = eep.find_profile([], 0xA5, 0x02, 0x20)
|
||||||
|
assert -10 == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('min').text)
|
||||||
|
assert +41.2 == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('max').text)
|
||||||
|
|
||||||
|
profile = eep.find_profile([], 0xA5, 0x02, 0x30)
|
||||||
|
assert -40 == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('min').text)
|
||||||
|
assert +62.3 == float(profile.find('value', {'shortcut': 'TMP'}).find('scale').find('max').text)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import enocean.utils
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_bit():
|
||||||
|
assert enocean.utils.get_bit(1, 0) == 1
|
||||||
|
assert enocean.utils.get_bit(8, 3) == 1
|
||||||
|
assert enocean.utils.get_bit(6, 2) == 1
|
||||||
|
assert enocean.utils.get_bit(6, 1) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_hex_string():
|
||||||
|
assert enocean.utils.to_hex_string(0) == '00'
|
||||||
|
assert enocean.utils.to_hex_string(15) == '0F'
|
||||||
|
assert enocean.utils.to_hex_string(16) == '10'
|
||||||
|
assert enocean.utils.to_hex_string(22) == '16'
|
||||||
|
|
||||||
|
assert enocean.utils.to_hex_string([0, 15, 16, 22]) == '00:0F:10:16'
|
||||||
|
assert enocean.utils.to_hex_string([0x00, 0x0F, 0x10, 0x16]) == '00:0F:10:16'
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_hex_string():
|
||||||
|
assert enocean.utils.from_hex_string('00') == 0
|
||||||
|
assert enocean.utils.from_hex_string('0F') == 15
|
||||||
|
assert enocean.utils.from_hex_string('10') == 16
|
||||||
|
assert enocean.utils.from_hex_string('16') == 22
|
||||||
|
|
||||||
|
assert enocean.utils.from_hex_string('00:0F:10:16') == [0, 15, 16, 22]
|
||||||
|
assert enocean.utils.from_hex_string('00:0F:10:16') == [0x00, 0x0F, 0x10, 0x16]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
def get_bit(byte, bit):
|
||||||
|
''' Get bit value from byte '''
|
||||||
|
return (byte >> bit) & 0x01
|
||||||
|
|
||||||
|
|
||||||
|
def combine_hex(data):
|
||||||
|
''' Combine list of integer values to one big integer '''
|
||||||
|
output = 0x00
|
||||||
|
for i, value in enumerate(reversed(data)):
|
||||||
|
output |= (value << i * 8)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def to_bitarray(data, width=8):
|
||||||
|
''' Convert data (list of integers, bytearray or integer) to bitarray '''
|
||||||
|
if isinstance(data, list) or isinstance(data, bytearray):
|
||||||
|
data = combine_hex(data)
|
||||||
|
return [True if digit == '1' else False for digit in bin(data)[2:].zfill(width)]
|
||||||
|
|
||||||
|
|
||||||
|
def from_bitarray(data):
|
||||||
|
''' Convert bit array back to integer '''
|
||||||
|
return int(''.join(['1' if x else '0' for x in data]), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def to_hex_string(data):
|
||||||
|
''' Convert list of integers to a hex string, separated by ":" '''
|
||||||
|
if isinstance(data, int):
|
||||||
|
return '%02X' % data
|
||||||
|
return ':'.join([('%02X' % o) for o in data])
|
||||||
|
|
||||||
|
|
||||||
|
def from_hex_string(hex_string):
|
||||||
|
reval = [int(x, 16) for x in hex_string.split(':')]
|
||||||
|
if len(reval) == 1:
|
||||||
|
return reval[0]
|
||||||
|
return reval
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Konfiguration
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# EnOcean
|
||||||
|
# -----------------------------
|
||||||
|
ENOCEAN_PORT = "/dev/ttyS4"
|
||||||
|
SENDER_ID = "FE:D0:A6:44"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# KNXD
|
||||||
|
# -----------------------------
|
||||||
|
KNXD_IP = "ip:172.20.21.168"
|
||||||
|
GA_FRONT = "11/1/14"
|
||||||
|
GA_REAR = "11/1/15"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)s: %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger("enocean-knx")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# KNX Senden über knxd
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
def knx_write(ip: str, ga: str, state: bool):
|
||||||
|
"""
|
||||||
|
Sendet Boolean an KNX über knxd (groupswrite).
|
||||||
|
"""
|
||||||
|
value = "1" if state else "0"
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["/usr/lib/knxd/groupswrite", ip, ga, value],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
log.info(f"KNX → {ga} = {value}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"KNX send failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# EnOcean Verarbeitung
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
last_button = None # "front" | "rear"
|
||||||
|
|
||||||
|
|
||||||
|
def process_packet(packet):
|
||||||
|
global last_button
|
||||||
|
|
||||||
|
try:
|
||||||
|
sender = packet.sender_hex
|
||||||
|
|
||||||
|
if sender != SENDER_ID:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Rohdaten
|
||||||
|
data = packet.data
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
event = data[1]
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Fronttür (Button BO)
|
||||||
|
# -------------------------
|
||||||
|
if event == 0x70:
|
||||||
|
last_button = "front"
|
||||||
|
knx_write(KNXD_IP, GA_FRONT, True)
|
||||||
|
log.info("Front door OPEN")
|
||||||
|
|
||||||
|
elif event == 0x00 and last_button == "front":
|
||||||
|
knx_write(KNXD_IP, GA_FRONT, False)
|
||||||
|
log.info("Front door CLOSED")
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Hecktür (Button BI)
|
||||||
|
# -------------------------
|
||||||
|
elif event == 0x50:
|
||||||
|
last_button = "rear"
|
||||||
|
knx_write(KNXD_IP, GA_REAR, True)
|
||||||
|
log.info("Rear door OPEN")
|
||||||
|
|
||||||
|
elif event == 0x00 and last_button == "rear":
|
||||||
|
knx_write(KNXD_IP, GA_REAR, False)
|
||||||
|
log.info("Rear door CLOSED")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Packet error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Main Loop
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log.info("Starting EnOcean → KNX gateway")
|
||||||
|
|
||||||
|
communicator = SerialCommunicator(port=ENOCEAN_PORT)
|
||||||
|
communicator.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
packet = communicator.receive.get()
|
||||||
|
process_packet(packet)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Stopping gateway")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
communicator.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example on getting the information about the EnOcean controller.
|
||||||
|
The sending is happening between the application and the EnOcean controller,
|
||||||
|
no wireless communication is taking place here.
|
||||||
|
|
||||||
|
The command used here is specified as 1.10.5 Code 03: CO_RD_VERSION
|
||||||
|
in the ESP3 document.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enocean.consolelogger import init_logging
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
from enocean.protocol.packet import Packet
|
||||||
|
from enocean.protocol.constants import PACKET
|
||||||
|
from enocean import utils
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
"""
|
||||||
|
'/dev/ttyUSB0' might change depending on where your device is.
|
||||||
|
To prevent running the app as root, change the access permissions:
|
||||||
|
'sudo chmod 777 /dev/ttyUSB0'
|
||||||
|
"""
|
||||||
|
communicator = SerialCommunicator(port=u'/dev/ttyUSB0', callback=None)
|
||||||
|
packet = Packet(PACKET.COMMON_COMMAND, [0x03])
|
||||||
|
|
||||||
|
communicator.daemon = True
|
||||||
|
communicator.start()
|
||||||
|
communicator.send(packet)
|
||||||
|
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
receivedPacket = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if receivedPacket.packet_type == PACKET.RESPONSE:
|
||||||
|
print('Return Code: %s' % utils.to_hex_string(receivedPacket.data[0]))
|
||||||
|
print('APP version: %s' % utils.to_hex_string(receivedPacket.data[1:5]))
|
||||||
|
print('API version: %s' % utils.to_hex_string(receivedPacket.data[5:9]))
|
||||||
|
print('Chip ID: %s' % utils.to_hex_string(receivedPacket.data[9:13]))
|
||||||
|
print('Chip Version: %s' % utils.to_hex_string(receivedPacket.data[13:17]))
|
||||||
|
print('App Description Version: %s' % utils.to_hex_string(receivedPacket.data[17:]))
|
||||||
|
print('App Description Version (ASCII): %s' % str(bytearray(receivedPacket.data[17:])))
|
||||||
|
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from enocean.consolelogger import init_logging
|
||||||
|
import enocean.utils
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
from enocean.protocol.packet import RadioPacket
|
||||||
|
from enocean.protocol.constants import PACKET, RORG
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
|
||||||
|
def assemble_radio_packet(transmitter_id):
|
||||||
|
return RadioPacket.create(rorg=RORG.BS4, rorg_func=0x20, rorg_type=0x01,
|
||||||
|
sender=transmitter_id,
|
||||||
|
CV=50,
|
||||||
|
TMP=21.5,
|
||||||
|
ES='true')
|
||||||
|
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
communicator = SerialCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
print('The Base ID of your module is %s.' % enocean.utils.to_hex_string(communicator.base_id))
|
||||||
|
|
||||||
|
if communicator.base_id is not None:
|
||||||
|
print('Sending example package.')
|
||||||
|
communicator.send(assemble_radio_packet(communicator.base_id))
|
||||||
|
|
||||||
|
# endless loop receiving radio packets
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.VLD:
|
||||||
|
packet.select_eep(0x05, 0x00)
|
||||||
|
packet.parse_eep()
|
||||||
|
for k in packet.parsed:
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS4:
|
||||||
|
# parse packet with given FUNC and TYPE
|
||||||
|
for k in packet.parse_eep(0x02, 0x05):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS1:
|
||||||
|
# alternatively you can select FUNC and TYPE explicitely
|
||||||
|
packet.select_eep(0x00, 0x01)
|
||||||
|
# parse it
|
||||||
|
packet.parse_eep()
|
||||||
|
for k in packet.parsed:
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.RPS:
|
||||||
|
for k in packet.parse_eep(0x02, 0x02):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from enocean.consolelogger import init_logging
|
||||||
|
import enocean.utils
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
from enocean.protocol.packet import RadioPacket
|
||||||
|
from enocean.protocol.constants import PACKET, RORG
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
|
||||||
|
def assemble_radio_packet(transmitter_id):
|
||||||
|
return RadioPacket.create(rorg=RORG.BS4, rorg_func=0x20, rorg_type=0x01,
|
||||||
|
sender=transmitter_id,
|
||||||
|
CV=50,
|
||||||
|
TMP=21.5,
|
||||||
|
ES='true')
|
||||||
|
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
communicator = SerialCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
print('The Base ID of your module is %s.' % enocean.utils.to_hex_string(communicator.base_id))
|
||||||
|
|
||||||
|
if communicator.base_id is not None:
|
||||||
|
print('Sending example package.')
|
||||||
|
communicator.send(assemble_radio_packet(communicator.base_id))
|
||||||
|
|
||||||
|
# endless loop receiving radio packets
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.VLD:
|
||||||
|
packet.select_eep(0x05, 0x00)
|
||||||
|
packet.parse_eep()
|
||||||
|
for k in packet.parsed:
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS4:
|
||||||
|
# parse packet with given FUNC and TYPE
|
||||||
|
for k in packet.parse_eep(0x02, 0x05):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS1:
|
||||||
|
# alternatively you can select FUNC and TYPE explicitely
|
||||||
|
packet.select_eep(0x00, 0x01)
|
||||||
|
# parse it
|
||||||
|
packet.parse_eep()
|
||||||
|
for k in packet.parsed:
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.RPS:
|
||||||
|
for k in packet.parse_eep(0x02, 0x02):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Example to show automatic UTE Teach-in responses using
|
||||||
|
http://www.g-media.fr/prise-gigogne-enocean.html
|
||||||
|
|
||||||
|
Waits for UTE Teach-ins, sends the response automatically and prints the ID of new device.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import enocean.utils
|
||||||
|
from enocean.communicators import SerialCommunicator
|
||||||
|
from enocean.protocol.packet import RadioPacket, UTETeachInPacket
|
||||||
|
from enocean.protocol.constants import RORG
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
|
||||||
|
def set_position(destination, percentage):
|
||||||
|
global communicator
|
||||||
|
communicator.send(
|
||||||
|
RadioPacket.create(rorg=RORG.VLD, rorg_func=0x05, rorg_type=0x00, destination=destination, sender=communicator.base_id, command=1, POS=percentage)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
communicator = SerialCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
print('The Base ID of your module is %s.' % enocean.utils.to_hex_string(communicator.base_id))
|
||||||
|
|
||||||
|
|
||||||
|
# set_position([0x05, 0x0F, 0x0B, 0xEA], 100)
|
||||||
|
# time.sleep(10)
|
||||||
|
set_position([0x05, 0x0F, 0x0B, 0xEA], 50)
|
||||||
|
|
||||||
|
|
||||||
|
print('Press and hold the teach-in button on the plug now, till it starts turning itself off and on (about 10 seconds or so...)')
|
||||||
|
devices_learned = []
|
||||||
|
|
||||||
|
# endless loop receiving radio packets
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if isinstance(packet, UTETeachInPacket):
|
||||||
|
print('New device learned! The ID is %s.' % (packet.sender_hex))
|
||||||
|
devices_learned.append(packet.sender)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
print('Devices learned during this session: %s' % (', '.join([enocean.utils.to_hex_string(x) for x in devices_learned])))
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Example to show automatic UTE Teach-in responses using
|
||||||
|
http://www.g-media.fr/prise-gigogne-enocean.html
|
||||||
|
|
||||||
|
Waits for UTE Teach-ins, sends the response automatically and prints the ID of new device.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import enocean.utils
|
||||||
|
from enocean.communicators import SerialCommunicator
|
||||||
|
from enocean.protocol.packet import RadioPacket, UTETeachInPacket
|
||||||
|
from enocean.protocol.constants import RORG
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(destination, output_value):
|
||||||
|
global communicator
|
||||||
|
communicator.send(
|
||||||
|
RadioPacket.create(rorg=RORG.VLD, rorg_func=0x01, rorg_type=0x01, destination=destination, sender=communicator.base_id, command=1, IO=0x1E, OV=output_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def turn_on(destination):
|
||||||
|
send_command(destination, 100)
|
||||||
|
|
||||||
|
|
||||||
|
def turn_off(destination):
|
||||||
|
send_command(destination, 0)
|
||||||
|
|
||||||
|
|
||||||
|
communicator = SerialCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
print('The Base ID of your module is %s.' % enocean.utils.to_hex_string(communicator.base_id))
|
||||||
|
|
||||||
|
# Example of turning switches on and off
|
||||||
|
turn_on([0x01, 0x94, 0xB9, 0x46])
|
||||||
|
# Needs a bit of sleep in between, working too fast :S
|
||||||
|
time.sleep(0.1)
|
||||||
|
turn_on([0x01, 0x94, 0xE3, 0xB9])
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
turn_off([0x01, 0x94, 0xB9, 0x46])
|
||||||
|
time.sleep(0.1)
|
||||||
|
turn_off([0x01, 0x94, 0xE3, 0xB9])
|
||||||
|
|
||||||
|
|
||||||
|
print('Press and hold the teach-in button on the plug now, till it starts turning itself off and on (about 10 seconds or so...)')
|
||||||
|
devices_learned = []
|
||||||
|
|
||||||
|
# endless loop receiving radio packets
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if isinstance(packet, UTETeachInPacket):
|
||||||
|
print('New device learned! The ID is %s.' % (packet.sender_hex))
|
||||||
|
devices_learned.append(packet.sender)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
print('Devices learned during this session: %s' % (', '.join([enocean.utils.to_hex_string(x) for x in devices_learned])))
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from enocean.consolelogger import init_logging
|
||||||
|
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||||
|
from enocean.communicators.utils import send_to_tcp_socket
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
communicator = SerialCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
send_to_tcp_socket('localhost', 9637, packet)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from enocean.consolelogger import init_logging
|
||||||
|
from enocean.communicators.tcpcommunicator import TCPCommunicator
|
||||||
|
from enocean.protocol.constants import PACKET, RORG
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
init_logging()
|
||||||
|
communicator = TCPCommunicator()
|
||||||
|
communicator.start()
|
||||||
|
while communicator.is_alive():
|
||||||
|
try:
|
||||||
|
# Loop to empty the queue...
|
||||||
|
packet = communicator.receive.get(block=True, timeout=1)
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS4:
|
||||||
|
for k in packet.parse_eep(0x02, 0x05):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.BS1:
|
||||||
|
for k in packet.parse_eep(0x00, 0x01):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
if packet.packet_type == PACKET.RADIO_ERP1 and packet.rorg == RORG.RPS:
|
||||||
|
for k in packet.parse_eep(0x02, 0x04):
|
||||||
|
print('%s: %s' % (k, packet.parsed[k]))
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
break
|
||||||
|
|
||||||
|
if communicator.is_alive():
|
||||||
|
communicator.stop()
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
from __future__ import print_function, unicode_literals, division, absolute_import
|
||||||
|
import codecs
|
||||||
|
from enocean.protocol.eep import EEP
|
||||||
|
|
||||||
|
ROW_FORMAT = '|{:8s}|{:50s}|{:8s}|{:70s}|\n'
|
||||||
|
|
||||||
|
|
||||||
|
eep = EEP()
|
||||||
|
|
||||||
|
with codecs.open('SUPPORTED_PROFILES.md', 'w', 'utf-8') as f_handle:
|
||||||
|
f_handle.write('# Supported profiles\n')
|
||||||
|
f_handle.write('All profiles (should) correspond to the official [EEP](http://www.enocean-alliance.org/eep/) by EnOcean.\n\n')
|
||||||
|
|
||||||
|
for telegram in eep.soup.find_all('telegram'):
|
||||||
|
f_handle.write('### %s (%s)\n' % (telegram['description'], telegram['rorg']))
|
||||||
|
for func in telegram.find_all('profiles'):
|
||||||
|
# f_handle.write('##### FUNC %s - %s\n' % (func['func'], func['description']))
|
||||||
|
for profile in func.find_all('profile'):
|
||||||
|
f_handle.write('##### RORG %s - FUNC %s - TYPE %s - %s\n\n' % (telegram['rorg'], func['func'], profile['type'], profile['description']))
|
||||||
|
|
||||||
|
for data in profile.find_all('data'):
|
||||||
|
header = []
|
||||||
|
|
||||||
|
if data.get('direction'):
|
||||||
|
header.append('direction: %s' % (data.get('direction')))
|
||||||
|
if data.get('command'):
|
||||||
|
header.append('command: %s' % (data.get('command')))
|
||||||
|
|
||||||
|
if header:
|
||||||
|
f_handle.write('###### %s\n' % ' '.join(header))
|
||||||
|
|
||||||
|
f_handle.write(ROW_FORMAT.format('shortcut', 'description', 'type', 'values'))
|
||||||
|
f_handle.write(ROW_FORMAT.format('--------', '--------------------------------------------------', '--------', '----'))
|
||||||
|
for child in data.children:
|
||||||
|
if child.name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
values = []
|
||||||
|
for item in child.children:
|
||||||
|
if item.name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.name == 'rangeitem':
|
||||||
|
values.append('%s-%s - %s' % (item['start'], item['end'], item['description']))
|
||||||
|
elif item.name == 'item':
|
||||||
|
values.append('%s - %s' % (item['value'], item['description']))
|
||||||
|
elif item.name == 'range':
|
||||||
|
parent = item.parent
|
||||||
|
|
||||||
|
range_min = float(item.find('min').text)
|
||||||
|
range_max = float(item.find('max').text)
|
||||||
|
scale = parent.find('scale')
|
||||||
|
scale_min = float(scale.find('min').text)
|
||||||
|
scale_max = float(scale.find('max').text)
|
||||||
|
|
||||||
|
values.append('%s-%s ↔ %s-%s %s' % (range_min, range_max, scale_min, scale_max, parent['unit']))
|
||||||
|
if not values:
|
||||||
|
f_handle.write(ROW_FORMAT.format(child['shortcut'], child['description'], child.name, ''))
|
||||||
|
continue
|
||||||
|
|
||||||
|
f_handle.write(ROW_FORMAT.format(child['shortcut'], child['description'], child.name, values[0]))
|
||||||
|
for i in range(1, len(values)):
|
||||||
|
f_handle.write(ROW_FORMAT.format('', '', '', values[i]))
|
||||||
|
f_handle.write('\n')
|
||||||
|
f_handle.write('\n')
|
||||||
|
|
||||||
|
f_handle.write('\n')
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
enum-compat>=0.0.2
|
||||||
|
pyserial>=3.0
|
||||||
|
beautifulsoup4>=4.3.2
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
WITH_TIMINGS=1 nosetests -s -q
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='enocean',
|
||||||
|
version='0.60.1',
|
||||||
|
description='EnOcean serial protocol implementation',
|
||||||
|
author='Kimmo Huoman',
|
||||||
|
author_email='kipenroskaposti@gmail.com',
|
||||||
|
url='https://github.com/kipe/enocean',
|
||||||
|
packages=[
|
||||||
|
'enocean',
|
||||||
|
'enocean.protocol',
|
||||||
|
'enocean.communicators',
|
||||||
|
],
|
||||||
|
scripts=[
|
||||||
|
'examples/enocean_example.py',
|
||||||
|
],
|
||||||
|
package_data={
|
||||||
|
'': ['EEP.xml']
|
||||||
|
},
|
||||||
|
install_requires=[
|
||||||
|
'enum-compat>=0.0.2',
|
||||||
|
'pyserial>=3.0',
|
||||||
|
'beautifulsoup4>=4.3.2',
|
||||||
|
])
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=EnOcean to KNX Gateway
|
||||||
|
After=network.target knxd.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/enocean
|
||||||
|
ExecStart=/opt/enocean/.venv/bin/python3 /opt/enocean/enocean2knx-gw.py
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
Reference in New Issue
Block a user