def confirm_and_spin(self, action_dict, *args, **kwargs): """ called by menu_exec. Collects user Confirmation if operation warrants it (Powering off or cycle outlets) and Generates appropriate spinner text returns tuple 0: Bool False indicates user abort otherwise True should be returned 1: str spinner_text used in menu_exec while function runs 3: str name (for rename operation) """ pwr = self.pwr menu = self.menu _func = action_dict["function"].__name__ _off_str = "{{red}}OFF{{norm}}" _on_str = "{{green}}ON{{norm}}" _cycle_str = "{{red}}C{{green}}Y{{red}}C{{green}}L{{red}}E{{norm}}" _type = _addr = None to_state = kwargs.get("desired_state") if _func in ["pwr_toggle", "pwr_cycle", "pwr_rename"]: _type = args[0].lower() _addr = args[1] _grp = action_dict["key"] if _type == "dli": port = kwargs["port"] if not port == "all": port_name = pwr.data["dli_power"][_addr][port]["name"] to_state = not pwr.data["dli_power"][_addr][port]["state"] elif _type == "esphome": port = port_name = kwargs["port"] if not port == "all": to_state = not pwr.data['defined'][_grp]['is_on'][port]['state'] else: port = f"{_type}:{_addr}" port_name = _grp to_state = not pwr.data["defined"][_grp]["is_on"] if _type == "dli" or _type == "tasmota" or _type == "esphome": host_short = utils.get_host_short(_addr) else: host_short = None prompt = spin_text = name = confirmed = None # init if _func == "pwr_all": if kwargs["action"] == "cycle": prompt = "{} Power Cycle All Powered {} Outlets".format( "" if _type is None else _type + ":" + host_short, _on_str ) spin_text = "Cycling All{} Ports".format( "" if _type is None else " " + _type + ":" + host_short ) elif kwargs["action"] == "toggle": if not kwargs["desired_state"]: prompt = "Power All{} Outlets {}".format( "" if _type is None else " " + _type + ":" + host_short, _off_str, ) spin_text = "Powering {} ALL{} Outlets".format( menu.format_line(kwargs["desired_state"]).text, "" if _type is None else _type + " :" + host_short, ) elif _func == "pwr_toggle": if _type == "dli" and port == "all": prompt = "Power {} ALL {} Outlets".format( _off_str if not to_state else _on_str, host_short ) elif not to_state: if _type == "dli": prompt = f"Power {_off_str} {host_short} Outlet {port}({port_name})" elif _type == "esphome": prompt = f"Power {_off_str} {host_short} Outlet {port}" else: # GPIO or TASMOTA prompt = f"Power {_off_str} Outlet {_grp}({_type}:{_addr})" spin_text = "Powering {} {}Outlet{}".format( menu.format_line(to_state).text, "ALL " if port == "all" else "", "s" if port == "all" else "", ) elif _func == "pwr_rename": try: name = input( "New name for{} Outlet {}: ".format( " " + host_short if host_short else "", port_name if not _type == "dli" else str(port) + "(" + port_name + ")", ) ) except KeyboardInterrupt: name = None confirmed = False print("") # So header doesn't print on same line as aborted prompt when DEBUG is on if name: old_name = port_name _rnm_str = "{red}{old_name}{norm} --> {green}{name}{norm}".format( red="{{red}}", green="{{green}}", norm="{{norm}}", old_name=old_name, name=name, ) if _type == "dli": prompt = "Rename {} Outlet {}: {} ".format( host_short, port, _rnm_str ) else: old_name = _grp prompt = "Rename Outlet {}:{} {} ".format( _type, host_short, _rnm_str ) spin_text = "Renaming Port" elif _func == "pwr_cycle": if _type == "dli" and port == "all": prompt = "Power {} ALL {} Outlets".format(_cycle_str, host_short) elif _type == "dli": prompt = "Cycle Power on {} Outlet {}({})".format( host_short, port, port_name ) elif _type == "esphome": _msg = f"{_grp}({host_short})" if _grp != host_short else f"{_grp}" prompt = f"Cycle Power on {_msg} Outlet {port}" else: # GPIO or TASMOTA prompt = "Cycle Power on Outlet {}({})".format(port_name, port) spin_text = "Cycling {}Outlet{}".format( "ALL " if port == "all" else "", "s" if port == "all" else "" ) if prompt: prompt = menu.format_line(prompt).text confirmed = ( confirmed if confirmed is not None else utils.user_input_bool(prompt) ) else: if _func != "pwr_rename": confirmed = True return confirmed, spin_text, name
def menu_exec(self, choice, menu_actions, calling_menu="main_menu"): '''Execute Menu Selection. This method needs to be overhauled. The ConsolePiAction object defined but not used in __init__ is part of the plan for overhaul menu will build insance of the object for each selection. That will be used to determine what action to perform and what to do after etc. ''' pwr = self.pwr if not config.debug and calling_menu not in ["dli_menu", "power_menu"]: os.system("clear") if ( not choice.lower or choice.lower in menu_actions and menu_actions[choice.lower] is None ): ( self.menu.rows, self.menu.cols, ) = ( utils.get_tty_size() ) # re-calc tty size in case they've adjusted the window return else: ch = choice.lower try: # Invalid Selection if isinstance(menu_actions[ch], dict): if menu_actions[ch].get("cmd"): # TimeStamp for picocom session log file if defined menu_actions[ch]["cmd"] = menu_actions[ch]["cmd"].replace( "{{timestamp}}", time.strftime("%F_%H.%M") ) # -- // AUTO POWER ON LINKED OUTLETS \\ -- if ( config.power and "pwr_key" in menu_actions[ch] ): self.exec_auto_pwron(menu_actions[ch]["pwr_key"]) # -- // Print pre-connect messsge if provided \\ -- if menu_actions[ch].get("pre_msg"): print(menu_actions[ch]["pre_msg"]) # --// execute the command \\-- try: _error = None if "exec_kwargs" in menu_actions[ch]: c = menu_actions[ch]["cmd"] _error = utils.do_shell_cmd( c, **menu_actions[ch]["exec_kwargs"] ) if _error and self.autopwr_wait: # TODO simplify this after moving to action object _method = "ssh -t" if "ssh" in c else "telnet" if "ssh" in _method: _h = ( c.split(f"{_method} ")[1] .split(" -p")[0] .split("@")[1] ) _p = int(c.split("-p ")[1]) elif _method == "telnet": c_list = c.split() _h = c_list[-2] _p = int(c_list[-1]) def wait_for_boot(): while True: try: if utils.is_reachable(_h, _p, silent=True): break else: time.sleep(3) except KeyboardInterrupt: self.autopwr_wait = False log.show("Connection Aborted") break print( "\nInitial Attempt Failed, but host is linked to an outlet that was" ) print("off. Host may still be booting\n") utils.spinner( f"Waiting for {_h} to boot, CTRL-C to Abort", wait_for_boot, ) if self.autopwr_wait: _error = utils.do_shell_cmd(c, **menu_actions[ch]["exec_kwargs"]) self.autopwr_wait = False else: c = shlex.split(menu_actions[ch]["cmd"]) result = subprocess.run(c, stderr=subprocess.PIPE) _stderr = result.stderr.decode("UTF-8") if _stderr or result.returncode == 1: _error = utils.error_handler(c, _stderr) if _error: log.show(_error) # -- // resize the terminal to handle serial connections that jack the terminal size \\ -- c = " ".join([str(i) for i in c]) if "picocom" in c: os.system( "/etc/ConsolePi/src/consolepi-commands/resize >/dev/null" ) except KeyboardInterrupt: log.show("Aborted last command based on user input") elif "function" in menu_actions[ch]: args = ( menu_actions[ch]["args"] if "args" in menu_actions[ch] else [] ) kwargs = ( menu_actions[ch]["kwargs"] if "kwargs" in menu_actions[ch] else {} ) confirmed, spin_text, name = self.confirm_and_spin( menu_actions[ch], *args, **kwargs ) if confirmed: # update kwargs with name from confirm_and_spin method if menu_actions[ch]["function"].__name__ == "pwr_rename": kwargs["name"] = name # // -- CALL THE FUNCTION \\-- if ( spin_text ): # start spinner if spin_text set by confirm_and_spin with Halo(text=spin_text, spinner="dots2"): response = menu_actions[ch]["function"]( *args, **kwargs ) else: # no spinner response = menu_actions[ch]["function"](*args, **kwargs) # --// Power Menus \\-- if calling_menu in ["power_menu", "dli_menu"]: if menu_actions[ch]["function"].__name__ == "pwr_all": with Halo( text="Refreshing Outlet States", spinner="dots" ): self.outlet_update( refresh=True, upd_linked=True ) # TODO can I move this to Outlets Class else: _grp = menu_actions[ch]["key"] _type = menu_actions[ch]["args"][0] _addr = menu_actions[ch]["args"][1] # --// EVAL responses for dli outlets \\-- if _type == "dli": host_short = utils.get_host_short(_addr) _port = menu_actions[ch]["kwargs"]["port"] # --// Operations performed on ALL outlets \\-- if ( isinstance(response, bool) and _port is not None ): if ( menu_actions[ch]["function"].__name__ == "pwr_toggle" ): self.spin.start( "Request Sent, Refreshing Outlet States" ) # threading.Thread(target=self.get_dli_outlets, # kwargs={'upd_linked': True, 'refresh': True}, name='pwr_toggle_refresh').start() upd_linked = ( True if calling_menu == "power_menu" else False ) # else dli_menu threading.Thread( target=self.outlet_update, kwargs={ "upd_linked": upd_linked, "refresh": True, }, name="pwr_toggle_refresh", ).start() if _grp in pwr.data["defined"]: pwr.data["defined"][_grp]["is_on"][ _port ]["state"] = response elif _port != "all": pwr.data["dli_power"][_addr][_port][ "state" ] = response else: # dli toggle all for t in threading.enumerate(): if ( t.name == "pwr_toggle_refresh" ): t.join() # if refresh thread is running join ~ # wait for it to complete. # TODO Don't think this works or below # wouldn't have been necessary. # toggle all returns True (ON) or False (OFF) if command # successfully sent. In reality the ports # may not be in the state yet, but dli is working it. # Update menu items to reflect end state for p in pwr.data[ "dli_power" ][_addr]: pwr.data["dli_power"][ _addr ][p]["state"] = response break self.spin.stop() # Cycle operation returns False if outlet is off, only valid on powered outlets elif ( menu_actions[ch]["function"].__name__ == "pwr_cycle" and not response ): log.show( f"{host_short} Port {_port} is Off. Cycle is not valid" ) elif ( menu_actions[ch]["function"].__name__ == "pwr_rename" ): if response: _name = pwr._dli[_addr].name(_port) if _grp in pwr.data.get( "defined", {} ): pwr.data["defined"][_grp][ "is_on" ][_port]["name"] = _name else: threading.Thread( target=self.outlet_update, kwargs={ "upd_linked": True, "refresh": True, }, name="pwr_rename_refresh", ).start() pwr.data["dli_power"][_addr][_port][ "name" ] = _name # --// str responses are errors append to error_msgs \\-- # TODO refactor response to use new cpi.response(...) elif ( isinstance(response, str) and _port is not None ): log.show(response) # --// Can Remove After Refactoring all responses to bool or str \\-- elif isinstance(response, int): if ( menu_actions[ch]["function"].__name__ == "pwr_cycle" and _port == "all" ): if response != 200: log.show( "Error Response Returned {}".format( response ) ) # This is a catch as for the most part I've tried to refactor so the pwr library # returns port state on success (True/False) else: if response in [200, 204]: log.show( "DEV NOTE: check pwr library ret=200 or 204" ) else: _action = menu_actions[ch][ "function" ].__name__ log.show( f"Error returned from dli {host_short} when " f"attempting to {_action} port {_port}" ) # --// EVAL responses for espHome outlets \\-- elif _type == "esphome": host_short = utils.get_host_short(_addr) _port = menu_actions[ch]["kwargs"]["port"] # --// Operations performed on ALL outlets \\-- if (isinstance(response, bool) and _port is not None): pwr.data['defined'][_grp]['is_on'][_port]['state'] = response if ( menu_actions[ch]["function"].__name__ == "pwr_cycle" and not response ): _msg = f"{_grp}({host_short})" if _grp != host_short else f"{_grp}" if _msg != _port: _msg = f"{_msg} Port {_port} is Off. Cycle is not valid" else: _msg = f"{_msg} is Off. Cycle is not valid" log.show(_msg) elif (isinstance(response, str) and _port is not None): log.show(response) # --// EVAL responses for GPIO and tasmota outlets \\-- else: if ( menu_actions[ch]["function"].__name__ == "pwr_toggle" ): if _grp in pwr.data.get("defined", {}): if isinstance(response, bool): pwr.data["defined"][_grp][ "is_on" ] = response else: pwr.data["defined"][_grp][ "errors" ] = response elif ( menu_actions[ch]["function"].__name__ == "pwr_cycle" and not response ): log.show( "Cycle is not valid for Outlets in the off state" ) elif ( menu_actions[ch]["function"].__name__ == "pwr_rename" ): log.show( "rename not yet implemented for {} outlets".format( _type ) ) elif calling_menu in ["key_menu", "rename_menu"]: if response: log.show(response) else: # not confirmed log.show("Operation Aborted by User") elif menu_actions[ch].__name__ in ["power_menu", "dli_menu"]: menu_actions[ch](calling_menu=calling_menu) else: menu_actions[ch]() except KeyError as e: if len(choice.orig) <= 2 or not self.exec_shell_cmd(choice.orig): log.show(f"Invalid selection {e}, please try again.") return True