Example #1
0
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")
Example #3
0
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)
Example #4
0
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
Example #5
0
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()
Example #6
0
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)
Example #7
0
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()
Example #8
0
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()
Example #9
0
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()
Example #10
0
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()
Example #11
0
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
Example #12
0
    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))
Example #13
0
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