def test_thread_manager_running(manager: ThreadManager): assert manager.get_thread(THREAD_ID) is None manager.add_thread(THREAD_ID, test_method) time.sleep(0.1) saved_thread = manager.get_thread(THREAD_ID) assert saved_thread.is_running() is False manager.start_threads() assert saved_thread.is_running() is True manager.__del__() assert saved_thread.is_running() is False
class NetworkServer(NetworkConnector, ABC): _validator: Validator _hostname: str _clients: [NetworkServerClient] _thread_manager: ThreadManager __client_list_lock: threading.Lock def __init__(self, hostname: str): super().__init__(hostname) self._logger.info(f"Starting {self.__class__.__name__}") self._clients = [] self.__client_list_lock = threading.Lock() self._thread_manager = ThreadManager() self._thread_manager.add_thread("thread_cleanup", self.__task_cleanup_clients) def __del__(self): self._logger.info(f"Stopping {self.__class__.__name__}") self._thread_manager.__del__() while self._clients: client = self._clients[0] self._remove_client(client.get_address()) def __task_cleanup_clients(self): for client in self._clients: if not client.is_connected(): self._logger.info(f"Lost connection to '{client.get_address()}'") self._remove_client(client.get_address()) time.sleep(1) def _add_client(self, client: NetworkServerClient): with self.__client_list_lock: self._clients.append(client) client.subscribe(self) self._logger.info(f"New connection from: {client.get_address()}") def _remove_client(self, address: str): client_index = 0 with self.__client_list_lock: for client in self._clients: if address == client.get_address(): self._logger.info(f"Removing Client '{address}'") buf_client: NetworkServerClient = self._clients.pop(client_index) buf_client.__del__() return client_index += 1 def _send_data(self, req: Request): remove_clients: [str] = [] with self.__client_list_lock: for client in self._clients: try: # TODO: Remove if possible client.send_request(req) except ClientDisconnectedError: self._logger.info(f"Connection to '{client.get_address()}' was lost") # Save clients index for removal remove_clients.append(client.get_address()) # remove 'dead' clients if remove_clients: self._logger.info(f"Removing stored data of {len(remove_clients)} clients") for client_address in remove_clients: self._remove_client(client_address) def get_client_count(self) -> int: return len(self._clients) def get_client_addresses(self) -> list[str]: addresses = [] for client in self._clients: addresses.append(client.get_address()) return addresses
class NetworkServerClient(Publisher): _host_name: str _address: str _response_method: response_callback_type _validator: Validator _logger: logging.Logger _thread_manager: ThreadManager __split_handler: SplitRequestHandler __out_queue: Queue __in_queue: Queue def __init__(self, host_name: str, address: str): super().__init__() self._logger = logging.getLogger(self.__class__.__name__) self._host_name = host_name self._address = address self._validator = Validator() self.__split_handler = SplitRequestHandler() self.__out_queue = Queue() self.__in_queue = Queue() self._thread_manager = ThreadManager() self._thread_manager.add_thread("send_thread", self.__task_send) self._thread_manager.add_thread("receive_thread", self.__task_receive) self._thread_manager.add_thread("publish_thread", self.__task_publish) def __del__(self): self._thread_manager.__del__() def __task_send(self): if not self.__out_queue.empty(): out_req = self.__out_queue.get() self._send(out_req) def __task_receive(self): in_req = self._receive() if in_req: self.__in_queue.put(in_req) def __task_publish(self): if not self.__in_queue.empty(): in_req = self.__in_queue.get() req = self.__split_handler.handle(in_req) if req: if req.get_sender() != self._host_name: req.set_callback_method(self._respond_to) self._forward_request(req) def _respond_to(self, req: Request, payload: dict, path: Optional[str] = None): if path: out_path = path else: out_path = req.get_path() receiver = req.get_sender() out_req = Request(out_path, req.get_session_id(), self._host_name, receiver, payload) self._send(out_req) def _forward_request(self, req: Request): self._logger.debug( f"Received Request by '{req.get_sender()}' at '{req.get_path()}': {req.get_payload()}" ) self._publish(req) def send_request(self, req: Request): self.__out_queue.put(req) @abstractmethod def _send(self, req: Request): pass @abstractmethod def _receive(self) -> Optional[Request]: pass @abstractmethod def is_connected(self) -> bool: pass def get_address(self) -> str: return self._address
def manager(): manager = ThreadManager() yield manager manager.__del__()
class HomebridgeNetworkConnector(LoggingInterface): _send_lock: threading.Lock _network_name: str _mqtt_client = mqtt.Client _mqtt_credentials: MqttCredentialsContainer _received_responses: Queue _received_messages: Queue _response_timeout: Optional[int] _thread_manager: ThreadManager _characteristic_update_callback: Optional[CharacteristicUpdateCallback] def __init__(self, network_name: str, mqtt_credentials: MqttCredentialsContainer, response_timeout: Optional[int] = None): super().__init__() self._response_timeout = response_timeout self._send_lock = threading.Lock() self._network_name = network_name self._mqtt_credentials = mqtt_credentials self._characteristic_update_callback = None self._received_responses = Queue() self._received_messages = Queue() self._mqtt_client = mqtt.Client(self._network_name + "_HomeBridge") self._logger.info("Connecting to homebridge mqtt broker") if self._mqtt_credentials.has_auth(): self._mqtt_client.username_pw_set(self._mqtt_credentials.username, self._mqtt_credentials.password) self._logger.info("Using auth for mqtt connection") try: self._mqtt_client.connect(self._mqtt_credentials.ip, self._mqtt_credentials.port, 15) except OSError: raise MqttConnectionError(self._mqtt_credentials.ip, self._mqtt_credentials.port) self._mqtt_client.on_message = self._gen_message_handler() self._mqtt_client.on_connect = self._generate_connect_callback() self._mqtt_client.on_disconnect = self._generate_disconnect_callback() self._mqtt_client.subscribe("homebridge/from/response") self._mqtt_client.subscribe("homebridge/from/set") self._mqtt_client.loop_start() self._thread_manager = ThreadManager() self._thread_manager.add_thread("received_request_handler", self._request_handler_thread) self._thread_manager.start_threads() def __del__(self): self._thread_manager.__del__() self._mqtt_client.disconnect() self._mqtt_client.__del__() def _gen_message_handler(self): """ Generates a response method to attach to the mqtt-connector as 'on_message' callback :return: The function handler """ def on_message(client, userdata, message): self._logger.debug(f"Received message at '{message.topic}'") topic = message.topic json_str = message.payload.decode("utf-8") try: body = json.loads(json_str) except json.decoder.JSONDecodeError: self._logger.error( "Couldn't decode json: '{}'".format(json_str)) return buf_req = HomeBridgeRequest(topic, body) if buf_req.topic == "homebridge/from/response": self._received_responses.put(buf_req) else: self._received_messages.put(buf_req) return on_message def _generate_connect_callback(self): def connect_callback(client, userdata, reasonCode, properties=None): self._logger.info("MQTT connected.") return connect_callback def _generate_disconnect_callback(self): def disconnect_callback(client, userdata, reasonCode, properties=None): self._logger.info("MQTT disconnected.") return disconnect_callback def _request_handler_thread(self): if not self._received_messages.empty(): req = self._received_messages.get() self._handle_request(req) def _handle_request(self, req: HomeBridgeRequest): """ Handles a request received by the mqtt client :param req: Request to handle :return: None """ # Check handle characteristic updates if req.topic == "homebridge/from/set": # TODO: validate with json schema if self._characteristic_update_callback: self._characteristic_update_callback( req.message["name"], req.message["characteristic"], req.message["value"]) def _send_request(self, req: HomeBridgeRequest, wait_for_response: Optional[int] = None) ->\ Optional[HomeBridgeRequest]: """ Sends a homebridge request and waits for an answer if wanted :param req: Request to be sent :param wait_for_response: The time that should be waited to receive an response before NoResponseError is raised :return: The response that was received :raises AckFalseError: If wait_for_response != None and ack 'False' was sent back :raises NoResponseError: If wait_for_response != None and no response was received """ with self._send_lock: req_id = random.randint(0, 10000) if wait_for_response is not None: req.set_request_id(req_id) self._received_responses = Queue() info = self._mqtt_client.publish(topic=req.topic, payload=json.dumps(req.message)) info.wait_for_publish() if wait_for_response is None: return None else: start_time = datetime.now() while start_time + timedelta( seconds=wait_for_response) > datetime.now(): if not self._received_responses.empty(): res: HomeBridgeRequest = self._received_responses.get() if res.get_request_id() == req_id: return res raise NoResponseError(req) def attach_characteristic_update_callback( self, callback: CharacteristicUpdateCallback): """ Attaches a callback to the connector to receive characteristic updates triggered on the external data source :param callback: Callback function to attach :return: None """ self._characteristic_update_callback = callback def add_gadget(self, gadget: Gadget) -> bool: try: buf_payload = HomebridgeEncoder().encode_gadget(gadget) except GadgetEncodeError as err: self._logger.error(err.args[0]) return False buf_req = HomeBridgeRequest("homebridge/to/add", buf_payload) try: response = self._send_request(buf_req, self._response_timeout) if response.get_ack() is True: return True return False except NoResponseError as err: self._logger.error(err.args[0]) return False def remove_gadget(self, gadget_name: str) -> bool: """ Removes the gadget from the external data source :param gadget_name: Name of the gadget that should be deleted :return: Whether deleting the gadget was successful or not """ buf_req = HomeBridgeRequest("homebridge/to/remove", {"name": gadget_name}) try: response = self._send_request(buf_req, self._response_timeout) if response.get_ack() is True: return True return False except NoResponseError as err: self._logger.error(err.args[0]) return False def get_gadget_info(self, gadget_name: str) -> Optional[dict]: """ Gets information about the selected gadget from the external source :param gadget_name: Name of the gadget to get information for :return: The information received about the gadget """ buf_req = HomeBridgeRequest("homebridge/to/get", {"name": "*_props"}) try: response = self._send_request(buf_req, self._response_timeout) if gadget_name not in response.message: return None return response.message[gadget_name] except NoResponseError as err: self._logger.error(err.args[0]) return None def update_characteristic(self, gadget_name: str, characteristic: str, value: int): """ Updates a characteristic of the selected gadget on the external data source :param gadget_name: Name of the gadget the characteristic belongs to :param characteristic: The characteristic to change :param value: The new value of the characteristic :return: Whether the change of the characteristic was successful or not """ buf_payload = { "name": gadget_name, "service_name": gadget_name, "characteristic": characteristic, "value": value } buf_req = HomeBridgeRequest("homebridge/to/set", buf_payload) self._send_request(buf_req, None)