New initial version

This commit is contained in:
root
2026-06-21 18:42:39 +00:00
commit 009484197f
45 changed files with 5538 additions and 0 deletions
+18
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
# enocean2knx-gw
EnOcean to KNX Gateway
+2
View File
@@ -0,0 +1,2 @@
[run]
relative_files = True
+3
View File
@@ -0,0 +1,3 @@
[flake8]
exclude = __init__.py
max_line_length = 120
+53
View File
@@ -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
View File
@@ -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/*
+19
View File
@@ -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/
+21
View File
@@ -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.
+27
View File
@@ -0,0 +1,27 @@
# Python EnOcean #
[![Build Status](https://travis-ci.org/kipe/enocean.svg?branch=master)](https://travis-ci.org/kipe/enocean)
[![Coverage Status](https://coveralls.io/repos/github/kipe/enocean/badge.svg?branch=master)](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`
+723
View File
@@ -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} |
View File
@@ -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
+10
View File
@@ -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()
+22
View File
@@ -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)
+38
View File
@@ -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
+148
View File
@@ -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
+41
View File
@@ -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
+233
View File
@@ -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
+435
View File
@@ -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()
+268
View File
@@ -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)
+30
View File
@@ -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]
+41
View File
@@ -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
+134
View File
@@ -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()
+60
View File
@@ -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()
+67
View File
@@ -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()
+67
View File
@@ -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()
+64
View File
@@ -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()
+77
View File
@@ -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()
+31
View File
@@ -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()
+39
View File
@@ -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()
+69
View File
@@ -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')
+3
View File
@@ -0,0 +1,3 @@
enum-compat>=0.0.2
pyserial>=3.0
beautifulsoup4>=4.3.2
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
WITH_TIMINGS=1 nosetests -s -q
+29
View File
@@ -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',
])
+14
View File
@@ -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