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
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
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
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
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)
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