示例#1
0
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
示例#2
0
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
示例#3
0
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
示例#4
0
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
示例#5
0
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"
            )
示例#6
0
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
示例#7
0
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
示例#8
0
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
示例#9
0
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
示例#10
0
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
示例#11
0
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
示例#12
0
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
示例#13
0
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))
示例#14
0
        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)
示例#15
0
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
示例#16
0
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)
示例#17
0
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