pax_global_header00006660000000000000000000000064140631416520014514gustar00rootroot0000000000000052 comment=4ec5cd11e7e6f518f285fa75188b1284d46fd6ad kipe-enocean-4ec5cd1/000077500000000000000000000000001406314165200145445ustar00rootroot00000000000000kipe-enocean-4ec5cd1/.coveragerc000066400000000000000000000000341406314165200166620ustar00rootroot00000000000000[run] relative_files = True kipe-enocean-4ec5cd1/.flake8000066400000000000000000000000651406314165200157200ustar00rootroot00000000000000[flake8] exclude = __init__.py max_line_length = 120 kipe-enocean-4ec5cd1/.github/000077500000000000000000000000001406314165200161045ustar00rootroot00000000000000kipe-enocean-4ec5cd1/.github/workflows/000077500000000000000000000000001406314165200201415ustar00rootroot00000000000000kipe-enocean-4ec5cd1/.github/workflows/python-package.yml000066400000000000000000000032711406314165200236010ustar00rootroot00000000000000# 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 kipe-enocean-4ec5cd1/.github/workflows/python-publish.yml000066400000000000000000000015411406314165200236520ustar00rootroot00000000000000# 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/* kipe-enocean-4ec5cd1/.gitignore000066400000000000000000000002121406314165200165270ustar00rootroot00000000000000bin include lib local share lib-python lib_pypy site-packages venv *.pyc *.egg-info build man MANIFEST dist pip-selfcheck.json .vscode/ kipe-enocean-4ec5cd1/LICENSE000066400000000000000000000020741406314165200155540ustar00rootroot00000000000000The 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. kipe-enocean-4ec5cd1/README.md000066400000000000000000000024401406314165200160230ustar00rootroot00000000000000# 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` kipe-enocean-4ec5cd1/SUPPORTED_PROFILES.md000066400000000000000000002130601406314165200177600ustar00rootroot00000000000000# 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} | kipe-enocean-4ec5cd1/enocean/000077500000000000000000000000001406314165200161545ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/__init__.py000066400000000000000000000000001406314165200202530ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/communicators/000077500000000000000000000000001406314165200210375ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/communicators/__init__.py000066400000000000000000000004101406314165200231430ustar00rootroot00000000000000''' 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 kipe-enocean-4ec5cd1/enocean/communicators/communicator.py000066400000000000000000000107211406314165200241120ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/communicators/serialcommunicator.py000066400000000000000000000030221406314165200253060ustar00rootroot00000000000000# -*- 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/ttyAMA0', 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') kipe-enocean-4ec5cd1/enocean/communicators/tcpcommunicator.py000066400000000000000000000027121406314165200246220ustar00rootroot00000000000000# -*- 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') kipe-enocean-4ec5cd1/enocean/communicators/tests/000077500000000000000000000000001406314165200222015ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/communicators/tests/test_communicator.py000066400000000000000000000045151406314165200263170ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/communicators/utils.py000066400000000000000000000005041406314165200225500ustar00rootroot00000000000000# -*- 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() kipe-enocean-4ec5cd1/enocean/consolelogger.py000066400000000000000000000014431406314165200213720ustar00rootroot00000000000000# -*- 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) kipe-enocean-4ec5cd1/enocean/decorators.py000066400000000000000000000025471406314165200207030ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/protocol/000077500000000000000000000000001406314165200200155ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/protocol/EEP.xml000066400000000000000000001663641406314165200211700ustar00rootroot00000000000000 255 0 -40.000000 0.000000 255 0 -30.000000 10.000000 255 0 -20.000000 20.000000 255 0 -10.000000 30.000000 255 0 0.000000 40.000000 255 0 10.000000 50.000000 255 0 20.000000 60.000000 255 0 30.000000 70.000000 255 0 40.000000 80.000000 255 0 50.000000 90.000000 255 0 60.000000 100.000000 255 0 -60.000000 20.000000 255 0 -50.000000 30.000000 255 0 -40.000000 40.000000 255 0 -30.000000 50.000000 255 0 -20.000000 60.000000 255 0 -10.000000 70.000000 255 0 0.000000 80.000000 255 0 10.000000 90.000000 255 0 20.000000 100.000000 255 0 30.000000 110.000000 255 0 40.000000 120.000000 255 0 50.000000 130.000000 1023 0 -10.000000 41.200000 1023 0 -40.000000 62.300000 0 250 0 100 0 250 0 40 0 255 0 100 0 1023 -20 +60 0 255 0.000000 5.100000 0 255 300.000000 30000.000000 0 255 600.000000 60000.000000 0 255 0.000000 5.100000 0 255 0.000000 510.000000 0 255 0.000000 1020.000000 0 250 0 5.000000 0 255 0 5.100000 0 255 0 510 0 255 0 51 0 200 0 100 0 255 0 2550 0 255 0 51 0 65535 0 65535 0 255 0 255 255 0 0 40 0 255 0 255 255 0 0 40 0 255 0 255 255 0 0 40 0 255 0 255 0 250 0 100 0 250 0 40 0 255 0 255 0 250 0 100 0 250 0 40 0 255 0 100 0 255 0 51.2 0 100 0 100 0 90 0 180 0 250 0 5.0 0 100 0 100 0 255 0 40 0 100 0 100 0 255 0 40 0 16777215 0.000000 16777215 0 15 0 15 255 0 0 40 1 65535 0.1 6553.5 0 255 0 255 0 255 0 255 kipe-enocean-4ec5cd1/enocean/protocol/__init__.py000066400000000000000000000000001406314165200221140ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/protocol/constants.py000066400000000000000000000051641406314165200224110ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/protocol/crc8.py000066400000000000000000000036431406314165200212340ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/protocol/eep.py000066400000000000000000000217461406314165200211520ustar00rootroot00000000000000# -*- 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 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 in EEP!') return None if rorg_func not in self.telegrams[eep_rorg].keys(): self.logger.warn('Cannot find func in EEP!') return None if rorg_type not in self.telegrams[eep_rorg][rorg_func].keys(): self.logger.warn('Cannot find type in EEP!') 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 kipe-enocean-4ec5cd1/enocean/protocol/packet.py000066400000000000000000000404201406314165200216360ustar00rootroot00000000000000# -*- 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() kipe-enocean-4ec5cd1/enocean/protocol/tests/000077500000000000000000000000001406314165200211575ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/protocol/tests/test_eep.py000066400000000000000000000223311406314165200233420ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/protocol/tests/test_packet.py000066400000000000000000000140471406314165200240450ustar00rootroot00000000000000# -*- 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 == [] kipe-enocean-4ec5cd1/enocean/protocol/tests/test_packet_creation.py000066400000000000000000000265251406314165200257350ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/enocean/protocol/tests/test_teachin.py000066400000000000000000000030521406314165200242030ustar00rootroot00000000000000# -*- 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] kipe-enocean-4ec5cd1/enocean/protocol/tests/test_temperature_sensors.py000066400000000000000000000031201406314165200266750ustar00rootroot00000000000000# -*- 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) kipe-enocean-4ec5cd1/enocean/tests/000077500000000000000000000000001406314165200173165ustar00rootroot00000000000000kipe-enocean-4ec5cd1/enocean/tests/test_utils.py000066400000000000000000000021341406314165200220670ustar00rootroot00000000000000# -*- 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] kipe-enocean-4ec5cd1/enocean/utils.py000066400000000000000000000022561406314165200176730ustar00rootroot00000000000000# -*- 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 kipe-enocean-4ec5cd1/examples/000077500000000000000000000000001406314165200163625ustar00rootroot00000000000000kipe-enocean-4ec5cd1/examples/co_rd_version_example.py000077500000000000000000000040131406314165200233030ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/examples/enocean_example.py000077500000000000000000000045371406314165200220730ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/examples/example_D2-05-00.py000066400000000000000000000035731406314165200213630ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/examples/example_DO21-11B-E.py000066400000000000000000000042611406314165200216220ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/examples/serial_to_tcp.py000077500000000000000000000014531406314165200215710ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/examples/tcp_server.py000077500000000000000000000024261406314165200211170ustar00rootroot00000000000000#!/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() kipe-enocean-4ec5cd1/generate_supported_profiles.py000077500000000000000000000065771406314165200227420ustar00rootroot00000000000000#!/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') kipe-enocean-4ec5cd1/requirements.txt000066400000000000000000000000671406314165200200330ustar00rootroot00000000000000enum-compat>=0.0.2 pyserial>=3.0 beautifulsoup4>=4.3.2 kipe-enocean-4ec5cd1/run_tests_with_timing.sh000077500000000000000000000000511406314165200215270ustar00rootroot00000000000000#!/bin/sh WITH_TIMINGS=1 nosetests -s -q kipe-enocean-4ec5cd1/setup.py000066400000000000000000000012371406314165200162610ustar00rootroot00000000000000#!/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', ])