def S4_2_9_2_Test(): "Serial interface guide s4.2.9.2 (page 23) test" # first test p, m = decode_packet('\\0538000108BAg', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert p.status_request == False assert p.application == 0x38 assert len(p.sal) == 1 assert isinstance(p.sal[0], LightingOffSAL) assert p.sal[0].group_address == 8 assert p.confirmation == 'g' assert m == None # second test p, m = decode_packet('\\05FF007A38004Ah', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert p.status_request == True assert p.application == 0x38 assert p.group_address == 0 assert p.confirmation == 'h' # no remainder assert m == None
def pretty_packet(packet: bytes, checksum: bool = True, strict: bool = True, server_packet: bool = True): packet, remainder = decode_packet(packet, checksum, strict, server_packet) print(packet)
def handle_data(self, buf: bytes) -> int: """ Decodes a single CBus event and calls an event handler appropriate to the event. Do not override this. :param buf: CBus event data :type buf: bytes :returns: Number of bytes consumed from the buffer """ logger.debug("Incoming data: %r", buf) p, remainder = decode_packet(buf, checksum=self.checksum, from_pci=not self.emulate_pci) if self.emulate_pci and remainder > 0: # Local echo self.echo(buf[:remainder]) if p is not None: logger.debug("Got packet: %s", p) self.handle_cbus_packet(p) return remainder
def decode_packet( self, data: bytes, checksum: bool = True, strict: bool = True, from_pci: bool = True, expected_position: Optional[int] = None) -> Optional[BasePacket]: """ Decodes a packet, and validates that the buffer position has consumed the packet. See ``cbus.packet.decode_packet`` for details. :param expected_position: If None (default), expect to consume the entire input data. Otherwise, an integer number of bytes that were expected to be consumed. :return: The parsed packet, or None if no packet was parsed. """ if expected_position is None: expected_position = len(data) packet, position = decode_packet(data, checksum, strict, from_pci) self.assertEqual( expected_position, position, f'Expected to parse {expected_position} bytes of input data: ' f'{data!r}') return packet
def S9_1_Test(): "Examples in quick start guide, s9.1" # turn on light 0x21 p, r = decode_packet('\\053800792129i', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal), 1 assert isinstance(p.sal[0], LightingOnSAL) assert p.sal[0].group_address, 0x21 # check that it encodes properly again assert p.encode(), '053800792129' assert p.confirmation, 'i' # turn off light 0x21 p, r = decode_packet('\\0538000121A1k', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], LightingOffSAL) assert p.sal[0].group_address == 0x21 # check that it encodes properly again assert p.encode() == '0538000121A1' assert p.confirmation == 'k' # ramp light 0x21 to 50% over 4 seconds p, r = decode_packet('\\0538000A217F19l', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], LightingRampSAL) assert p.sal[0].group_address == 0x21 assert p.sal[0].duration == 4 # rounding must be done to 2 decimal places, as the value isn't actually # 50%, but 49.8039%. next value is 50.1%. assert round(p.sal[0].level, 2) == 0.5 # check that it encodes properly again assert p.encode() == '0538000A217F19' assert p.confirmation == 'l'
def issue2_test(): "Handle the null lighting packet described in Issue #2." # lighting packet from the server, lighting application, source address 0x06 # sometimes cbus units emit these null lighting commands because of an off-by-one issue? p, r = decode_packet('05063800BD') assert isinstance(p, PointToMultipointPacket) assert p.application == 0x38 assert p.source_address == 0x06 assert len(p.sal) == 0 assert r == None
def Bennett_get_unit_type_test(): "Example of 'get unit type' (identify type) from Geoffry Bennett's protocol reverse engineering docs" p, r = decode_packet('\\0699002101', server_packet=False, checksum=False) assert isinstance(p, PointToPointPacket), 'Packet is not PointToPointPacket' assert p.unit_address == 0x99 assert len(p.cal) == 1 assert isinstance(p.cal[0], IdentifyCAL) assert p.cal[0].attribute == 1 assert p.encode() == '0699002101'
def S9_2_Test(): "Example in s9.2 (Serial Interface Guide) of decoding a reply CAL" p, r = decode_packet("8604990082300328", server_packet=True) assert isinstance(p, PointToPointPacket), "Packet is not PointToPointPacket" assert p.source_address == 4 assert p.unit_address == 0x99 assert len(p.cal) == 1 assert isinstance(p.cal[0], ReplyCAL) assert p.cal[0].parameter == 0x30 assert p.cal[0].data == "\x03" assert p.encode() == "8604990082300328"
def S9_2_Test(): "Example in s9.2 (Serial Interface Guide) of decoding a reply CAL" p, r = decode_packet('8604990082300328', server_packet=True) assert isinstance(p, PointToPointPacket), 'Packet is not PointToPointPacket' assert p.source_address == 4 assert p.unit_address == 0x99 assert len(p.cal) == 1 assert isinstance(p.cal[0], ReplyCAL) assert p.cal[0].parameter == 0x30 assert p.cal[0].data == '\x03' assert p.encode() == '8604990082300328'
def S2_11_Test(): "Examples in Lighting Application s2.11" # switch on light at GA 0x93 p, r = decode_packet('\\0538007993B7j', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], LightingOnSAL) assert p.sal[0].group_address == 0x93 # check that it encodes properly again assert p.encode() == '0538007993B7' assert p.confirmation == 'j'
def S6_4_Test(): "Examples in serial interface guide, s6.4" # Switch on light at GA 8 p, r = decode_packet('\\0538000108BAg', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], LightingOffSAL) assert p.sal[0].group_address == 8 # check that it encodes properly again assert p.encode() == '0538000108BA' assert p.confirmation == 'g' # concatenated packet p, r = decode_packet('\\05380001087909090A25h', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 3 # turn off light 8 assert isinstance(p.sal[0], LightingOffSAL) assert p.sal[0].group_address == 8 # turn on light 9 assert isinstance(p.sal[1], LightingOnSAL) assert p.sal[1].group_address == 9 # terminate ramp on light 10) assert isinstance(p.sal[2], LightingTerminateRampSAL) assert p.sal[2].group_address == 10 # check that it encodes properly again assert p.encode() == '05380001087909090A25' assert p.confirmation == 'h'
def S8_11_Test(): "Example in enable control application guide, s8.11 (page 7)" # Set the network variable 0x37 to 0x82 p, r = decode_packet('\\05CB0002378275g', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], EnableSetNetworkVariableSAL) assert p.sal[0].variable == 0x37 assert p.sal[0].value == 0x82 ## check that it encodes properly again assert p.encode() == '05CB0002378275' assert p.confirmation == 'g'
def S23_13_3_Test(): "Example in s23.13.3 of decoding request refresh" # Request refreeh command # documentation is wrong here: # - says 05DF00100C # - should be 05DF00110308 p, r = decode_packet('\\05DF00110308g', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], ClockRequestSAL) ## check that it encodes properly again assert p.encode() == '05DF00110308' assert p.confirmation == 'g'
def lighting_encode_decode_client_test(): "self-made tests of encode then decode, with packets from a client." orig = PointToMultipointPacket(application=APP_LIGHTING) orig.sal.append(LightingOnSAL(orig, 27)) data = orig.encode() d, r = decode_packet(data, server_packet=False) assert isinstance(orig, PointToMultipointPacket) assert len(orig.sal) == len(d.sal) assert isinstance(d.sal[0], LightingOnSAL) assert orig.sal[0].group_address == d.sal[0].group_address # ensure there is no remaining data to be parsed assert r == None
def S9_11_Test(): "Example in temperature broadcast application guide, s9.11" # Temperature of 25 degrees at group 5 # note, the guide actually states that there is a checksum, but no # checksum is actually on the packet! p, r = decode_packet('\\05190002056477g', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], TemperatureBroadcastSAL) assert p.sal[0].group_address == 5 assert p.sal[0].temperature == 25 ## check that it encodes properly again assert p.encode() == '05190002056477' assert p.confirmation == 'g'
def S23_13_2_Test(): "Example in s23.13.2 of decoding a date" # Set network date to 2005-02-25 (Friday) p, r = decode_packet('\\05DF000E0207D502190411g', server_packet=False) assert isinstance(p, PointToMultipointPacket) assert len(p.sal) == 1 assert isinstance(p.sal[0], ClockUpdateSAL) assert p.sal[0].is_date assert not p.sal[0].is_time assert isinstance(p.sal[0].val, date) assert p.sal[0].val.year == 2005 assert p.sal[0].val.month == 2 assert p.sal[0].val.day == 25 assert p.sal[0].val.weekday() == 4 # friday ## check that it encodes properly again assert p.encode() == '05DF000E0207D502190411' assert p.confirmation == 'g'
def temperature_encode_decode_test(): "self-made tests of encode then decode" orig = PointToMultipointPacket(application=APP_TEMPERATURE) orig.source_address = 5 orig.sal.append(TemperatureBroadcastSAL(orig, 10, 0.5)) orig.sal.append(TemperatureBroadcastSAL(orig, 11, 56)) data = orig.encode() d, r = decode_packet(data) assert isinstance(orig, PointToMultipointPacket) assert orig.source_address == d.source_address assert len(orig.sal) == len(d.sal) for x in range(len(d.sal)): assert isinstance(d.sal[x], TemperatureBroadcastSAL) assert orig.sal[x].group_address == d.sal[x].group_address assert orig.sal[x].temperature == d.sal[x].temperature # ensure there is no remaining data to be parsed assert r == None
def S23_13_1_Test(): "Example in s23.13.1 of decoding a time" # Set network time to 10:43:23 with no DST offset # Slight change from guide: p, r = decode_packet('\\05DF000D010A2B1700C2g', server_packet=False) assert isinstance(p, PointToMultipointPacket), "Packet is not PointToMultipointPacket" assert len(p.sal) == 1 assert isinstance(p.sal[0], ClockUpdateSAL) assert p.sal[0].is_time assert not p.sal[0].is_date assert isinstance(p.sal[0].val, time) assert p.sal[0].val.hour == 10 assert p.sal[0].val.minute == 43 assert p.sal[0].val.second == 23 # Library doesn't handle DST offset, so this flag is dropped. ## check that it encodes properly again # fuzzy match to allow packet that has no DST information assert p.encode() in ['05DF000D010A2B1700C2', '05DF000D010A2B17FFC3'] assert p.confirmation == 'g'
def pretty_packet(packet, checksum=True, strict=True, server_packet=True): packet, remainder = decode_packet(packet, checksum, strict, server_packet) print packet
def decode_cbus_event(self, line): """ Decodes a CBus event and calls an event handler appropriate to the event. Do not override this. :param line: CBus event data :type line: str :returns: Remaining unparsed data (str) or None on error. :rtype: str or NoneType """ while line: last_line = line p, line = decode_packet(line, checksum=True, server_packet=True) if line == last_line: # infinite loop! log.msg('dce: bug: infinite loop detected on %r', line) return # decode special packets if p == None: log.msg("dce: packet == None") continue log.msg('dce: packet: %r' % p) if isinstance(p, SpecialServerPacket): if isinstance(p, PCIErrorPacket): self.on_pci_cannot_accept_data() continue elif isinstance(p, ConfirmationPacket): self.on_confirmation(p.code, p.success) continue log.msg('dce: unhandled SpecialServerPacket') elif isinstance(p, PointToMultipointPacket): for s in p.sal: if isinstance(s, LightingSAL): # lighting application if isinstance(s, LightingRampSAL): self.on_lighting_group_ramp(p.source_address, s.group_address, s.duration, s.level) elif isinstance(s, LightingOnSAL): self.on_lighting_group_on(p.source_address, s.group_address) elif isinstance(s, LightingOffSAL): self.on_lighting_group_off(p.source_address, s.group_address) elif isinstance(s, LightingTerminateRampSAL): self.on_lighting_group_terminate_ramp(p.source_address, s.group_address) else: log.msg('dce: unhandled lighting SAL type: %r', s) break elif isinstance(s, ClockSAL): if isinstance(s, ClockRequestSAL): self.on_clock_request(p.source_address) elif isinstance(s, ClockUpdateSAL): self.on_clock_update(p.source_address, s.variable, s.val) else: log.msg('dce: unhandled SAL type: %r', s) break else: log.msg('dce: unhandled other packet %r', p) continue
def decode_cbus_event(self, line): """ Decodes a CBus event and calls an event handler appropriate to the event. Do not override this. :param line: CBus event data :type line: str :returns: Remaining unparsed data (str) or None on error. :rtype: str or NoneType """ # pass the data to the protocol decoder p, remainder = decode_packet(line, checksum=self.checksum, server_packet=False) # check for special commands, and handle them. if p == None: log.msg("dce: packet == None") return remainder if isinstance(p, SpecialClientPacket): # do special things # full reset if isinstance(p, ResetPacket): self.on_reset() return remainder # smart+connect shortcut if isinstance(p, SmartConnectShortcutPacket): self.basic_mode = False self.checksum = True self.connect = True return remainder log.msg('dce: unknown SpecialClientPacket: %r', p) elif isinstance(p, PointToMultipointPacket): # is this a status inquiry if p.status_request == None: # status request # TODO log.msg('dce: unhandled status request packet') else: # application command for s in p.sal: if isinstance(s, LightingSAL): # lighting application if isinstance(s, LightingRampSAL): self.on_lighting_group_ramp(s.group_address, s.duration, s.level) elif isinstance(s, LightingOnSAL): self.on_lighting_group_on(s.group_address) elif isinstance(s, LightingOffSAL): self.on_lighting_group_off(s.group_address) elif isinstance(s, LightingTerminateRampSAL): self.on_lighting_group_terminate_ramp(s.group_address) else: log.msg('dce: unhandled lighting SAL type: %r' % s) return remainder elif isinstance(s, ClockSAL): if isinstance(s, ClockUpdateSAL): self.on_clock_update(s.variable, s.val) elif isinstance(s, ClockRequestSAL): self.on_clock_request() else: log.msg('dce: unhandled clock SAL type: %r' % s) else: log.msg('dce: unhandled SAL type: %r' % s) return remainder elif isinstance(p, DeviceManagementPacket): # TODO: send proper confirmation, from p55 of serial interface guide if p.parameter == 0x21: # application address 1 application_addr1 = p.value elif p.parameter == 0x22: # application address 2 application_addr2 = p.value elif p.parameter == 0x3E: # interface options 2 # TODO: implement pass elif p.parameter == 0x42: # interface options 3 # TODO: implement pass elif p.parameter in (0x30, 0x41): # interface options 1 / power up options 1 self.connect = self.checksum = self.monitor = self.idmon = False self.basic_mode = True if p.value & 0x01: self.connect = True if p.value & 0x02: # reserved, ignored. pass if p.value & 0x04: # TODO: xon/xoff handshaking. not supported. pass if p.value & 0x08: # srchk (checksum checking) self.checksum = True if p.value & 0x10: # smart mode self.basic_mode = False self.local_echo = False if p.value & 0x20: # monitor mode self.monitor = True if p.value & 0x40: # idmon self.idmon = True else: log.msg('dce: unhandled DeviceManagementPacket (%r = %r)' % (p.parameter, p.value)) return remainder else: log.msg('dce: unhandled packet type: %r', p) return remainder # TODO: handle parameters if p.confirmation: self.send_confirmation(p.confirmation, True) return remainder
def decode_cbus_event(self, line): """ Decodes a CBus event and calls an event handler appropriate to the event. Do not override this. :param line: CBus event data :type line: str :returns: Remaining unparsed data (str) or None on error. :rtype: str or NoneType """ while line: last_line = line p, line = decode_packet(line, checksum=True, server_packet=True) if line == last_line: # infinite loop! log.msg('dce: bug: infinite loop detected on %r', line) return # decode special packets if p == None: log.msg("dce: packet == None") continue log.msg('dce: packet: %r' % p) if isinstance(p, SpecialServerPacket): if isinstance(p, PCIErrorPacket): self.on_pci_cannot_accept_data() continue elif isinstance(p, ConfirmationPacket): self.on_confirmation(p.code, p.success) continue log.msg('dce: unhandled SpecialServerPacket') elif isinstance(p, PointToMultipointPacket): for s in p.sal: if isinstance(s, LightingSAL): # lighting application if isinstance(s, LightingRampSAL): self.on_lighting_group_ramp( p.source_address, s.group_address, s.duration, s.level) elif isinstance(s, LightingOnSAL): self.on_lighting_group_on(p.source_address, s.group_address) elif isinstance(s, LightingOffSAL): self.on_lighting_group_off(p.source_address, s.group_address) elif isinstance(s, LightingTerminateRampSAL): self.on_lighting_group_terminate_ramp( p.source_address, s.group_address) else: log.msg('dce: unhandled lighting SAL type: %r', s) break elif isinstance(s, ClockSAL): if isinstance(s, ClockRequestSAL): self.on_clock_request(p.source_address) elif isinstance(s, ClockUpdateSAL): self.on_clock_update(p.source_address, s.variable, s.val) else: log.msg('dce: unhandled SAL type: %r', s) break else: log.msg('dce: unhandled other packet %r', p) continue