/// <summary>
        /// Handler for Google Home commands.
        /// </summary>
        /// <param name="deviceCommandExecutionEvent">The device command to handle.</param>
        private async void HandleGoogleHomeCommand(DeviceCommandExecutionEvent deviceCommandExecutionEvent)
        {
            var device = _deviceRepository.Get(deviceCommandExecutionEvent.DeviceId);

            if (device.Disabled)
            {
                return;
            }

            // Find all supported commands for the device
            var deviceSupportedCommands = device.Traits
                                          .SelectMany(x => x.Commands)
                                          .ToDictionary(x => x.Key, x => x.Value);

            // Check if device supports the requested command class
            var execution = deviceCommandExecutionEvent.Execution;

            if (deviceSupportedCommands.ContainsKey(execution.Command))
            {
                // Handle command delegation
                var shortCommandName = execution.Command.Substring(execution.Command.LastIndexOf('.') + 1);
                var deviceTopicName  = Regex.Replace(deviceCommandExecutionEvent.DeviceId, @"\s", string.Empty);
                var delegateTopic    = $"{TopicRoot}/execution/{deviceTopicName}/{shortCommandName}";
                var delegatePayload  = execution.Params != null?JsonConvert.SerializeObject(execution.Params) : "{}";

                await MqttClient.PublishAsync(new MqttApplicationMessageBuilder()
                                              .WithTopic(delegateTopic)
                                              .WithPayload(delegatePayload)
                                              .WithAtLeastOnceQoS()
                                              .Build())
                .ConfigureAwait(false);

                // Find the specific commands supported parameters it can handle
                var deviceSupportedCommandParams = deviceSupportedCommands[execution.Command] ?? new Dictionary <string, string>();

                // Handle remaining command state param negotiation
                if (execution.Params != null)
                {
                    // TODO: Remove the Where filter here eventually
                    // Flatten the parameters, ignore old delegate underscores
                    var flattenedParams = execution.Params
                                          .Where(x => x.Key != "_")
                                          .ToDictionary(x => x.Key, x => x.Value)
                                          .ToFlatDictionary();

                    foreach (var parameter in flattenedParams)
                    {
                        // Check if device supports the requested parameter
                        if (deviceSupportedCommandParams.ContainsKey(parameter.Key))
                        {
                            // Handle remapping of command param to state key
                            var stateKey = CommandToStateKeyMapper.Map(parameter.Key);

                            // Find the DeviceState object that provides configuration for mapping state/command values
                            var deviceState = device.Traits
                                              .Where(x => x.Commands.ContainsKey(execution.Command))
                                              .Where(x => x.State != null)
                                              .SelectMany(x => x.State)
                                              .Where(x => x.Key == stateKey)
                                              .Select(x => x.Value)
                                              .FirstOrDefault();

                            // Build the MQTT message
                            var topic = deviceSupportedCommandParams[parameter.Key];
                            if (!string.IsNullOrEmpty(topic))
                            {
                                string payload = null;
                                if (deviceState != null)
                                {
                                    payload = deviceState.MapValueToMqtt(parameter.Value);
                                }
                                else
                                {
                                    payload = parameter.Value.ToString();
                                    _log.LogWarning("Received supported command '{Command}' but cannot find matched state config, sending command value '{Payload}' without ValueMap", execution.Command, payload);
                                }

                                await MqttClient.PublishAsync(new MqttApplicationMessageBuilder()
                                                              .WithTopic(topic)
                                                              .WithPayload(payload)
                                                              .WithAtLeastOnceQoS()
                                                              .Build())
                                .ConfigureAwait(false);
                            }
                        }
                    }
                }
            }
        }
Exemple #2
0
        /// <summary>
        /// Handles a <see cref="Models.Request.ExecuteIntent"/>.
        /// </summary>
        /// <param name="intent">Intent to process.</param>
        /// <returns>A <see cref="Models.Response.ExecutionResponsePayload"/>.</returns>
        public Models.Response.ExecutionResponsePayload Handle(Models.Request.ExecuteIntent intent)
        {
            _log.LogInformation(string.Format(
                                    "Received EXECUTE intent for commands: {0}",
                                    string.Join(",", intent.Payload.Commands
                                                .SelectMany(x => x.Execution)
                                                .Select(x => x.Command))));

            var executionResponsePayload = new Models.Response.ExecutionResponsePayload();

            // Get all device ids from commands to split into per-device responses
            var deviceIds = intent.Payload.Commands
                            .SelectMany(x => x.Devices.Select(y => y.Id))
                            .Distinct();

            foreach (var deviceId in deviceIds)
            {
                var device = _deviceRepository.Get(deviceId);
                if (device == null)
                {
                    // Device not found
                    executionResponsePayload.Commands.Add(new Models.Response.Command
                    {
                        Ids = new List <string> {
                            deviceId
                        },
                        ErrorCode = "deviceNotFound",
                        Status    = Models.Response.CommandStatus.Error
                    });

                    continue;
                }

                var commands   = intent.Payload.Commands.Where(x => x.Devices.Any(y => y.Id == deviceId));
                var executions = commands.SelectMany(x => x.Execution);

                // Prepare device response payload
                var deviceCommandResponse = new Models.Response.Command
                {
                    Ids = new List <string> {
                        deviceId
                    },
                    Status = Models.Response.CommandStatus.Success
                };

                // Validate challenge check
                foreach (var execution in executions)
                {
                    var challengeResult = ValidateChallenges(device, execution);
                    if (challengeResult != null)
                    {
                        deviceCommandResponse.Status = Models.Response.CommandStatus.Error;

                        if (execution.Challenge != null)
                        {
                            // Challenge failed
                            deviceCommandResponse.ErrorCode       = challengeResult;
                            deviceCommandResponse.ChallengeNeeded = new Models.Response.ChallengeResponse
                            {
                                Type = challengeResult
                            };
                        }
                        else
                        {
                            // Challenge required
                            deviceCommandResponse.ErrorCode       = "challengeNeeded";
                            deviceCommandResponse.ChallengeNeeded = new Models.Response.ChallengeResponse
                            {
                                Type = challengeResult
                            };
                        }

                        break;
                    }
                }

                // Challenge missing or failed
                if (deviceCommandResponse.Status != Models.Response.CommandStatus.Success)
                {
                    executionResponsePayload.Commands.Add(deviceCommandResponse);
                    continue;
                }

                // Publish command and build state response
                var schemas = TraitSchemaProvider.GetTraitSchemas();
                foreach (var command in commands)
                {
                    foreach (var execution in command.Execution)
                    {
                        // Convert command to an event to publish now that its passed all verifications
                        var commandEvent = new DeviceCommandExecutionEvent {
                            DeviceId = deviceId, Execution = execution
                        };
                        _messageHub.Publish(commandEvent);

                        // Generate state for response
                        var states = new Dictionary <string, object>();
                        var trait  = device.Traits.FirstOrDefault(x => x.Commands.ContainsKey(execution.Command));
                        if (trait != null)
                        {
                            var traitSchema   = schemas.FirstOrDefault(x => x.Trait == trait.Trait);
                            var commandSchema = traitSchema.CommandSchemas.FirstOrDefault(x => x.Command == execution.Command.ToEnum <CommandType>());

                            var googleState = trait.GetGoogleStateFlattened(_stateCache, traitSchema);

                            // Map incoming params to "fake" state changes to override existing state value
                            var replacedParams = execution.Params != null
                                ? execution.Params.ToFlatDictionary().ToDictionary(kvp => CommandToStateKeyMapper.Map(kvp.Key), kvp => kvp.Value)
                                : new Dictionary <string, object>();

                            foreach (var state in googleState)
                            {
                                // Decide to use existing state cache value, or attempt to take from transformed execution params
                                var value = replacedParams.ContainsKey(state.Key)
                                    ? replacedParams[state.Key]
                                    : state.Value;

                                // Only add to state response if specified in the command result schema, or fallback state schema
                                if (commandSchema.ResultsValidator != null)
                                {
                                    if (commandSchema.ResultsValidator.FlattenedPathExists(state.Key))
                                    {
                                        states.Add(state.Key, value);
                                    }
                                }
                                else if (traitSchema.StateSchema != null)
                                {
                                    if (traitSchema.StateSchema.Validator.FlattenedPathExists(state.Key))
                                    {
                                        states.Add(state.Key, value);
                                    }
                                }
                            }
                        }

                        // Add explicit online if not specified by state mappings
                        if (!states.ContainsKey("online"))
                        {
                            states.Add("online", true);
                        }

                        // Add any processed states
                        deviceCommandResponse.States = states.ToNestedDictionary();
                    }
                }

                executionResponsePayload.Commands.Add(deviceCommandResponse);
            }

            return(executionResponsePayload);
        }