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
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
# -*- 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)
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() def __init__(self, type, data=[], optional=[]): self.type = type self.rorg = RORG.UNDEFINED self.rorg_func = None self.rorg_type = None self.rorg_manufacturer = None self.data = data self.optional = optional self.status = 0 self.parsed = {} 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) def __unicode__(self): return self.__str__() def __eq__(self, other): return self.type == other.type and self.rorg == other.rorg and self.data == other.data and self.optional == other.optional @property def _bit_data(self): if self.rorg == RORG.RPS or self.rorg == RORG.BS1: return enocean.utils.to_bitarray(self.data[1], 8) if self.rorg == RORG.BS4: return enocean.utils.to_bitarray(self.data[1:5], 32) @_bit_data.setter def _bit_data(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]) # # 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 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 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: p = RadioPacket(packet_type, data, opt_data) elif packet_type == PACKET.RESPONSE: p = ResponsePacket(packet_type, data, opt_data) else: p = Packet(packet_type, data, opt_data) return PARSE_RESULT.OK, buf, p @staticmethod def create(packet_type, rorg, func, type, direction=None, destination=[0xFF, 0xFF, 0xFF, 0xFF], sender=[0xDE, 0xAD, 0xBE, 0xEF], learn=False, **kwargs): ''' Creates an packet ready for sending. Uses rorg, func and 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, and BS4. 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]: # At least for now, only support these RORGS. raise ValueError('RORG not supported by this function.') 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.') p = Packet(packet_type) p.rorg = rorg p.data = [p.rorg] # Initialize data depending on the profile. p.data.extend([0] * 4 if rorg == RORG.BS4 else [0]) p.data.extend(sender) # Always use sub-telegram 3, maximum dbm (as per spec, when sending), # and no security (security not supported as per EnOcean Serial Protocol). p.optional = [3] + destination + [0xFF] + [0] p.select_eep(func, type, direction) p.set_eep(kwargs) if rorg in [RORG.BS1, RORG.BS4] and not learn: if rorg == RORG.BS1: p.data[1] |= (1 << 3) if rorg == RORG.BS4: p.data[4] |= (1 << 3) p.data.append(p.status) # Parse the built package, so it corresponds to the received packages # For example, stuff like checking RadioPacket.learn should be set. p = Packet.parse_msg(p.build())[2] p.rorg = rorg p.parse_eep(func, type, direction) return p 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, func, type, direction=None): ''' Set EEP based on FUNC and TYPE ''' # set EEP profile self.rorg_func = func self.rorg_type = type self._profile = self.eep.find_profile(self.rorg, func, type, direction) return self._profile is not None def parse_eep(self, func=None, type=None, direction=None): ''' Parse EEP based on FUNC and TYPE ''' # set EEP profile, if demanded if func is not None and type is not None: self.select_eep(func, type, direction) # 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.append(crc8.calc(ords[1:5])) ords.extend(self.data) ords.extend(self.optional) ords.append(crc8.calc(ords[6:])) return ords