def test_event_handler_failure(): # eeprom_request_bin = binascii.unhexlify('500800009f004037') eeprom_response_bin = binascii.unhexlify( '524700009f0041133e001e0e0400000000060a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121510010705004e85' ) eh = EventMessageHandler(lambda m: "beer") eh.handle = mock.MagicMock() loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) mh.register_handler(eh) assert 1 == len(mh.handlers) message = ReadEEPROMResponse.parse(eeprom_response_bin) coro = asyncio.ensure_future(mh._handle_message(message)) loop.run_until_complete(coro) assert coro.result( ) is None # failed to parse response message return None. Maybe needs to throw something. assert 1 == len(mh.handlers) eh.handle.assert_not_called()
async def test_wait_for_message(mocker): loop = asyncio.get_event_loop() msg = Container() mm = AsyncMessageManager() task1 = loop.create_task(mm.wait_for_message()) mm.schedule_message_handling(msg) assert await task1 == msg assert len(mm.raw_handlers) == 0 assert len(mm.handlers) == 0
def test_handler_timeout(): def event_handler(message): print("event") return "event" async def get_eeprom_result(mhm): try: return await mhm.wait_for_message( lambda m: m.fields.value.po.command == 0x5, timeout=0.1) except asyncio.TimeoutError: return None async def post_eeprom_message(mhm): await asyncio.sleep(0.2) eeprom_response_bin = binascii.unhexlify( '524700009f0041133e001e0e0400000000060a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121510010705004e85' ) return await mhm._handle_message( ReadEEPROMResponse.parse(eeprom_response_bin)) loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) # running task_handle_wait = loop.create_task(asyncio.sleep(0.1)) task_get_eeprom = loop.create_task(get_eeprom_result(mh)) loop.create_task(post_eeprom_message(mh)) assert 0 == len(mh.handlers) loop.run_until_complete( asyncio.gather(task_handle_wait, task_get_eeprom, loop=loop)) assert 1 == len(mh.handlers) event_handler = EventMessageHandler(event_handler) mh.register_handler(event_handler) event_response_bin = b'\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc' task_handle_event1 = mh.schedule_message_handling( LiveEvent.parse(event_response_bin)) loop.run_until_complete(task_handle_event1) assert 1 == len(mh.handlers) result = task_get_eeprom.result() assert result is None
async def test_handler_exception(mocker): loop = asyncio.get_event_loop() msg = Container(fields=Container( value=Container(po=Container(command=0xE), event_source=0xFF))) mm = AsyncMessageManager() eh = EventMessageHandler(callback=mocker.MagicMock( side_effect=Exception("screw it"))) mm.register_handler(eh) with pytest.raises(Exception): await mm.schedule_message_handling(msg) assert len(mm.handler_registry) == 1
def __init__(self, connection: Connection, interface: InterfaceManager, retries=3): self.panel = None # type: Panel self.connection = connection self.retries = retries self.message_manager = AsyncMessageManager() self.work_loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop self.receive_worker_task = None self.message_manager.register_handler(EventMessageHandler(self.handle_event)) self.message_manager.register_handler(ErrorMessageHandler(self.handle_error)) self.data = defaultdict(Type) # dictionary of Type self.reset()
def test_handler_timeout(): def event_handler(message): print("event received") async def get_eeprom_result(mhm): return await mhm.wait_for_message( lambda m: m.fields.value.po.command == 0x5, timeout=0.1) async def post_eeprom_message(mhm): await asyncio.sleep(0.2) eeprom_response_bin = binascii.unhexlify( "524700009f0041133e001e0e0400000000060a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121510010705004e85" ) return await mhm.schedule_message_handling( ReadEEPROMResponse.parse(eeprom_response_bin)) loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) # running task_get_eeprom = loop.create_task(get_eeprom_result(mh)) loop.create_task(post_eeprom_message(mh)) assert 0 == len(mh.handler_registry) with pytest.raises(asyncio.TimeoutError): loop.run_until_complete(task_get_eeprom) assert 0 == len(mh.handler_registry) # Also test EventMessageHandler event_handler = EventMessageHandler(event_handler) mh.register_handler(event_handler) event_response_bin = b"\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc" task_handle_event1 = mh.schedule_message_handling( LiveEvent.parse(event_response_bin)) assert 1 == len(mh.handler_registry) loop.run_until_complete(task_handle_event1) assert 1 == len(mh.handler_registry)
def test_event_handler(): eh = EventMessageHandler(print_beer) loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) mh.register_handler(eh) assert 1 == len(mh.handler_registry) payload = b"\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc" message = LiveEvent.parse(payload) coro = asyncio.ensure_future(mh.schedule_message_handling(message)) loop.run_until_complete(coro) assert 1 == len(mh.handler_registry)
def test_event_handler(): eh = EventMessageHandler(lambda m: "beer") loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) mh.register_handler(eh) assert 1 == len(mh.handlers) payload = b'\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc' message = LiveEvent.parse(payload) coro = asyncio.ensure_future(mh._handle_message(message)) loop.run_until_complete(coro) assert "beer" == coro.result() assert 1 == len(mh.handlers)
def test_handler_two_messages(): def event_handler(message): print("event") async def get_eeprom_result(mhm): return await mhm.wait_for_message( lambda m: m.fields.value.po.command == 0x5) event_response_bin = b"\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc" eeprom_response_bin = binascii.unhexlify( "524700009f0041133e001e0e0400000000060a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121510010705004e85" ) loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) event_handler = EventMessageHandler(event_handler) mh.register_handler(event_handler) # running task_handle_wait = loop.create_task(asyncio.sleep(0.1)) task_get_eeprom = loop.create_task(get_eeprom_result(mh)) task_handle_event1 = mh.schedule_message_handling( LiveEvent.parse(event_response_bin)) mh.schedule_message_handling(ReadEEPROMResponse.parse(eeprom_response_bin)) task_handle_event2 = mh.schedule_message_handling( LiveEvent.parse(event_response_bin)) # assert 2 == len(mh.handlers) loop.run_until_complete( asyncio.gather(task_handle_wait, task_get_eeprom, loop=loop)) assert 1 == len(mh.handler_registry) assert task_handle_event1.done() assert isinstance(task_get_eeprom.result(), Container) assert 1 == len(mh.handler_registry)
async def test_handler_unregister(mocker): cb = mocker.MagicMock() h_name = "removable" handler = PersistentHandler(cb, name=h_name) mm = AsyncMessageManager() mm.register_handler(handler) assert len(mm.handler_registry) == 1 mm.deregister_handler(h_name) assert len(mm.handler_registry) == 0
async def test_raw_handler_register_unregister(mocker): cb = mocker.MagicMock() msg = Container() h_name = 'removable' handler = RAWMessageHandler(cb, name=h_name) mm = AsyncMessageManager() mm.register_handler(handler) assert len(mm.raw_handlers) == 1 mm.deregister_handler(h_name) assert len(mm.raw_handlers) == 0
def test_handler_two_messages(mocker): def event_handler(message): print("event") return "event" async def get_eeprom_result(mhm): return await mhm.wait_for(lambda m: m.fields.value.po.command == 0x5) event_response_bin = b'\xe2\xff\xad\x06\x14\x13\x01\x04\x0e\x10\x00\x01\x05\x00\x00\x00\x00\x00\x02Living room \x00\xcc' eeprom_response_bin = binascii.unhexlify( '524700009f0041133e001e0e0400000000060a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121510010705004e85' ) loop = asyncio.get_event_loop() mh = AsyncMessageManager(loop) event_handler = EventMessageHandler(event_handler) mh.register_handler(event_handler) # running task_handle_wait = loop.create_task(asyncio.sleep(0.1)) task_get_eeprom = loop.create_task(get_eeprom_result(mh)) task_handle_event1 = loop.create_task( mh.handle_message(LiveEvent.parse(event_response_bin))) loop.create_task( mh.handle_message(ReadEEPROMResponse.parse(eeprom_response_bin))) task_handle_event2 = loop.create_task( mh.handle_message(LiveEvent.parse(event_response_bin))) # assert 2 == len(mh.handlers) loop.run_until_complete( asyncio.gather(task_handle_wait, task_get_eeprom, loop=loop)) assert 1 == len(mh.handlers) assert task_handle_event1.result( ) == "event" # failed to parse response message return None. Maybe needs to throw something. assert isinstance(task_get_eeprom.result(), Container) assert task_handle_event2.result() == "event" assert 1 == len(mh.handlers)
async def test_persistent_raw_handler(mocker): cb = mocker.MagicMock() msg = Container() handler = RAWMessageHandler(cb) mm = AsyncMessageManager() mm.register_handler(handler) assert len(mm.raw_handlers) == 1 task1 = mm.schedule_raw_message_handling(msg) await task1 task2 = mm.schedule_raw_message_handling(msg) await task2 assert cb.call_count == 2 cb.assert_has_calls([mocker.call(msg), mocker.call(msg)]) assert len(mm.raw_handlers) == 1
async def test_persistent_handler(mocker): cb = mocker.MagicMock() msg1 = Container() msg2 = Container() handler = PersistentHandler(cb) mm = AsyncMessageManager() mm.register_handler(handler) assert len(mm.handler_registry) == 1 task1 = mm.schedule_message_handling(msg1) await task1 task2 = mm.schedule_message_handling(msg2) await task2 assert task1.done() assert task2.done() assert cb.call_count == 2 cb.assert_has_calls([mocker.call(msg1), mocker.call(msg2)]) assert len(mm.handler_registry) == 1
class Paradox: def __init__(self, connection, interface, retries=3): self.panel = None # type: Panel self.connection = connection self.retries = retries self.interface = interface self.message_manager = AsyncMessageManager() self.work_loop = asyncio.get_event_loop( ) # type: asyncio.AbstractEventLoop self.receive_worker_task = None self.message_manager.register_handler( EventMessageHandler(self.handle_event)) self.message_manager.register_handler( ErrorMessageHandler(self.handle_error)) self.data = defaultdict(Type) # dictionary of Type self.reset() def reset(self): # Keep track of alarm state self.data['system'] = dict(power=dict(label='power', key='power', id=0), rf=dict(label='rf', key='rf', id=1), troubles=dict(label='troubles', key='troubles', id=2)) self.last_power_update = 0 self.run = STATE_STOP self.loop_wait = True self.status_cache = dict() def connect(self) -> bool: task = self.work_loop.create_task(self.connect_async()) self.work_loop.run_until_complete(task) return task.result() async def connect_async(self): self.disconnect() # socket needs to be also closed logger.info("Connecting to interface") if not await self.connection.connect(): logger.error('Failed to connect to interface') self.run = STATE_STOP return False self.run = STATE_STOP self.connection.timeout(0.5) logger.info("Connecting to panel") # Reset all states self.reset() if not self.panel: self.panel = create_panel(self) self.connection.variable_message_length( self.panel.variable_message_length) try: logger.info("Initiating communication") reply = await self.send_wait( self.panel.get_message('InitiateCommunication'), None, reply_expected=0x07) if reply: logger.info("Found Panel {} version {}.{} build {}".format( (reply.fields.value.label.strip(b'\0 ').decode( cfg.LABEL_ENCODING)), reply.fields.value.application.version, reply.fields.value.application.revision, reply.fields.value.application.build)) else: raise ConnectionError( "Panel did not replied to InitiateCommunication") logger.info("Starting communication") reply = await self.send_wait( self.panel.get_message('StartCommunication'), args=dict(source_id=0x02), reply_expected=0x00) if reply is None: raise ConnectionError( "Panel did not replied to StartCommunication") if reply.fields.value.product_id is not None: self.panel = create_panel( self, reply.fields.value.product_id ) # Now we know what panel it is. Let's # recreate panel object. result = await self.panel.initialize_communication( reply, cfg.PASSWORD) if not result: raise ConnectionError("Failed to initialize communication") # Now we need to start async message reading worker self.run = STATE_RUN self.receive_worker_task = self.work_loop.create_task( self.receive_worker()) if cfg.SYNC_TIME: await self.sync_time() if cfg.DEVELOPMENT_DUMP_MEMORY: if hasattr(self.panel, 'dump_memory') and callable( self.panel.dump_memory): logger.warn("Requested memory dump. Dumping...") await self.panel.dump_memory() logger.warn("Memory dump completed. Exiting pai.") raise SystemExit() else: logger.warn( "Requested memory dump, but current panel type does not support it yet." ) await self.panel.update_labels() logger.info("Connection OK") self.loop_wait = False return True except ConnectionError as e: logger.error("Failed to connect: %s" % str(e)) except Exception: logger.exception("Connect error") self.run = STATE_STOP return False async def sync_time(self): logger.debug("Synchronizing panel time") now = time.localtime() args = dict(century=int(now.tm_year / 100), year=int(now.tm_year % 100), month=now.tm_mon, day=now.tm_mday, hour=now.tm_hour, minute=now.tm_min) reply = await self.send_wait(self.panel.get_message('SetTimeDate'), args, reply_expected=0x03, timeout=10) if reply is None: logger.warn("Could not set panel time") else: logger.info("Panel time synchronized") def loop(self): task = self.work_loop.create_task(self.async_loop()) self.work_loop.run_until_complete(task) async def async_loop(self): logger.debug("Loop start") while self.run != STATE_STOP: while self.run == STATE_PAUSE: await asyncio.sleep(5) # May happen when out of sleep if self.run == STATE_STOP: break self.loop_wait = True tstart = time.time() try: for i in cfg.STATUS_REQUESTS: logger.debug("Requesting status: %d" % i) reply = await self.panel.request_status(i) if reply is not None: tstart = time.time() self.panel.handle_status(reply) else: logger.error("No reply to status request: %d" % i) except ConnectionError: raise except Exception: logger.exception("Loop") # cfg.Listen for events while time.time( ) - tstart < cfg.KEEP_ALIVE_INTERVAL and self.run == STATE_RUN and self.loop_wait: await asyncio.sleep(min(time.time() - tstart, 1)) async def receive_worker(self): logger.debug("Receive worker started") async_supported = asyncio.iscoroutinefunction(self.connection.read) try: while True: logger.debug("Receive worker loop") if async_supported: await self.receive() else: await self.receive() await asyncio.sleep( 0.1 ) # we need this until we use fully async receive. This lets other loop events to continue their work except asyncio.CancelledError: logger.debug("Receive worker canceled") logger.debug("Receive worker stopped") async def receive(self, timeout=5.0): # TODO: Get rid of receive worker # with serial_lock: data = self.connection.read(timeout=timeout) if isinstance(data, Awaitable): try: data = await data except asyncio.TimeoutError: return None # Retry if no data was available if data is None or len(data) == 0: return None self.message_manager.schedule_raw_message_handling(data) try: recv_message = self.panel.parse_message(data, direction='frompanel') if cfg.LOGGING_DUMP_MESSAGES: logger.debug(recv_message) # No message if recv_message is None: logger.debug("Unknown message: %s" % (" ".join("{:02x} ".format(c) for c in data))) return None if self.run != STATE_PAUSE: self.message_manager.schedule_message_handling( recv_message) # schedule handling in the loop except Exception: logging.exception("Error parsing message") return None def send_wait_simple(self, message=None, timeout=5.0, wait=True) -> Optional[bytes]: # Connection closed if not self.connection.connected: raise ConnectionError('Not connected') with serial_lock: if message is not None: self.connection.timeout(timeout) self.connection.write(message) if not wait: return None data = self.connection.read(timeout=timeout) if isinstance(data, Awaitable): future = asyncio.run_coroutine_threadsafe(data, self.work_loop) try: data = future.result(5) except asyncio.TimeoutError: data = None return data async def send_wait(self, message_type=None, args=None, message=None, retries=5, timeout=0.5, reply_expected=None) -> Optional[Container]: # Connection closed if not self.connection.connected: raise ConnectionError('Not connected') if message is None and message_type is not None: message = message_type.build(dict(fields=dict(value=args))) while retries >= 0: retries -= 1 with serial_lock: if message is not None: self.connection.timeout(timeout) self.connection.write(message) if reply_expected is not None: self.work_loop.create_task(self.receive(timeout)) if isinstance(reply_expected, Callable): reply = await self.message_manager.wait_for( reply_expected, timeout=timeout * 2) elif isinstance(reply_expected, Iterable): reply = await self.message_manager.wait_for( lambda m: any(m.fields.value.po.command == expected for expected in reply_expected), timeout=timeout * 2) else: reply = await self.message_manager.wait_for( lambda m: m.fields.value.po.command == reply_expected, timeout=timeout * 2) if reply: return reply return None @staticmethod def _select(haystack, needle) -> Sequence[int]: """ Helper function to select objects from provided dictionary :param haystack: dictionary :param needle: :return: Sequence[int] list of object indexes """ selected = [] # type: Sequence[int] if needle == 'all' or needle == '0': selected = list(haystack) else: if needle.isdigit() and 0 < int(needle) < len(haystack): el = haystack.get(int(needle)) else: el = haystack.get(needle) if el: if "id" not in el: raise Exception("Invalid dictionary of elements provided") selected = [el["id"]] return selected def control_zone(self, zone, command) -> bool: logger.debug("Control Zone: {} - {}".format(zone, command)) zones_selected = self._select(self.data['zone'], zone) # type: Sequence[int] # Not Found if len(zones_selected) == 0: return False # Apply state changes accepted = False try: coro = self.panel.control_zones(zones_selected, command) future = asyncio.run_coroutine_threadsafe(coro, self.work_loop) accepted = future.result(10) except NotImplementedError: logger.error( 'control_zones is not implemented for this alarm type') except asyncio.TimeoutError: logger.error('control_zones timeout') future.cancel() # Refresh status self.loop_wait = False return accepted def control_partition(self, partition, command) -> bool: logger.debug("Control Partition: {} - {}".format(partition, command)) partitions_selected = self._select(self.data['partition'], partition) # type: Sequence[int] # Not Found if len(partitions_selected) == 0: return False # Apply state changes accepted = False try: coro = self.panel.control_partitions(partitions_selected, command) future = asyncio.run_coroutine_threadsafe(coro, self.work_loop) accepted = future.result(10) except NotImplementedError: logger.error( 'control_partitions is not implemented for this alarm type') except asyncio.TimeoutError: logger.error('control_partitions timeout') future.cancel() # Apply state changes # Refresh status self.loop_wait = False return accepted def control_output(self, output, command) -> bool: logger.debug("Control Output: {} - {}".format(output, command)) outputs_selected = self._select(self.data['pgm'], output) # Not Found if len(outputs_selected) == 0: return False # Apply state changes accepted = False try: coro = self.panel.control_outputs(outputs_selected, command) future = asyncio.run_coroutine_threadsafe(coro, self.work_loop) accepted = future.result(10) except NotImplementedError: logger.error( 'control_outputs is not implemented for this alarm type') except asyncio.TimeoutError: logger.error('control_outputs timeout') future.cancel() # Apply state changes # Refresh status self.loop_wait = False return accepted def get_label(self, label_type, label_id): if label_type in self.data: el = self.data[label_type].get(label_id) if el: return el.get("label") def handle_event(self, message): """Process cfg.Live Event Message and dispatch it to the interface module""" try: evt = event.Event(self.panel.event_map, message, label_provider=self.get_label) logger.debug("Handle Event: {}".format(evt)) # Temporary to catch labels/properties in wrong places # TODO: REMOVE if evt.type in self.data: if not evt.id: logger.warn( "Missing element ID in {}/{}, m/m: {}/{}, message: {}". format(evt.type, evt.label or '?', evt.major, evt.minor, evt.message)) else: el = self.data[evt.type].get(evt.id) if not el: logger.warn( "Missing element with ID {} in {}/{}".format( evt.id, evt.type, evt.label)) else: for k in evt.change: if k not in el: logger.warn( "Missing property {} in {}/{}".format( k, evt.type, evt.label)) if evt.label != el.get("label"): logger.warn( "Labels differ {} != {} in {}/{}".format( el.get("label"), evt.label, evt.type, evt.label)) else: logger.warn("Missing type {} for event: {}.{} {}".format( evt.type, evt.major, evt.minor, evt.message)) # Temporary end if len( evt.change ) > 0 and evt.type in self.data and evt.id in self.data[evt.type]: self.update_properties(evt.type, evt.id, evt.change, notify=NotifyPropertyChange.NO) # Publish event if self.interface is not None: self.interface.event(evt) except Exception as e: logger.exception("Handle event") def update_properties(self, element_type, type_key, change, notify=NotifyPropertyChange.DEFAULT, publish=PublishPropertyChange.DEFAULT): try: elements = self.data[element_type] except KeyError: logger.debug('Error: "%s" key is missing from data' % element_type) return if type_key not in elements: return # Publish changes and update state for property_name, property_value in change.items(): if property_name.startswith('_'): # skip private properties continue # Virtual property "Trouble" # True if element has ANY type of alarm if 'trouble' in property_name and property_name != 'trouble': if property_value: self.update_properties(element_type, type_key, dict(trouble=True), notify=notify, publish=publish) else: r = False for kk, vv in elements[type_key].items(): if 'trouble' in kk: r = r or elements[type_key][kk] self.update_properties(element_type, type_key, dict(trouble=r), notify=notify, publish=publish) if property_name in elements[type_key]: old = elements[type_key][property_name] if old != change[property_name] or publish == PublishPropertyChange.YES \ or cfg.PUSH_UPDATE_WITHOUT_CHANGE: logger.debug("Change {}/{}/{} from {} to {}".format( element_type, elements[type_key]['key'], property_name, old, property_value)) elements[type_key][property_name] = property_value self.interface.change(element_type, elements[type_key]['key'], property_name, property_value, initial=False) # Trigger notifications for Partitions changes # Ignore some changes as defined in the configuration # TODO: Move this to another place? try: if notify != NotifyPropertyChange.NO and ( (element_type == "partition" and ('partition' not in cfg.LIMITS or type_key in cfg.LIMITS['partition']) and property_name not in cfg.PARTITIONS_CHANGE_NOTIFICATION_IGNORE) or ('trouble' in property_name)): self.interface.notify( "Paradox", "{} {} {}".format(elements[type_key]['key'], property_name, property_value), logging.INFO) except Exception: logger.exception("Trigger notifications") else: elements[type_key][ property_name] = property_value # Initial value suppress = 'trouble' not in property_name self.interface.change(element_type, elements[type_key]['key'], property_name, property_value, initial=suppress) def handle_error(self, message): """Handle ErrorMessage""" error_enum = message.fields.value.message if error_enum == 'panel_not_connected': self.disconnect() else: message = self.panel.get_error_message(error_enum) logger.error("Got ERROR Message: {}".format(message)) def disconnect(self): logger.info("Disconnecting from the Alarm Panel") self.run = STATE_STOP self.loop_wait = False self.clean_session() if self.connection.connected: self.connection.close() logger.info("Disconnected from the Alarm Panel") async def pause(self): logger.info("Pausing PAI") if self.run == STATE_RUN: logger.info("Pausing from the Alarm Panel") self.run = STATE_PAUSE self.loop_wait = False # EVO IP150 IP Interface does not work if we send this # await self.send_wait(self.panel.get_message('CloseConnection'), None) async def resume(self): logger.info("Resuming PAI") if self.run == STATE_PAUSE: await self.connect_async() def clean_session(self): logger.info("Clean Session") if self.connection.connected: if not self.panel: panel = create_panel(self) else: panel = self.panel logger.info("Cleaning previous session. Closing connection") # Write directly as this can be called from other contexts self.connection.write( panel.get_message('CloseConnection').build(dict()))