def copy_modify_alarm(speaker, action, args, soco_function, use_local_speaker_list): alarm_id = args[0] alarm_parms = args[1] # Find the alarm alarms = soco.alarms.get_alarms(speaker) for alarm in alarms: if alarm_id == alarm.alarm_id: break else: error_report( "Alarm ID '{}' not found; use the 'alarms' action to find the integer ID".format( alarm_id ) ) return False # Create a new alarm from the existing one new_alarm = copy(alarm) new_alarm._alarm_id = None new_alarm.zone = speaker # Apply modifications if not _modify_alarm_object(new_alarm, alarm_parms): return False # Save the new alarm try: new_alarm.save() except soco.exceptions.SoCoUPnPException: error_report("Failed to copy/move alarm; did you modify the start time?") return False return True
def parse_m3u(m3u_file: str) -> List[Track]: with open(m3u_file, "r") as infile: # Parse file contents. Files with an M3U/M3U8 extension must follow conventions. if m3u_file.lower().endswith(".m3u") or m3u_file.lower().endswith( ".m3u8"): line = infile.readline() if not line.startswith("#EXTM3U"): error_report( "File '{}' lacks '#EXTM3U' as first line".format(m3u_file)) return [] playlist = [] song = Track(None, None, None) for line in infile: line = line.strip() if line.startswith("#EXTINF:"): # pull length and title from #EXTINF line length, title = line.split("#EXTINF:")[1].split(",", 1) song = Track(length, title, None) elif line.startswith("#"): # Comment line pass elif len(line) != 0: # pull song path from all other, non-blank lines song.path = line playlist.append(song) # reset the song variable so it doesn't use the same EXTINF more than once song = Track(None, None, None) return playlist
def modify_alarm(speaker, action, args, soco_function, use_local_speaker_list): alarm_ids = args[0].lower().split(",") all_alarms = soco.alarms.get_alarms(speaker) if alarm_ids[0] == "all": alarms = set(all_alarms) else: alarms = set() for alarm_id in alarm_ids: for alarm in all_alarms: if alarm_id == alarm.alarm_id: alarms.add(alarm) break else: print("Alarm ID '{}' not found".format(alarm_id)) for index, alarm in enumerate(alarms): if not _modify_alarm_object(alarm, args[1]): continue try: logging.info("Saving alarm '{}'".format(alarm.alarm_id)) alarm.save() if index < len(alarms) - 1: # Allow alarm update to stabilise logging.info( "Waiting {}s after saving alarm '{}'".format( ALARM_SAVE_DELAY, alarm.alarm_id ) ) time.sleep(ALARM_SAVE_DELAY) except soco.exceptions.SoCoUPnPException: error_report("Failed to modify alarm {}".format(alarm.alarm_id)) continue return True
def add_alarm(speaker, action, args, soco_function, use_local_speaker_list): new_alarm = soco.alarms.Alarm(zone=speaker) if not _modify_alarm_object(new_alarm, args[0]): return False try: new_alarm.save() except soco.exceptions.SoCoUPnPException: error_report("Failed to create alarm") return False return True
def process_wait(sequence: List): if sequence[0] in ["wait", "wait_for"]: duration = 0 if len(sequence) != 2: error_report( "Action 'wait' requires 1 parameter (check spaces around the ':' separator?)" ) return action = sequence[1].lower() try: duration = convert_to_seconds(action) except ValueError: error_report( "Action 'wait' requires positive number of hours, seconds or minutes + 'h/m/s', or HH:MM(:SS)" ) logging.info("Waiting for {}s".format(duration)) time.sleep(duration) # Special case: the 'wait_until' action elif sequence[0] in ["wait_until"]: if len(sequence) != 2: error_report( "'wait_until' requires 1 parameter (check spaces around the ':' separator?)" ) return try: action = sequence[1].lower() duration = seconds_until(action) logging.info("Waiting for {}s".format(duration)) time.sleep(duration) except ValueError: error_report( "'wait_until' requires parameter: time in 24hr HH:MM(:SS) format" )
def play_local_file(speaker: SoCo, pathname: str, end_on_pause: bool = False) -> bool: if not path.exists(pathname): error_report("File '{}' not found".format(pathname)) return False directory, filename = path.split(pathname) if not is_supported_type(filename): error_report( "Unsupported file type; must be one of: {}".format(SUPPORTED_TYPES) ) return False # Make filename compatible with URL naming url_filename = urllib.parse.quote(filename) server_ip = get_server_ip(speaker) if not server_ip: error_report("Can't determine an IP address for web server") return False logging.info("Using server IP address: {}".format(server_ip)) # Start the webserver (runs in a daemon thread) speaker_ips = [] for zone in speaker.all_zones: speaker_ips.append(zone.ip_address) httpd = http_server(server_ip, directory, url_filename, speaker_ips) if not httpd: error_report("Cannot create HTTP server") return False # This ensures that other running invocations of 'play_file' # receive their stop events, and terminate. logging.info("Stopping speaker '{}'".format(speaker.player_name)) speaker.stop() # Assemble the URI uri = "http://" + server_ip + ":" + str(httpd.server_port) + "/" + url_filename logging.info("Playing file '{}' from directory '{}'".format(filename, directory)) logging.info("Playback URI: {}".format(uri)) logging.info("Send URI to '{}' for playback".format(speaker.player_name)) speaker.play_uri(uri) logging.info("Setting flag to stop playback on CTRL-C") set_speaker_playing_local_file(speaker) logging.info("Waiting 1s for playback to start") time.sleep(1.0) logging.info("Waiting for playback to stop") wait_until_stopped(speaker, uri, end_on_pause) logging.info("Playback stopped ... terminating web server") httpd.shutdown() logging.info("Web server terminated") set_speaker_playing_local_file(None) return True
def wait_until_stopped(speaker: SoCo, uri: str, end_on_pause: bool): playing_states = ["PLAYING", "TRANSITIONING"] if not end_on_pause: playing_states.append("PAUSED_PLAYBACK") logging.info("Playing states = {}".format(playing_states)) try: sub = speaker.avTransport.subscribe(auto_renew=True) remember_event_sub(sub) except Exception as e: error_report("Exception {}".format(e)) return while True: try: event = sub.events.get(timeout=1.0) state = event.variables["transport_state"] logging.info("Event received: playback state = '{}'".format(state)) if state not in playing_states: logging.info( "Speaker '{}' in state '{}'".format( speaker.player_name, event.variables["transport_state"] ) ) break # Check that the expected URI is still playing try: current_uri = event.variables["current_track_meta_data"].get_uri() except: # Can only call get_uri() on certain datatypes current_uri = "" if current_uri != uri: logging.info("Playback URI changed: exit event wait loop") break except: pass event_unsubscribe(sub) forget_event_sub(sub) return
def play_directory_files(speaker: SoCo, directory: str, options: str = "") -> bool: """Play all the valid audio files in a directory. Ignores subdirectories""" tracks = [] try: with scandir(directory) as files: for file in files: if is_supported_type(file.name): tracks.append(path.abspath(path.join(directory, file.name))) except FileNotFoundError: error_report("Directory '{}' not found".format(directory)) return False tracks.sort() logging.info("Files to to play: {}".format(tracks)) play_file_list(speaker, tracks, options) return True
def play_m3u_file(speaker: SoCo, m3u_file: str, options: str = "") -> bool: if not path.exists(m3u_file): error_report("File '{}' not found".format(m3u_file)) return False logging.info("Parsing file contents'{}'".format(m3u_file)) track_list = parse_m3u(m3u_file) if len(track_list) == 0: error_report("No tracks found in '{}'".format(m3u_file)) return False directory, _ = path.split(m3u_file) if directory != "": chdir(directory) tracks = [str(Path(track.path).absolute()) for track in track_list] # type:ignore logging.info("Files to to play: {}".format(tracks)) play_file_list(speaker, tracks, options) return True
def move_or_copy_alarm(speaker, alarm_id, copy=True): alarms = soco.alarms.get_alarms(speaker) for alarm in alarms: if alarm_id == alarm.alarm_id: break else: error_report("Alarm ID '{}' not found".format(alarm_id)) return False if alarm.zone == speaker: error_report("Cannot copy/move an alarm to the same speaker") return False alarm.zone = speaker if copy is True: alarm._alarm_id = None try: alarm.save() except soco.exceptions.SoCoUPnPException: error_report("Failed to copy/move alarm") return False if copy is True: print("Alarm ID '{}' created".format(alarm.alarm_id)) return True
def get_latest_version() -> Union[str, None]: try: file = urlopen(init_file_url, timeout=3.0) except Exception as e: error_report( "Unable to get latest version information from GitHub: {}".format(e) ) return None for line in file: decoded_line = line.decode("utf-8") if "__version__" in decoded_line: latest_version = ( decoded_line.replace("__version__ = ", "") .replace('"', "") .replace("\n", "") ) logging.info("Latest version is v{}".format(latest_version)) break else: logging.info("Unable to find latest version") return None return latest_version
def snooze_alarm(speaker, action, args, soco_function, use_local_speaker_list): """Snooze an alarm that's currently playing""" duration = args[0].lower() # HH:MM:SS format h_m_s = duration.split(":") if len(h_m_s) == 3: try: if not ( 0 <= int(h_m_s[0]) <= 23 and 0 <= int(h_m_s[1]) <= 59 and 0 <= int(h_m_s[2]) <= 59 ): raise ValueError except (ValueError, TypeError): logging.info("Invalid snooze duration: '{}'".format(args[0])) parameter_type_error( action, "A valid HH:MM:SS duration, or an integer number of minutes", ) return False # Simple 'Nm' or 'N' for N minutes of snooze else: try: duration = abs(int(duration.replace("m", ""))) minutes = str(duration % 60).zfill(2) hours = str(int(duration / 60)).zfill(2) duration = hours + ":" + minutes + ":00" except ValueError: logging.info("Invalid snooze duration: '{}'".format(args[0])) parameter_type_error( action, "An integer number of minutes, or HH:MM:SS format", ) return False logging.info("Sending snooze command using duration '{}'".format(duration)) try: speaker.avTransport.SnoozeAlarm([("InstanceID", 0), ("Duration", duration)]) except SoCoUPnPException as error: logging.info("Exception: {}".format(error)) if error.error_code == "701": error_report("Can only snooze a playing alarm") elif error.error_code == "402": error_report("Invalid snooze duration: '{}'".format(duration)) else: error_report("{}".format(error)) return False return True
def main(): # Create the argument parser parser = argparse.ArgumentParser( prog="sonos-discover", usage="%(prog)s", description="Sonos speaker discovery utility", ) parser.add_argument( "--print", "-p", action="store_true", default=False, help= "Print the contents of the current speaker information file, and exit", ) parser.add_argument( "--delete-local-speaker-cache", "-d", action="store_true", default=False, help="Delete the local speaker cache, if it exists", ) parser.add_argument( "--subnets", type=str, help= "Specify the networks or IP addresses to search, in dotted decimal/CIDR format", ) # The rest of the optional args are common configure_common_args(parser) # Parse the command line args = parser.parse_args() configure_logging(args.log) if args.version: version() exit(0) if args.docs: docs() exit(0) if args.logo: logo() exit(0) if args.check_for_update: print_update_status() exit(0) # Create the Speakers object speaker_list = Speakers() if args.print: if speaker_list.load(): speaker_list.print() exit(0) else: error_report("No current speaker data") if args.delete_local_speaker_cache: try: file = speaker_list.remove_save_file() print("Removed file: {}".format(file)) exit(0) except Exception: error_report("No current speaker data file") # Parameter validation for various args message = check_args(args) if message: error_report(message) speaker_list._network_threads = args.network_discovery_threads speaker_list._network_timeout = args.network_discovery_timeout speaker_list._min_netmask = args.min_netmask if args.subnets is not None: speaker_list.subnets = args.subnets.split(",") try: speaker_list.discover() saved = speaker_list.save() speaker_list.print() if saved: print("Saved speaker data at: {}\n".format( speaker_list.save_pathname)) else: print( "No speakers discovered. No cache data saved or overwritten.") except Exception as e: error_report(str(e))
error_report(message) speaker_list._network_threads = args.network_discovery_threads speaker_list._network_timeout = args.network_discovery_timeout speaker_list._min_netmask = args.min_netmask if args.subnets is not None: speaker_list.subnets = args.subnets.split(",") try: speaker_list.discover() saved = speaker_list.save() speaker_list.print() if saved: print("Saved speaker data at: {}\n".format( speaker_list.save_pathname)) else: print( "No speakers discovered. No cache data saved or overwritten.") except Exception as e: error_report(str(e)) if __name__ == "__main__": # Catch all untrapped exceptions try: main() exit(0) except Exception as error: error_report(str(error)) exit(1)
def play_file_list(speaker: SoCo, tracks: List[str], options: str = "") -> bool: """Play a list of files (tracks) with absolute pathnames.""" options = options.lower() # Check for invalid options invalid = set(options) - set("psri") if invalid: error_report("Invalid option(s) '{}' supplied".format(invalid)) return False if options != "": # Grab back stdout from api.run_command() sys.stdout = sys.__stdout__ if "r" in options: # Choose a single random track track = choice(tracks) tracks = [track] logging.info("Choosing random track: {}".format(track)) elif "s" in options: logging.info("Shuffling playlist") # For some reason, 'shuffle(tracks)' does not work tracks = sample(tracks, len(tracks)) # Interactive mode keypress_process = None if "i" in options: print("Interactive mode actions: (N)ext, (P)ause, (R)esume + RETURN") try: logging.info("Interactive mode ... starting keypress process") keypress_process = Process(target=interaction_manager, args=(speaker.ip_address, ), daemon=True) keypress_process.start() logging.info("Process PID {} created".format(keypress_process.pid)) except Exception as e: logging.info("Exception ignored: {}".format(e)) keypress_process = None zero_pad = len(str(len(tracks))) for index, track in enumerate(tracks): if not path.exists(track): print("Error: file not found:", track) continue if not is_supported_type(track): print("Error: unsupported file type:", track) continue if "p" in options: print( "Playing {} of {}:".format( str(index + 1).zfill(zero_pad), len(tracks)), track, ) play_local_file(speaker, track) if keypress_process: keypress_process.terminate() return True
def main(): # Create the argument parser parser = argparse.ArgumentParser( prog="sonos", usage="%(prog)s <options> SPEAKER_NAME_OR_IP ACTION <parameters> < : ...>", description="Command line utility for controlling Sonos speakers", ) # A variable number of arguments depending on the action parser.add_argument( "parameters", nargs="*", help="Sequences of SPEAKER ACTION <parameters> : ..." ) # Optional arguments parser.add_argument( "--use-local-speaker-list", "-l", action="store_true", default=False, help="Use the local speaker list instead of SoCo discovery", ) parser.add_argument( "--refresh-local-speaker-list", "-r", action="store_true", default=False, help="Refresh the local speaker list", ) parser.add_argument( "--actions", action="store_true", default=False, help="Print the list of available actions", ) parser.add_argument( "--commands", action="store_true", default=False, help="Print the list of available actions", ) parser.add_argument( "--interactive", "-i", action="store_true", default=False, help="Enter interactive mode", ) parser.add_argument( "--no-env", action="store_true", default=False, help="Ignore the 'SPKR' environment variable, if set", ) parser.add_argument( "--sk", action="store_true", default=False, help="Enter single keystroke mode in the interactive shell", ) parser.add_argument( "--save_aliases", type=str, help="Save the current shell aliases to the supplied filename and exit", ) parser.add_argument( "--load_aliases", type=str, help="Load shell aliases from the supplied filename and exit (aliases are merged)", ) parser.add_argument( "--overwrite_aliases", type=str, help="Overwrite current shell aliases with those from the supplied filename and exit", ) # The rest of the optional args are common configure_common_args(parser) # Parse the command line args = parser.parse_args() configure_logging(args.log) signals = [SIGINT, SIGTERM] logging.info("Setting up handlers for: {}".format(signals)) for sig in signals: signal(sig, sig_handler) if args.version: version() exit(0) if args.docs: docs() exit(0) if args.logo: logo() exit(0) if args.check_for_update: print_update_status() exit(0) if args.actions or args.commands: list_actions() exit(0) if args.save_aliases: am = AliasManager() am.load_aliases() if am.save_aliases_to_file(args.save_aliases): print("Saved shell aliases to '{}'".format(args.save_aliases), flush=True) else: print( "Failed to save shell aliases to '{}'".format(args.save_aliases), flush=True, ) exit(0) if args.load_aliases: am = AliasManager() am.load_aliases() if am.load_aliases_from_file(args.load_aliases): print( "Loaded and merged shell aliases from '{}'".format(args.load_aliases), flush=True, ) else: print( "Failed to load shell aliases from '{}'".format(args.load_aliases), flush=True, ) exit(0) if args.overwrite_aliases: am = AliasManager() if am.load_aliases_from_file(args.overwrite_aliases): print( "Loaded and saved shell aliases from '{}'".format( args.overwrite_aliases ), flush=True, ) else: print( "Failed to load shell aliases from '{}'".format(args.overwrite_aliases), flush=True, ) exit(0) if len(args.parameters) == 0 and not args.interactive: print( "No parameters supplied. Use 'sonos --help' for usage information.", flush=True, ) exit(1) message = check_args(args) if message: error_report(message) use_local_speaker_list = args.use_local_speaker_list env_local = env.get(ENV_LOCAL) if env_local is not None: if env.get(ENV_LOCAL).lower() == "true" and not args.no_env: logging.info( "Env. var. '{}' set to 'TRUE ... using local speaker list".format( ENV_LOCAL ) ) use_local_speaker_list = True if use_local_speaker_list: speaker_list = Speakers( network_threads=args.network_discovery_threads, network_timeout=args.network_discovery_timeout, min_netmask=args.min_netmask, ) if args.refresh_local_speaker_list or not speaker_list.load(): logging.info("Start speaker discovery") speaker_list.discover() speaker_list.save() set_speaker_list(speaker_list) else: # Create the local speaker cache in the utils module create_speaker_cache( max_threads=args.network_discovery_threads, scan_timeout=args.network_discovery_timeout, min_netmask=args.min_netmask, ) # Is $SPKR set in the environment? env_speaker = None if not args.no_env: env_speaker = env.get(ENV_SPKR) if env_speaker: logging.info("Found 'SPKR' environment variable: '{}'".format(env_speaker)) else: logging.info("No 'SPKR' environment variable set") if args.interactive: sk = bool(args.sk) speaker_name = None if len(args.parameters): speaker_name = args.parameters[0] interactive_loop( speaker_name, args.log, use_local_speaker_list=use_local_speaker_list, no_env=args.no_env, single_keystroke=sk, ) exit(0) cli_parser = CLIParser() cli_parser.parse(args.parameters) sequences = cli_parser.get_sequences() cumulative_exit_code = 0 # Loop through processing command sequences logging.info("Found {} action sequence(s): {}".format(len(sequences), sequences)) rewindable_sequences = RewindableList(sequences) loop_iterator = None sequence_pointer = 0 # There is a notional 'loop' action before the first command sequence loop_pointer = -1 loop_start_time = None loop_duration = None # Keep track of SPKR environment label insertions, to avoid repeats # when looping env_spkr_inserted = [False for i in range(len(rewindable_sequences))] for sequence in rewindable_sequences: try: speaker_name = sequence[0] # Special case: the 'loop_to_start' action if speaker_name.lower() == "loop_to_start": if len(sequence) != 1: error_report("Action 'loop_to_start' takes no parameters") # Reset pointers, rewind and continue loop_pointer = -1 sequence_pointer = 0 logging.info("Rewind to start of command sequences") rewindable_sequences.rewind() continue # Special case: the 'loop' action if speaker_name.lower() == "loop": if len(sequence) == 2: if loop_iterator is None: try: loop_iterator = int(sequence[1]) if loop_iterator <= 0: raise ValueError logging.info( "Looping for {} iteration(s)".format(loop_iterator) ) except ValueError: error_report( "Action 'loop' takes no parameters, or a number of iterations (> 0)" ) cumulative_exit_code += 1 continue loop_iterator -= 1 logging.info("Loop iterator countdown = {}".format(loop_iterator)) if loop_iterator <= 0: # Reset variables, stop iteration and continue loop_iterator = None loop_pointer = sequence_pointer sequence_pointer += 1 continue logging.info("Rewinding to command number {}".format(loop_pointer + 2)) rewindable_sequences.rewind_to(loop_pointer + 1) sequence_pointer = loop_pointer + 1 continue # Special case: the 'loop_for' action if speaker_name.lower() == "loop_for": if len(sequence) != 2: error_report( "Action 'loop_for' requires one parameter (check spaces around the ':' separator)" ) if loop_start_time is None: loop_start_time = time.time() try: loop_duration = convert_to_seconds(sequence[1]) except ValueError: error_report( "Action 'loop_for' requires one parameter (duration >= 0)" ) cumulative_exit_code += 1 logging.info( "Starting action 'loop_for' for duration {}s".format( loop_duration ) ) else: if time.time() - loop_start_time >= loop_duration: logging.info( "Ending action 'loop_for' after duration {}s".format( loop_duration ) ) loop_start_time = None continue logging.info("Rewinding to command number {}".format(loop_pointer + 2)) rewindable_sequences.rewind_to(loop_pointer + 1) sequence_pointer = loop_pointer + 1 continue # Special case: the 'loop_until' action if speaker_name.lower() == "loop_until": if len(sequence) != 2: error_report( "Action 'loop_until' requires one parameter (check spaces around the ':' separator)" ) if loop_start_time is None: loop_start_time = time.time() try: loop_duration = seconds_until(sequence[1]) except: error_report( "Action 'loop_until' requires one parameter (stop time)" ) cumulative_exit_code += 1 logging.info( "Starting action 'loop_until' for duration {}s".format( loop_duration ) ) else: if time.time() - loop_start_time >= loop_duration: logging.info( "Ending action 'loop_until' after duration {}s".format( loop_duration ) ) loop_start_time = None continue logging.info("Rewinding to command number {}".format(loop_pointer + 2)) rewindable_sequences.rewind_to(loop_pointer + 1) sequence_pointer = loop_pointer + 1 continue # Special case: the 'wait' actions if speaker_name in ["wait", "wait_for", "wait_until"]: process_wait(sequence) continue # Use the speaker name from the environment? if env_speaker: if env_spkr_inserted[sequence_pointer] is False: logging.info( "Getting speaker name '{}' from the $SPKR environment variable".format( env_speaker ) ) sequence.insert(0, env_speaker) speaker_name = env_speaker env_spkr_inserted[sequence_pointer] = True # General action processing if len(sequence) < 2: error_report( "At least 2 parameters required in action sequence '{}'; did you supply a speaker name?".format( sequence ) ) action = sequence[1].lower() args = sequence[2:] if speaker_name.lower() == "_all_": if use_local_speaker_list: speakers = speaker_list.get_all_speakers() else: speakers = get_all_speakers(use_scan=True) logging.info( "Performing action '{}' on all visible, coordinator speakers".format( action ) ) for speaker in speakers: if speaker.is_visible and speaker.is_coordinator: logging.info( "Performing action '{}' on speaker '{}'".format( action, speaker.player_name ) ) print(speaker.player_name + ": ", end="", flush=True) exit_code, output_msg, error_msg = run_command( speaker, action, *args, use_local_speaker_list=use_local_speaker_list, ) if exit_code == 0: if len(output_msg) != 0: print(output_msg, flush=True) else: print("OK", flush=True) elif len(error_msg) != 0: print(error_msg, file=sys.stderr, flush=True) cumulative_exit_code += exit_code else: speaker = get_speaker(speaker_name, use_local_speaker_list) if not speaker: print( "Error: Speaker '{}' not found".format(speaker_name), file=sys.stderr, flush=True, ) cumulative_exit_code += 1 else: # Special case of 'track_follow' action if action in ["track_follow", "tf", "track_follow_compact", "tfc"]: if len(args) > 0: print( "Error: Action '{}' takes no parameters".format(action), file=sys.stderr, flush=True, ) continue # Does not return compact = action in ["track_follow_compact", "tfc"] track_follow( speaker, use_local_speaker_list=use_local_speaker_list, break_on_pause=False, compact=compact, ) # Standard action processing logging.info( "Invoking 'run_command' with '{} {} ...'".format( speaker, action ) ) exit_code, output_msg, error_msg = run_command( speaker, action, *args, use_local_speaker_list=use_local_speaker_list, ) if exit_code == 0 and len(output_msg) != 0: print(output_msg, flush=True) elif len(error_msg) != 0: print(error_msg, file=sys.stderr, flush=True) cumulative_exit_code += exit_code except Exception as e: print("Error:", str(e), flush=True) cumulative_exit_code += 1 sequence_pointer += 1 exit(cumulative_exit_code)
def _modify_alarm_object(alarm: soco.alarms.Alarm, parms_string: str) -> bool: alarm_parameters = parms_string.split(",") if len(alarm_parameters) != 8: error_report( "8 comma-separated parameters required for alarm modification specification" ) return False start_time = alarm_parameters[0] if not start_time == "_": try: alarm.start_time = datetime.strptime(start_time, "%H:%M").time() except ValueError: error_report("Invalid time format: {}".format(start_time)) return False duration = alarm_parameters[1] if not duration == "_": try: alarm.duration = datetime.strptime(duration, "%H:%M").time() except ValueError: error_report("Invalid time format: {}".format(duration)) return False recurrence = alarm_parameters[2] if not recurrence == "_": if not soco.alarms.is_valid_recurrence(recurrence): error_report("'{}' is not a valid recurrence string".format(recurrence)) return False alarm.recurrence = recurrence enabled = alarm_parameters[3].lower() if not enabled == "_": if enabled in ["on", "yes"]: enabled = True elif enabled in ["off", "no"]: enabled = False else: error_report( "Alarm must be enabled 'on' or 'off', not '{}'".format( alarm_parameters[3] ) ) return False alarm.enabled = enabled uri = alarm_parameters[4] if not uri == "_": if uri.lower() == "chime": uri = None alarm.program_uri = uri play_mode = alarm_parameters[5].upper() if not play_mode == "_": play_mode_options = [ "NORMAL", "SHUFFLE_NOREPEAT", "SHUFFLE", # Note: this means SHUFFLE and REPEAT "REPEAT_ALL", "REPEAT_ONE", "SHUFFLE_REPEAT_ONE", ] if play_mode not in play_mode_options: error_report( "Play mode is '{}', should be one of:\n {}".format( alarm_parameters[5], play_mode_options ) ) return False alarm.play_mode = play_mode volume = alarm_parameters[6] if not volume == "_": try: volume = int(volume) if not 0 <= volume <= 100: error_report( "Alarm volume must be between 0 and 100, not '{}'".format( alarm_parameters[6] ) ) return False except ValueError: error_report( "Alarm volume must be an integer between 0 and 100, not '{}'".format( alarm_parameters[6] ) ) return False alarm.volume = volume include_linked = alarm_parameters[7].lower() if not include_linked == "_": if include_linked in ["on", "yes"]: include_linked = True elif include_linked in ["off", "no"]: include_linked = False else: error_report( "Linked zones must be enabled 'on' or 'off', not '{}'".format( alarm_parameters[7] ) ) return False alarm.include_linked_zones = include_linked return True