class PowerCommunicator(object): """ Uses a serial port to communicate with the power modules. """ @Inject def __init__(self, power_serial=INJECTED, power_store=INJECTED, pubsub=INJECTED, time_keeper_period=60, address_mode_timeout=300): # type: (RS485, PowerStore, PubSub, int, int) -> None """ Default constructor. :param power_serial: Serial port to communicate with :type power_serial: Instance of :class`RS485` :param verbose: Print all serial communication to stdout. """ self.__verbose = logger.level >= logging.DEBUG self.__serial = power_serial self.__serial_lock = RLock() self.__cid = 1 self.__address_mode = False self.__address_mode_stop = False self.__address_thread = None # type: Optional[Thread] self.__address_mode_timeout = address_mode_timeout self.__power_store = power_store self.__pubsub = pubsub self.__last_success = 0 # type: float if time_keeper_period != 0: self.__time_keeper = TimeKeeper( self, power_store, time_keeper_period) # type: Optional[TimeKeeper] else: self.__time_keeper = None self.__communication_stats_calls = { 'calls_succeeded': [], 'calls_timedout': [] } # type: Dict[str, List] self.__communication_stats_bytes = { 'bytes_written': 0, 'bytes_read': 0 } # type: Dict[str, int] self.__debug_buffer = { 'read': {}, 'write': {} } # type: Dict[str,Dict[float,str]] self.__debug_buffer_duration = 300 def start(self): # type: () -> None """ Start the power communicator. """ if self.__time_keeper is not None: self.__time_keeper.start() def stop(self): # type: () -> None if self.__time_keeper is not None: self.__time_keeper.stop() def get_communication_statistics(self): # type: () -> Dict[str, Any] ret = {} # type: Dict[str, Any] ret.update(self.__communication_stats_calls) ret.update(self.__communication_stats_bytes) return ret def reset_communication_statistics(self): # type: () -> None self.__communication_stats_calls = { 'calls_succeeded': [], 'calls_timedout': [] } self.__communication_stats_bytes = { 'bytes_written': 0, 'bytes_read': 0 } def get_communicator_health(self): # type: () -> HEALTH stats = self.get_communication_statistics() calls_timedout = [call for call in stats['calls_timedout']] calls_succeeded = [call for call in stats['calls_succeeded']] if len(calls_timedout) == 0: # If there are no timeouts at all return CommunicationStatus.SUCCESS all_calls = sorted(calls_timedout + calls_succeeded) if len(all_calls) <= 10: # Not enough calls made to have a decent view on what's going on logger.warning( 'Observed energy communication failures, but not enough calls') return CommunicationStatus.UNSTABLE if not any(t in calls_timedout for t in all_calls[-10:]): logger.warning( 'Observed energy communication failures, but recent calls recovered' ) # The last X calls are successfull return CommunicationStatus.UNSTABLE calls_last_x_minutes = [t for t in all_calls if t > time.time() - 180] if len(calls_last_x_minutes) <= 5: logger.warning( 'Observed energy communication failures, but not recent enough' ) # Not enough recent calls return CommunicationStatus.UNSTABLE ratio = len([t for t in calls_last_x_minutes if t in calls_timedout ]) / float(len(calls_last_x_minutes)) if ratio < 0.25: # Less than 25% of the calls fail, let's assume everything is just "fine" logger.warning( 'Observed energy communication failures, but there\'s only a failure ratio of {:.2f}%' .format(ratio * 100)) return CommunicationStatus.UNSTABLE else: return CommunicationStatus.FAILURE def get_debug_buffer(self): # type: () -> Dict[str, Dict[Any, Any]] return self.__debug_buffer def get_seconds_since_last_success(self): # type: () -> float """ Get the number of seconds since the last successful communication. """ if self.__last_success == 0: return 0 # No communication - return 0 sec since last success else: return time.time() - self.__last_success def __get_cid(self): # type: () -> int """ Get a communication id """ (ret, self.__cid) = (self.__cid, (self.__cid % 255) + 1) return ret def __debug(self, action, data): # type: (str, Optional[bytearray]) -> None if self.__verbose and data is not None: logger.debug("%.3f %s power: %s" % (time.time(), action, printable(data))) def __write_to_serial(self, data): # type: (bytearray) -> None """ Write data to the serial port. :param data: the data to write """ self.__debug('writing to', data) self.__serial.write(data) self.__communication_stats_bytes['bytes_written'] += len(data) threshold = time.time() - self.__debug_buffer_duration self.__debug_buffer['write'][time.time()] = printable(data) for t in self.__debug_buffer['write'].keys(): if t < threshold: del self.__debug_buffer['write'][t] def do_command(self, address, cmd, *data): # type: (int, PowerCommand, DataType) -> Tuple[Any, ...] """ Send a command over the serial port and block until an answer is received. If the power module does not respond within the timeout period, a CommunicationTimedOutException is raised. :param address: Address of the power module :type address: 2 bytes string :param cmd: the command to execute :param data: data for the command :raises: :class`CommunicationTimedOutException` if power module did not respond in time :raises: :class`InAddressModeException` if communicator is in address mode :returns: dict containing the output fields of the command """ if self.__address_mode: raise InAddressModeException() def do_once(_address, _cmd, *_data): # type: (int, PowerCommand, DataType) -> Tuple[Any, ...] """ Send the command once. """ try: cid = self.__get_cid() send_data = _cmd.create_input(_address, cid, *_data) self.__write_to_serial(send_data) if _address == power_api.BROADCAST_ADDRESS: self.__communication_stats_calls['calls_succeeded'].append( time.time()) self.__communication_stats_calls[ 'calls_succeeded'] = self.__communication_stats_calls[ 'calls_succeeded'][-50:] return () # No reply on broadcast messages ! else: tries = 0 while True: # In this loop we might receive data that didn't match the expected header. This might happen # if we for some reason had a timeout on the previous call, and we now read the response # to that call. In this case, we just re-try (up to 3 times), as the correct data might be # next in line. header, response_data = self.__read_from_serial() if not _cmd.check_header(header, _address, cid): if _cmd.is_nack( header, _address, cid) and response_data == bytearray([2]): raise UnkownCommandException('Unknown command') tries += 1 logger.warning( "Header did not match command ({0})".format( tries)) if tries == 3: raise Exception( "Header did not match command ({0})". format(tries)) else: break self.__last_success = time.time() return_data = _cmd.read_output(response_data) self.__communication_stats_calls['calls_succeeded'].append( time.time()) self.__communication_stats_calls[ 'calls_succeeded'] = self.__communication_stats_calls[ 'calls_succeeded'][-50:] return return_data except CommunicationTimedOutException: self.__communication_stats_calls['calls_timedout'].append( time.time()) self.__communication_stats_calls[ 'calls_timedout'] = self.__communication_stats_calls[ 'calls_timedout'][-50:] raise with self.__serial_lock: try: return do_once(address, cmd, *data) except UnkownCommandException: # This happens when the module is stuck in the bootloader. logger.error("Got UnkownCommandException") do_once(address, power_api.bootloader_jump_application()) time.sleep(1) return self.do_command(address, cmd, *data) except CommunicationTimedOutException: # Communication timed out, try again. return do_once(address, cmd, *data) except Exception as ex: logger.exception("Unexpected error: {0}".format(ex)) time.sleep(0.25) return do_once(address, cmd, *data) def start_address_mode(self): # type: () -> None """ Start address mode. :raises: :class`InAddressModeException` if communicator is in maintenance mode. """ if self.__address_mode: raise InAddressModeException() self.__address_mode = True self.__address_mode_stop = False with self.__serial_lock: self.__address_thread = BaseThread(name='poweraddressmode', target=self.__do_address_mode) self.__address_thread.daemon = True self.__address_thread.start() def __do_address_mode(self): # type: () -> None """ This code is running in a thread when in address mode. """ if self.__power_store is None: self.__address_mode = False self.__address_thread = None return expire = time.time() + self.__address_mode_timeout address_mode = power_api.set_addressmode(power_api.ENERGY_MODULE) address_mode_p1c = power_api.set_addressmode(power_api.P1_CONCENTRATOR) want_an_address_8 = power_api.want_an_address(power_api.POWER_MODULE) want_an_address_12 = power_api.want_an_address(power_api.ENERGY_MODULE) want_an_address_p1c = power_api.want_an_address( power_api.P1_CONCENTRATOR) set_address = power_api.set_address(power_api.ENERGY_MODULE) set_address_p1c = power_api.set_address(power_api.P1_CONCENTRATOR) # AGT start data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) data = address_mode_p1c.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) # Wait for WAA and answer. while not self.__address_mode_stop and time.time() < expire: try: header, data = self.__read_from_serial() if set_address.check_header_partial( header) or set_address_p1c.check_header_partial( header): continue version = None if want_an_address_8.check_header_partial(header): version = power_api.POWER_MODULE elif want_an_address_12.check_header_partial(header): version = power_api.ENERGY_MODULE elif want_an_address_p1c.check_header_partial(header): version = power_api.P1_CONCENTRATOR if version is None: logger.warning( "Received unexpected message in address mode") else: (old_address, cid) = (header[:2][1], header[2:3]) # Ask power_controller for new address, and register it. new_address = self.__power_store.get_free_address() if self.__power_store.module_exists(old_address): self.__power_store.readdress_power_module( old_address, new_address) else: self.__power_store.register_power_module( new_address, version) # Send new address to module if version == power_api.P1_CONCENTRATOR: address_data = set_address_p1c.create_input( old_address, ord(cid), new_address) else: # Both power- and energy module share the same API address_data = set_address.create_input( old_address, ord(cid), new_address) self.__write_to_serial(address_data) except CommunicationTimedOutException: pass # Didn't receive a command, no problem. except Exception as exception: logger.exception("Got exception in address mode: %s", exception) # AGT stop data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) data = address_mode_p1c.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) self.__address_mode = False def stop_address_mode(self): # type: () -> None """ Stop address mode. """ if not self.__address_mode: raise Exception("Not in address mode !") self.__address_mode_stop = True if self.__address_thread: self.__address_thread.join() self.__address_thread = None master_event = MasterEvent(MasterEvent.Types.POWER_ADDRESS_EXIT, {}) self.__pubsub.publish_master_event(PubSub.MasterTopics.POWER, master_event) def in_address_mode(self): # type: () -> bool """ Returns whether the PowerCommunicator is in address mode. """ return self.__address_mode def __read_from_serial(self): # type: () -> Tuple[bytearray, bytearray] """ Read a PowerCommand from the serial port. """ phase = 0 index = 0 header = bytearray() length = 0 data = bytearray() crc = 0 command = bytearray() try: while phase < 8: byte = self.__serial.read_queue.get(True, 0.25) command += byte self.__communication_stats_bytes['bytes_read'] += 1 if phase == 0: # Skip non 'R' bytes if byte == bytearray(b'R'): phase = 1 else: phase = 0 elif phase == 1: # Expect 'T' if byte == bytearray(b'T'): phase = 2 else: raise Exception("Unexpected character") elif phase == 2: # Expect 'R' if byte == bytearray(b'R'): phase = 3 index = 0 else: raise Exception("Unexpected character") elif phase == 3: # Read the header fields header += byte index += 1 if index == 8: length = ord(byte) if length > 0: phase = 4 index = 0 else: phase = 5 elif phase == 4: # Read the data data += byte index += 1 if index == length: phase = 5 elif phase == 5: # Read the CRC code crc = ord(byte) phase = 6 elif phase == 6: # Expect '\r' if byte == bytearray(b'\r'): phase = 7 else: raise Exception("Unexpected character") elif phase == 7: # Expect '\n' if byte == bytearray(b'\n'): phase = 8 else: raise Exception("Unexpected character") if PowerCommand.get_crc(header, data) != crc: raise Exception('CRC doesn\'t match') except Empty: raise CommunicationTimedOutException('Communication timed out') except Exception: self.__debug('reading from', command) raise finally: self.__debug('reading from', command) threshold = time.time() - self.__debug_buffer_duration self.__debug_buffer['read'][time.time()] = printable(command) for t in self.__debug_buffer['read'].keys(): if t < threshold: del self.__debug_buffer['read'][t] return header, data
class PowerCommunicator(object): """ Uses a serial port to communicate with the power modules. """ def __init__(self, serial, power_controller, verbose=False, time_keeper_period=60, address_mode_timeout=300): """ Default constructor. :param serial: Serial port to communicate with :type serial: Instance of :class`RS485` :param verbose: Print all serial communication to stdout. :type verbose: boolean. """ self.__serial = serial self.__serial_lock = RLock() self.__serial_bytes_written = 0 self.__serial_bytes_read = 0 self.__cid = 1 self.__address_mode = False self.__address_mode_stop = False self.__address_thread = None self.__address_mode_timeout = address_mode_timeout self.__power_controller = power_controller self.__last_success = 0 if time_keeper_period != 0: self.__time_keeper = TimeKeeper(self, power_controller, time_keeper_period) else: self.__time_keeper = None self.__verbose = verbose def start(self): """ Start the power communicator. """ if self.__time_keeper is not None: self.__time_keeper.start() def get_bytes_written(self): """ Get the number of bytes written to the power modules. """ return self.__serial_bytes_written def get_bytes_read(self): """ Get the number of bytes read from the power modules. """ return self.__serial_bytes_read def get_seconds_since_last_success(self): """ Get the number of seconds since the last successful communication. """ if self.__last_success == 0: return 0 # No communication - return 0 sec since last success else: return time.time() - self.__last_success def __get_cid(self): """ Get a communication id """ (ret, self.__cid) = (self.__cid, (self.__cid % 255) + 1) return ret @staticmethod def __log(action, data): if data is not None: LOGGER.info("%.3f %s power: %s" % (time.time(), action, printable(data))) def __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ if self.__verbose: PowerCommunicator.__log('writing to', data) self.__serial.write(data) self.__serial_bytes_written += len(data) def do_command(self, address, cmd, *data): """ Send a command over the serial port and block until an answer is received. If the power module does not respond within the timeout period, a CommunicationTimedOutException is raised. :param address: Address of the power module :type address: 2 bytes string :param cmd: the command to execute :type cmd: :class`PowerCommand` :param data: data for the command :raises: :class`CommunicationTimedOutException` if power module did not respond in time :raises: :class`InAddressModeException` if communicator is in address mode :returns: dict containing the output fields of the command """ if self.__address_mode: raise InAddressModeException() def do_once(_address, _cmd, *_data): """ Send the command once. """ cid = self.__get_cid() send_data = _cmd.create_input(_address, cid, *_data) self.__write_to_serial(send_data) if _address == power_api.BROADCAST_ADDRESS: return None # No reply on broadcast messages ! else: tries = 0 while True: # In this loop we might receive data that didn't match the expected header. This might happen # if we for some reason had a timeout on the previous call, and we now read the response # to that call. In this case, we just re-try (up to 3 times), as the correct data might be # next in line. header, response_data = self.__read_from_serial() if not _cmd.check_header(header, _address, cid): if _cmd.is_nack(header, _address, cid) and response_data == "\x02": raise UnkownCommandException() tries += 1 LOGGER.warning("Header did not match command ({0})".format(tries)) if tries == 3: raise Exception("Header did not match command ({0})".format(tries)) else: break self.__last_success = time.time() return _cmd.read_output(response_data) with self.__serial_lock: try: return do_once(address, cmd, *data) except UnkownCommandException: # This happens when the module is stuck in the bootloader. LOGGER.error("Got UnkownCommandException") do_once(address, power_api.bootloader_jump_application()) time.sleep(1) return self.do_command(address, cmd, *data) except CommunicationTimedOutException: # Communication timed out, try again. return do_once(address, cmd, *data) except Exception as ex: LOGGER.exception("Unexpected error: {0}".format(ex)) time.sleep(0.25) return do_once(address, cmd, *data) def start_address_mode(self): """ Start address mode. :raises: :class`InAddressModeException` if communicator is in maintenance mode. """ if self.__address_mode: raise InAddressModeException() self.__address_mode = True self.__address_mode_stop = False with self.__serial_lock: self.__address_thread = Thread(target=self.__do_address_mode, name="PowerCommunicator address mode thread") self.__address_thread.daemon = True self.__address_thread.start() def __do_address_mode(self): """ This code is running in a thread when in address mode. """ expire = time.time() + self.__address_mode_timeout address_mode = power_api.set_addressmode() want_an_address_8 = power_api.want_an_address(power_api.POWER_API_8_PORTS) want_an_address_12 = power_api.want_an_address(power_api.POWER_API_12_PORTS) set_address = power_api.set_address() # AGT start data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) # Wait for WAA and answer. while not self.__address_mode_stop and time.time() < expire: try: header, data = self.__read_from_serial() waa_8 = want_an_address_8.check_header_partial(header) waa_12 = want_an_address_12.check_header_partial(header) if not waa_8 and not waa_12: LOGGER.warning("Received non WAA/WAD message in address mode") else: (old_address, cid) = (ord(header[:2][1]), header[2:3]) # Ask power_controller for new address, and register it. new_address = self.__power_controller.get_free_address() if self.__power_controller.module_exists(old_address): self.__power_controller.readdress_power_module(old_address, new_address) else: version = power_api.POWER_API_8_PORTS if waa_8 else power_api.POWER_API_12_PORTS self.__power_controller.register_power_module(new_address, version) # Send new address to power module address_data = set_address.create_input(old_address, ord(cid), new_address) self.__write_to_serial(address_data) except CommunicationTimedOutException: pass # Didn't receive a command, no problem. except Exception as exception: traceback.print_exc() LOGGER.warning("Got exception in address mode: %s", exception) # AGT stop data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) self.__address_mode = False self.__address_thread = None def stop_address_mode(self): """ Stop address mode. """ if not self.__address_mode: raise Exception("Not in address mode !") self.__address_mode_stop = True self.__address_thread.join() def in_address_mode(self): """ Returns whether the PowerCommunicator is in address mode. """ return self.__address_mode def __read_from_serial(self): """ Read a PowerCommand from the serial port. """ phase = 0 index = 0 header = "" length = 0 data = "" crc = 0 command = "" try: while phase < 8: byte = self.__serial.read_queue.get(True, 0.25) command += byte self.__serial_bytes_read += 1 if phase == 0: # Skip non 'R' bytes if byte == 'R': phase = 1 else: phase = 0 elif phase == 1: # Expect 'T' if byte == 'T': phase = 2 else: raise Exception("Unexpected character") elif phase == 2: # Expect 'R' if byte == 'R': phase = 3 index = 0 else: raise Exception("Unexpected character") elif phase == 3: # Read the header fields header += byte index += 1 if index == 8: length = ord(byte) if length > 0: phase = 4 index = 0 else: phase = 5 elif phase == 4: # Read the data data += byte index += 1 if index == length: phase = 5 elif phase == 5: # Read the CRC code crc = ord(byte) phase = 6 elif phase == 6: # Expect '\r' if byte == '\r': phase = 7 else: raise Exception("Unexpected character") elif phase == 7: # Expect '\n' if byte == '\n': phase = 8 else: raise Exception("Unexpected character") if crc7(header + data) != crc: raise Exception("CRC doesn't match") except Queue.Empty: raise CommunicationTimedOutException() finally: if self.__verbose: PowerCommunicator.__log('reading from', command) return header, data
class PowerCommunicator(object): """ Uses a serial port to communicate with the power modules. """ @Inject def __init__(self, power_serial=INJECTED, power_controller=INJECTED, verbose=False, time_keeper_period=60, address_mode_timeout=300): """ Default constructor. :param power_serial: Serial port to communicate with :type power_serial: Instance of :class`RS485` :param verbose: Print all serial communication to stdout. :type verbose: boolean. """ self.__serial = power_serial self.__serial_lock = RLock() self.__serial_bytes_written = 0 self.__serial_bytes_read = 0 self.__cid = 1 self.__address_mode = False self.__address_mode_stop = False self.__address_thread = None self.__address_mode_timeout = address_mode_timeout self.__power_controller = power_controller self.__last_success = 0 if time_keeper_period != 0: self.__time_keeper = TimeKeeper(self, power_controller, time_keeper_period) else: self.__time_keeper = None self.__verbose = verbose def start(self): """ Start the power communicator. """ if self.__time_keeper is not None: self.__time_keeper.start() def get_bytes_written(self): """ Get the number of bytes written to the power modules. """ return self.__serial_bytes_written def get_bytes_read(self): """ Get the number of bytes read from the power modules. """ return self.__serial_bytes_read def get_seconds_since_last_success(self): """ Get the number of seconds since the last successful communication. """ if self.__last_success == 0: return 0 # No communication - return 0 sec since last success else: return time.time() - self.__last_success def __get_cid(self): """ Get a communication id """ (ret, self.__cid) = (self.__cid, (self.__cid % 255) + 1) return ret @staticmethod def __log(action, data): if data is not None: logger.info("%.3f %s power: %s" % (time.time(), action, printable(data))) def __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ if self.__verbose: PowerCommunicator.__log('writing to', data) self.__serial.write(data) self.__serial_bytes_written += len(data) def do_command(self, address, cmd, *data): """ Send a command over the serial port and block until an answer is received. If the power module does not respond within the timeout period, a CommunicationTimedOutException is raised. :param address: Address of the power module :type address: 2 bytes string :param cmd: the command to execute :type cmd: :class`PowerCommand` :param data: data for the command :raises: :class`CommunicationTimedOutException` if power module did not respond in time :raises: :class`InAddressModeException` if communicator is in address mode :returns: dict containing the output fields of the command """ if self.__address_mode: raise InAddressModeException() def do_once(_address, _cmd, *_data): """ Send the command once. """ cid = self.__get_cid() send_data = _cmd.create_input(_address, cid, *_data) self.__write_to_serial(send_data) if _address == power_api.BROADCAST_ADDRESS: return None # No reply on broadcast messages ! else: tries = 0 while True: # In this loop we might receive data that didn't match the expected header. This might happen # if we for some reason had a timeout on the previous call, and we now read the response # to that call. In this case, we just re-try (up to 3 times), as the correct data might be # next in line. header, response_data = self.__read_from_serial() if not _cmd.check_header(header, _address, cid): if _cmd.is_nack(header, _address, cid) and response_data == "\x02": raise UnkownCommandException('Unknown command') tries += 1 logger.warning( "Header did not match command ({0})".format(tries)) if tries == 3: raise Exception( "Header did not match command ({0})".format( tries)) else: break self.__last_success = time.time() return _cmd.read_output(response_data) with self.__serial_lock: try: return do_once(address, cmd, *data) except UnkownCommandException: # This happens when the module is stuck in the bootloader. logger.error("Got UnkownCommandException") do_once(address, power_api.bootloader_jump_application()) time.sleep(1) return self.do_command(address, cmd, *data) except CommunicationTimedOutException: # Communication timed out, try again. return do_once(address, cmd, *data) except Exception as ex: logger.exception("Unexpected error: {0}".format(ex)) time.sleep(0.25) return do_once(address, cmd, *data) def start_address_mode(self): """ Start address mode. :raises: :class`InAddressModeException` if communicator is in maintenance mode. """ if self.__address_mode: raise InAddressModeException() self.__address_mode = True self.__address_mode_stop = False with self.__serial_lock: self.__address_thread = Thread( target=self.__do_address_mode, name="PowerCommunicator address mode thread") self.__address_thread.daemon = True self.__address_thread.start() def __do_address_mode(self): """ This code is running in a thread when in address mode. """ if self.__power_controller is None: self.__address_mode = False self.__address_thread = None expire = time.time() + self.__address_mode_timeout address_mode = power_api.set_addressmode() want_an_address_8 = power_api.want_an_address( power_api.POWER_API_8_PORTS) want_an_address_12 = power_api.want_an_address( power_api.POWER_API_12_PORTS) set_address = power_api.set_address() # AGT start data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) # Wait for WAA and answer. while not self.__address_mode_stop and time.time() < expire: try: header, data = self.__read_from_serial() waa_8 = want_an_address_8.check_header_partial(header) waa_12 = want_an_address_12.check_header_partial(header) if not waa_8 and not waa_12: logger.warning( "Received non WAA/WAD message in address mode") else: (old_address, cid) = (ord(header[:2][1]), header[2:3]) # Ask power_controller for new address, and register it. new_address = self.__power_controller.get_free_address() if self.__power_controller.module_exists(old_address): self.__power_controller.readdress_power_module( old_address, new_address) else: version = power_api.POWER_API_8_PORTS if waa_8 else power_api.POWER_API_12_PORTS self.__power_controller.register_power_module( new_address, version) # Send new address to power module address_data = set_address.create_input( old_address, ord(cid), new_address) self.__write_to_serial(address_data) except CommunicationTimedOutException: pass # Didn't receive a command, no problem. except Exception as exception: traceback.print_exc() logger.warning("Got exception in address mode: %s", exception) # AGT stop data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) self.__address_mode = False self.__address_thread = None def stop_address_mode(self): """ Stop address mode. """ if not self.__address_mode: raise Exception("Not in address mode !") self.__address_mode_stop = True self.__address_thread.join() def in_address_mode(self): """ Returns whether the PowerCommunicator is in address mode. """ return self.__address_mode def __read_from_serial(self): """ Read a PowerCommand from the serial port. """ phase = 0 index = 0 header = "" length = 0 data = "" crc = 0 command = "" try: while phase < 8: byte = self.__serial.read_queue.get(True, 0.25) command += byte self.__serial_bytes_read += 1 if phase == 0: # Skip non 'R' bytes if byte == 'R': phase = 1 else: phase = 0 elif phase == 1: # Expect 'T' if byte == 'T': phase = 2 else: raise Exception("Unexpected character") elif phase == 2: # Expect 'R' if byte == 'R': phase = 3 index = 0 else: raise Exception("Unexpected character") elif phase == 3: # Read the header fields header += byte index += 1 if index == 8: length = ord(byte) if length > 0: phase = 4 index = 0 else: phase = 5 elif phase == 4: # Read the data data += byte index += 1 if index == length: phase = 5 elif phase == 5: # Read the CRC code crc = ord(byte) phase = 6 elif phase == 6: # Expect '\r' if byte == '\r': phase = 7 else: raise Exception("Unexpected character") elif phase == 7: # Expect '\n' if byte == '\n': phase = 8 else: raise Exception("Unexpected character") if crc7(header + data) != crc: raise Exception("CRC doesn't match") except Empty: raise CommunicationTimedOutException('Communication timed out') finally: if self.__verbose: PowerCommunicator.__log('reading from', command) return header, data