def _safe_reboot(self, max_retries: int=3) -> str: t0 = time.time() try_count = 1 while try_count <= max_retries+1 or max_retries < 0: self._logger.debug("Starting reboot attempt "+str(try_count)+" of " + (str(max_retries+1) if max_retries >= 0 else "<unlimited>")+"...") self._logger.debug("Requesting reboot...") self._reboot() sleep_length = max(0, round(self._reboot_delay / 1000.0)) self._logger.debug("Waiting "+str(sleep_length)+" seconds while device reboots...") time.sleep(sleep_length) self._logger.debug("Verifying device is back up and responsive after reboot...") if self._check(): break # This reboot attempt failed. Try again or give up: if try_count >= max_retries+1: raise linkplayctl.APIException("Failed to bring device back up after " + str(max_retries) + " reboot attempts. Giving up.") self._logger.debug("Device is not responding after reboot. Trying again...") try_count = try_count + 1 # Reboot was successful elapsed_time = "{:,}".format(round((time.time()-t0), 1)) if try_count > 1: self._logger.info("Device required " + str(try_count) + " reboots to become responsive." + " Elapsed time: "+elapsed_time+"s") else: self._logger.debug("Device is responsive after first reboot. Elapsed time: " + elapsed_time + "s") return "OK"
def _reboot(self) -> str: """Internal, non-blocking method for performing reboots""" response = self._send("reboot") if response.status_code != 200 or response.content.decode("utf-8") != "OK": raise linkplayctl.APIException("Failed to reboot: " + "Status "+str(response.status_code)+" Content: "+response.content) return response.content.decode("utf-8")
def prompt_off(self) -> str: """Disable voice prompts and notifications""" self._logger.info("Turning voice prompts off...") response = self._send("PromptDisable") if response.status_code != 200: raise linkplayctl.APIException("Failed to disable prompts: Status code="+str(response.status_code)) return response.content.decode("utf-8")
def shutdown(self) -> str: """Request an immediate device shutdown""" self._logger.info("Requesting shutdown...") response = self._send("getShutdown") if response.status_code != 200: raise linkplayctl.APIException("Failed to shutdown: Status code="+str(response.status_code)) return response.content.decode("utf-8")
def _validate_preset(self, number: object) -> int: """Internal method to validate and return preset as an integer between 1 and 6, inclusive""" try: number = int(number) if number < 1 or number > 6: raise linkplayctl.APIException except (ValueError, linkplayctl.APIException): raise linkplayctl.APIException("Preset number must be an integer between 1 and 6, inclusive") return number
def wifi_status(self) -> str: """Get the current status of the WiFi connection""" self._logger.info("Retrieving WiFi connection status...") inverse_wifi_statuses = {v: k for k, v in self._wifi_statuses.items()} response = self._send("wlanGetConnectState").content.decode("utf-8") try: return inverse_wifi_statuses[response] except KeyError: raise linkplayctl.APIException("Received unrecognized wifi status: '"+str(response)+"'")
def wifi_auth(self, auth_type: str = None, new_pass: str = None): """Get or set the network authentication parameters :returns: str Device response (usually "OK") if set, dict of auth values if get """ if auth_type is None: self._logger.info("Retrieving WiFi authentication information...") return {k: v for (k, v) in self._device_info().items() if (k in ["securemode", "auth", "encry", "psk"])} self._logger.info("Setting WiFi authentication type to '"+str(auth_type)+"'" +(" with pass '"+str(new_pass)+"'" if new_pass else "")+"...") try: auth_value = self._auth_types[auth_type] except KeyError: raise linkplayctl.APIException("Authentication type must be one of ["+", ".join(self._auth_types.keys())+"]") if auth_value and not new_pass: raise linkplayctl.APIException("Authentication type '"+str(auth_type)+"' requires a non-empty password") response = self._send("setNetwork:"+str(auth_value)+":"+str(new_pass) if new_pass is not None else "") self._logger.debug("Authentication set. Device is rebooting...") return response.content.decode("utf-8")
def _json_decode(self, s: object) -> object: """Decode the given object as JSON""" try: s = s.content except AttributeError: pass try: s = s.decode("utf-8") except UnicodeDecodeError: pass try: return json.JSONDecoder().decode(str(s)) except ValueError: # json.JSONDecodeError is better for > 3.4 raise linkplayctl.APIException("Expected JSON from API, got: '"+str(s)+"'")
def name(self, name: str = None) -> str: """Get or set the device name to be used for services such as Airplay""" if not name: self._logger.info("Retrieving device name...") return self._device_info().get("DeviceName") self._logger.info("Setting device name to '"+str(name)+"'...") if not isinstance(name, str) or not name: raise AttributeError("Device name must be a non-empty string") response = self._send("setDeviceName:"+name) if response.status_code != 200: raise linkplayctl.APIException("Failed to set device name to '"+name+"'") return response.content.decode("utf-8")
def _loop(self, mode: str = None) -> str: """Internal method to get or set the current looping mode (includes shuffle and repeat setting)""" if mode is None: inverse_loop_modes = {v: k for k, v in self._loop_modes.items()} self._logger.debug("Requesting current loop mode...") loopval = self._player_info().get('loop') self._logger.debug("Current loop mode value is '"+str(loopval)+"'. Mapping to mode names...") try: return inverse_loop_modes[int(loopval)] except KeyError: raise linkplayctl.APIException("Received unknown loop mode value '"+str(loopval)+"' from device") try: value = self._loop_modes[str(mode)] self._logger.debug("Setting loop mode to '"+str(mode)+"' [value: '"+str(value)+"']...") except KeyError: try: value = int(mode) if int(mode) in self._loop_modes.values() else -1 except: raise linkplayctl.APIException("Cannot set unknown loop mode '"+str(mode)+"'") inverse_loop_modes = {v: k for k, v in self._loop_modes.items()} self._logger.debug("Setting loop mode to '"+str(inverse_loop_modes[value])+"' [value: '" +str(value)+"']...") return self._send('setPlayerCmd:loopmode:'+str(value)).content.decode("utf-8")
def equalizer(self, mode: str=None) -> str: """Get or set the equalizer mode""" if mode is None: self._logger.info("Retrieving current equalizer setting...") inverse_eq_modes = {v: k for k, v in self._equalizer_modes.items()} value = self._json_decode(self._send("getEqualizer")) try: mode = inverse_eq_modes[value] except KeyError: raise linkplayctl.APIException("Received unknown equalizer mode value '"+str(value)+"'") self._logger.info("Received equalizer mode '"+mode+"' (value "+str(value)+")") return mode self._logger.info("Setting equalizer to '"+str(mode)+"'...") try: mode_value = self._equalizer_modes[mode] except KeyError: eq_values = ' '.join(self._equalizer_modes) raise AttributeError("Equalizer mode must be one of ["+eq_values+"], not '"+str(mode)+"'") self._logger.info("Equalizer mode '" + str(mode)+"' maps to value "+str(mode_value)) response = self._send("setPlayerCmd:equalizer:"+str(mode_value)) if response.status_code != 200: raise linkplayctl.APIException("Failed to set equalizer to mode '"+str(mode)+"' (value '"+str(mode_value)+"'") return response.content.decode("utf-8")
def quiet_reboot(self) -> str: """Reboot the device quietly, i.e., without boot jingle. Returns when complete, usually ~120 seconds.""" t0 = time.time() sleep_length = max(0, round(self._reboot_delay/1000.0)) self._logger.info("Requesting quiet reboot...") if self._reboot_delay > 5000: self._logger.info("Note: This request may take "+str(sleep_length)+" seconds or more to finish") self._logger.debug("Getting current volume...") old_volume = self._volume() self._logger.debug("Saving current volume '"+str(old_volume)+"' and setting new volume to '" + str(self._quiet_reboot_volume)+"'...") self._volume(self._quiet_reboot_volume) self._logger.debug("Verifying volume has been correctly set to minimum...") if int(self._volume()) != self._quiet_reboot_volume: raise linkplayctl.APIException("Failed to set volume to minimum before quiet reboot") self._safe_reboot() self._logger.debug("Restoring previous volume '" + str(old_volume) + "'...") self._volume(old_volume) self._logger.debug("Confirming new volume is set to '" + str(old_volume) + "'...") if old_volume != int(self._volume()): raise linkplayctl.APIException("Failed to restore old volume '"+str(old_volume)+"' after reboot") elapsed_time = "{:,}".format(round((time.time()-t0)*1000, 1)) self._logger.debug("Quiet reboot complete. Elapsed time: "+str(elapsed_time)+"ms") return "OK"
def _volume(self, value: object = None): """ Internal method to get/set volume to an absolute value between 0 and 100 or a relative value -100 to +100 :returns: int volume, or "OK" on volume set """ if value is None: return int(self._player_info().get("vol")) try: if isinstance(value, str) and (value.startswith('+') or value.startswith('-')): self._logger.debug("Adjusting volume by " + str(value) + ". Getting old volume...") new_volume = max(0, min(100, self._volume()+int(math.floor(float(value))))) self._logger.debug("Adjusting volume "+str(value)+" to "+str(new_volume)+"...") else: new_volume = max(0, min(100, int(math.floor(float(value))))) self._logger.debug("Setting volume to " + str(int(new_volume))) except ValueError: raise AttributeError("Volume must be between 0 and 100 or -100 to +100, inclusive, not '"+str(value)+"'") response = self._send("setPlayerCmd:vol:" + str(new_volume)) if response.status_code != 200: raise linkplayctl.APIException("Failed to set volume to '"+str(new_volume)+"'") return response.content.decode("utf-8")