Exemple #1
0
def listener(q, port, protocol):
    # Signal that we have spawned then loop until terminated, pulling messages
    # of the UDP port and putting them back onto the queue.

    q.put("Started")

    log_debug("Listen on port %d, protocol %s", port, protocol)

    if protocol == "udp":
        sock_type = socket.SOCK_DGRAM
    else:
        sock_type = socket.SOCK_STREAM

    sock = socket.socket(socket.AF_INET, sock_type)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', int(port)))

    if protocol == "tcp":
        log_debug("Set socket to listen")
        sock.listen(1)
        (sock, address) = sock.accept()
        log_debug(
            "Accepted incoming connection from address %s on protocol %s",
            pp.pformat(address), protocol)

    while (True):
        log_debug("Waiting for data on protocol %s", protocol)
        (message, address) = sock.recvfrom(1024)
        log_debug("Received data %s from address %s on protocol %s", message,
                  pp.pformat(address), protocol)
        q.put(message)
 def __init__(self, manufacturer, device, use_S3):
     self.manufacturer = manufacturer
     self.device = device
     self.device_details = {}
     self.use_S3 = use_S3
     log_debug("Creating Device object for manufacturer %s/device %s",
               manufacturer, device)
     log_debug("Using S3 rather than static files? %s", self.use_S3)
Exemple #3
0
def handle_non_discovery(request, command_sequences, device_power_map,
                         device_state):
    # We have received a directive for some capability interface, which we have
    # to now act on.
    # The command_sequences structure is a dict telling us what to do.  It
    # is a nested dict with the following structure:
    #
    # { endpoint:
    #     { capability:
    #         { directive:
    #             [ list of specific commands ]
    #         }
    #     }
    # }
    #
    # The device_power_map dict tells us which devices are needed for
    # which endpoints, plus for each device whether or not it has separate
    # on/off commands or (evil) a single power toggle.  We use the combo of
    # current state, device involvement and toggle vs. on/off to decide
    # (a) whether to skip any commmands and (b) any additional commands
    # to send for devices that should be switched off.

    # Extract the key fields from the request and check it's one we recognise
    capability, directive, payload, endpoint_id = unpack_request(request)
    verify_request(command_sequences, endpoint_id, capability, directive)

    log_info("Received directive %s on capability %s for endpoint %s",
             directive, capability, endpoint_id)

    # If this is a PowerController capability we need to figure out
    # what to turn on/off
    if capability == "PowerController":
        log_debug("Turn things on/off")
        new_device_status, status_changed = set_power_states(
            directive, endpoint_id, device_state, device_power_map,
            PAUSE_BETWEEN_COMMANDS, payload)
    else:
        new_device_status = {}
        status_changed = False

    # Get the list of commands we need to respond to this directive
    commands_list = command_sequences[endpoint_id][capability][directive]

    for command_tuple in commands_list:
        for verb in command_tuple:
            run_command(verb, command_tuple[verb], PAUSE_BETWEEN_COMMANDS,
                        payload)

    time.sleep(PAUSE_BETWEEN_COMMANDS)

    response = construct_response(request)

    log_info("Did device power on/off status change? %s", status_changed)

    return response, new_device_status, status_changed
 def __init__(self, user_id):
     try:
         self.use_S3 = not (os.environ['USE_STATIC_FILES'] == "Y")
     except KeyError:
         self.use_S3 = True
     self.user_id = user_id
     self.user_details = {}
     self.model = {}
     self.device_status = {}
     self.devicesDB = {}
     log_debug("Create a User object for user %s", user_id)
     log_debug("Using S3 for storage? %s", self.use_S3)
 def get_model(self):
     if not self.model:
         if self.use_S3:
             log_debug("Retrieve model from S3")
             self.model = read_S3state(BUCKET_USERDB,
                                       self.user_id + KEY_USER_MODEL)
         else:
             global G_MODEL
             self.model = copy.deepcopy(G_MODEL)
             log_debug("Retrieved model from memory: %s",
                       pp.pformat(self.model))
     return self.model
def extract_token_from_request(request):
    # Find the OAuth2 token from either a discovery or directive request.
    locations = [ 'endpoint', 'payload' ]
    token = None

    for l in locations:
        if l in request['directive']:
            if 'scope' in request['directive'][l]:
                token = request['directive'][l]['scope']['token']

    log_debug("Token passed in request = %s", token)
    return token
 def get_device_status(self):
     if not self.device_status:
         if self.use_S3:
             log_debug("Retrieve device status from S3")
             self.device_status = read_S3state(
                 BUCKET_USERDB, self.user_id + KEY_USER_DEVICE_STATUS)
         else:
             global G_DEVICE_STATUS
             self.device_status = copy.deepcopy(G_DEVICE_STATUS)
             log_debug("Retrieved device status from memory: %s",
                       pp.pformat(self.device_status))
     return self.device_status
 def set_device_status(self, device_status):
     log_info("Set device status for user %s to be %s", self.user_id,
              pp.pformat(device_status))
     self.device_status = device_status
     if self.use_S3:
         log_debug("Secure device status to S3")
         write_S3state(BUCKET_USERDB, self.user_id + KEY_USER_DEVICE_STATUS,
                       device_status)
     else:
         global G_DEVICE_STATUS
         G_DEVICE_STATUS = copy.deepcopy(device_status)
         log_debug("Secured device status to memory: %s",
                   pp.pformat(G_DEVICE_STATUS))
def SendToKIRA(target, mesg, repeat, repeatDelay):
    log_info("Send to %s with repeat %d, delay %.3f; message %s", target,
             repeat, repeatDelay, mesg)

    host, port = target.split(":")
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', int(port)))
    for i in range(repeat + 1):
        log_debug("Sending %s", mesg.encode('utf-8'))
        sock.sendto(mesg.encode('utf-8'), (host, int(port)))
        if i < repeat:
            time.sleep(repeatDelay)
    sock.close()
def read_S3state(bucket, key):
    state = {}

    blob, version = read_object(BUCKET_ROOT + bucket, KEY_ROOT + key)

    if version != S3_SCHEMA_VERSION:
        log_error("Schema mismatch: read %s, code at %s", version,
                  S3_SCHEMA_VERSION)
    else:
        state = pickle.loads(blob)
        log_debug("Read %s/%s state from S3: %s", bucket, key,
                  pp.pformat(state))

    return state
def find_target(device, targets):
    # Check which target is associated with this device
    if 'target' in device:
        target = device['target']
        log_debug("This device is associated with target %s", target)
    else:
        target = 'primary'
        log_debug("No target specified - assume primary")

    if target in targets:
        t = targets[target]
    else:
        t = None

    return t
Exemple #12
0
def listener(q, port):
    # Signal that we have spawned then loop until terminated, pulling messages
    # of the UDP port and putting them back onto the queue.

    q.put("Started")

    log_debug("Listen on port %d", port)

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', int(port)))

    while (True):
        (message, address) = sock.recvfrom(1024)
        q.put(message)
def StepIRCommands(device, capability, command_list):
    cap_to_check = which_capability(capability)

    device_name = device['friendly_name']
    device_logname = device['log_name']
    device_details = device['details']
    target = device['target']

    output_cmd = {}

    if cap_to_check in device_details['supports']:
        log_debug("Device %s supports the %s capability", device_name,
                  capability)

        output_cmd['StepIRCommands'] = {}

        found = False

        log_debug("StepIRCommands should check for key %s",
                  command_list['key'])
        output_cmd['StepIRCommands']['key'] = command_list['key']

        for i in ['+ve', '-ve']:
            found = False

            for command in command_list[i]:
                if not found:
                    log_debug("Look for command %s", command)

                    if command in device_details['IRcodes']:
                        log_debug("Device %s supports command %s", device_name,
                                  command)

                        # Only want to find one command e.g. PowerOn or PowerToggle
                        # We include the device name as we will need that when
                        # later figuring out what to turn on/off
                        output_cmd['StepIRCommands'][
                            i] = construct_specific_IR_command(
                                device_details, command, target, device_name,
                                device_logname)
                        found = True
                else:
                    log_debug("...doesn't support this capability")

    return output_cmd
def extract_user(request):
    user = "******"
    if 'TEST_USER' in os.environ:
        # Currently we are testing with a hard-coded user name
        user = os.environ['TEST_USER']
        log_debug("User name passed as env var = %s", user)
    else:
        # User name must be retrieved from a token, either passed in as an env
        # var or extracted from the real request.
        if 'TEST_TOKEN' in os.environ:
            OAuth2_token = os.environ['TEST_TOKEN']
            log_debug("Token passed as env var = %s", OAuth2_token)
        else:
            # Real request.  Extract token.
            OAuth2_token = extract_token_from_request(request)
        
        user = get_user_from_token(OAuth2_token)

    return user
def read_object(bucket_name, key_name):
    blob = b''
    version = ""
    s3 = boto3.client('s3')

    # Check if bucket exists
    try:
        response = s3.get_object(Bucket=bucket_name, Key=key_name)

        blob = response['Body'].read()
        version = response['Metadata']['schema_version']
        log_debug(
            "Returned %d bytes of schema version %s reading object %s from bucket %s",
            len(blob), version, key_name, bucket_name)

    except botocore.exceptions.ClientError as e:
        log_error("Error %s reading object %s from bucket %s", pp.pformat(e),
                  key_name, bucket_name)

    return blob, version
    def create_model(self):
        # Create a model for this user, plus initialise the device status to
        # 'all devices off'
        log_debug("Create model for user %s", self.user_id)
        self.get_details()
        user_devices = self.user_details['devices']
        device_state = {}

        for user_device in user_devices:
            manufacturer = user_device['manufacturer']
            model = user_device['model']
            d = Device(manufacturer, model, self.use_S3)
            if manufacturer not in self.devicesDB:
                self.devicesDB[manufacturer] = {}
            self.devicesDB[manufacturer][model] = d.get()
            device_state[user_device['friendly_name']] = False

        self.model = model_user_and_devices(self.user_details, self.devicesDB)
        if self.use_S3:
            log_debug("Secure model to S3")
            write_S3state(BUCKET_USERDB, self.user_id + KEY_USER_MODEL,
                          self.model)
        else:
            global G_MODEL
            G_MODEL = copy.deepcopy(self.model)
            log_debug("Secured model to memory: %s", pp.pformat(G_MODEL))
        self.set_device_status(device_state)
def write_object(bucket_name, key_name, blob, version):
    s3 = boto3.client('s3')

    # Check if bucket exists
    try:
        s3.head_bucket(Bucket=bucket_name)
        log_debug("Bucket %s exists", bucket_name)
    except botocore.exceptions.ClientError as e:
        error_code = int(e.response['Error']['Code'])
        if error_code == 403:
            log_error("Denied access to bucket %s", bucket_name)
        elif error_code == 404:
            log_debug("Bucket %s does not exist - creating", bucket_name)
            bucket = s3.create_bucket(
                ACL='public-read-write',
                Bucket=bucket_name,
                CreateBucketConfiguration={'LocationConstraint': REGION})
        else:
            log_error("Error %d checking bucket %s", error_code, bucket_name)

    try:
        metadata = {"schema_version": version}
        s3.put_object(Bucket=bucket_name,
                      Key=key_name,
                      Body=blob,
                      ACL='public-read-write',
                      Metadata=metadata)
        log_debug("Written object %s to bucket %s", key_name, bucket_name)
    except botocore.exceptions.ClientError as e:
        error_code = int(e.response['Error']['Code'])
        log_error("Error %d writing object %s to bucket %s", error_code,
                  key_name, bucket_name)
def InputChoice(device, capability, command_list):
    device_name = device['friendly_name']
    device_logname = device['log_name']
    device_details = device['details']
    target = device['target']

    output_cmd = {}

    # Add commands to set all the devices in the chain to the correct input
    # values.
    if 'required_input' in device:
        log_debug("Need to set %s to input %s", device_name,
                  device['required_input'])
        log_debug("device_details: %s", pp.pformat(device_details['IRcodes']))

        output_cmd['SingleIRCommand'] = {}
        output_cmd['SingleIRCommand'][
            'single'] = construct_specific_IR_command(device_details,
                                                      device['required_input'],
                                                      target, device_name,
                                                      device_logname)

    return output_cmd
def verify_request(primitives, endpoint, capability, directive):
    # Check we know about this endpoint
    if endpoint in primitives:
        log_debug("Recognise endpoint %s", endpoint)

        if capability in primitives[endpoint]:
            log_debug("Recognise capability %s", capability)

            if directive in primitives[endpoint][capability]:
                log_debug("Recognise directive %s", directive)
            else:
                log_error("Don't recognise directive %s", directive)
        else:
            log_error("Don't recognise capability %s", capability)
    else:
        log_error("Don't recognise endpoint %s", endpoint)
Exemple #20
0
def get_user_from_token(token):
    log_debug("Token is %s", token)

    url = LWA_PROFILE_URL + urllib.parse.urlencode({'access_token': token})
    r = requests.get(url=url)

    if r.status_code == 200:
        log_debug("Amazon profile returned is:")
        log_debug(json.dumps(r.json(), indent=4))

        body = r.json()
        user = body['user_id']
    else:
        log_error("Amazon look up returned an error %d", r.status_code)
        log_error(json.dumps(r.json(), indent=4))

        user = "******"

    return user
def verify_devices(devices, database):
    # Check we recognise the list of devices the user has.  As we'er running
    # as a lambda, don't worry too much about raising and catching exceptions;
    # the important thing is to log it.
    log_debug("Validate user devices exist in DB")
    bad_device = False
    for device in devices:
        log_debug("Check user device %s", device['friendly_name']) 
        manu = device['manufacturer']
        model = device['model']
        if manu in database:
            if model in database[manu]:
                log_debug("Manu %s, model %s found OK", manu, model)
            else:
                log_error("Device %s with manu %s has incorrect model %s", device['friendly_name'], manu, model)
                bad_device = True
        else:
            log_error("Device %s has incorrect manu %s", device['friendly_name'], manu)
            bad_device = True
Exemple #22
0
def lambda_handler(request, context):
    # Main lambda handler.  We simply switch on the directive type.

    log_info("Received request")
    log_info(json.dumps(request, indent=4))

    user_id = extract_user(request)
    log_info("Request is for user %s", user_id)

    u = User(user_id)

    if is_discovery(request):
        # On discovery requests we always re-model the user.  This is the
        # natural point to model, as it is the only mechanism to report to
        # Alexa any changes in a user's devices.
        # Note that creating the model also resets the device status to
        # 'all off'.
        log_debug("Discovery: model the user")
        u.create_model()
        model = u.get_model()
        response = handle_discovery(model['discovery_response'])
    else:
        log_debug("Normal directive: retrieve the model")
        model = u.get_model()
        log_debug("Model is %s", pp.pformat(model))
        device_status = u.get_device_status()
        response, new_device_status, status_changed = handle_non_discovery(
            request, model['command_sequences'], model['device_power_map'],
            device_status)
        if status_changed:
            log_info("Device status changed - updating")
            u.set_device_status(new_device_status)

    log_info("Response:")
    log_info(json.dumps(response, indent=4, sort_keys=True))

    #validate_message(response)

    return response
def construct_power_map(user_details, global_database):
    # We need to understand (a) which devices are active in which endpoints and
    # (b) whether they have sensible PowerOn/Off commands or just support the
    # useless PowerToggle (why?) which can result in us getting out of sync.
    # We will later combine this with the current device status to ensure that
    # we (a) turn off now-unused devices when switching between endpoints and
    # (b) get the power polarity correct.
    # device_power_map is a dict with form
    # { 'device': {
    #      'room': <room>, 					# which room this device is
    #      'toggle': 'True/False',          # Is PowerToggle used?
    #      'endpoints': {
    #          '<endpoint>' : 'True/False', # Is device used in this endpoint?
    #       },
    #      'commands': {}                   # Set of commands corresponding to
    #                                         power on/off/toggle
    #    }
    # }
    #
    # We fill in the power toggle status here; endpoints are added as we find
    # the capabilities.
    log_debug("Construct device power map")

    user_targets = user_details['targets']
    user_devices = user_details['devices']

    device_power_map = {}

    for this_device in user_devices:
        friendly_name = this_device['friendly_name']
        log_debug("Examine device %s", friendly_name)

        device_power_map[friendly_name] = {}
        this_device_map = device_power_map[friendly_name]
        this_device_map['room'] = this_device['room']
        this_device_map['endpoints'] = {}

        device_details = find_user_device_in_DB(this_device, global_database)

        # Does it use PowerToggle?
        if 'PowerToggle' in device_details['IRcodes']:
            log_debug("Uses PowerToggle")
            this_device_map['toggle'] = True
        else:
            log_debug("Does not use PowerToggle")
            this_device_map['toggle'] = False

        # Store the set of commands corresponding to the power directives.
        this_device_map['commands'] = {}
        chain = [{
            "friendly_name":
            this_device['friendly_name'],
            "log_name":
            this_device['manufacturer'] + " " + this_device['model'],
            "details":
            device_details,
            "target":
            find_target(this_device, user_targets)
        }]

        power_directives = CAPABILITY_DIRECTIVES_TO_COMMANDS[
            'DevicePowerController']

        for directive in power_directives:
            log_debug("Find specific commands corresponding to %s", directive)
            specific_commands = construct_command_sequence(
                chain, "DevicePowerController", power_directives[directive])
            this_device_map['commands'][directive] = specific_commands

    return device_power_map
def SingleIRCommand(device, capability, command_list):
    log_debug("Have list of single IR commands to check: %s",
              pp.pformat(command_list))
    cap_to_check = which_capability(capability)
    log_debug("Checking for capability %s", capability)

    device_name = device['friendly_name']
    device_logname = device['log_name']
    device_details = device['details']
    target = device['target']

    output_cmd = {}

    log_debug("Checking device details: %s", pp.pformat(device_details))

    if cap_to_check in device_details['supports']:
        log_debug("Device %s supports the %s capability", device_name,
                  capability)

        output_cmd['SingleIRCommand'] = {}

        found = False

        for command in command_list:
            if not found:
                log_debug("Look for command %s", command)

                if command in device_details['IRcodes']:
                    log_debug("Device %s supports command %s", device_name,
                              command)

                    # Only want to find one command e.g. PowerOn or PowerToggle
                    # We include the device name as we will need that when
                    # later figuring out what to turn on/off
                    output_cmd['SingleIRCommand'][
                        'single'] = construct_specific_IR_command(
                            device_details, command, target, device_name,
                            device_logname)
                    found = True
                else:
                    log_debug("...doesn't support this capability")

    return output_cmd
def get_connected_device(user_devices, global_database, device):
    next_device_name = device['connected_to']['next_device']
    log_debug("Next connected device is %s", next_device_name)
    device  = find_device_from_friendly_name(user_devices, next_device_name)
    device_details = find_user_device_in_DB(device, global_database)
    return next_device_name, device, device_details
def write_S3state(bucket, key, state):
    blob = pickle.dumps(state)
    write_object(BUCKET_ROOT + bucket, KEY_ROOT + key, blob, S3_SCHEMA_VERSION)
    log_debug("Wrote %s/%s state to S3: %s", bucket, key, pp.pformat(state))
def construct_endpoint_chain(user_details, root_device, global_database):
    # Identify the chain of devices included in an endpoint chain plus the set of
    # capabilities it supports.
    # This is the union of all capabilities supported by the devices we are
    # aggregating to form this endpoint.
    user_targets = user_details['targets']
    user_devices = user_details['devices']

    log_debug("Find the set of capabilities for the activity rooted in %s",
              root_device['friendly_name'])

    endpoint = new_endpoint(root_device['friendly_name'].replace(" ", ""),
                            root_device['manufacturer'],
                            root_device['description'])

    capabilities = {}
    chain = []

    device = root_device
    device_details = find_user_device_in_DB(device, global_database)
    is_audio = ('A_source' in device_details['roles'])
    log_debug("Is the activity audio only? %d", is_audio)

    reached_end = False
    required_input = None

    while not reached_end:
        for capability in device_details['supports']:
            log_debug("Activity supports %s capability via device %s",
                      capability, device['friendly_name'])
            capabilities[capability] = 'supported'

        this_link = {
            "friendly_name": device['friendly_name'],
            "log_name": device['manufacturer'] + " " + device['model'],
            "details": device_details,
            "target": find_target(device, user_targets)
        }

        if required_input != None:
            this_link['required_input'] = required_input

        if 'connected_to' in device:
            old_device = device
            device_friendly_name, device, device_details = get_connected_device(
                user_devices, global_database, device)
            if is_audio and ('display' in device_details['roles']):
                log_debug(
                    "Connected to a display, but audio only source - end of chain"
                )
                reached_end = True
            else:
                required_input = old_device['connected_to']['input']
        else:
            reached_end = True
            log_debug("Reached end of activity chain")

        chain.append(this_link)

    log_debug("List of capabilities for this activity:\n%s",
              pp.pformat(capabilities))
    log_debug("Device chain involved in this activity:\n%s", pp.pformat(chain))
    return endpoint, capabilities, chain
def model_user_and_devices(user_details, device_database):
	# Here we model the user's devices and activities.
	# 
	# It is important to understand:
	# - the inputs to this model
	# - the Alexa data model
	# - how we map user devices -> Alexa objects
	# - why we have to treat power differently
	# - the set of models we return from here.
	#
	# Inputs to this model
	# --------------------
	# 
	# There are two inputs to this modelling exercise.
	#
	# 1. A global database of devices.  This is a dict, structured by
	# manufacturer then device, which for each device contains
	# - what real-world roles it can play (e.g. audio source, or audio + video)
	# - which Alexa capabilities it can support (see below)
	# - a map of IR command names -> IR codes.
	# This database is stored in S3.
	#
	# 2. A user's details.  This has two key pieces of info.
	# - The set of KIRA targets to send commands to (IP addresses + ports); we
	# support multiple devices per-user).
	# - A list of the user's devices, including what the user wants to call 
	# them, how they are linked together (e.g. what TV input a Blu-ray player
	#  is connected to) and how they are grouped into rooms.
	# User details are stored in S3.
	#
	# Alexa data model
	# ----------------
	# 
	# The key Alexa concepts are endpoints, capabilities and directives.
	#
	# Endpoints are things like TV or Blu-ray.
	# 
	# Capabilities (aka interfaces) are groups of related features such as a 
	# ChannelController or PowerController.  We model each physical
	# device as supporting a set of capabilities; the capabilities we
	# return for an endpoint is the union of all capabilities supported
	# by the devices being aggregated into that endpoint.
	#
	# Directives are the individual commands within each capability e.g.
	# Play or AdjustVolume.
	#
	# Mapping user devices -> Alexa data model
	# ----------------------------------------
	#
	# Alexa "expects" endpoints to be 1-1 with physical devices, but instead
	# we aggregate multiple physical devices into a single endpoint so that 
	# they can all be controlled simultaneously.  So our model is that any 
	# devices which the user has which are audio or audio+video sources are
	# mapped to an endpoint, and we then follow the chain of connectivity
	# from those sources to create a list of each device included in that
	# endpoint.
	#
	# We then model the capabilities supported by the endpoint as being the 
	# union of the capabilities supported by each device included in the 
	# endpoint.
	#
	# For each directive of each capability, we then create a list of the 
	# specific commands we must send to implement that directive on each of
	# the devices supporting that capability (typically, there will only be
	# one device in the chain doing so e.g. only one device supporting 
	# StepSpeaker for volume control).  We do this via a dict representing the
	# Alexa schema; for each directive of each capability it has a list of
	# command names to search for in the device database for the appropriate
	# devices e.g. for the AdjustVolume directive of the StepSpeaker capability
	# we search for commands called VolumeUp and VolumeDown.
	#
	# The commands we extract may be simple unconditional "send this IR seq"
	# commands, or more complex commands that are parameterised by values
	# in the directive e.g. the ChangeChannel directive of the
	# ChannelController capability extracts the IR sequences corresponding to 
	# digits 0-9; the value channel passed in the payload of the directive
	# then determines which IR sequences are sent.
	#
	# Why power is different
	# ----------------------
	#
	# The scheme outlined above works well for everything apart from power
	# commands, for two reasons.
	#
	# - We potentially have to send commands to devices *not* in the endpoint
	# the user command nominally addresses. For example, if the user issues
	# "Turn on TV" then "Turn on CD", then (assuming the CD is being played
	# back through speakers not the TV) we should turn off the TV as part of
	# handling the latter command.  That means we have to have awareness of
	# the power state of all devices.
	#
	# - Some evil device manufacturers just support a "power toggle" IR command
	# rather than separate "power on" and "power off".  This makes awareness
	# of state essential, as the commands are not idempotent e.g. if we simply
	# mapped "Turn on TV" to sending "power toggle" to the TV, then if the user
	# issued successive "Turn on TV" commands, the second one would turn *off*
	# the TV.
	#
	# Models
	# ------
	#
	# Given the above, we model and return the following.
	#
	# discovery_response
	#
	# This is a dict corresponding to the JSON structure to return to Alexa in
	# response to Discovery commands, and includes the full set of endpoints 
	# and their supported capabilities.
	#
	# command_sequence
	#
	# This is a dict indexed by endpoint then capability then directive, 
	# containing a structure corresponding to the set of IR commands to send
	# for that directive of that capability for that endpoint.  The lambda 
	# handler then simply sends that sequence of IR commands, parameterised as
	# necessary by values in the directive payload.
	#
	# device_power_map
	#
	# This is a dict indexed by device which includes info on
	# - which endpoints the device participates in
	# - whether it is a "toggle" or "on/off" device
	# - the set of IR commands corresponding to power toggle/on/off commands.
	# The lambda handler then uses this whenever it receives a directive for
	# the PowerController capability.  In conjunction with the device state,
	# it works out which devices need to be turned off.  Power manipulation
	# (and associated setting of inputs) for devices in the endpoint is handled
	# by the normal command sequence processing.
	log_info("Auto-generating model")

	# Extract the lists of targets and devices from the user details, and check
	# they're not duff.
	user_targets = user_details['targets']
	user_devices = user_details['devices']

	verify_devices(user_devices, device_database)
	
	discovery_response = []
	command_sequences = {}
	
	# We can't construct the full power map until we have the list of endpoints
	# but we can extract whether each device is a toggle or on/off plus the 
	# list of its IR commands.
	device_power_map = construct_power_map(user_details, device_database)
	
	# Now construct the list of endpoints, and use to flesh out the discovery
	# response, command sequence and power map.
	for this_device in user_devices:
		log_debug("User has device %s", this_device['friendly_name'])	

		device_details = find_user_device_in_DB(this_device, device_database)

		is_video_source = ('AV_source' in device_details['roles'])
		is_audio_source = ('A_source' in device_details['roles'])
		is_source = (is_video_source or is_audio_source)

		if is_source:
			log_debug("It's a source; map it to an endpoint")

			# We now need to find the chain of devices in this endpoint chain, 
			# plus the union of their capabilities.
			endpoint, capabilities, chain = construct_endpoint_chain(user_details, this_device, device_database)
			endpoint_id = endpoint['endpointId']

			for link in chain:
				log_debug("Marking device %s involved in endpoint %s", link['friendly_name'], endpoint_id)
				device_power_map[link['friendly_name']]['endpoints'][endpoint_id] = True
			
			# Now go through the capabilities, and as well as constructing the
			# appropriate discovery response construct the set of commands for
			# each primitive.
			command_sequences[endpoint_id] = {}

			for capability in capabilities:
				log_debug("Add capability %s to endpoint response", capability)

				# Append the section of the discovery response for this
				# capability for this endpoint.  This is the set of directives
				# we support for this capability, and is taken direct from the
				# Alexa schema.
				endpoint['capabilities'].append(CAPABILITY_DISCOVERY_RESPONSES[capability])

				# Now construct the set of IR commands for each directive of
				# this capability.
				# The schema includes the set of command names to look for,
				# for each directive of each capability.
				command_sequences[endpoint_id][capability] = {}
				directives_to_commands = CAPABILITY_DIRECTIVES_TO_COMMANDS[capability]

				for directive in directives_to_commands:
					specific_commands = construct_command_sequence(chain,
																   capability,
																   directives_to_commands[directive])
					command_sequences[endpoint_id][capability][directive] = specific_commands

			# Add the constructed endpoint info to what we return
			discovery_response.append(endpoint)

	log_debug("Device power map = %s", pp.pformat(device_power_map))

	model = {
				'discovery_response': discovery_response,
				'command_sequences': command_sequences,
				'device_power_map': device_power_map
			}

	return model
def construct_command_sequence(device_chain, capability, generic_commands):
    # Construct the sequence of commands corresponding to a particular
    # directive of a particular capability, for example "Play" for a
    # "PlaybackController".
    #
    # The generic_commands is a dictionary of commands to be interpreted when a
    # directive is received, each consisting of a primitive plus parameters as
    # follows.
    #
    #  SingleIRCommands	- Send a single IR command.  Primitive has a list of
    #                     command names to search for in each device; use the
    #					  first match found	for each relevant device
    #
    #  StepIRCommands   - Directive is of form increase/decrease by N.
    #					  Primitive is a dict with +ve and -ve command lists,
    #                     interpreted as above.
    #
    #  DigitsIRComamnds - Directive is a number to be converted to a sequence
    #                     of IR commands for each decimal digit.  Primitives
    #                     is a dict with list of possible commands for each
    #					  digit.
    #
    #  InputChoice      - Send IRCommands to set all the devices in the
    #					  activity to the correct input setting.  Typically
    #					  used in "power on".
    #
    #  Pause            - Pause before sending anything further.  Typically
    #  					  used to wait for kit to switch on.  Primitive is
    # 					  length of pause in seconds.
    #
    # We map this to the specific sequence of commands for this users devices
    # by substituting from the global database of devices.
    #
    # Note that we rely on dictionary ordering - not true for Python 2, true
    # for Python 3.6 onwards.
    #
    # We do this by looping through all the devices in the activity, and for
    # those which support this capability, looking for matches for the set of
    # instructions.
    commands = []

    log_debug(
        "Converting generic primitives %s to specific commands for capability %s for device chain %s",
        pp.pformat(generic_commands), capability, pp.pformat(device_chain))

    for primitive in generic_commands:
        log_debug("This primitive is %s", primitive)

        for link in device_chain:
            device = link['friendly_name']
            device_logname = link['log_name']
            device_details = link['details']
            target = link['target']

            log_debug("Check device %s", device)

            commands.append(globals()[primitive](link, capability,
                                                 generic_commands[primitive]))

    log_debug("Commands for this directive:\n%s", pp.pformat(commands))

    return commands
 def set_details(self, details):
     log_debug("Set user details for user %s", self.user_id)
     self.user_details = details
     if self.use_S3:
         write_S3state(BUCKET_USERDB, self.user_id + KEY_USER_DETAILS,
                       details)