コード例 #1
0
ファイル: config.py プロジェクト: chiisaa/ConsolePi
    def get_outlets_from_file(self):
        '''Get outlets defined in config

        returns:
            dict: with following keys (all values are dicts)
                linked: linked outlets from config (linked to serial adapters- auto pwr-on)
                dli_power: dict any dlis in config have all ports represented here
                failures: failure to connect to any outlets will result in an entry here
                    outlet_name: failure description
        '''
        outlet_data = self.cfg_yml.get('POWER')
        if not outlet_data:  # fallback to legacy json config
            outlet_data = self.get_json_file(self.static.get('POWER_FILE'))

        if not outlet_data:
            if self.power:
                log.show('Power Function Disabled - Configuration Not Found')
                self.power = False
            self.outlet_types = []
            return outlet_data

        types = []
        by_dev = {}
        for k in outlet_data:
            if outlet_data[k].get('linked_devs'):
                outlet_data[k]['linked_devs'] = utils.format_dev(outlet_data[k]['linked_devs'],
                                                                 hosts=self.hosts, with_path=True)
                self.linked_exists = True
                for dev in outlet_data[k]['linked_devs']:
                    _type = outlet_data[k].get('type').lower()
                    if _type == 'dli':
                        _this = [f"{k}:{[int(p) for p in utils.listify(outlet_data[k]['linked_devs'][dev])]}"]
                    elif _type == 'esphome':
                        _linked = utils.listify(outlet_data[k]['linked_devs'][dev])
                        _this = [f'{k}:{[p for p in _linked]}']
                    else:
                        _this = [k]
                    by_dev[dev] = _this if dev not in by_dev else by_dev[dev] + _this
            else:
                outlet_data[k]['linked_devs'] = []

            if outlet_data[k]["type"].lower() not in types:
                types.append(outlet_data[k]["type"].lower())

            if outlet_data[k]['type'].upper() == 'GPIO' and isinstance(outlet_data[k].get('address'), str) \
                    and outlet_data[k]['address'].isdigit():
                outlet_data[k]['address'] = int(outlet_data[k]['address'])

        self.outlet_types = types
        outlet_data = {
            'defined': outlet_data,
            'linked': by_dev,
            'dli_power': {},
            'failures': {}
            }

        return outlet_data
コード例 #2
0
    def pwr_all(self, outlets=None, action='toggle', desired_state=None):
        '''
        Returns List of responses representing state of outlet after exec
            Valid response is Bool where True = ON
            Errors are returned in str format
        '''
        if action == 'toggle' and desired_state is None:
            return 'Error: desired final state must be provided'  # should never hit this

        if outlets is None:
            outlets = self.pwr_get_outlets()['defined']
        responses = []
        for grp in outlets:
            outlet = outlets[grp]
            noff = True if 'noff' not in outlet else outlet['noff']
            if action == 'toggle':
                # skip any defined dlis that don't have any linked_outlets defined
                # if not outlet['type'] == 'dli' or outlet.get('linked_devs')):
                if outlet['type'] == 'dli':
                    if outlet.get('linked_devs'):
                        responses.append(self.pwr_toggle(outlet['type'], outlet['address'], desired_state=desired_state,
                                        port=self.update_linked_devs(outlet)[1] ,  # NoQA
                                        noff=noff, noconfirm=True))
                elif outlet['type'] == 'esphome':
                    _relays = utils.listify(outlet.get('relays'))
                    for p in _relays:
                        responses.append(self.pwr_toggle(outlet['type'], outlet['address'], desired_state=desired_state,
                                         port=p, noff=noff, noconfirm=True))
                else:
                    responses.append(self.pwr_toggle(outlet['type'], outlet['address'], desired_state=desired_state,
                                     noff=noff, noconfirm=True))
            elif action == 'cycle':
                if outlet['type'] == 'dli':
                    if 'linked_ports' in outlet:
                        linked_ports = utils.listify(outlet['linked_ports'])
                        for p in linked_ports:
                            # Start a thread for each port run in parallel
                            # menu status for (linked) power menu is updated on load
                            threading.Thread(
                                    target=self.pwr_cycle,
                                    args=[outlet['type'], outlet['address']],
                                    kwargs={'port': p, 'noff': noff},
                                    name=f'cycle_{p}'
                                ).start()
                elif outlet['type'] == 'esphome':
                    relays = utils.listify(outlet.get('relays', []))
                    for p in relays:
                        # Start a thread for each port run in parallel
                        threading.Thread(
                                target=self.pwr_cycle,
                                args=[outlet['type'], outlet['address']],
                                kwargs={'port': p, 'noff': noff},
                                name=f'cycle_{p}'
                            ).start()
                else:
                    threading.Thread(
                            target=self.pwr_cycle,
                            args=[outlet['type'], outlet['address']],
                            kwargs={'noff': noff},
                            name='cycle_{}'.format(outlet['address'])
                        ).start()

        # Wait for all threads to complete
        while True:
            threads = 0
            for t in threading.enumerate():
                if 'cycle' in t.name or 'toggle_' in t.name:
                    threads += 1
            if threads == 0:
                break

        return responses
コード例 #3
0
    def pwr_get_outlets(self, outlet_data={}, upd_linked=False, failures={}):
        '''Get Details for Outlets defined in ConsolePi.yaml power section

        On Menu Launch this method is called in parallel (threaded) for each outlet
        On Refresh all outlets are passed to the method

        params: - All Optional
            outlet_data:dict, The outlets that need to be updated, if not provided will get all outlets defined in ConsolePi.yaml
            upd_linked:Bool, If True will update just the linked ports, False is for dli and will update
                all ports for the dli.
            failures:dict: when refreshing outlets pass in previous failures so they can be re-tried
        '''
        # re-attempt connection to failed power controllers on refresh
        if not failures:
            failures = outlet_data.get('failures') if outlet_data.get('failures') else self.data.get('failures')

        outlet_data = self.data.get('defined') if not outlet_data else outlet_data
        if failures:
            outlet_data = {**outlet_data, **failures}
            failures = {}

        dli_power = self.data.get('dli_power', {})

        for k in outlet_data:
            outlet = outlet_data[k]
            _start = time.time()
            # -- // GPIO \\ --
            if outlet['type'].upper() == 'GPIO':
                if not is_rpi:
                    log.warning('GPIO Outlet Defined, GPIO Only Supported on RPi - ignored', show=True)
                    continue
                noff = True if 'noff' not in outlet else outlet['noff']
                GPIO.setup(outlet['address'], GPIO.OUT)
                outlet_data[k]['is_on'] = bool(GPIO.input(outlet['address'])) if noff \
                    else not bool(GPIO.input(outlet['address']))

            # -- // tasmota \\ --
            elif outlet['type'] == 'tasmota':
                response = self.do_tasmota_cmd(outlet['address'])
                outlet['is_on'] = response
                if response not in [0, 1, True, False]:
                    failures[k] = outlet_data[k]
                    failures[k]['error'] = f'[PWR-TASMOTA] {k}:{failures[k]["address"]} {response} - Removed'
                    log.warning(failures[k]['error'], show=True)

            # -- // esphome \\ --
            elif outlet['type'] == 'esphome':
                # TODO have do_esphome accept list, slice, or str for one or multiple relays
                relays = utils.listify(outlet.get('relays', k))  # if they have not specified the relay try name of outlet
                outlet['is_on'] = {}
                for r in relays:
                    response = self.do_esphome_cmd(outlet['address'], r)
                    outlet['is_on'][r] = {'state': response, 'name': r}
                    if response not in [True, False]:
                        failures[k] = outlet_data[k]
                        failures[k]['error'] = f'[PWR-ESP] {k}:{failures[k]["address"]} {response} - Removed'
                        log.warning(failures[k]['error'], show=True)

            # -- // dli \\ --
            elif outlet['type'].lower() == 'dli':
                if TIMING:
                    dbg_line = '------------------------ // NOW PROCESSING {} \\\\ ------------------------'.format(k)
                    print('\n{}'.format('=' * len(dbg_line)))
                    print('{}\n{}\n{}'.format(dbg_line, outlet_data[k], '-' * len(dbg_line)))
                    print('{}'.format('=' * len(dbg_line)))

                # -- // VALIDATE CONFIG FILE DATA FOR DLI \\ --
                all_good = True  # initial value
                for _ in ['address', 'username', 'password']:
                    if not outlet.get(_):
                        all_good = False
                        failures[k] = outlet_data[k]
                        failures[k]['error'] = f'[PWR-DLI {k}] {_} missing from {failures[k]["address"]} ' \
                            'configuration - skipping'
                        log.error(f'[PWR-DLI {k}] {_} missing from {failures[k]["address"]} '
                                  'configuration - skipping', show=True)
                        break
                if not all_good:
                    continue

                (this_dli, _update) = self.load_dli(outlet['address'], outlet['username'], outlet['password'])
                if this_dli is None or this_dli.dli is None:
                    failures[k] = outlet_data[k]
                    failures[k]['error'] = '[PWR-DLI {}] {} Unreachable - Removed'.format(k, failures[k]['address'])
                    log.warning(f"[PWR-DLI {k}] {failures[k]['address']} Unreachable - Removed", show=True)
                else:
                    if TIMING:
                        xstart = time.time()
                        print('this_dli.outlets: {} {}'.format(this_dli.outlets, 'update' if _update else 'init'))
                        print(json.dumps(dli_power, indent=4, sort_keys=True))

                    # upd_linked is for faster update in power menu only refreshes data for linked ports vs entire dli
                    if upd_linked and self.data['dli_power'].get(outlet['address']):
                        if outlet.get('linked_devs'):
                            (outlet, _p) = self.update_linked_devs(outlet)
                            if k in outlet_data:
                                outlet_data[k]['is_on'] = this_dli[_p]
                            else:
                                log.error(f'[PWR GET_OUTLETS] {k} appears to be unreachable')

                            # TODO not actually using the error returned this turned into a hot mess
                            if isinstance(outlet['is_on'], dict) and not outlet['is_on']:
                                all_good = False
                            # update dli_power for the refreshed / linked ports
                            else:
                                for _ in outlet['is_on']:
                                    dli_power[outlet['address']][_] = outlet['is_on'][_]
                    else:
                        if _update:
                            dli_power[outlet['address']] = this_dli.get_dli_outlets()  # data may not be fresh trigger dli update

                            # handle error connecting to dli during refresh - when connect worked on menu launch
                            if not dli_power[outlet['address']]:
                                failures[k] = outlet_data[k]
                                failures[k]['error'] = f"[PWR-DLI] {k} {failures[k]['address']} Unreachable - Removed"
                                log.warning(f'[PWR-DLI {k}] {failures[k]["address"]} Unreachable - Removed',
                                            show=True)
                                continue
                        else:  # dli was just instantiated data is fresh no need to update
                            dli_power[outlet['address']] = this_dli.outlets

                        if outlet.get('linked_devs'):
                            (outlet, _p) = self.update_linked_devs(outlet)

                if TIMING:
                    print('[TIMING] this_dli.outlets: {}'.format(time.time() - xstart))  # TIMING

            log.debug(f'dli {k} Updated. Elapsed Time(secs): {time.time() - _start}')
            # -- END for LOOP for k in outlet_data --

        # Move failed outlets from the keys that populate the menu to the 'failures' key
        # failures are displayed in the footer section of the menu, then re-tried on refresh
        # TODO this may be causing - RuntimeError: dictionary changed size during iteration
        # in pwr_start_update_threads. witnessed on mdnsreg daemon on occasion (Move del logic after wait_for_threads?)
        for _dev in failures:
            if outlet_data.get(_dev):
                del outlet_data[_dev]
            if self.data['defined'].get(_dev):
                del self.data['defined'][_dev]
            if failures[_dev]['address'] in dli_power:
                del dli_power[failures[_dev]['address']]
            self.data['failures'][_dev] = failures[_dev]

        # restore outlets that failed on menu launch but found reachable during refresh
        for _dev in outlet_data:
            if _dev not in self.data['defined']:
                self.data['defined'][_dev] = outlet_data[_dev]
            if _dev in self.data['failures']:
                del self.data['failures'][_dev]

        self.data['dli_power'] = dli_power

        return self.data
コード例 #4
0
ファイル: menu.py プロジェクト: victorhooi/ConsolePi
    def menu_formatting(self,
                        section,
                        sub=None,
                        text=None,
                        footer={},
                        width=MIN_WIDTH,
                        l_offset=1,
                        index=1,
                        do_print=True,
                        do_format=True):

        mlines = []
        max_len = None
        # footer options also supports an optional formatting dict
        # place '_rjust' in the list and the subsequent item should be a dict
        #
        # _rjust: {dict} right justify addl text on same line with
        # one of the other footer options.
        # i.e.
        footer_options = {
            'power': ['p', 'Power Control Menu'],
            'dli': ['d', '[dli] Web Power Switch Menu'],
            'rshell': ['rs', 'Remote Shell Menu'],
            'key': ['k', 'Distribute SSH public Key to Remote Hosts'],
            'shell': ['sh', 'Enter Local Shell'],
            'rn': ['rn', 'Rename Adapters'],
            'refresh': ['r', 'Refresh'],
            'sync': ['s', 'Sync with cloud'],
            'con': [
                'c',
                'Change Default Serial Settings (devices marked with ** only)'
            ],
            'picohelp': ['h', 'Display Picocom Help'],
            'back': ['b', 'Back'],
            'x': ['x', 'Exit']
        }

        # -- append any errors from menu builder
        # self.error_msgs += self.menu.error_msgs
        # self.menu.error_msgs = []

        # -- Adjust width if there is an error msg longer then the current width
        # -- Delete any errors defined in ignore errors
        # TODO Move all menu formatting to it's own library - clean this up
        # Think I process errors here and maybe in print_mlines as well
        # addl processing in FOOTER

        if log.error_msgs:
            # TODO maybe move to log class
            _error_lens = []
            for _error in log.error_msgs:
                for e in self.ignored_errors:
                    _e = _error.strip('\r\n')
                    if hasattr(e, 'match') and e.match(_e):
                        log.error_msgs.remove(_error)
                        break
                    elif isinstance(e, str) and (e == _error or e in _error):
                        log.error_msgs.remove(_error)
                        break
                    else:
                        _error_lens.append(self.format_line(_error).len)
            if _error_lens:
                width = width if width >= max(_error_lens) + 5 else max(
                    _error_lens) + 5

            width = width if width <= self.cols else self.cols

        # --// HEADER \\--
        if section == 'header':
            # ---- CLEAR SCREEN -----
            if not config.debug:
                os.system('clear')
            mlines.append('=' * width)
            line = self.format_line(text)
            _len = line.len
            fmtd_header = line.text
            a = width - _len
            b = (a / 2) - 2
            if text:
                c = int(b) if b == int(b) else int(b) + 1
                if isinstance(text, list):
                    for t in text:
                        mlines.append(' {0} {1} {2}'.format(
                            '-' * int(b), t, '-' * c))
                else:
                    mlines.append(' {0} {1} {2}'.format(
                        '-' * int(b), fmtd_header, '-' * c))
            mlines.append('=' * width)

        # --// BODY \\--
        elif section == 'body':
            max_len = 0
            blines = list(text) if isinstance(text, str) else text
            pad = True if len(blines) + index > 10 else False
            indent = l_offset + 4 if pad else l_offset + 3
            width_list = []
            for _line in blines:
                # -- format spacing of item entry --
                _i = str(index) + '. ' if not pad or index > 9 else str(
                    index) + '.  '
                # -- generate line and calculate line length --
                _line = ' ' * l_offset + _i + _line
                line = self.format_line(_line)
                width_list.append(line.len)
                mlines.append(line.text)
                index += 1
            max_len = 0 if not width_list else max(width_list)
            if sub:
                # -- Add sub lines to top of menu item section --
                x = ((max_len - len(sub)) / 2) - (l_offset + (indent / 2))
                mlines.insert(0, '')
                width_list.insert(0, 0)
                if do_format:
                    mlines.insert(
                        1, '{0}{1} {2} {3}'.format(
                            ' ' * indent, '-' * int(x), sub, '-' *
                            int(x) if x == int(x) else '-' * (int(x) + 1)))
                    width_list.insert(1, len(mlines[1]))
                else:
                    mlines.insert(1, ' ' * indent + sub)
                    width_list.insert(1, len(mlines[1]))
                max_len = max(
                    width_list
                )  # update max_len in case subheading is the longest line in the section
                mlines.insert(2, ' ' * indent + '-' * (max_len - indent))
                width_list.insert(2, len(mlines[2]))

            # -- adding padding to line to full width of longest line in section --
            mlines = self.pad_lines(mlines, max_len,
                                    width_list)  # Refactoring in progress

        # --// FOOTER \\--
        elif section == 'footer':
            #######
            # Being Depricated. Remove once converted
            #######
            if text and isinstance(text, (str, list)):
                mlines.append('')
                text = [text] if isinstance(text, str) else text
                for t in text:
                    if '{{r}}' in t:
                        _t = t.split('{{r}}')
                        mlines.append('{}{}'.format(
                            _t[0], _t[1].rjust(width - len(_t[0]))))
                    else:
                        # mlines.append(self.format_line(t)[1])
                        mlines.append(self.format_line(t).text)

            # TODO temp indented this to be under text to avoid conflict during refactor
                mlines += [' x.  exit', '']
                mlines.append('=' * width)

            ########
            # REDESIGNED FOOTER LOGIC
            ########
            if footer:
                opts = utils.listify(footer.get('opts', []))
                if 'x' not in opts:
                    opts.append('x')
                no_match_overrides = no_match_rjust = []  # init
                pre_text = post_text = foot_text = []  # init
                # replace any pre-defined options with those passed in as overrides
                if footer.get('overrides') and isinstance(
                        footer['overrides'], dict):
                    footer_options = {**footer_options, **footer['overrides']}

                    no_match_overrides = [
                        e for e in footer['overrides']
                        if e not in footer_options
                        and e not in footer.get('rjust', {})
                    ]

                # update footer_options with any specially formmated (rjust) additions
                if footer.get('rjust'):
                    r = footer.get('rjust')
                    f = footer_options
                    foot_overrides = {
                        k: [
                            f[k][0], '{}{}'.format(
                                f[k][1], r[k].rjust(width - len(
                                    f' {f[k][0]}.{" " if len(f[k][0]) == 2 else "  "}{f[k][1]}'
                                )))
                        ]
                        for k in r if k in f
                    }

                    footer_options = {**footer_options, **foot_overrides}
                    no_match_rjust = [
                        e for e in footer['rjust'] if e not in footer_options
                    ]

                if footer.get('before'):
                    footer['before'] = [footer['before']] if isinstance(
                        footer['before'], str) else footer['before']
                    pre_text = [f' {line}' for line in footer['before']]

                if opts:
                    f = footer_options
                    foot_text = [
                        f' {f[k][0]}.{" " if len(f[k][0]) == 2 else "  "}{f[k][1]}'
                        for k in opts if k in f
                    ]

                if footer.get('after'):
                    footer['after'] = [footer['after']] if isinstance(
                        footer['after'], str) else footer['after']
                    post_text = [f' {line}' for line in footer['after']]

                mlines = mlines + [''] + pre_text + foot_text + post_text + [
                    ''
                ] + ['=' * width]
                # TODO probably simplify to make this a catch all at the end of this method
                # mlines = [self.format_line(line)[1] for line in mlines]
                mlines = [self.format_line(line).text for line in mlines]

                # log errors if non-match overrides/rjust options were sent
                if no_match_overrides + no_match_rjust:
                    log.error(
                        f'menu_formatting passed options ({",".join(no_match_overrides + no_match_rjust)})'
                        ' that lacked a match in footer_options = No impact to menu',
                        log=True,
                        level='error')

            # --// ERRORs - append to footer \\-- #
            if len(log.error_msgs) > 0:
                errors = log.error_msgs
                for _error in errors:
                    error = self.format_line(_error)
                    x = ((width - (error.len + 4)) / 2)
                    mlines.append('{0}{1}{2}{3}{0}'.format(
                        self.log_sym_2bang, ' ' * int(x), error.text,
                        ' ' * int(x) if x == int(x) else ' ' * (int(x) + 1)))

                if errors:  # TODO None Type added to list after rename  why
                    mlines.append('=' * width)
                if do_print:
                    log.error_msgs = []  # clear error messages after print

        else:
            log.error_msgs.append(
                'formatting function passed an invalid section')

        # --// DISPLAY THE MENU \\--
        if do_print:
            for _line in mlines:
                print(_line)
                self.menu_rows += 1  # TODO DEBUGGING make easier then remove
        # TODO refactor max_len to widest_line as thats what it is
        return mlines, max_len
コード例 #5
0
ファイル: menu.py プロジェクト: victorhooi/ConsolePi
    def print_menu(self,
                   body,
                   subs=None,
                   header=None,
                   subhead=None,
                   footer=None,
                   foot_fmt=None,
                   col_pad=4,
                   force_cols=False,
                   do_cols=True,
                   do_format=True,
                   by_tens=False):
        '''
        format and print current menu.

        build the content and in the calling method and pass into this function for format & printing
        params:
            body: a list of lists or list of strings, where each inner list is made up of text for each
                    menu-item in that logical section/group.
            subs: a list of sub-head lines that map to each inner body list.  This is the header for
                the specific logical grouping of menu-items. body and subs lists should be of = len
            header: The main Header text for the menu
            footer: an optional text string or list of strings to be added to the menu footer.
            footer: {dict} - where footer['opts'] is list of 'strs' to match key from footer_options dict defined in
                menu_formatting method.  Determines what menu options are displayed in footer.
                (defaults options: x. Exit)
            col_pad: how many spaces will be placed between horizontal menu sections.
            force_cols: By default the menu will print as a single column, with force_cols=True
                    it will bypass the vertical fit test - print section in cols horizontally
            foot_fmt: {dict} - Optional formatting dict.  top-level should be designated keywork that
                specifies supported formatting options (_rjust = right justify).  2nd level should be
                the footer_options key to match on where the value = the text.  Example:
                foot_fmt={'_rjust': {'back': 'menu # alone will toggle the port'}} ~ will result in
                b.  Back                                            menu # alone will toggle the port
                where 'b.  Back' comes from the pre-defined foot_opts dict.
            do_cols: bool, If specified and set to False will bypass horizontal column printing and
                    resulting in everything printing vertically on one screen
            do_format: bool, Only applies to sub_head auto formatting.  If specified and set to False
                    will not perform formatting on sub-menu text.
                    Auto formatting results in '------- text -------' (width of section)
            by_tens: Will start each section @ 1, 11, 21, 31... unless the section is greater than 10
                    menu_action statements should match accordingly

        '''
        line_dict = od({
            'header': {
                'lines': header
            },
            'body': {
                'sections': [],
                'rows': [],
                'width': []
            },
            'footer': {
                'lines': footer
            }
        })

        # Determine header and footer length used to determine if we can print with
        # a single column
        subs = utils.listify(subs)
        subhead = utils.listify(subhead)
        if subhead:
            subhead = [
                f"{' ' + line if not line.startswith(' ') else line}"
                for line in subhead
            ]
            subhead.insert(0, '')
            if not subs:
                subhead.append('')

        head_len = len(
            self.menu_formatting('header', text=header, do_print=False)[0])
        if subhead:
            head_len += len(subhead)
        elif not subs:
            head_len += 1  # blank line added during print

        # TODO REMOVE TEMP during re-factor
        if isinstance(footer, dict):
            foot_lines = self.menu_formatting('footer',
                                              footer=footer,
                                              do_print=False)[0]
            foot_len = len(foot_lines)
            line_dict['footer']['lines'] = foot_lines
        else:
            foot_len = len(
                self.menu_formatting('footer', text=footer, do_print=False)[0])
        '''
        generate list for each sections where each line is padded to width of longest line
        collect width of longest line and # of rows/menu-entries for each section

        All of this is used to format the header/footer width and to ensure consistent formatting
        during print of multiple columns
        '''
        # if str was passed place in list to iterate over
        if isinstance(body, str):
            body = [body]

        # ensure body is a list of lists for mapping with list of subs
        body = [body] if len(body) >= 1 and isinstance(body[0], str) else body

        # if subs is not None:
        #     subs = [subs] if not isinstance(subs, list) else subs

        i = 0
        item = start = 1
        for _section in body:
            if by_tens and i > 0:
                item = start + 10 if item <= start + 10 else item
                start += 10
            _item_list, _max_width = self.menu_formatting(
                'body',
                text=_section,
                sub=subs if subs is None else subs[i],
                index=item,
                do_print=False,
                do_format=do_format)
            line_dict['body']['width'].append(_max_width)
            line_dict['body']['rows'].append(len(_item_list))
            line_dict['body']['sections'].append(_item_list)
            item = item + len(_section)
            i += 1
        '''
        print multiple sections vertically - determine best cut point to start next column
        '''
        _rows = line_dict['body']['rows']
        tot_body_rows = sum(_rows)  # The # of rows to be printed
        # TODO what if rows for 1 section is greater than term rows
        tty_body_avail = (self.rows - head_len - foot_len)
        _begin = 0
        _end = 1
        _iter_start_stop = []
        _pass = 0
        # # -- won't fit in a single column calc sections we can put in the column
        # # #if not tot_body_rows < tty_body_avail:   # Force at least 2 cols while testing
        _r = []
        [_r.append(r) for r in _rows if r not in _r
         ]  # deteremine if all sections are of equal size (common for dli)
        if len(_r) == 1 or force_cols:
            for x in range(0, len(line_dict['body']['sections'])):
                _iter_start_stop.append([x, x + 1])
                # _tot_width.append(sum(body['width'][x:x + 1]) + (col_pad * (cols - 1)))
                next
        else:
            while True:
                r = sum(_rows[_begin:_end])
                if not r >= tty_body_avail and not r >= tot_body_rows / 2:
                    _end += 1
                else:
                    if r > tty_body_avail and _end > 1:
                        if _begin != _end - 1:  # NoQA Indicates the individual section is > then avail rows so give up until paging implemented
                            _end = _end - 1
                    if not _end == (len(_rows)):
                        _iter_start_stop.append([_begin, _end])
                        _begin = _end
                        _end = _begin + 1

                if _end == (len(_rows)):
                    _iter_start_stop.append([_begin, _end])
                    break
                if _pass > len(_rows) + 20:  # should not hit this anymore
                    log.info(
                        f'menu formatter exceeded {len(_rows) + 20} passses and gave up!!!',
                        show=True)
                    break
                _pass += 1

        sections = []
        _tot_width = []
        for _i in _iter_start_stop:
            this_max_width = 0 if not line_dict['body']['width'][
                _i[0]:_i[1]] else max(line_dict['body']['width'][_i[0]:_i[1]])
            _tot_width.append(this_max_width)
            _column_list = []
            for _s in line_dict['body']['sections'][_i[0]:_i[1]]:
                for _line in _s:
                    _fnl_line = '{:{_len}}'.format(_line, _len=this_max_width)
                    _s[_s.index(_line)] = _fnl_line
                _column_list += _s
            sections.append(_column_list)

        line_dict['body']['sections'] = sections

        # -- set the initial # of columns
        body = line_dict['body']
        cols = len(body['sections']) if len(
            body['sections']) <= MAX_COLS else MAX_COLS
        if not force_cols:  # TODO OK to remove and refactor tot_1_col_len is _tot_body_rows calculated above
            # TODO tot_1_col_len is inaccurate
            tot_1_col_len = sum(line_dict['body']['rows']) + len(line_dict['body']['rows']) \
                            + head_len + foot_len
            cols = 1 if not do_cols or tot_1_col_len < self.rows else cols

        # -- if any footer or subhead lines are longer adjust _tot_width (which is the longest line from any section)
        # TODO This is likely wrong if there are formatters {{}} in the footer, the return should be fully formmated
        foot = self.menu_formatting('footer',
                                    text=line_dict['footer']['lines'],
                                    do_print=False)[0]
        _foot_width = [len(line) for line in foot]
        if isinstance(_tot_width, int):
            _tot_width = [_tot_width]
        _tot_width = max(_foot_width + _tot_width)

        if subhead:
            _subhead_width = [len(line) for line in subhead]
            _tot_width = max(_subhead_width) if max(
                _subhead_width) > _tot_width else _tot_width

        if MIN_WIDTH < self.cols:
            _tot_width = MIN_WIDTH if _tot_width < MIN_WIDTH else _tot_width

        # -- // Generate Final Body Rows \\ --
        # _final_rows = []
        pad = ' ' * col_pad
        _final_rows = [] if not body['sections'] else body['sections'][0]

        for s in body['sections']:
            if body['sections'].index(s) == 0:
                continue
            else:
                if len(_final_rows) > len(s):
                    for _spaces in range(len(_final_rows) - len(s)):
                        s.append(' ' * len(s[0]))
                elif len(s) > len(_final_rows):
                    for _spaces in range(len(s) - len(_final_rows)):
                        _final_rows.append(' ' * len(_final_rows[0]))
                _final_rows = [a + pad + b for a, b in zip(_final_rows, s)]

        # --// PRINT MENU \\--
        if _final_rows:
            _tot_width = len(_final_rows[0]) if len(
                _final_rows[0]) > _tot_width else _tot_width
        else:
            _tot_width = 0
        self.menu_cols = _tot_width  # FOR DEBUGGING
        self.menu_formatting('header',
                             text=header,
                             width=_tot_width,
                             do_print=True)
        if subhead:
            for line in subhead:
                print(line)
                self.menu_rows += 1
        elif not subs:  # TODO remove auto first blank line from subhead/subs and have formatter always do 1st blank line
            print('')  # Add blank line after header if no subhead and no subs
            self.menu_rows += 1
        for row in _final_rows:  # TODO print here, also can print in the formatter method
            print(row)
            self.menu_rows += 1

        # TODO REMOVE TEMP during re-factor
        if isinstance(footer, dict):
            self.menu_formatting('footer',
                                 footer=footer,
                                 width=_tot_width,
                                 do_print=True)
        else:
            self.menu_formatting('footer',
                                 text=footer,
                                 width=_tot_width,
                                 do_print=True)
コード例 #6
0
ファイル: config.py プロジェクト: Pack3tL0ss/ConsolePi
    def get_outlets_from_file(self):
        '''Get outlets defined in config

        returns:
            dict: with following keys (all values are dicts)
                linked: linked outlets from config (linked to serial adapters- auto pwr-on)
                dli_power: dict any dlis in config have all ports represented here
                failures: failure to connect to any outlets will result in an entry here
                    outlet_name: failure description
        '''
        outlet_data = self.cfg_yml.get('POWER')
        if not outlet_data:  # fallback to legacy json config
            outlet_data = self.get_json_file(self.static.get('POWER_FILE'))

        if not outlet_data:
            if self.power:
                log.show('Power Function Disabled - Configuration Not Found')
                self.power = False
            self.outlet_types = []
            return outlet_data

        types = []
        by_dev: Dict[str, Any] = {}
        for k in outlet_data:
            _type = outlet_data[k].get('type').lower()
            relays = [] if _type != "esphome" else utils.listify(
                outlet_data[k].get('relays', k))
            linked = outlet_data[k].get('linked_devs', {})

            if linked:
                outlet_data[k]['linked_devs'] = utils.format_dev(
                    outlet_data[k]['linked_devs'],
                    hosts=self.hosts,
                    with_path=True)
                self.linked_exists = True
                for dev in outlet_data[k]['linked_devs']:
                    if _type == 'dli':
                        self.do_dli_menu = True
                        _this = [
                            f"{k}:{[int(p) for p in utils.listify(outlet_data[k]['linked_devs'][dev])]}"
                        ]
                    elif _type == 'esphome':
                        _linked = utils.listify(
                            outlet_data[k]['linked_devs'][dev])
                        _this = [f'{k}:{[p for p in _linked]}']
                    else:
                        _this = [k]
                    by_dev[dev] = _this if dev not in by_dev else by_dev[
                        dev] + _this
            else:
                outlet_data[k]['linked_devs'] = {}

            if outlet_data[k]["type"].lower() not in types:
                types.append(outlet_data[k]["type"].lower())

            if outlet_data[k]['type'].upper() == 'GPIO' and isinstance(outlet_data[k].get('address'), str) \
                    and outlet_data[k]['address'].isdigit():
                outlet_data[k]['address'] = int(outlet_data[k]['address'])

            # This block determines if we should show dli_menu / if any esphome outlets match criteria to show
            # in dli menu (anytime it has exactly 8 outlets, if it has > 1 relay and not all are linked)
            if not self.do_dli_menu and _type == "esphome" and len(relays) > 1:
                if len(relays) == 8 or not linked:
                    self.do_dli_menu = True
                elif [r for r in relays if f"'{r}'" not in str(linked)]:
                    self.do_dli_menu = True

        self.outlet_types = types
        outlet_data = {
            'defined': outlet_data,
            'linked': by_dev,
            'dli_power': {},
            'esp_power': {},
            'failures': {}
        }

        return outlet_data