Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

enocean

Package Overview
Dependencies
Maintainers
2
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

enocean - npm Package Compare versions

Comparing version
0.31
to
0.40
+38
enocean/decorators.py
# -*- 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
# -*- 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
+1
-1
Metadata-Version: 1.0
Name: enocean
Version: 0.31
Version: 0.40
Summary: EnOcean serial protocol implementation

@@ -5,0 +5,0 @@ Home-page: https://github.com/kipe/enocean

setup.py
enocean/__init__.py
enocean/consolelogger.py
enocean/decorators.py
enocean/utils.py
enocean.egg-info/PKG-INFO

@@ -5,0 +7,0 @@ enocean.egg-info/SOURCES.txt

# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import logging

@@ -11,7 +11,5 @@

from enocean.protocol.packet import Packet
from enocean.protocol.constants import PARSE_RESULT
from enocean.protocol.constants import PACKET, PARSE_RESULT, RETURN_CODE
logger = logging.getLogger('enocean.communicators.Communicator')
class Communicator(threading.Thread):

@@ -22,3 +20,5 @@ '''

'''
def __init__(self, callback=None):
logger = logging.getLogger('enocean.communicators.Communicator')
def __init__(self, callback=None, teach_in=True):
super(Communicator, self).__init__()

@@ -34,10 +34,15 @@ # Create an event to stop the thread

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:
p = self.transmit.get(block=False)
logger.info('Sending packet')
logger.debug(p)
return p
packet = self.transmit.get(block=False)
self.logger.info('Sending packet')
self.logger.debug(packet)
return packet
except queue.Empty:

@@ -49,3 +54,3 @@ pass

if not isinstance(packet, Packet):
logger.error('Object to send must be an instance of Packet')
self.logger.error('Object to send must be an instance of Packet')
return False

@@ -62,3 +67,3 @@ self.transmit.put(packet)

while True:
status, self._buffer, p = Packet.parse_msg(self._buffer)
status, self._buffer, packet = Packet.parse_msg(self._buffer, communicator=self)
# If message is incomplete -> break the loop

@@ -69,7 +74,41 @@ if status == PARSE_RESULT.INCOMPLETE:

# If message is OK, add it to receive queue or send to the callback method
if status == PARSE_RESULT.OK and p:
if status == PARSE_RESULT.OK and packet:
if self.__callback is None:
self.receive.put(p)
self.receive.put(packet)
else:
self.__callback(p)
logger.debug(p)
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:
# 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
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import logging

@@ -8,7 +8,7 @@ import serial

logger = logging.getLogger('enocean.communicators.SerialCommunicator')
class SerialCommunicator(Communicator):
''' Serial port communicator class for EnOcean radio '''
logger = logging.getLogger('enocean.communicators.SerialCommunicator')
def __init__(self, port='/dev/ttyAMA0', callback=None):

@@ -20,3 +20,3 @@ super(SerialCommunicator, self).__init__(callback)

def run(self):
logger.info('SerialCommunicator started')
self.logger.info('SerialCommunicator started')
while not self._stop_flag.is_set():

@@ -26,6 +26,6 @@ # If there's messages in transmit queue

while True:
p = self._get_from_send_queue()
if not p:
packet = self._get_from_send_queue()
if not packet:
break
self.__ser.write(bytearray(p.build()))
self.__ser.write(bytearray(packet.build()))

@@ -36,3 +36,3 @@ # Read chars from serial port as hex numbers

except serial.SerialException:
logger.error('Serial port exception! (device disconnected or multiple access on port?)')
self.logger.error('Serial port exception! (device disconnected or multiple access on port?)')
break

@@ -42,2 +42,2 @@ self.parse()

self.__ser.close()
logger.info('SerialCommunicator stopped')
self.logger.info('SerialCommunicator stopped')
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import logging

@@ -8,7 +8,7 @@ import socket

logger = logging.getLogger('enocean.communicators.TCPCommunicator')
class TCPCommunicator(Communicator):
''' Socket communicator class for EnOcean radio '''
logger = logging.getLogger('enocean.communicators.TCPCommunicator')
def __init__(self, host='', port=9637):

@@ -20,3 +20,3 @@ super(TCPCommunicator, self).__init__()

def run(self):
logger.info('TCPCommunicator started')
self.logger.info('TCPCommunicator started')
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

@@ -32,16 +32,16 @@ sock.bind((self.host, self.port))

continue
logger.debug('Client connected')
self.logger.debug('Client "%s" connected' % (addr))
client.settimeout(0.5)
while True and not self._stop_flag.is_set():
try:
d = client.recv(2048)
data = client.recv(2048)
except socket.timeout:
break
if not d:
if not data:
break
self._buffer.extend(bytearray(d))
self._buffer.extend(bytearray(data))
self.parse()
client.close()
logger.debug('Client disconnected')
self.logger.debug('Client disconnected')
sock.close()
logger.info('TCPCommunicator stopped')
self.logger.info('TCPCommunicator stopped')
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import socket

@@ -7,5 +7,5 @@

def send_to_tcp_socket(host, port, packet):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(str(bytearray(packet.build())))
s.close()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.send(str(bytearray(packet.build())))
sock.close()
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import logging
def init_logging(level=logging.DEBUG):
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')

@@ -12,5 +12,11 @@

ch = logging.StreamHandler()
ch.setLevel(level)
ch.setFormatter(formatter)
logger.addHandler(ch)
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)
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
from enum import IntEnum

@@ -53,2 +53,3 @@

SEC_ENCAPS = 0x31
UTE = 0xD4

@@ -65,3 +66,3 @@

# Starts from the end, so works on messages of all length.
class DB0:
class DB0(object):
BIT_0 = -1

@@ -77,3 +78,3 @@ BIT_1 = -2

class DB1:
class DB1(object):
BIT_0 = -9

@@ -89,3 +90,3 @@ BIT_1 = -10

class DB2:
class DB2(object):
BIT_0 = -17

@@ -101,3 +102,3 @@ BIT_1 = -18

class DB3:
class DB3(object):
BIT_0 = -25

@@ -111,1 +112,34 @@ BIT_1 = -26

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
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
# https://gist.github.com/hypebeast/3833758
crcTable = (
CRC_TABLE = (
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38,

@@ -38,5 +38,5 @@ 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77,

def calc(msg):
runningCRC = 0
for c in msg:
runningCRC = crcTable[runningCRC & 0xFF ^ c & 0xFF]
return runningCRC
checksum = 0
for byte in msg:
checksum = CRC_TABLE[checksum & 0xFF ^ byte & 0xFF]
return checksum
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
from __future__ import print_function, unicode_literals, division, absolute_import
import os
import logging
from collections import OrderedDict
from bs4 import BeautifulSoup
import logging
logger = logging.getLogger('enocean.protocol.eep')
import enocean.utils
from enocean.protocol.constants import RORG
path = os.path.dirname(os.path.realpath(__file__))
class EEP(object):
logger = logging.getLogger('enocean.protocol.eep')
class EEP(object):
def __init__(self):
self.ok = False
self.init_ok = False
self.telegrams = {}
try:
with open(os.path.join(path, 'EEP.xml'), 'r') as f:
self.soup = BeautifulSoup(f.read(), "html.parser")
self.ok = True
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'EEP.xml'), 'r') as xml_file:
self.soup = BeautifulSoup(xml_file.read(), "html.parser")
self.init_ok = True
self.__load_xml()
except IOError:
logger.warn('Cannot load protocol file!')
pass
# 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 _get_hex(self, nmb):
''' Get hex-representation of number '''
return '0x%02X' % (nmb)
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')
}
def _get_raw(self, value, bitarray):
@staticmethod
def _get_raw(source, bitarray):
''' Get raw data as integer, based on offset and size '''
offset = int(value['offset'])
size = int(value['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)
def _get_value(self, value, data):
@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(value, data)
raw_value = self._get_raw(source, bitarray)
rng = value.find('range')
rng = source.find('range')
rng_min = float(rng.find('min').text)
rng_max = float(rng.find('max').text)
scl = value.find('scale')
scl = source.find('scale')
scl_min = float(scl.find('min').text)

@@ -46,5 +78,5 @@ scl_max = float(scl.find('max').text)

return {
value['shortcut']: {
'description': value['description'],
'unit': value['unit'],
source['shortcut']: {
'description': source.get('description'),
'unit': source['unit'],
'value': (scl_max - scl_min) / (rng_max - rng_min) * (raw_value - rng_min) + scl_min,

@@ -55,11 +87,14 @@ 'raw_value': raw_value,

def _get_enum(self, value, data):
def _get_enum(self, source, bitarray):
''' Get enum value, based on the data in XML '''
raw_value = self._get_raw(value, data)
value_desc = value.find('item', {'value': str(raw_value)})
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 {
value['shortcut']: {
'description': value['description'],
'unit': value.get('unit', ''),
'value': value_desc['description'],
source['shortcut']: {
'description': source.get('description'),
'unit': source.get('unit', ''),
'value': value_desc['description'].format(value=raw_value),
'raw_value': raw_value,

@@ -69,46 +104,124 @@ }

def find_profile(self, rorg, func, type):
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.ok:
return None, None
if not self.init_ok:
self.logger.warn('EEP.xml not loaded!')
return None
rorg = self.soup.find('telegram', {'rorg': self._get_hex(rorg)})
if not rorg:
logger.warn('Cannot find rorg in EEP!')
return None, None
if eep_rorg not in self.telegrams.keys():
self.logger.warn('Cannot find rorg in EEP!')
return None
func = rorg.find('profiles', {'func': self._get_hex(func)})
if not func:
logger.warn('Cannot find func in EEP!')
return None, None
if rorg_func not in self.telegrams[eep_rorg].keys():
self.logger.warn('Cannot find func in EEP!')
return None
profile = func.find('profile', {'type': self._get_hex(type)})
if not profile:
logger.warn('Cannot find type in EEP!')
return None, None
if rorg_type not in self.telegrams[eep_rorg][rorg_func].keys():
self.logger.warn('Cannot find type in EEP!')
return None
data_description = profile.find('data')
if not data_description:
logger.warn('Cannot find data description in EEP!')
data_description = []
profile = self.telegrams[eep_rorg][rorg_func][rorg_type]
return profile, data_description
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)
def get_values(self, rorg, func, type, data):
''' Get keys and values from data, matching RORG, FUNC and TYPE '''
if not self.ok:
return [], {}
# If eep_command is defined, so should be data.command
return profile.find('data', {'command': str(command)}, recursive=False)
profile, data_description = self.find_profile(rorg, func, type)
if not profile or not data_description:
# 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 = {}
for d in data_description.contents:
if not d.name:
output = OrderedDict({})
for source in profile.contents:
if not source.name:
continue
if d.name == 'value':
output.update(self._get_value(d, data))
if d.name == 'enum':
output.update(self._get_enum(d, data))
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

@@ -13,3 +13,3 @@ <?xml version="1.0" encoding="utf-8"?>

</enum>
<enum description="Energy bow" shortcut="EBO" offset="3" size="1">
<enum description="Energy bow" shortcut="EB" offset="3" size="1">
<item description="released" value="0" />

@@ -28,5 +28,34 @@ <item description="pressed" value="1" />

</enum>
<status description="T21" shortcut="T21" offset="2" size="1" />
<status description="NU" shortcut="NU" offset="3" size="1" />
</data>
</profile>
</profiles>
<profiles func="0x05" description="Detectors">
<profile type="0x01" description="Liquid Leakage Sensor (mechanic harvester)">
<data>
<enum description="Water Sensor" shortcut="WAS" offset="0" size="8">
<rangeitem description="not specified" start="0" end="16" />
<item description="Water detected" value="17" />
<rangeitem description="not specified" start="18" end="255" />
</enum>
<status description="T21" shortcut="T21" offset="2" size="1" />
<status description="NU" shortcut="NU" offset="3" size="1" />
</data>
</profile>
</profiles>
<profiles func="0x10" description="Mechanical Handle">
<profile type="0x00" description="Window Handle">
<data>
<enum description="Window handle" shortcut="WIN" offset="2" size="2">
<item description="Moved from up to vertical" value="0" />
<item description="Moved from vertical to up" value="1" />
<item description="Moved from down to vertical" value="2" />
<item description="Moved from vertical to down" value="3" />
</enum>
<status description="T21" shortcut="T21" offset="2" size="1" />
<status description="NU" shortcut="NU" offset="3" size="1" />
</data>
</profile>
</profiles>
</telegram>

@@ -47,5 +76,61 @@ <telegram rorg="0xD5" type="1BS" description="1BS Telegram">

<profiles func="0x02" description="Temperature Sensors">
<profile type="0x01" description="Temperature Sensor Range -40°C to 0°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-40.000000</min>
<max>0.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x02" description="Temperature Sensor Range -30°C to +10°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-30.000000</min>
<max>10.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x03" description="Temperature Sensor Range -20°C to +20°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-20.000000</min>
<max>20.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x04" description="Temperature Sensor Range -10°C to +30°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-10.000000</min>
<max>30.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x05" description="Temperature Sensor Range 0°C to +40°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="C">
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>

@@ -62,3 +147,313 @@ <min>255</min>

</profile>
<profile type="0x06" description="Temperature Sensor Range +10°C to +50°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>10.000000</min>
<max>50.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x07" description="Temperature Sensor Range +20°C to +60°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>20.000000</min>
<max>60.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x08" description="Temperature Sensor Range +30°C to +70°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>30.000000</min>
<max>70.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x09" description="Temperature Sensor Range +40°C to +80°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>40.000000</min>
<max>80.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x0A" description="Temperature Sensor Range +50°C to +90°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>50.000000</min>
<max>90.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x0B" description="Temperature Sensor Range +60°C to +100°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>60.000000</min>
<max>100.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x10" description="Temperature Sensor Range -60°C to +20°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-60.000000</min>
<max>20.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x11" description="Temperature Sensor Range -50°C to +30°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-50.000000</min>
<max>30.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x12" description="Temperature Sensor Range -40°C to +40°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-40.000000</min>
<max>40.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x13" description="Temperature Sensor Range -30°C to +50°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-30.000000</min>
<max>50.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x14" description="Temperature Sensor Range -20°C to +60°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-20.000000</min>
<max>60.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x15" description="Temperature Sensor Range -10°C to +70°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>-10.000000</min>
<max>70.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x16" description="Temperature Sensor Range 0°C to +80°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>0.000000</min>
<max>80.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x17" description="Temperature Sensor Range +10°C to +90°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>10.000000</min>
<max>90.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x18" description="Temperature Sensor Range +20°C to +100°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>20.000000</min>
<max>100.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x19" description="Temperature Sensor Range +30°C to +110°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>30.000000</min>
<max>110.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x1A" description="Temperature Sensor Range +40°C to +120°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>40.000000</min>
<max>120.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x1B" description="Temperature Sensor Range +50°C to +130°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>50.000000</min>
<max>130.000000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x20" description="10 Bit Temperature Sensor Range -10°C to +41.2°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="14" size="10" unit="°C">
<range>
<min>1023</min>
<max>0</max>
</range>
<scale>
<min>-10.000000</min>
<max>41.200000</max>
</scale>
</value>
</data>
</profile>
<profile type="0x30" description="10 Bit Temperature Sensor Range -40°C to +62.3°C">
<data>
<value description="Temperature (linear)" shortcut="TMP" offset="14" size="10" unit="°C">
<range>
<min>1023</min>
<max>0</max>
</range>
<scale>
<min>-40.000000</min>
<max>62.300000</max>
</scale>
</value>
</data>
</profile>
</profiles>
<profiles func="0x04" description="Temperature and Humidity Sensor">
<profile type="0x01" description="Range 0°C to +40°C and 0% to 100%">
<data>
<value description="Rel. Humidity (linear)" shortcut="HUM" offset="8" size="8" unit="%">
<range>
<min>0</min>
<max>250</max>
</range>
<scale>
<min>0</min>
<max>100</max>
</scale>
</value>
<value description="Temperature (linear)" shortcut="TMP" offset="16" size="8" unit="°C">
<range>
<min>0</min>
<max>250</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
<enum description="Availability of the Temperature Sensor" shortcut="TSN" offset="30" size="1">
<item description="not available" value="0" />
<item description="available" value="1" />
</enum>
</data>
</profile>
</profiles>
<profiles func="0x06" description="Light Sensor">

@@ -104,3 +499,505 @@ <profile type="0x01" description="Range 300lx to 60.000lx">

</profiles>
<profiles func="0x08" description="Light, Temperature and Occupancy Sensor">
<profile description="Range 0lx to 510lx, 0°C to +51°C and Occupancy Button" type="0x01">
<data>
<value shortcut="SVC" description="Supply voltage (linear)" offset="0" size="8" unit="V">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>5.100000</max>
</scale>
</value>
<value shortcut="ILL" description="Illumination (linear)" offset="8" size="8" unit="lx">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>510</max>
</scale>
</value>
<value shortcut="TMP" description="Temperature (linear)" offset="16" size="8" unit="°C">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>51</max>
</scale>
</value>
<enum shortcut="PIRS" description="PIR Status" offset="30" size="1">
<item description="PIR on" value="0" />
<item description="PIR off" value="1" />
</enum>
<enum shortcut="OCC" description="Occupancy Button" offset="31" size="1">
<item description="Button pressed" value="0" />
<item description="Button released" value="1" />
</enum>
</data>
</profile>
</profiles>
<profiles description="Room Operating Panel" func="0x10">
<profile description="Temperature Sensor and Set Point" type="0x03">
<data>
<value shortcut="SP" description="Set Point (linear)" offset="8" size="8" unit="%">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>255</max>
</scale>
</value>
<value shortcut="TMP" description="Temperature (linear)" offset="16" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
</data>
</profile>
<profile description="Temperature and Humidity Sensor and Set Point" type="0x12">
<data>
<value shortcut="SP" description="Set Point (linear)" offset="0" size="8" unit="">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>255</max>
</scale>
</value>
<value shortcut="HUM" description="Rel. Humidity (linear)" offset="8" size="8" unit="%">
<range>
<min>0</min>
<max>250</max>
</range>
<scale>
<min>0</min>
<max>100</max>
</scale>
</value>
<value shortcut="TMP" description="Temperature (linear)" offset="16" size="8" unit="°C">
<range>
<min>0</min>
<max>250</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
<enum description="LRN Bit" shortcut="LRNB" offset="28" size="1">
<item description="Teach-in telegram" value="0" />
<item description="Data telegram" value="1" />
</enum>
</data>
</profile>
</profiles>
<profiles description="Controller Status" func="0x11">
<profile description="Temperature Controller Output" type="0x02">
<data>
<value shortcut="CVAR" description="Actual value of controller" offset="0" size="8" unit="%">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>100</max>
</scale>
</value>
<enum shortcut="FAN" description="Actual value of fan" offset="8" size="8" unit="%">
<item description="State 0 Manual" value="0" />
<item description="State 1 Manual" value="1" />
<item description="State 2 Manual" value="2" />
<item description="State 3 Manual" value="3" />
<item description="State 0 Automatic" value="16" />
<item description="State 1 Automatic" value="17" />
<item description="State 2 Automatic" value="18" />
<item description="State 3 Automatic" value="19" />
<item description="Not Available" value="255" />
</enum>
<value shortcut="ASP" description="Actual Setpoint" offset="16" size="8" unit="C">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>51.2</max>
</scale>
</value>
<enum shortcut="ALR" description="Alarm" offset="24" size="1">
<item description="No alarm" value="0" />
<item description="Alarm" value="1" />
</enum>
<enum shortcut="CTM" description="Controller mode" offset="25" size="1">
<item description="Heating" value="1" />
<item description="Cooling" value="2" />
<item description="Off" value="3" />
</enum>
<enum shortcut="CTS" description="Controller state" offset="27" size="1">
<item description="Automatic" value="0" />
<item description="Override" value="1" />
</enum>
<enum shortcut="ERH" description="Energy hold-off" offset="29" size="1">
<item description="Normal" value="0" />
<item description="Energy hold-off / Dew point" value="1" />
</enum>
<enum shortcut="RO" description="Room occupancy" offset="30" size="2">
<item description="Occupied" value="0" />
<item description="Unoccupied" value="1" />
<item description="StandBy" value="2" />
<item description="Frost" value="3" />
</enum>
</data>
</profile>
</profiles>
<profiles func="0x14" description="Multi-Func Sensor">
<profile type="0x01" description="Single Input Contact (Window/Door), Supply voltage monitor">
<data>
<value description="Supply voltage / super cap. (linear); 251 - 255 reserved for error code" shortcut="SVC" offset="0" size="8" unit="V">
<range>
<min>0</min>
<max>250</max>
</range>
<scale>
<min>0</min>
<max>5.0</max>
</scale>
</value>
<enum description="Contact" shortcut="CT" offset="31" size="1">
<item description="open" value="1" />
<item description="closed" value="0" />
</enum>
</data>
</profile>
</profiles>
<profiles description="HVAC Components" func="0x20">
<profile description="Battery Powered Actuator (BI-DIR)" type="0x01">
<data direction="1">
<value shortcut="CV" description="Current Value" offset="0" size="8" unit="%">
<range>
<min>0</min>
<max>100</max>
</range>
<scale>
<min>0</min>
<max>100</max>
</scale>
</value>
<enum shortcut="SO" description="Service On" offset="8" size="1">
<item description="off" value="0" />
<item description="on" value="1" />
</enum>
<enum shortcut="ENIE" description="Energy input enabled" offset="9" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="ES" description="Energy storage sufficiently charged" offset="10" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="BCAP" description="Battery capacity; change battery next days" offset="11" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="CCO" description="Contact, cover open" offset="12" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="FTS" description="Failure Temperature sensor, out of range" offset="13" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="DWO" description="Detection, window open" offset="14" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="ACO" description="Actuator obstructed" offset="15" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<value shortcut="TMP" description="Temperature (linear)" offset="16" size="8" unit="°C">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
</data>
<data direction="2">
<value shortcut="SP" description="Valve Position" offset="0" size="8" unit="%">
<range>
<min>0</min>
<max>100</max>
</range>
<scale>
<min>0</min>
<max>100</max>
</scale>
</value>
<value shortcut="TMP" description="Temperature from RCU" offset="8" size="8" unit="°C">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
<enum shortcut="RIN" description="Run init sequence" offset="16" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="LFS" description="Lift set" offset="17" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="VO" description="Valve open / maintenance" offset="18" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="VC" description="Valve closed" offset="19" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="SB" description="Summer bit, Reduction of energy consumption" offset="20" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="SPS" description="Set point selection" offset="21" size="1">
<item description="Valve position" value="0" />
<item description="Temperature set point" value="1" />
</enum>
<enum shortcut="SPN" description="Set point inverse" offset="22" size="1">
<item description="false" value="0" />
<item description="true" value="1" />
</enum>
<enum shortcut="RCU" description="Select function" offset="23" size="1">
<item description="RCU" value="0" />
<item description="service on" value="1" />
</enum>
</data>
</profile>
</profiles>
<profiles func="0x12" description="Atomated Meter Reading (AMR)">
<profile type="0x01" description="Electricity">
<data>
<value shortcut="MR" description="current value in W or cumulative value in kWh" offset="0" size="24" unit="">
<range>
<min>0</min>
<max>16777215</max>
</range>
<scale>
<min>0.000000</min>
<max>16777215</max>
</scale>
</value>
<value shortcut="TI" description="Tariff info" offset="24" size="4" unit="">
<range>
<min>0</min>
<max>15</max>
</range>
<scale>
<min>0</min>
<max>15</max>
</scale>
</value>
<enum shortcut="DT" description="Current value or cumulative value" offset="29" size="1">
<item description="kWh" value="0" />
<item description="W" value="1" />
</enum>
<enum shortcut="DIV" description="Divisor for value" offset="30" size="2">
<item description="x/1" value="0" />
<item description="x/10" value="1" />
<item description="x/100" value="2" />
<item description="x/1000" value="3" />
</enum>
</data>
</profile>
</profiles>
<profiles func="0x30" description="Digital Input">
<profile type="0x03" description="Digital Inputs, Wake and Temperature">
<data>
<value shortcut="TMP" description="Temperature (linear)" offset="8" size="8" unit="°C">
<range>
<min>255</min>
<max>0</max>
</range>
<scale>
<min>0</min>
<max>40</max>
</scale>
</value>
<enum shortcut="WA0" description="Value of wake signal" offset="19" size="1">
<item description="Low" value="0" />
<item description="High" value="1" />
</enum>
<enum shortcut="DI3" description="Digital Input 3" offset="20" size="1">
<item description="Low" value="0" />
<item description="High" value="1" />
</enum>
<enum shortcut="DI2" description="Digital Input 2" offset="21" size="1">
<item description="Low" value="0" />
<item description="High" value="1" />
</enum>
<enum shortcut="DI1" description="Digital Input 1" offset="22" size="1">
<item description="Low" value="0" />
<item description="High" value="1" />
</enum>
<enum shortcut="DI0" description="Digital Input 0" offset="23" size="1">
<item description="Low" value="0" />
<item description="High" value="1" />
</enum>
</data>
</profile>
</profiles>
<profiles func="0x38" description="Central Command">
<profile type="0x08" description="Gateway">
<command description="Command ID" shortcut="CMD" offset="0" size="8">
<item description="Dimming" value="2" />
</command>
<data command="2">
<enum shortcut="CMD" description="Command ID" offset="0" size="8">
<rangeitem description="Command ID {value}" start="0" end="13" />
</enum>
<value shortcut="EDIM" description="Dimming value" offset="8" size="8" unit="%">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>255</max>
</scale>
</value>
<value shortcut="RMP" description="Ramping time" offset="16" size="8" unit="s">
<range>
<min>0</min>
<max>255</max>
</range>
<scale>
<min>0</min>
<max>255</max>
</scale>
</value>
<enum description="LRN Bit" shortcut="LRNB" offset="28" size="1">
<item description="Teach-in Telegram" value="0" />
<item description="Data Telegram" value="1" />
</enum>
<enum description="Dimming Range" shortcut="EDIMR" offset="29" size="1">
<item description="Absolute value" value="0" />
<item description="Relative value" value="1" />
</enum>
<enum description="Store final value" shortcut="STR" offset="30" size="1">
<item description="No" value="0" />
<item description="Yes" value="1" />
</enum>
<enum description="Switching command" shortcut="SW" offset="31" size="1">
<item description="Off" value="0" />
<item description="On" value="1" />
</enum>
</data>
</profile>
</profiles>
</telegram>
<telegram rorg="0xD2" type="VLD" description="VLD Telegram">
<profiles func="0x01" description="Electronic switches and dimmers with Energy Measurement and Local Control">
<profile type="0x01" description="Electronic switch with Local Control">
<command description="command indentifier" shortcut="CMD" offset="4" size="4">
<item description="Actuator Set Output" value="1" />
<!-- <item description="Actuator Set Local" value="2" /> -->
<!-- <item description="Actuator Status Query" value="3" /> -->
<item description="Actuator Status Response" value="4" />
<!-- <item description="Actuator Set Measurement" value="5" /> -->
<!-- <item description="Actuator Measurement Query" value="6" /> -->
<!-- <item description="Actuator Measurement Response" value="7" /> -->
<!-- <item description="Actuator Set Pilot Wire Mode" value="8" /> -->
<!-- <item description="Actuator Pilot Wire Mode Query" value="9" /> -->
<!-- <item description="Actuator Pilot Wire Mode Response" value="10" /> -->
<!-- <item description="Actuator Set External Interface Settings" value="11" /> -->
<!-- <item description="Actuator External Interface Settings Query" value="12" /> -->
<!-- <item description="Actuator External Interface Settings Response" value="13" /> -->
</command>
<data command="4" bits="3">
<enum description="Power Failure" shortcut="PF" offset="0" size="1">
<item description="Power Failure Detection disabled/not supported" value="0" />
<item description="Power Failure Detection enabled" value="1" />
</enum>
<enum description="Power Failure Detection" shortcut="PFD" offset="1" size="1">
<item description="Power Failure Detection not detected/not supported/disabled" value="0" />
<item description="Power Failure Detection Detected" value="1" />
</enum>
<enum description="Command indentifier" shortcut="CMD" offset="4" size="4">
<rangeitem description="Command ID {value}" start="0" end="13" />
</enum>
<enum description="Over current switch off" shortcut="OC" offset="8" size="1">
<item description="Over current switch off: ready / not supported" value="0" />
<item description="Over current switch off: executed" value="1" />
</enum>
<enum description="Error level" shortcut="EL" offset="9" size="2">
<item description="Error level 0: hardware OK" value="0" />
<item description="Error level 1: hardware warning" value="1" />
<item description="Error level 2: hardware failure" value="2" />
<item description="Error level not supported" value="3" />
</enum>
<enum description="I/O channel" shortcut="IO" offset="11" size="5">
<rangeitem description="Output channel {value} (to load)" start="0" end="29" />
<item description="Not applicable, do not use" value="30" />
<item description="Input channel (from mains supply)" value="31" />
</enum>
<enum description="Local control" shortcut="LC" offset="16" size="1">
<item description="Local control disabled / not supported" value="0" />
<item description="Local control enabled" value="1" />
</enum>
<enum description="Output value" shortcut="OV" offset="17" size="7">
<item description="Output value 0% or OFF" value="0" />
<rangeitem description="Output value {value}% or ON" start="1" end="100" />
<rangeitem description="Not used" start="101" end="126" />
<item description="output value not valid / not set" value="127" />
</enum>
</data>
<data command="1" bits="3">
<enum description="Command indentifier" shortcut="CMD" offset="4" size="4">
<rangeitem description="Command ID {value}" start="0" end="13" />
</enum>
<enum description="Dim value" shortcut="DV" offset="8" size="3">
<item description="Switch to new output value" value="0" />
<item description="Dim to new output level - dim timer 1" value="1" />
<item description="Dim to new output level - dim timer 2" value="2" />
<item description="Dim to new output level - dim timer 3" value="3" />
<item description="Stop dimming" value="4" />
</enum>
<enum description="I/O channel" shortcut="IO" offset="11" size="5">
<rangeitem description="Output channel {value} (to load)" start="0" end="29" />
<item description="All output channels supported by the device" value="30" />
<item description="Input channel (from mains supply)" value="31" />
</enum>
<enum description="Output value" shortcut="OV" offset="17" size="7">
<item description="Output value 0% or OFF" value="0" />
<rangeitem description="Output value {value}% or ON" start="1" end="100" />
<rangeitem description="Not used" start="101" end="126" />
<item description="output value not valid / not set" value="127" />
</enum>
</data>
</profile>
</profiles>
</telegram>
</telegrams>
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division
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
from enocean.protocol.constants import PACKET, RORG, PARSE_RESULT, DB0, DB2, DB3, DB4, DB6
logger = logging.getLogger('enocean.protocol.packet')
eep = EEP()
class Packet(object):

@@ -20,17 +19,33 @@ '''

'''
def __init__(self, type, data=[], optional=[]):
self.type = 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.data = data
self.optional = optional
self.bit_data = []
self.bit_optional = []
self.parsed = {}
self.rorg_manufacturer = 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.type, [hex(o) for o in self.data], [hex(o) for o in self.optional], self.parsed)
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)

@@ -41,31 +56,43 @@ def __unicode__(self):

def __eq__(self, other):
return self.type == other.type and self.rorg == other.rorg and self.data == other.data and self.optional == other.optional
return self.packet_type == other.packet_type and self.rorg == other.rorg and self.data == other.data and self.optional == other.optional
def _get_bit(self, byte, bit):
''' Get bit value from byte '''
return (byte >> bit) & 0x01
@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)
def _combine_hex(self, data):
''' Combine list of integer values to one big integer '''
output = 0x00
for i, d in enumerate(reversed(data)):
output |= (d << i * 8)
return output
@_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])
def _to_bitarray(self, data, width=8):
''' Convert data (list of integers, bytearray or integer) to bitarray '''
if isinstance(data, list) or isinstance(data, bytearray):
data = self._combine_hex(data)
return [True if digit == '1' else False for digit in bin(data)[2:].zfill(width)]
# # 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))
def _from_bitarray(self, data):
''' Convert bit array back to integer '''
return int(''.join(['1' if x else '0' for x in data]), 2)
# @_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])
def _to_hex_string(self, data):
''' Convert list of integers to a hex string, separated by ":" '''
return ':'.join([('%02X' % o) for o in data])
@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):
def parse_msg(buf, communicator=None):
'''

@@ -85,3 +112,3 @@ Parses message from buffer.

# Convert to list, as index -method isn't defined for bytearray
buf = buf[list(buf).index(0x55):]
buf = [ord(x) if not isinstance(x, int) else x for x in buf[list(buf).index(0x55):]]
try:

@@ -110,3 +137,3 @@ data_len = (buf[1] << 8) | buf[2]

# Fail if doesn't match message
logger.error('Header CRC error!')
Packet.logger.error('Header CRC error!')
# Return CRC_MISMATCH

@@ -116,3 +143,3 @@ return PARSE_RESULT.CRC_MISMATCH, buf, None

# Fail if doesn't match message
logger.error('Data CRC error!')
Packet.logger.error('Data CRC error!')
# Return CRC_MISMATCH

@@ -123,24 +150,141 @@ return PARSE_RESULT.CRC_MISMATCH, buf, None

if packet_type == PACKET.RADIO:
p = RadioPacket(packet_type, data, opt_data)
# Need to handle UTE Teach-in here, as it's a separate packet type...
if data[0] == RORG.UTE:
packet = UTETeachIn(packet_type, data, opt_data, communicator=communicator)
# Send a response automatically, works only if
# - communicator is set
# - communicator.teach_in == True
packet.send_response()
else:
packet = RadioPacket(packet_type, data, opt_data)
elif packet_type == PACKET.RESPONSE:
p = ResponsePacket(packet_type, data, opt_data)
packet = ResponsePacket(packet_type, data, opt_data)
elif packet_type == PACKET.EVENT:
packet = EventPacket(packet_type, data, opt_data)
else:
p = Packet(packet_type, data, opt_data)
packet = Packet(packet_type, data, opt_data)
return PARSE_RESULT.OK, buf, p
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
- 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:
# At least for now, only support PACKET.RADIO.
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)
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 parse_eep(self, func, type):
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 '''
provides, values = eep.get_values(self.rorg, func, type, self.bit_data)
# 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.type)]
ords = [0x55, (data_length >> 8) & 0xFF, data_length & 0xFF, len(self.optional), int(self.packet_type)]
ords.append(crc8.calc(ords[1:5]))

@@ -154,8 +298,5 @@ ords.extend(self.data)

class RadioPacket(Packet):
destination = 0
destination_hex = ''
destination = [0xFF, 0xFF, 0xFF, 0xFF]
dBm = 0
status = 0
sender = 0
sender_hex = ''
sender = [0xFF, 0xFF, 0xFF, 0xFF]
learn = True

@@ -168,9 +309,27 @@ contains_eep = False

@staticmethod
def create(rorg, rorg_func, rorg_type, direction=None, command=None,
destination=None, sender=None, learn=False, **kwargs):
return Packet.create(PACKET.RADIO, 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._combine_hex(self.optional[1:5])
self.destination_hex = self._to_hex_string(self.optional[1:5])
self.destination = self.optional[1:5]
self.dBm = -self.optional[5]
self.status = self.data[-1]
self.sender = self._combine_hex(self.data[-5:-1])
self.sender_hex = self._to_hex_string(self.data[-5:-1])
self.sender = self.data[-5:-1]
# Default to learn == True, as some devices don't have a learn button

@@ -180,18 +339,16 @@ self.learn = True

self.rorg = self.data[0]
if self.rorg == RORG.RPS:
self.bit_data = self._to_bitarray(self.data[1], 8)
# parse learn bit and FUNC/TYPE, if applicable
if self.rorg == RORG.BS1:
self.bit_data = self._to_bitarray(self.data[1], 8)
self.learn = not self.bit_data[DB0.BIT_3]
self.learn = not self._bit_data[DB0.BIT_3]
if self.rorg == RORG.BS4:
self.bit_data = self._to_bitarray(self.data[1:5], 32)
self.learn = not self.bit_data[DB0.BIT_3]
self.learn = not self._bit_data[DB0.BIT_3]
if self.learn:
self.contains_eep = self.bit_data[DB0.BIT_7]
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 = self._from_bitarray(self.bit_data[DB3.BIT_7:DB3.BIT_1])
self.rorg_type = self._from_bitarray(self.bit_data[DB3.BIT_1:DB2.BIT_2])
# Try to parse by EEP
self.parse_eep(self.rorg_func, self.rorg_type)
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))

@@ -201,2 +358,81 @@ return super(RadioPacket, self).parse()

class UTETeachIn(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
def __init__(self, packet_type, data=None, optional=None, communicator=None):
self.__communicator = communicator
super(UTETeachIn, self).__init__(packet_type=packet_type, data=data, optional=optional)
@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(UTETeachIn, 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])
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, data=data, optional=optional)
def send_response(self, response=TEACHIN_ACCEPTED):
if self.__communicator is None:
self.logger.error('Communicator not set, cannot send UTE teach-in response.')
return
if not self.__communicator.teach_in:
self.logger.info('Communicator not set to teach-in mode, not sending UTE teach-in response.')
return
self.logger.info('Sending response to UTE teach-in.')
self.__communicator.send(
self._create_response_packet(self.__communicator.base_id))
class ResponsePacket(Packet):

@@ -203,0 +439,0 @@ response = 0

#!/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 Packet
from enocean.protocol.packet import RadioPacket
from enocean.protocol.constants import PACKET, RORG

@@ -15,21 +16,40 @@ import sys

p = Packet(PACKET.COMMON_COMMAND, [0x08])
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()
c = SerialCommunicator()
c.start()
c.send(p)
while c.is_alive():
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...
p = c.receive.get(block=True, timeout=1)
if p.type == PACKET.RADIO and p.rorg == RORG.BS4:
for k in p.parse_eep(0x02, 0x05):
print('%s: %s' % (k, p.parsed[k]))
if p.type == PACKET.RADIO and p.rorg == RORG.BS1:
for k in p.parse_eep(0x00, 0x01):
print('%s: %s' % (k, p.parsed[k]))
if p.type == PACKET.RADIO and p.rorg == RORG.RPS:
for k in p.parse_eep(0x02, 0x04):
print('%s: %s' % (k, p.parsed[k]))
packet = communicator.receive.get(block=True, timeout=1)
if packet.packet_type == PACKET.RADIO 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 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 and packet.rorg == RORG.RPS:
for k in packet.parse_eep(0x02, 0x02):
print('%s: %s' % (k, packet.parsed[k]))
except queue.Empty:

@@ -43,3 +63,3 @@ continue

if c.is_alive():
c.stop()
if communicator.is_alive():
communicator.stop()
Metadata-Version: 1.0
Name: enocean
Version: 0.31
Version: 0.40
Summary: EnOcean serial protocol implementation

@@ -5,0 +5,0 @@ Home-page: https://github.com/kipe/enocean

[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

@@ -9,3 +9,3 @@ #!/usr/bin/env python

name='enocean',
version='0.31',
version='0.40',
description='EnOcean serial protocol implementation',

@@ -12,0 +12,0 @@ author='Kimmo Huoman',