class SetBasedAsyncPopPool(collabc.AsyncGenerator, AsyncPopulationPool[H]): """An asynchronous population pool backed by a set.""" def __init__(self, ivals: Optional[AbstractSet[H]] = None): self._stopped = False self._set = Set[H]() if ivals is None else {ivals} self._ac = None self._stopped = False async def apopulate(self, val: H, *args: H) -> None: #Can't populate a closed pool: if self._stopped: raise StopAsyncIteration if self._ac is None: self._ac = ACondition() if args is None or len(args) == 0: self._set.add(val) count = 1 else: argset = {args} argset.add(val) count = len(argset) self._set |= argset async with self._ac: self._ac.notify(count) async def asend(self, value: Optional[H]) -> Optional[H]: if self._stopped: raise StopAsyncIteration if self._ac is None: self._ac = ACondition() if value is None: async with self._ac: while len(self._set) == 0: self._ac.wait() if self._stopped: raise StopAsyncIteration return self._set.pop() else: if value not in self._set: self._set.add(value) await self._ac.acquire() self._ac.notify() self._ac.release() async def athrow(self, typ, val=None, tb=None) -> None: try: return await super().athrow(typ, val, tb) except (Exception, GeneratorExit) as exc: self._stopped = True if self._ac is not None: await self._ac.acquire() self._ac.notify_all() self._ac.release() raise StopAsyncIteration from exc
async def manipulate_condition(condition: asyncio.Condition): print("starting manipulate_condition") # consumer의 시작을 잠깐 지연시킨다 await asyncio.sleep(0.1) for i in range(1, 3): async with condition: print(f"notifying {i} condumers") condition.notify(i) await asyncio.sleep(0.1) async with condition: print("notifying remaining consumers") condition.notify_all() print("ending manupulate_condition")
async def main(): cond = Condition() fs = list([workers(cond, i) for i in range(10)]) workers(cond, 11) workers(cond, 12) await sleep(0.1) async with cond: for i in range(4): print('notify {} workers'.format(i)) cond.notify(i) await sleep(0.1) async with cond: await sleep(0.5) print('notify all') cond.notify_all() await wait(fs)
class TokenBucket: """A token bucket.""" def __init__(self, rate: float, bucket_size: int) -> None: """Constructor for TokenBucket. Args: rate (float): The number of tokens added to the bucket per second. A token is added to the bucket every 1/rate seconds. bucket_size (int): The maximum number of tokens the bucket can hold. Raises: ValueError: When rate or bucket_size less than or equal to 0. """ if rate <= 0: raise ValueError("rate must be > 0") if bucket_size <= 0: raise ValueError("bucket size must be > 0") self._rate: Final[float] = rate self._bucket_size: Final[int] = bucket_size self.n_token = bucket_size self._cond = Condition(Lock()) _token_filler_worker.register(self) async def fill(self, n: int = 1) -> None: """Fill the bucket with n tokens.""" async with self._cond: self.n_token = min(self.n_token + n, self._bucket_size) self._cond.notify() async def consume(self, n: int = 1) -> None: """Consume n tokens from the bucket.""" async with self._cond: while self.n_token < n: await self._cond.wait() else: self.n_token -= n
class AuthView(): def __init__(self, client, stdscr): self.stdscr = stdscr self.client = client self.inputevent = Condition() self.inputs = "" self.w, self.h = curses.COLS, curses.LINES self.fin = False self.showinput = True async def textinput(self): self.stdscr.addstr("\n> ") self.stdscr.refresh() self.inputs = "" with await self.inputevent: await self.inputevent.wait() out = self.inputs self.inputs = "" self.stdscr.addstr("\n") self.stdscr.refresh() return out async def run(self): await self.client.connect() self.stdscr.addstr("connected") self.auth = await self.client.is_user_authorized() draw_logo(self.stdscr) if not self.auth: while True: self.stdscr.addstr("Please enter your phone number: ") self.stdscr.refresh() self.phone = await self.textinput() self.phone = self.phone.replace("+", "00").replace(" ", "") try: response = await self.client.sign_in(phone=self.phone) debug(str(response)) break except telethon.errors.rpcerrorlist.FloodWaitError as err: self.stdscr.addstr( f"The telegram servers blocked you for too many retries ({err.seconds}s remaining). " ) self.stdscr.refresh() except telethon.errors.rpcerrorlist.PhoneNumberInvalidError as err: self.stdscr.addstr("Incorrect phone number. ") self.stdscr.refresh() except Exception as err: debug(f"Uncaught Error: {err}") self.stdscr.addstr(f"Uncaught Error: {err}") self.stdscr.refresh() self.stdscr.addstr( "Now authentificate with the code telegram sent to you.") self.stdscr.refresh() while True: done = False try: self.code = await self.textinput() await self.client.sign_in(code=self.code) done = True except telethon.errors.rpcerrorlist.PhoneCodeInvalidError: self.stdscr.addstr( "The authentification code was wrong. Please try again." ) self.stdscr.refresh() except telethon.errors.rpcerrorlist.SessionPasswordNeededError: self.showinput = False self.stdscr.addstr("A 2FA password is required to log in.") self.stdscr.refresh() while True: self.passwd = await self.textinput() try: await self.client.sign_in(password=self.passwd) done = True break except telethon.errors.PasswordHashInvalidError: self.stdscr.addstr( "Incorrect password. Try again.") self.stdscr.refresh() except Exception as err: debug(f"Uncaught Error: {err}") self.stdscr.addstr(f"Uncaught Error: {err}") self.stdscr.refresh() if done: break self.stdscr.addstr( "Authentification successfull. Please wait until the client has finished loading." ) self.stdscr.refresh() async def handle_key(self, key): if key == "RETURN": with await self.inputevent: self.inputevent.notify() elif key == "BACKSPACE": self.inputs = self.inputs[0:-1] else: self.inputs += key y, _ = self.stdscr.getyx() if self.showinput: self.stdscr.addstr(y, 2, self.inputs) else: self.stdscr.addstr(y, 2, "*" * len(self.inputs)) self.stdscr.clrtoeol() self.stdscr.refresh()
class DiscoveryService(AbstractDiscoveryService, DatagramProtocol, Listener): """Discovery protocol class. Not for external use.""" def __init__(self, loop: AbstractEventLoop = None, session: ClientSession = None) -> None: """Start the discovery protocol using the supplied loop. raises: RuntimeError: If attempted to start the protocol when it is already running. """ self._controllers = {} # type: Dict[str, Controller] self._disconnected = set() # type: Set[str] self._listeners = [] # type: List[Listener] self._close_task = None # type: Optional[Task] _LOG.info("Starting discovery protocol") if not loop: if session: self.loop = session.loop else: self.loop = asyncio.get_event_loop() else: self.loop = loop self.session = session self._own_session = session is None self._transport = None # type: Optional[DatagramTransport] self._scan_condition = Condition(loop=self.loop) # type: Condition self._tasks = [] # type: List[Future] # Async context manager interface async def __aenter__(self) -> AbstractDiscoveryService: await self.start_discovery() return self async def __aexit__(self, exc_type, exc_value, traceback): await self.close() def _task_done_callback(self, task): if task.exception(): _LOG.exception("Uncaught exception", exc_info=task.exception()) self._tasks.remove(task) # managing the task list. def create_task(self, coro) -> Task: """Create a task in the event loop. Keeps track of created tasks.""" task = self.loop.create_task(coro) # type: Task self._tasks.append(task) task.add_done_callback(self._task_done_callback) return task # Listeners. def add_listener(self, listener: Listener) -> None: """Add a discovered listener. All existing controllers will be passed to the listener.""" self._listeners.append(listener) def callback(): for controller in self._controllers.values(): listener.controller_discovered(controller) self.loop.call_soon(callback) def remove_listener(self, listener: Listener) -> None: """Remove a listener""" self._listeners.remove(listener) def controller_discovered(self, ctrl: Controller) -> None: _LOG.info("New controller found: id=%s ip=%s", ctrl.device_uid, ctrl.device_ip) for listener in self._listeners: with LogExceptions("controller_discovered"): listener.controller_discovered(ctrl) def controller_disconnected(self, ctrl: Controller, ex: Exception) -> None: _LOG.warning("Connection to controller lost: id=%s ip=%s", ctrl.device_uid, ctrl.device_ip) self._disconnected.add(ctrl.device_uid) self.loop.create_task(self._rescan()) for listener in self._listeners: with LogExceptions("controller_disconnected"): listener.controller_disconnected(ctrl, ex) def controller_reconnected(self, ctrl: Controller) -> None: _LOG.warning("Controller reconnected: id=%s ip=%s", ctrl.device_uid, ctrl.device_ip) self._disconnected.remove(ctrl.device_uid) for listener in self._listeners: with LogExceptions("controller_reconnected"): listener.controller_reconnected(ctrl) def controller_update(self, ctrl: Controller) -> None: for listener in self._listeners: with LogExceptions("controller_update"): listener.controller_update(ctrl) def zone_update(self, ctrl: Controller, zone: Zone) -> None: for listener in self._listeners: with LogExceptions("zone_update"): listener.zone_update(ctrl, zone) @property def controllers(self) -> Dict[str, Controller]: """Dictionary of all the currently discovered controllers""" return self._controllers # Non-context versions of starting. async def start_discovery(self) -> None: if self._own_session: self.session = ClientSession(loop=self.loop) await self.loop.create_datagram_endpoint(lambda: self, local_addr=('0.0.0.0', UPDATE_PORT), allow_broadcast=True) def connection_made( self, transport: DatagramTransport ) -> None: # type: ignore # noqa: E501 if self._close_task: transport.close() return assert not self._transport, "Another connection made" self._transport = transport self.create_task(self._scan_loop()) def _get_broadcasts(self): for ifAddr in map(netifaces.ifaddresses, netifaces.interfaces()): inetAddrs = ifAddr.get(netifaces.AF_INET) if not inetAddrs: continue for inetAddr in inetAddrs: broadcast = inetAddr.get('broadcast') if broadcast: yield broadcast def _send_broadcasts(self): for broadcast in self._get_broadcasts(): _LOG.debug("Sending discovery message to addr %s", broadcast) self._transport.sendto(DISCOVERY_MSG, (broadcast, DISCOVERY_PORT)) async def _scan_loop(self) -> None: assert self._transport, "Should be impossible" while True: self._send_broadcasts() try: async with timeout(DISCOVERY_RESCAN if self. _disconnected else DISCOVERY_SLEEP): async with self._scan_condition: await self._scan_condition.wait() except asyncio.TimeoutError: pass if self._close_task: return async def rescan(self) -> None: if self.is_closed: raise ConnectionError("Already closed") _LOG.debug("Manual rescan of controllers triggered.") await self._rescan() async def _rescan(self) -> None: async with self._scan_condition: self._scan_condition.notify() # Closing the connection async def close(self) -> None: """Close the transport""" if self._close_task: await self._close_task return _LOG.info("Close called on discovery service.") self._close_task = Task.current_task(self.loop) if self._transport: self._transport.close() await self._rescan() if self._own_session and self.session: await self.session.close() await asyncio.wait(self._tasks) def connection_lost(self, exc): _LOG.debug("Connection Lost") if not self._close_task: _LOG.error("Connection Lost unexpectedly: %s", repr(exc)) self.loop.create_task(self.close()) @property def is_closed(self) -> bool: if self._transport: return self._transport.is_closing() return self._close_task is not None def error_received(self, exc): _LOG.warning("Error passed and ignored to error_recieved: %s", repr(exc)) def _find_by_addr(self, addr: str) -> Optional[Controller]: for _, ctrl in self._controllers.items(): if ctrl.device_ip == addr[0]: return ctrl return None async def _wrap_update(self, coro): try: await coro except ConnectionError as ex: _LOG.warning("Unable to complete %s due to connection error: %s", coro, repr(ex)) def datagram_received(self, data, addr): _LOG.debug("Datagram Recieved %s", data) if self._close_task: return self._process_datagram(data, addr) def _process_datagram(self, data, addr): if data in (DISCOVERY_MSG, CHANGED_SCHEDULES): # ignore pass elif data == CHANGED_SYSTEM: ctrl = self._find_by_addr(addr) if not ctrl: return self.create_task(self._wrap_update(ctrl._refresh_system())) # pylint: disable=protected-access # noqa: E501 elif data == CHANGED_ZONES: ctrl = self._find_by_addr(addr) if not ctrl: return self.create_task(self._wrap_update(ctrl._refresh_zones())) # pylint: disable=protected-access # noqa: E501 else: self._discovery_recieved(data) def _discovery_recieved(self, data): message = data.decode().split(',') if len(message) < 3 or message[0] != 'ASPort_12107': _LOG.warning("Invalid Message Received: %s", data.decode()) return if len(message) > 3 and message[3] != 'iZone': return device_uid = message[1].split('_')[1] device_ip = message[2].split('_')[1] if device_uid not in self._controllers: # Create new controller. # We don't have to set the loop here since it's set for # the thread already. controller = self._create_controller(device_uid, device_ip) async def initialize_controller(): try: await controller._initialize() # pylint: disable=protected-access # noqa: E501 except ConnectionError as ex: _LOG.warning( "Can't connect to discovered server at IP '%s'" " exception: %s", device_ip, repr(ex)) return self._controllers[device_uid] = controller self.controller_discovered(controller) self.create_task(initialize_controller()) else: controller = self._controllers[device_uid] controller._refresh_address(device_ip) # pylint: disable=protected-access # noqa: E501 def _create_controller(self, device_uid, device_ip): return Controller(self, device_uid=device_uid, device_ip=device_ip)
class MainView(): def __init__(self, client, stdscr): self.stdscr = stdscr self.client = client self.inputevent = Condition() self.client.add_event_handler(self.on_message, events.NewMessage) self.client.add_event_handler(self.on_user_update, events.UserUpdate) # TODO # self.client.add_event_handler(self.on_read, events.MessageRead) self.text_emojis = True self.macros = {} self.macro_recording = None self.macro_sequence = [] self.inputs = "" self.inputs_cursor = 0 self.edit_message = None self.popup = None self.drawtool = drawtool.Drawtool(self) self.fin = False from config import colors as colorconfig self.colors = colorconfig.get_colors() self.ready = False self.search_result = None self.search_index = None self.search_box = "" self.vimline_box = "" self.command_box = "" # index corresponds to the index in self.dialogs self.selected_chat = 0 # index offset self.selected_chat_offset = 0 self.selected_message = None self.mode = "normal" self.modestack = [] async def quit(self): self.fin = True with await self.inputevent: self.inputevent.notify() async def on_user_update(self, event): user_id = event.user_id if event.online != None: for dialog in self.dialogs: if event.online == True: dialog["online_until"] = event.until elif dialog["online_until"]: now = datetime.datetime.now().astimezone() until = dialog["online_until"].astimezone() if (now - until).seconds > 0: dialog["online_until"] = None dialog["online"] = False if dialog["dialog"].entity.id == user_id: dialog["online"] = event.online async def on_message(self, event): # move chats with news up for idx, dialog in enumerate(self.dialogs): if dialog["dialog"].id == event.chat_id: # stuff to do upon arriving messages newmessage = await self.client.get_messages(dialog["dialog"], 1) dialog["messages"].insert(0, newmessage[0]) if not event.out: dialog["unread_count"] += 1 os.system(f"notify-send -i apps/telegram \"{dialog['dialog'].name}\" \"{newmessage[0].message}\"") front = self.dialogs.pop(idx) self.dialogs = [front] + self.dialogs break #old dead code # # auto adjust relative replys to match shifted message offsets # if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id: # if self.inputs.startswith("r"): # num = self.inputs[1:].split()[0] # try: ## num = int(s[1:].split()[0]) # number = int(num) # msg = self.inputs.replace("r" + num, "r" + str(number+1)) # self.inputs = msg # except: # pass # dont switch the dialoge upon arriving messages if idx == self.selected_chat: self.selected_chat = 0 elif idx > self.selected_chat: self.selected_chat += 1 elif idx < self.selected_chat: pass await self.drawtool.redraw() async def textinput(self): self.inputs = "" with await self.inputevent: await self.inputevent.wait() if self.fin: return "" out = self.inputs self.inputs = "" return out async def run(self): try: chats = await self.client.get_dialogs() except sqlite3.OperationalError: self.stdscr.addstr("Database is locked. Cannot connect with this session. Aborting") self.stdscr.refresh() await self.textinput() await self.quit() self.dialogs = [ { "dialog": dialog, "unread_count": dialog.unread_count, "online": dialog.entity.status.to_dict()["_"] == "UserStatusOnline" if hasattr(dialog.entity, "status") and dialog.entity.status else None, "online_until": None, # "last_seen": dialog.entity.status.to_dict()["was_online"] if online == False else None, } for dialog in chats ] await self.drawtool.redraw() self.ready = True while True: s = await self.textinput() if self.fin: return if s.startswith("r"): try: num = int(s[1:].split()[0]) except: continue s = s.replace("r" + str(num) + " ", "") reply_msg = self.dialogs[self.selected_chat]["messages"][num] s = emojis.encode(s) reply = await reply_msg.reply(s) await self.on_message(reply) elif s.startswith("media"): try: num = int(s[5:].split()[0]) except: continue message = self.dialogs[self.selected_chat]["messages"][num] if message.media: os.makedirs("/tmp/tttc/", exist_ok=True) path = await self.client.download_media(message.media, "/tmp/tttc/") # TODO mute calls if message.media.photo: sizes = message.media.photo.sizes w, h = sizes[0].w, sizes[0].h # w, h basesize = 1500 w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" os.system(f"echo -e '{w3m_command}' | {W3MIMGDISPLAY} & disown") await self.textinput() else: subprocess.call(["xdg-open", "{shlex.quote(path)}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) #os.system(f"(xdg-open {shlex.quote(path)} 2>&1 > /dev/null) & disown") else: s = emojis.encode(s) outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) await self.on_message(outgoing_message) await self.drawtool.redraw() def select_next_chat(self): # if wrapping not allowed: # self.selected_chat = min(self.selected_chat + 1, len(self.dialogs) - 1) self.selected_chat = (self.selected_chat + 1) % (len(self.dialogs)) self.center_selected_chat() def select_prev_chat(self): # if wrapping not allowed: # self.selected_chat = max(self.selected_chat - 1, 0) self.selected_chat = (self.selected_chat - 1) % (len(self.dialogs)) self.center_selected_chat() def center_selected_chat(self): if self.selected_chat < self.drawtool.chats_num // 2: self.selected_chat_offset = 0 elif self.selected_chat > len(self.dialogs) - self.drawtool.chats_num // 2: self.selected_chat_offset = len(self.dialogs) - self.drawtool.chats_num else: self.selected_chat_offset = self.selected_chat - self.drawtool.chats_num // 2 def select_chat(self, index): if index < -1 or index >= len(self.dialogs): return if index == -1: index = len(self.dialogs) - 1 while index < self.selected_chat: self.select_prev_chat() else: while index > self.selected_chat: self.select_next_chat() def is_subsequence(self, xs, ys): xs = list(xs) for y in ys: if xs and xs[0] == y: xs.pop(0) return not xs def search_chats(self, query = None): if query is None: query = self.search_box if query is None: return # we dont search for "" filter_function = self.is_subsequence filter_function = lambda x, y: x in y self.search_result = [ idx for (idx, dialog) in enumerate(self.dialogs) if filter_function(query.lower(), get_display_name(dialog["dialog"].entity).lower())] self.search_index = -1 def search_next(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_left(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() return self.search_index = (self.search_index + 1) % len(self.search_result) index = self.search_result[self.search_index] self.select_chat(index) self.center_selected_chat() def search_prev(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_right(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index]) self.center_selected_chat() return self.search_index = (self.search_index - 1) % len(self.search_result) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() async def call_command(self): command = self.vimline_box if command == "q": await self.quit() elif command == "pfd": m = "" for i in range(len(self.inputs)): m += self.inputs[i].lower() if i%2==0 else self.inputs[i].lower().swapcase() self.inputs = m async def send_message(self): if not self.inputs: return s = self.inputs s = emojis.encode(s) outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) await self.on_message(outgoing_message) await self.mark_read() self.center_selected_chat() self.inputs = "" async def mark_read(self): chat = self.dialogs[self.selected_chat] dialog = chat["dialog"] lastmessage = chat["messages"][0] await self.client.send_read_acknowledge(dialog, lastmessage) self.dialogs[self.selected_chat]["unread_count"] = 0 async def show_media(self, num = None): if not num: return message = self.dialogs[self.selected_chat]["messages"][num] if message.media: os.makedirs("/tmp/tttc/", exist_ok=True) # TODO test if file exists, ask for confirmation to replace or download again path = await self.client.download_media(message.media, "/tmp/tttc/") if hasattr(message.media, "photo") and False: sizes = message.media.photo.sizes w, h = sizes[0].w, sizes[0].h # w, h basesize = 300 w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" echo_sp = subprocess.Popen(["echo", "-e", f"{w3m_command}"], stdout = subprocess.PIPE) w3m_sp = subprocess.Popen([f"{W3MIMGDISPLAY}"], stdin = echo_sp.stdout) else: subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) def popup_message(self, question): self.modestack.append(self.mode) self.mode = "popupmessage" async def action_handler(self, key): pass self.popup = (action_handler, question) def spawn_popup(self, action_handler, question): # on q press self.modestack.append(self.mode) self.mode = "popup" self.popup = (action_handler, question) async def handle_key(self, key, redraw = True): if self.mode == "popupmessage": self.mode = self.modestack.pop() if not self.ready: return if key == "RESIZE": await self.drawtool.resize() return if self.macro_recording: if key != "q": self.macro_sequence.append(key) if self.mode == "search": if key == "ESCAPE" or key == "RETURN": self.mode = "normal" elif key == "BACKSPACE": if self.search_box == "": self.mode = "normal" else: self.search_box = self.search_box[0:-1] self.search_chats() self.search_next() else: self.search_box += key self.search_chats() self.search_next() elif self.mode == "vimmode": if key == "ESCAPE": self.mode = "normal" elif key == "RETURN": await self.call_command() self.vimline_box = "" self.mode = "normal" elif key == "BACKSPACE": if self.vimline_box == "": self.mode = "normal" else: self.vimline_box = self.vimline_box[0:-1] else: self.vimline_box += key elif self.mode == "normal": num = None try: num = int(key) except: pass if num is not None: self.command_box += str(num) await self.drawtool.redraw() return elif key == ":": self.mode = "vimmode" self.vimline_box = "" elif key == "RETURN" or key == "y": await self.send_message() elif key == "Q": await self.quit() elif key == "q": if self.macro_recording == None: # start macro recording async def record_macro(self, key): if "a" < key.lower() < "z": self.macro_recording = key self.popup_message(f"recording into {key}") else: self.popup_message(f"Register must be [a-zA-Z]") self.spawn_popup(record_macro, "Record into which register?") else: # end macro recording self.macros[self.macro_recording] = self.macro_sequence self.macro_recording = None self.macro_sequence = [] elif key == "@": # execute macro async def ask_macro(self, key): if key in self.macros.keys(): macro = self.macros[key] debug(macro) for k in macro: await self.handle_key(k, redraw = False) else: self.popup_message(f"No such macro @{key}") self.spawn_popup(ask_macro, "Execute which macro?") elif key == "C": self.select_prev_chat() elif key == "c": self.select_next_chat() elif key == "E": self.text_emojis ^= True elif key == "R": await self.mark_read() elif key == "d": if self.command_box: try: n = int(self.command_box) except: return if n >= len(self.dialogs[self.selected_chat]["messages"]): #TODO: alert user self.popup_message("No message by that id.") await self.drawtool.redraw() return async def action_handler(self, key): if key in ["y","Y"]: to_delete = self.dialogs[self.selected_chat]["messages"][n] await to_delete.delete() self.dialogs[self.selected_chat]["messages"].pop(n) self.command_box = "" self.mode = "normal" question = f"Are you really sure you want to delete message {n}? [y/N]" self.spawn_popup(action_handler, question) await self.drawtool.redraw() elif key == "e": if self.command_box: try: n = int(self.command_box) except: return self.edit_message = self.dialogs[self.selected_chat]["messages"][n] self.mode = "edit" self.inputs = emojis.decode(self.edit_message.text) self.command_box = "" elif key == "r": if self.command_box: try: n = int(self.command_box) except: return reply_to = self.dialogs[self.selected_chat]["messages"][n] s = emojis.encode(self.inputs) reply = await reply_to.reply(s) await self.on_message(reply) self.command_box = "" self.inputs = "" elif key == "m": if self.command_box: try: n = int(self.command_box) except: return self.command_box = "" await self.show_media(n) elif key == "M": self.center_selected_chat() elif key == "HOME" or key == "g": self.select_chat(0) elif key == "END" or key == "G": self.select_chat(-1) elif key == "i": self.mode = "insert" elif key == "n": self.search_next() elif key == "N": self.search_prev() elif key == "/": self.mode = "search" self.search_box = "" elif key == " ": self.drawtool.show_indices ^= True elif self.mode == "popup": action, _ = self.popup # I think this could break self.mode = self.modestack.pop() await action(self, key) elif self.mode == "edit": if key == "ESCAPE": async def ah(self, key): if key in ["Y", "y", "RETURN"]: edit = await self.edit_message.edit(self.inputs) await self.on_message(edit) # TODO: update message in chat # this on_message call does not work reliably self.mode = "normal" else: self.popup_message("Edit discarded.") self.mode = "normal" self.spawn_popup(ah, "Do you want to save the edit? [Y/n]") elif key == "LEFT": self.insert_move_left() elif key == "RIGHT": self.insert_move_right() elif key == "BACKSPACE": self.inputs = self.inputs[0:-1] elif key == "RETURN": self.inputs += "\n" else: self.inputs += key elif self.mode == "insert": if key == "ESCAPE": self.mode = "normal" elif key == "LEFT": self.insert_move_left() elif key == "RIGHT": self.insert_move_right() elif key == "BACKSPACE": self.inputs = self.inputs[0:-1] elif key == "RETURN": self.inputs += "\n" else: self.inputs += key self.command_box = "" if redraw: await self.drawtool.redraw() def insert_move_left(self): self.inputs_cursor = max(0, self.cursor - 1) def insert_move_right(self): self.inputs_cursor = min(len(self.inputs), self.cursor + 1) async def handle_key_old(self, key): if key == "RETURN": with await self.inputevent: self.inputevent.notify() elif key == "": chat = self.dialogs[self.selected_chat]["dialog"] last_message = self.dialogs[self.selected_chat]["messages"][0] await self.client.send_read_acknowledge(chat, max_id=last_message.id) self.dialogs[self.selected_chat]["unread_count"] = 0 else: self.inputs += key self.drawtool.redraw()
class StreamIO(StreamIOBase): def __init__(self, initial_bytes=None): # type: (Optional[bytes]) -> None super(StreamIO, self).__init__(initial_bytes) self._alock = ALock() self._acond = ACondition(self._alock) self._wake_task = None async def at_eof(self): return self._has_eof and self._buf.seek(0, SEEK_END) == self._pos async def read1(self): async with self._acond: self._buf.seek(self._pos, SEEK_SET) try: data = self._buf.read() if len(data) <= 0 and not self._has_eof: await self._acond.wait() return self._buf.read() return data finally: self._pos = self.auto_reduce() async def read(self, size=-1): async with self._acond: self._buf.seek(self._pos, SEEK_SET) try: data = bytearray() while True: b = self._buf.read(size) data += b if (size != -1 and len(b) >= size) or self._has_eof: return data if size != -1: size -= len(b) self._pos = self.auto_reduce() await self._acond.wait() self._buf.seek(self._pos, SEEK_SET) finally: self._pos = self.auto_reduce() async def readline(self, size=-1): async with self._acond: self._buf.seek(self._pos, SEEK_SET) try: data = bytearray() while True: b = self._buf.readline(size) data += b if (len(data) > 0 and data[-1] == b"\n") or ( size != -1 and len(b) >= size) or self._has_eof: return data if size != -1: size -= len(b) self._pos = self.auto_reduce() await self._acond.wait() self._buf.seek(self._pos, SEEK_SET) finally: self._pos = self.auto_reduce() async def readlines(self, hint=-1): async with self._acond: while not self._has_eof: await self._acond.wait() self._buf.seek(self._pos, SEEK_SET) try: return self._buf.readlines(hint) finally: self._pos = self.auto_reduce() async def _wake_read(self): async with self._acond: self._acond.notify() def _reset_wake_task(self, task): if self._wake_task is task: self._wake_task = None def wake_read(self): if self._wake_task is None or self._wake_task.done(): loop = asyncio.get_running_loop() self._wake_task = loop.create_task(self._wake_read()) self._wake_task.add_done_callback(self._reset_wake_task) def write(self, b): # type: (Union[bytes, bytearray]) -> None """write data in event loop""" self._buf.seek(0, SEEK_END) self._buf.write(b) self.wake_read() def eof_received(self): self._has_eof = True self.wake_read()
class MainView(): def __init__(self, client, stdscr): self.stdscr = stdscr self.client = client self.inputevent = Condition() self.client.add_event_handler(self.on_message, events.NewMessage) self.client.add_event_handler(self.on_user_update, events.UserUpdate) # TODO # self.client.add_event_handler(self.on_read, events.MessageRead) self.text_emojis = True self.macros = {} self.macro_recording = None self.macro_sequence = [] self.popup_input = None self.last_saved_location = "/tmp/tttc/" # the offset which messages are being displayed. # the number corresponds to the lowest message shown on screen as the messages are drawing upwards self.message_offset = 0 self.tab_selection = 0 self.inputs = "" self.inputs_cursor = 0 self.edit_message = None self.popup = None self.drawtool = drawtool.Drawtool(self) self.fin = False from config import colors as colorconfig self.colors = colorconfig.get_colors() self.ready = False self.search_result = None self.search_index = None self.search_box = "" self.vimline_box = "" self.command_box = "" self._dialogs = [] self._dialogs_updated = False self.num_pinned = 0 # index corresponds to the index in self.dialogs self.selected_chat = 0 # index offset self.selected_chat_offset = 0 self.modestack = ["normal"] self.key_handler = KeyHandler(self) self.forward_messages = [] @property def dialogs(self): if not self._dialogs_updated: return self._dialogs self._update_dialogs() return self._dialogs def _update_dialogs(self): # remove updated flag self._dialogs_updated = False # store old selected chat and restore it after sorting selected_chat = self._dialogs[self.selected_chat] # remove archived chats -- they're archived for a reason self._dialogs = [ dialog for dialog in self._dialogs if not dialog["dialog"].archived ] # sort pinned to top: inverse(lexicographic(is pinned, date)) self._dialogs.sort(key = lambda x: (x["dialog"].pinned, x["dialog"].date)) self._dialogs = self._dialogs[::-1] self.num_pinned = sum( 1 for dialog in self._dialogs if dialog["dialog"].pinned ) # restore selected chat for idx, dialog in enumerate(self._dialogs): if dialog == selected_chat: self.selected_chat = idx break @dialogs.setter def dialogs(self, newdialogs): self._dialogs_updated = True self._dialogs = newdialogs @property def mode(self): try: return self.modestack[-1] except IndexError: self.modestack = ["normal"] return "normal" @mode.setter def mode(self, newmode): # i think we might need this if self.modestack == ["normal"] and newmode == "normal": return self.modestack.append(newmode) async def quit(self): self.fin = True with await self.inputevent: self.inputevent.notify() async def on_user_update(self, event): user_id = event.user_id if event.online != None: for dialog in self.dialogs: if event.online == True: dialog["online_until"] = event.until elif dialog["online_until"]: now = datetime.datetime.now().astimezone() until = dialog["online_until"].astimezone() if (now - until).seconds > 0: dialog["online_until"] = None dialog["online"] = False if dialog["dialog"].entity.id == user_id: dialog["online"] = event.online async def on_forward(self, n): dialog = self.dialogs[self.selected_chat] newmessages = await self.client.get_messages(dialog["dialog"], n) for message in newmessages[::-1]: dialog["messages"].insert(0, message) dialog["dialog"].date = message.date async def toggle_pin(self): dialog = self.dialogs[self.selected_chat]["dialog"] dialog.pinned = not dialog.pinned out = await self.client(ToggleDialogPinRequest(dialog.input_entity, dialog.pinned)) self._update_dialogs() newpos = 0 for i in range(len(self.dialogs)): if self.dialogs[i]["dialog"] == dialog: newpos = i break self.select_chat(newpos) async def on_message(self, event): for idx, dialog in enumerate(self.dialogs): if dialog["dialog"].id == event.chat_id: newmessage = (await self.client.get_messages(dialog["dialog"], 1))[0] # add new message to list of messages dialog["messages"].insert(0, newmessage) # update date of dialog to sort it up dialog["dialog"].date = newmessage.date # send notification, update unread count if not event.out: dialog["unread_count"] += 1 os.system(f"notify-send -i apps/telegram \"{dialog['dialog'].name}\" \"{newmessage[0].message}\"") break # restore order self._update_dialogs() await self.drawtool.redraw() async def run(self): try: chats = await self.client.get_dialogs() except sqlite3.OperationalError: self.stdscr.addstr("Database is locked. Cannot connect with this session. Aborting") self.stdscr.refresh() await self.quit() self.dialogs = [ { "dialog": dialog, "unread_count": dialog.unread_count, "online": dialog.entity.status.to_dict()["_"] == "UserStatusOnline" if hasattr(dialog.entity, "status") and dialog.entity.status else None, "online_until": None, "downloads": dict(), "messages": [] # "last_seen": dialog.entity.status.to_dict()["was_online"] if online == False else None, } for dialog in chats ] self._update_dialogs() self.selected_chat = 0 await self.drawtool.redraw() self.ready = True def select_next_chat(self): self.message_offset = 0 # if wrapping not allowed: # self.selected_chat = min(self.selected_chat + 1, len(self.dialogs) - 1) self.selected_chat = (self.selected_chat + 1) % (len(self.dialogs)) self.center_selected_chat() def select_prev_chat(self): self.message_offset = 0 # if wrapping not allowed: # self.selected_chat = max(self.selected_chat - 1, 0) self.selected_chat = (self.selected_chat - 1) % (len(self.dialogs)) self.center_selected_chat() def center_selected_chat(self): if self.selected_chat < self.drawtool.chats_num // 2: self.selected_chat_offset = 0 elif self.selected_chat > len(self.dialogs) - self.drawtool.chats_num // 2: self.selected_chat_offset = len(self.dialogs) - self.drawtool.chats_num else: self.selected_chat_offset = self.selected_chat - self.drawtool.chats_num // 2 def select_chat(self, index): self.message_offset = 0 if index < -1 or index >= len(self.dialogs): return if index == -1: index = len(self.dialogs) - 1 while index < self.selected_chat: self.select_prev_chat() else: while index > self.selected_chat: self.select_next_chat() def is_subsequence(self, xs, ys): xs = list(xs) for y in ys: if xs and xs[0] == y: xs.pop(0) return not xs def search_chats(self, query = None): if query is None: query = self.search_box if query is None: return # we dont search for "" filter_function = self.is_subsequence filter_function = lambda x, y: x in y self.search_result = [ idx for (idx, dialog) in enumerate(self.dialogs) if filter_function(query.lower(), get_display_name(dialog["dialog"].entity).lower())] self.search_index = -1 def search_next(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_left(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() return self.search_index = (self.search_index + 1) % len(self.search_result) index = self.search_result[self.search_index] self.select_chat(index) self.center_selected_chat() def search_prev(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_right(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index]) self.center_selected_chat() return self.search_index = (self.search_index - 1) % len(self.search_result) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() async def call_command(self): command = self.vimline_box if command == "q": await self.quit() elif command == "pfd": m = "" for i in range(len(self.inputs)): m += self.inputs[i].lower() if i%2==0 else self.inputs[i].lower().swapcase() self.inputs = m async def send_message(self): if not self.inputs: return s = self.inputs s = emojis.encode(s) outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) await self.on_message(outgoing_message) await self.mark_read() self.center_selected_chat() self.inputs = "" self.inputs_cursor = 0 async def mark_read(self): chat = self.dialogs[self.selected_chat] dialog = chat["dialog"] lastmessage = chat["messages"][0] await self.client.send_read_acknowledge(dialog, lastmessage) self.dialogs[self.selected_chat]["unread_count"] = 0 async def download_attachment(self, num = None, force = False): if num is None: return message = self.dialogs[self.selected_chat]["messages"][num] if message.id in self.dialogs[self.selected_chat]["downloads"] and not force: logging.info(self.dialogs[self.selected_chat]["downloads"]) self.popup_message(f"File was previously downloaded as: {self.dialogs[self.selected_chat]['downloads'][message.id]}. Use the force (to download anyway).") return self.popup_input = f"{os.path.dirname(self.last_saved_location)}/" async def handler(self, key): if key == "ESCAPE": self.popup_input = None return True # done processing the popup elif key == "BACKSPACE": if self.popup_input == "": pass else: self.popup_input = self.popup_input[0:-1] elif key == "RETURN": # save message filename = self.popup_input or '/tmp/tttc/' async def cb(recv, maximum): percentage = 100 * recv / maximum downloadtext = f"downloading to {filename}: {percentage:.2f}%" self.popup[1] = downloadtext self.popup_input = None await self.drawtool.redraw() if (percentage == 100): # we auto clear the popup here self.modestack.pop() path = await self.client.download_media(message.media, filename, progress_callback = cb) if not path: self.popup_message(f"Could not save file. Maybe there is no attachment?") return True self.dialogs[self.selected_chat]["downloads"][message.id] = path self.last_saved_location = path # now we can work with the downloaded file, show it in filesystem self.popup_input = None self.popup_message(f"Saved as {path}") return True else: self.popup_input += key return False self.spawn_popup(handler, "Save file anew as: " if force else "Save file as: ") async def open_link(self, num = None): def httpify(s): if s.startswith("http"): return s return f"https://{s}" if num is None: return message = self.dialogs[self.selected_chat]["messages"][num] if message.entities: links = [ text for (entity_type, text) in message.get_entities_text() if entity_type.to_dict()["_"] == "MessageEntityUrl" ] if len(links) == 1: # if there is a unique link to open, open it. link = links[0] debug(["xdg-open", f"{httpify(link)}"]) subprocess.Popen(["xdg-open", f"{httpify(link)}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) elif len(links) > 1: # user selects which link to open self.tab_selection = 0 async def handler(self, key): if key == "TAB": self.tab_selection = (self.tab_selection + 1) % len(links) self.popup[1] = f"Select link to open (TAB): {links[self.tab_selection]}" elif key == "ESCAPE": self.modestack.pop() elif key == "RETURN": link = links[self.tab_selection] debug(["xdg-open", f"{httpify(link)}"]) subprocess.Popen(["xdg-open", f"{httpify(link)}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) self.modestack.pop() self.spawn_popup(handler, f"Select link to open (TAB): {links[self.tab_selection]}") async def show_media(self, num = None): if num is None: return message = self.dialogs[self.selected_chat]["messages"][num] if message.id not in self.dialogs[self.selected_chat]["downloads"]: self.popup_message("No media found for this message. Did you download it?") return path = self.dialogs[self.selected_chat]["downloads"][message.id] subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) def popup_message(self, question): #self.modestack.append(self.mode) self.mode = "popupmessage" async def action_handler(self, key): return True self.popup = [action_handler, question] def spawn_popup(self, action_handler, question): # on q press #self.modestack.append(self.mode) self.mode = "popup" self.popup = [action_handler, question] async def handle_key(self, key, redraw = True): await self.key_handler.handle_key(key) await self.drawtool.redraw() def modify_input(self, key): if key == "LEFT": self.insert_move_left() elif key == "RIGHT": self.insert_move_right() elif key == "BACKSPACE": if self.inputs_cursor != 0: self.inputs = self.inputs[:self.inputs_cursor - 1] + self.inputs[self.inputs_cursor:] self.inputs_cursor -= 1 elif key == "DEL": if self.inputs_cursor != len(self.inputs): self.inputs = self.inputs[:self.inputs_cursor] + self.inputs[self.inputs_cursor + 1:] elif key == "RETURN": self.inputs += "\n" self.inputs_cursor += 1 elif len(key) == 1: input_string = list(self.inputs) input_string.insert(self.inputs_cursor, key) self.inputs = "".join(input_string) self.inputs_cursor += 1 #self.inputs.insert(self.inputs_cursor, key) def insert_move_left(self): self.inputs_cursor = max(0, self.inputs_cursor - 1) def insert_move_right(self): self.inputs_cursor = min(len(self.inputs), self.inputs_cursor + 1) async def handle_key_old(self, key): if key == "RETURN": with await self.inputevent: self.inputevent.notify() elif key == "": chat = self.dialogs[self.selected_chat]["dialog"] last_message = self.dialogs[self.selected_chat]["messages"][0] await self.client.send_read_acknowledge(chat, max_id=last_message.id) self.dialogs[self.selected_chat]["unread_count"] = 0 else: self.inputs += key self.drawtool.redraw()
class Pool(asyncio.AbstractServer): def __init__(self, minsize: int = 1, maxsize: int = 10, loop=None, **kwargs): self._maxsize = maxsize self._minsize = minsize self._connection_kwargs = kwargs self._terminated: Set[Connection] = set() self._used: Set[Connection] = set() self._cond = Condition() self._closing = False self._closed = False self._free: Deque[Connection] = collections.deque(maxlen=maxsize) self._loop = loop if maxsize <= 0: raise ValueError("maxsize is expected to be greater than zero") if minsize < 0: raise ValueError( "minsize is expected to be greater or equal to zero") if minsize > maxsize: raise ValueError("minsize is greater than max_size") @property def maxsize(self): return self._maxsize @property def minsize(self): return self._minsize @property def freesize(self): return len(self._free) @property def size(self): return self.freesize + len(self._used) @property def cond(self): return self._cond async def release(self, connection: Connection): """Release free connection back to the connection pool. This is **NOT** a coroutine. """ fut = self._loop.create_future() fut.set_result(None) if connection in self._terminated: self._terminated.remove(connection) return fut self._used.remove(connection) if connection.connected: if self._closing: await connection.close() else: self._free.append(connection) fut = self._loop.create_task(self._wakeup()) return fut async def _wakeup(self): async with self._cond: self._cond.notify() def _wait(self): return len(self._terminated) > 0 def acquire(self): return _PoolAcquireContextManager(self._acquire(), self) async def _acquire(self) -> Connection: if self._closing: raise RuntimeError("Cannot acquire connection after closing pool") async with self._cond: while True: await self.initialize() if self._free: conn = self._free.popleft() self._used.add(conn) return conn else: await self._cond.wait() async def initialize(self): while self.size < self.minsize: conn = await connect(**self._connection_kwargs) self._free.append(conn) self._cond.notify() async def clear(self): """Close all free connections in pool.""" async with self._cond: while self._free: conn = self._free.popleft() await conn.close() self._cond.notify() async def wait_closed(self): """Wait for closing all pool's connections.""" if self._closed: return if not self._closing: raise RuntimeError(".wait_closed() should be called " "after .close()") while self._free: conn = self._free.popleft() await conn.close() async with self._cond: while self.size > self.freesize: await self._cond.wait() self._closed = True def close(self): """Close pool. Mark all pool connections to be closed on getting back to pool. Closed pool doesn't allow to acquire new connections. """ if self._closed: return self._closing = True def terminate(self): """Terminate pool. Close pool with instantly closing all acquired connections also. """ self.close() for conn in self._used: conn.close() self._terminated.add(conn) self._used.clear() async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): self.close() await self.wait_closed()
class Controller: """Interface to IZone controller""" class Mode(Enum): """Valid controller modes""" COOL = 'cool' HEAT = 'heat' VENT = 'vent' DRY = 'dry' AUTO = 'auto' FREE_AIR = 'free_air' class Fan(Enum): """All fan modes""" LOW = 'low' MED = 'med' HIGH = 'high' AUTO = 'auto' DictValue = Union[str, int, float] ControllerData = Dict[str, DictValue] REQUEST_TIMEOUT = 3 """Time to wait for results from server.""" CONNECT_RETRY_TIMEOUT = 20 """Cool-down period for retrying to connect to the controller""" _VALID_FAN_MODES: Dict[str, List[Fan]] = { 'disabled': [Fan.LOW, Fan.MED, Fan.HIGH], '3-speed': [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO], '2-speed': [Fan.LOW, Fan.HIGH, Fan.AUTO], 'var-speed': [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO], } def __init__(self, discovery, device_uid: str, device_ip: str) -> None: """Create a controller interface. Usually this is called from the discovery service. If neither device UID or address are specified, will search network for exactly one controller. If UID is specified then the addr is ignored. Args: device_uid: Controller UId as a string (eg: mine is '000013170') If specified, will search the network for a matching device device_addr: Device network address. Usually specified as IP address Raises: ConnectionAbortedError: If id is not set and more than one iZone instance is discovered on the network. ConnectionRefusedError: If no iZone discovered, or no iZone device discovered at the given IP address or UId """ self._ip = device_ip self._discovery = discovery self._device_uid = device_uid self.zones: List[Zone] = [] self.fan_modes: List[Controller.Fan] = [] self._system_settings: Controller.ControllerData = {} self._fail_exception = None self._reconnect_condition = Condition() self._sending_lock = Lock() """Create a session for sending commands iZone controller doesnt like sessions being kept alive, so disabled""" self._session = requests.session() self._session.keep_alive = False async def _initialize(self) -> None: """Initialize the controller, does not complete until the system is initialised.""" await self._refresh_system(notify=False) self.fan_modes = Controller._VALID_FAN_MODES[str( self._system_settings['FanAuto'])] zone_count = int(self._system_settings['NoOfZones']) self.zones = [Zone(self, i) for i in range(zone_count)] await self._refresh_zones(notify=False) @property def device_ip(self) -> str: """IP Address of the unit""" return self._ip @property def device_uid(self) -> str: '''UId of the unit''' return self._device_uid @property def discovery(self) -> 'AbstractDiscoveryService': return self._discovery @property def is_on(self) -> bool: """True if the system is turned on""" return self._get_system_state('SysOn') == 'on' async def set_on(self, value: bool) -> None: """Turn the system on or off.""" await self._set_system_state('SysOn', 'SystemON', 'on' if value else 'off') @property def mode(self) -> 'Mode': """System mode, cooling, heating, etc""" if self.free_air: return self.Mode.FREE_AIR return self.Mode(self._get_system_state('SysMode')) async def set_mode(self, value: Mode): """Set system mode, cooling, heating, etc Async method, await to ensure command revieved by system. """ if value == Controller.Mode.FREE_AIR: if self.free_air: return if not self.free_air_enabled: raise AttributeError("Free air system is not enabled") await self._set_system_state('FreeAir', 'FreeAir', 'on') else: if self.free_air: await self._set_system_state('FreeAir', 'FreeAir', 'off') await asyncio.sleep(0.5) await self._set_system_state('SysMode', 'SystemMODE', value.value) @property def fan(self) -> 'Fan': """The current fan level.""" return self.Fan(self._get_system_state('SysFan')) async def set_fan(self, value: Fan) -> None: """The fan level. Not all fan modes are allowed depending on the system. Async method, await to ensure command revieved by system. Raises: AttributeError: On setting if the argument value is not valid """ if value not in self.fan_modes: raise AttributeError("Fan mode {} not allowed".format(value.value)) await self._set_system_state( 'SysFan', 'SystemFAN', value.value, 'medium' if value is Controller.Fan.MED else value.value) @property def sleep_timer(self) -> int: """Current setting for the sleep timer.""" return int(self._get_system_state('SleepTimer')) async def set_sleep_timer(self, value: int): """The sleep timer. Valid settings are 0, 30, 60, 90, 120 Async method, await to ensure command revieved by system. Raises: AttributeError: On setting if the argument value is not valid """ time = int(value) if time < 0 or time > 120 or time % 30 != 0: raise AttributeError( 'Invalid Sleep Timer \"{}\", must be divisible by 30'.format( value)) await self._set_system_state('SleepTimer', 'SleepTimer', value, time) @property def free_air_enabled(self) -> bool: """Test if the system has free air system available""" return self._get_system_state('FreeAir') != 'disabled' @property def free_air(self) -> bool: """True if the free air system is turned on. False if unavailable or off """ return self._get_system_state('FreeAir') == 'on' async def set_free_air(self, value: bool) -> None: """Turn the free air system on or off. Async method, await to ensure command revieved by system. Raises: AttributeError: If attempting to set the state of the free air system when it is not enabled. """ if not self.free_air_enabled: raise AttributeError('Free air is disabled') await self._set_system_state('FreeAir', 'FreeAir', 'on' if value else 'off') @property def temp_supply(self) -> float: """Current supply, or in duct, air temperature.""" return float(self._get_system_state('Supply')) @property def temp_setpoint(self) -> float: """AC unit setpoint temperature. This is the unit target temp with with rasMode == RAS, or with rasMode == master and ctrlZone == 13. """ return float(self._get_system_state('Setpoint')) async def set_temp_setpoint(self, value: float): """AC unit setpoint temperature. This is the unit target temp with with rasMode == RAS, or with rasMode == master and ctrlZone == 13. Args: value: Valid settings are between ecoMin and ecoMax, at 0.5 degree units. Raises: AttributeError: On setting if the argument value is not valid. Can still be set even if the mode isn't appropriate. """ if value % 0.5 != 0: raise AttributeError( 'SetPoint \'{}\' not rounded to nearest 0.5'.format(value)) if value < self.temp_min or value > self.temp_max: raise AttributeError( 'SetPoint \'{}\' is out of range'.format(value)) await self._set_system_state('Setpoint', 'UnitSetpoint', value, str(value)) @property def temp_return(self) -> float: """The return, or room, air temperature""" return float(self._get_system_state('Temp')) @property def eco_lock(self) -> bool: """True if eco lock setting is on.""" return self._get_system_state('EcoLock') == 'true' @property def temp_min(self) -> float: """The value for the eco lock minimum, or 15 if eco lock not set""" return float( self._get_system_state('EcoMin')) if self.eco_lock else 15.0 @property def temp_max(self) -> float: """The value for the eco lock maxium, or 30 if eco lock not set""" return float( self._get_system_state('EcoMax')) if self.eco_lock else 30.0 @property def ras_mode(self) -> str: """This indicates the current selection of the Return Air temperature Sensor. Possible values are: master: the AC unit is controlled from a CTS, which is manually selected RAS: the AC unit is controller from its own return air sensor zones: the AC unit is controlled from a CTS, which is automatically selected dependant on the cooling/ heating need of zones. """ return self._get_system_state('RAS') @property def zone_ctrl(self) -> int: """This indicates the zone that currently controls the AC unit. Value interpreted in combination with rasMode""" return int(self._get_system_state('CtrlZone')) @property def zones_total(self) -> int: """This indicates the number of zones the system is configured for.""" return int(self._get_system_state('NoOfZones')) @property def zones_const(self) -> int: """This indicates the number of constant zones the system is configured for.""" return self._get_system_state('NoOfConst') @property def sys_type(self) -> str: """This indicates the type of the iZone system connected. Possible values are: 110: the system is zone control only and all the zones are OPEN/CLOSE zones 210: the system is zone control only. Zones can be temperature controlled, dependant on the zone settings. 310: the system is zone control and unit control. """ return self._get_system_state('SysType') async def _refresh_system(self, notify: bool = True) -> None: """Refresh the system settings.""" values: Controller.ControllerData = await self._get_resource( 'SystemSettings') if self._device_uid != values['AirStreamDeviceUId']: _LOG.error("_refresh_system called with unmatching device ID") return self._system_settings = values if notify: self._discovery.controller_update(self) async def _refresh_zones(self, notify: bool = True) -> None: """Refresh the Zone information.""" zones = int(self._system_settings['NoOfZones']) await asyncio.gather( *[self._refresh_zone_group(i, notify) for i in range(0, zones, 4)]) async def _refresh_zone_group(self, group: int, notify: bool = True): assert group in [0, 4, 8] zone_data_part = await self._get_resource(f"Zones{group+1}_{group+4}") for i in range(min(len(self.zones) - group, 4)): zone_data = zone_data_part[i] self.zones[i + group]._update_zone(zone_data, notify) #pylint: disable=protected-access async def _trigger_reconnect(self): async with self._reconnect_condition: self._reconnect_condition.notify() def _refresh_address(self, address): """Called from discovery to update the address""" if self._ip == address: return self._ip = address # Signal to the retry connection loop to have another go. if self._fail_exception: self._discovery.create_task(self._trigger_reconnect()) def _get_system_state(self, state): self._ensure_connected() return self._system_settings[state] async def _set_system_state(self, state, command, value, send=None): if send is None: send = value if self._system_settings[state] == value: return async with self._sending_lock: await self._send_command_async(command, send) # Need to refresh immediately after setting. try: await self._refresh_system() except ConnectionError: pass def _ensure_connected(self) -> None: if self._fail_exception: raise ConnectionError("Unable to connect to the controller" ) from self._fail_exception def _failed_connection(self, ex): if self._fail_exception: self._fail_exception = ex return self._fail_exception = ex self._discovery.controller_disconnected(self, ex) self._discovery.create_task(self._retry_connection()) async def _retry_connection(self) -> None: while True: await self._discovery.rescan() try: async with timeout(Controller.CONNECT_RETRY_TIMEOUT): # event will be fired if reconnected async with self._reconnect_condition: await self._reconnect_condition.wait() except asyncio.TimeoutError: pass _LOG.info("Attempting to reconnect to server uid=%s ip=%s", self.device_uid, self.device_ip) try: await asyncio.gather(self._refresh_system(notify=False), self._refresh_zones(notify=False)) self._fail_exception = None self._discovery.controller_reconnected(self) self._discovery.controller_update(self) for zone in self.zones: self._discovery.zone_update(self, zone) break except ConnectionError as ex: # Expected, just carry on. _LOG.warning( "Reconnect attempt for uid=%s failed with exception: %s", self.device_uid, ex.__repr__()) async def _get_resource(self, resource: str): try: session = self._discovery.session async with session.get( 'http://%s/%s' % (self.device_ip, resource), timeout=Controller.REQUEST_TIMEOUT) as response: return await response.json() except (asyncio.TimeoutError, aiohttp.ClientError) as ex: self._failed_connection(ex) raise ConnectionError( "Unable to connect to the controller") from ex async def _send_command_async(self, command: str, data: Any): body = {command: data} url = f"http://{self.device_ip}/{command}" _LOG.info("Sending to URL: %s command: %s", url, json.dumps(body)) if False: try: session = self._discovery.session async with session.post(url, timeout=Controller.REQUEST_TIMEOUT, json=body) as response: response.raise_for_status() except (asyncio.TimeoutError, aiohttp.ClientError) as ex: self._failed_connection(ex) raise ConnectionError( "Unable to connect to the controller") from ex else: # Do this synchonously. For some reason, this doesn't work with aiohttp body = {command: data} url = f"http://{self.device_ip}/{command}" headers = {'Connection': 'close'} try: with self._session.post(url, timeout=Controller.REQUEST_TIMEOUT, data=json.dumps(body), headers=headers) as response: response.raise_for_status() except requests.exceptions.RequestException as ex: self._failed_connection(ex) raise ConnectionError( "Unable to connect to the controller") from ex
async def game(request, ws): # ============================ round1 ============================ global condition_player_waiting if condition_player_waiting is None: condition_player_waiting = Condition() token = await ws.recv() if isinstance(token, bytes): token = token.decode('utf-8') print(f"token received: {token}") my_address = utils.get_address_from_token(token) address_rooms.pop(my_address, None) opposite_address = None # first player if not game_room: game_room_id = '0x' + os.urandom(32).hex() game_room[game_room_id] = (my_address, None) async with condition_player_waiting: await condition_player_waiting.wait() # second player else: game_room_id, (opposite_address, _) = next(iter(game_room.items())) game_room.pop(game_room_id) GameDispatcher.save_response_to_both_players(game_room_id, my_address, opposite_address) async with condition_player_waiting: condition_player_waiting.notify() await ws.send(json.dumps(address_rooms[my_address])) # ============================ round2 ============================ global condition_start_game, first_start_game_result if condition_start_game is None: condition_start_game = Condition() start_game_tx_hash = await ws.recv() if isinstance(start_game_tx_hash, bytes): start_game_tx_hash = start_game_tx_hash.decode('utf-8') print(f"start_game_tx_hash : {start_game_tx_hash}") # first player if not first_start_game_result: first_start_game_result = start_game_tx_hash result = await GameDispatcher.get_result_loop(start_game_tx_hash) async with condition_start_game: await condition_start_game.wait() # second player else: result = await GameDispatcher.get_result_loop(start_game_tx_hash) async with condition_start_game: condition_start_game.notify() first_start_game_result = '' # clear memory await ws.send('success') # ============================ round3 ============================ # ========== mini round 1: reveal game ========== global first_reveal_game_result, condition_reveal_game, condition_end_game if condition_reveal_game is None: condition_reveal_game = Condition() reveal_game_tx_hash = await ws.recv() if isinstance(reveal_game_tx_hash, bytes): reveal_game_tx_hash = reveal_game_tx_hash.decode('utf-8') print(f"reveal_game_tx_hash : {start_game_tx_hash}") # first player if not first_reveal_game_result: first_reveal_game_result = reveal_game_tx_hash result = await GameDispatcher.get_result_loop(reveal_game_tx_hash) async with condition_reveal_game: await condition_reveal_game.wait() # second player else: result = await GameDispatcher.get_result_loop(reveal_game_tx_hash) async with condition_reveal_game: condition_reveal_game.notify() first_reveal_game_result = '' # clear memory # ========== mini round 2: end game ========== if condition_end_game is None: condition_end_game = Condition() # first player if opposite_address is None: async with condition_end_game: await condition_end_game.wait() # second player who knows both addresses else: end_game_tx_hash = utils.send_end_game_request( game_room_id=game_room_id, addr1=my_address, addr2=opposite_address ) print(f"end_game_tx_hash : {end_game_tx_hash}") result = await GameDispatcher.get_result_loop(end_game_tx_hash) dice_result = result['eventLogs'][0]['data'] dd = { dice_result[0]: int(dice_result[2], 16), dice_result[1]: int(dice_result[3], 16), 'end_game_tx_hash': end_game_tx_hash } end_game_results[my_address] = dd end_game_results[opposite_address] = dd.copy() print(end_game_results) async with condition_end_game: condition_end_game.notify() print(my_address) print(end_game_results) my_result = end_game_results[my_address].pop(my_address) end_game_tx_hash = end_game_results[my_address].pop('end_game_tx_hash') opposite_result = [dice for address, dice in end_game_results[my_address].items()][0] end_game_results.pop(my_address) final_result = { 'end_game_tx_hash': end_game_tx_hash, 'player_dice_result': my_result, "opposite_dice_result": opposite_result } await ws.send(json.dumps(final_result))
class Controller: """Interface to IZone controller""" class Mode(Enum): """Valid controller modes""" COOL = "cool" HEAT = "heat" VENT = "vent" DRY = "dry" AUTO = "auto" FREE_AIR = "free_air" class Fan(Enum): """All fan modes""" LOW = "low" MED = "med" HIGH = "high" TOP = "top" AUTO = "auto" DictValue = Union[str, int, float] ControllerData = Dict[str, DictValue] REQUEST_TIMEOUT = 3 """Time to wait for results from server.""" REFRESH_INTERVAL = 25.0 """Interval between refreshes of data.""" UPDATE_REFRESH_DELAY = 5.0 """Delay after updating data before a refresh.""" _VALID_FAN_MODES = { "disabled": [Fan.LOW, Fan.MED, Fan.HIGH], "unknown": [Fan.LOW, Fan.MED, Fan.HIGH, Fan.TOP, Fan.AUTO], "3-speed": [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO], "2-speed": [Fan.LOW, Fan.HIGH, Fan.AUTO], "var-speed": [Fan.LOW, Fan.MED, Fan.HIGH, Fan.AUTO], } # type: Dict[str, List[Fan]] def __init__(self, discovery, device_uid: str, device_ip: str, is_v2: bool, is_ipower: bool) -> None: """Create a controller interface. Usually this is called from the discovery service. If neither device UID or address are specified, will search network for exactly one controller. If UID is specified then the addr is ignored. Args: device_uid: Controller UId as a string (eg: mine is '000013170') If specified, will search the network for a matching device device_addr: Device network address. Usually specified as IP address Raises: ConnectionAbortedError: If id is not set and more than one iZone instance is discovered on the network. ConnectionRefusedError: If no iZone discovered, or no iZone device discovered at the given IP address or UId """ self._ip = device_ip self._discovery = discovery self._device_uid = device_uid self._is_v2 = is_v2 self._is_ipower = is_ipower self.zones = [] # type: List[Zone] self.fan_modes = [] # type: List[Controller.Fan] self._system_settings = {} # type: Controller.ControllerData self._power = None # type: Optional[Power] self._initialised = False self._fail_exception = None self._sending_lock = Lock() self._scan_condition = Condition() async def _initialize(self) -> None: """Initialize the controller, does not complete until the system is initialised.""" await self._refresh_system(notify=False) self.fan_modes = Controller._VALID_FAN_MODES[str( self._system_settings.get("FanAuto", "disabled"))] zone_count = int(self._system_settings["NoOfZones"]) self.zones = [Zone(self, i) for i in range(zone_count)] await self._refresh_zones(notify=False) if self._is_ipower: self._power = Power(self) await self._power.init() if self._power.enabled: await self._refresh_power(notify=False) else: self._power = None self._initialised = True self._discovery.create_task(self._poll_loop()) async def _poll_loop(self) -> None: while True: try: async with timeout(Controller.REFRESH_INTERVAL): async with self._scan_condition: await self._scan_condition.wait() # triggered rescan, short delay await asyncio.sleep(Controller.UPDATE_REFRESH_DELAY) except asyncio.TimeoutError: pass if self._discovery.is_closed: return # pylint: disable=broad-except try: _LOG.debug("Polling unit %s.", self._device_uid) await self._refresh_all() except ConnectionError: _LOG.debug("Poll failed due to exeption.", exc_info=True) except Exception: _LOG.error("Unexpected exception", exc_info=True) async def _rescan(self) -> None: async with self._scan_condition: self._scan_condition.notify() @property def connected(self) -> bool: """True if the controller is currently connected""" return self._fail_exception is None @property def power(self) -> Optional[Power]: """Power info""" return self._power @property def device_ip(self) -> str: """IP Address of the unit""" return self._ip @property def device_uid(self) -> str: """UId of the unit""" return self._device_uid @property def is_v2(self) -> bool: """True if this is a v2 controller""" return self._is_v2 @property def discovery(self): """The discovery service""" return self._discovery @property def is_on(self) -> bool: """True if the system is turned on""" return self._get_system_state("SysOn") == "on" async def set_on(self, value: bool) -> None: """Turn the system on or off.""" await self._set_system_state("SysOn", "SystemON", "on" if value else "off") @property def mode(self) -> "Mode": """System mode, cooling, heating, etc""" if self.free_air: return self.Mode.FREE_AIR return self.Mode(self._get_system_state("SysMode")) async def set_mode(self, value: Mode): """Set system mode, cooling, heating, etc.""" if value == Controller.Mode.FREE_AIR: if self.free_air: return if not self.free_air_enabled: raise AttributeError("Free air system is not enabled") await self._set_system_state("FreeAir", "FreeAir", "on") else: if self.free_air: await self._set_system_state("FreeAir", "FreeAir", "off") await self._set_system_state("SysMode", "SystemMODE", value.value) @property def fan(self) -> "Fan": """The current fan level.""" return self.Fan(self._get_system_state("SysFan")) async def set_fan(self, value: Fan) -> None: """The fan level. Not all fan modes are allowed depending on the system. Raises: AttributeError: On setting if the argument value is not valid """ if value not in self.fan_modes: raise AttributeError("Fan mode {} not allowed".format(value.value)) await self._set_system_state( "SysFan", "SystemFAN", value.value, "medium" if value is Controller.Fan.MED else value.value, ) @property def sleep_timer(self) -> int: """Current setting for the sleep timer.""" return int(self._get_system_state("SleepTimer")) async def set_sleep_timer(self, value: int): """The sleep timer. Valid settings are 0, 30, 60, 90, 120 Raises: AttributeError: On setting if the argument value is not valid """ time = int(value) if time < 0 or time > 120 or time % 30 != 0: raise AttributeError( 'Invalid Sleep Timer "{}", must be divisible by 30'.format( value)) await self._set_system_state("SleepTimer", "SleepTimer", value, time) @property def free_air_enabled(self) -> bool: """Test if the system has free air system available""" return self._get_system_state("FreeAir") != "disabled" @property def free_air(self) -> bool: """True if the free air system is turned on. False if unavailable or off""" return self._get_system_state("FreeAir") == "on" async def set_free_air(self, value: bool) -> None: """Turn the free air system on or off. Raises: AttributeError: If attempting to set the state of the free air system when it is not enabled. """ if not self.free_air_enabled: raise AttributeError("Free air is disabled") await self._set_system_state("FreeAir", "FreeAir", "on" if value else "off") @property def temp_supply(self) -> Optional[float]: """Current supply, or in duct, air temperature.""" return float(self._get_system_state("Supply")) or None @property def temp_setpoint(self) -> Optional[float]: """AC unit setpoint temperature. This is the unit target temp with with rasMode == RAS, or with rasMode == master and ctrlZone == 13. """ return float(self._get_system_state("Setpoint")) or None async def set_temp_setpoint(self, value: float): """AC unit setpoint temperature. This is the unit target temp with with rasMode == RAS, or with rasMode == master and ctrlZone == 13. Args: value: Valid settings are between ecoMin and ecoMax, at 0.5 degree units. Raises: AttributeError: On setting if the argument value is not valid. Can still be set even if the mode isn't appropriate. """ if value % 0.5 != 0: raise AttributeError( "SetPoint '{}' not rounded to nearest 0.5".format(value)) if value < self.temp_min or value > self.temp_max: raise AttributeError("SetPoint '{}' is out of range".format(value)) await self._set_system_state("Setpoint", "UnitSetpoint", value, str(value)) @property def temp_return(self) -> Optional[float]: """The return, or room, air temperature""" return float(self._get_system_state("Temp")) or None @property def eco_lock(self) -> bool: """True if eco lock setting is on.""" return self._get_system_state("EcoLock") == "true" @property def temp_min(self) -> float: """The value for the eco lock minimum, or 15 if eco lock not set""" return float( self._get_system_state("EcoMin")) if self.eco_lock else 15.0 @property def temp_max(self) -> float: """The value for the eco lock maxium, or 30 if eco lock not set""" return float( self._get_system_state("EcoMax")) if self.eco_lock else 30.0 @property def ras_mode(self) -> str: """This indicates the current selection of the Return Air temperature Sensor. Possible values are: master: the AC unit is controlled from a CTS, which is manually selected. RAS: the AC unit is controller from its own return air sensor zones: the AC unit is controlled from a CTS, which is automatically selected dependant on the cooling/ heating need of zones. """ return self._get_system_state("RAS") @property def zone_ctrl(self) -> int: """This indicates the zone that currently controls the AC unit. Value interpreted in combination with rasMode""" return int(self._get_system_state("CtrlZone")) @property def zones_total(self) -> int: """This indicates the number of zones the system is configured for.""" return int(self._get_system_state("NoOfZones")) @property def zones_const(self) -> int: """This indicates the number of constant zones the system is configured for.""" return self._get_system_state("NoOfConst") @property def sys_type(self) -> str: """This indicates the type of the iZone system connected. Possible values are: 110: the system is zone control only and all the zones are OPEN/CLOSE zones 210: the system is zone control only. Zones can be temperature controlled, dependant on the zone settings. 310: the system is zone control and unit control. """ return self._get_system_state("SysType") async def _refresh_all(self, notify: bool = True) -> None: zones = int(self._system_settings["NoOfZones"]) # this has to be done sequentially await self._refresh_system(notify) await self._refresh_power(notify) for i in range(0, zones, 4): await self._refresh_zone_group(i, notify) async def _refresh_system(self, notify: bool = True) -> None: """Refresh the system settings.""" values = await self._get_resource( "SystemSettings") # type: Controller.ControllerData # noqa if self._device_uid != values["AirStreamDeviceUId"]: _LOG.error("_refresh_system called with unmatching device ID") return self._system_settings = values if notify: self._discovery.controller_update(self) async def _refresh_power(self, notify: bool = True) -> None: if self._power is None or not self._power.enabled: return updated = await self._power.refresh() if updated and notify: self._discovery.power_update(self) async def _refresh_zones(self, notify: bool = True) -> None: """Refresh the Zone information.""" zones = int(self._system_settings["NoOfZones"]) await asyncio.gather( *[self._refresh_zone_group(i, notify) for i in range(0, zones, 4)]) async def _refresh_zone_group(self, group: int, notify: bool = True): assert group in [0, 4, 8] zone_data_part = await self._get_resource("Zones{0}_{1}".format( group + 1, group + 4)) for i in range(min(len(self.zones) - group, 4)): zone_data = zone_data_part[i] # pylint: disable=protected-access self.zones[i + group]._update_zone(zone_data, notify) def _refresh_address(self, address): """Called from discovery to update the address""" self._ip = address # Signal to the retry connection loop to have another go. if self._fail_exception: self._discovery.create_task(self._retry_connection()) def _get_system_state(self, state): self._ensure_connected() return self._system_settings.get(state) async def _set_system_state(self, state, command, value, send=None): if send is None: send = value await self._send_command_async(command, send) # Update state and trigger rescan self._system_settings[state] = value self._discovery.controller_update(self) await self._rescan() def _ensure_connected(self) -> None: if self._fail_exception: raise ConnectionError("Unable to connect to the controller" ) from self._fail_exception def _failed_connection(self, ex): if self._fail_exception: self._fail_exception = ex return self._fail_exception = ex if not self._initialised: return self._discovery.controller_disconnected(self, ex) async def _retry_connection(self) -> None: _LOG.info( "Attempting to reconnect to server uid=%s ip=%s", self.device_uid, self.device_ip, ) try: await self._refresh_all(notify=False) self._fail_exception = None self._discovery.controller_update(self) for zone in self.zones: self._discovery.zone_update(self, zone) self._discovery.power_update(self) self._discovery.controller_reconnected(self) except ConnectionError: # Expected, just carry on. _LOG.warning( "Reconnect attempt for uid=%s failed with exception", self.device_uid, exc_info=True, ) async def _get_resource(self, resource: str): try: session = self._discovery.session async with self._sending_lock, session.get( "http://%s/%s" % (self.device_ip, resource), timeout=Controller.REQUEST_TIMEOUT, ) as response: try: return await response.json(content_type=None) except JSONDecodeError as ex: text = await response.text() if text[-4:] == "{OK}": return json.loads(text[:-4]) _LOG.error('Decode error for "%s"', text, exc_info=True) raise ConnectionError( "Unable to decode response from the controller" ) from ex except (asyncio.TimeoutError, aiohttp.ClientError) as ex: self._failed_connection(ex) raise ConnectionError( "Unable to connect to the controller") from ex async def _send_command_async(self, command: str, data: Any): # For some reason aiohttp fragments post requests, which causes # the server to fail disgracefully. Implimented rough and dirty # HTTP POST client. loop = asyncio.get_running_loop() on_complete = loop.create_future() device_ip = self.device_ip response = [] class _PostProtocol(asyncio.Protocol): def connection_made(self, transport): body = json.dumps({command: data}).encode() header = ("POST /" + command + " HTTP/1.1\r\n" + "Host: " + device_ip + "\r\n" + "Content-Length: " + str(len(body)) + "\r\n" + "\r\n").encode() _LOG.debug("Writing message to " + device_ip + body.decode()) transport.write(header + body) # pylint: disable=attribute-defined-outside-init self.transport = transport def data_received(self, data): response.append(data.decode()) def eof_received(self): self.transport.close() lines = "".join(response).split("\r\n") if not lines: return parts = lines[0].split(" ") if len(parts) != 3: return if int(parts[1]) != 200: on_complete.set_exception( aiohttp.ClientResponseError(None, None, status=int(parts[1]), message=parts[2])) elif len(lines) > 2 and len(lines[-2]) == 0: on_complete.set_result(lines[-1]) else: on_complete.set_result(None) # The server doesn't tolerate multiple requests in fly concurrently try: async with self._sending_lock, timeout( Controller.REQUEST_TIMEOUT) as timer: # pylint: disable=unnecessary-lambda transport, _ = await loop.create_connection( # type: ignore # noqa: E501 lambda: _PostProtocol(), self.device_ip, 80) # wait for response to be recieved. await on_complete if timer.expired: if transport: transport.close() raise asyncio.TimeoutError() return on_complete.result() except (OSError, asyncio.TimeoutError, aiohttp.ClientError) as ex: self._failed_connection(ex) raise ConnectionError("Unable to connect to controller") from ex