Exemplo n.º 1
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
Exemplo n.º 2
0
def handle_discovery(discovery_response):
    # Handle discovery requests.  This is straightforward: we have already
    # mapped the users set of devices to an auto-generated list of activities
    # (endpoints), so just return them.
    log_info("Reply to discovery")

    response = DISCOVERY_RESPONSE
    response['event']['payload']['endpoints'] = discovery_response
    response['event']['header']['messageId'] = get_uuid()

    return response
Exemplo n.º 3
0
 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))
Exemplo n.º 4
0
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()
Exemplo n.º 5
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
Exemplo n.º 6
0
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