public void CanMap(string input, string expectedOutput)
        {
            // Act
            var result = CommandToStateKeyMapper.Map(input);

            // Assert
            Assert.Equal(expectedOutput, result);
        }
        /// <summary>
        /// Hanlder for Google Home commands.
        /// </summary>
        /// <param name="command">The command to handle.</param>
        private async void HandleGoogleHomeCommand(Command command)
        {
            foreach (var commandDevice in command.Devices)
            {
                var device = _deviceRepository.Get(commandDevice.Id);
                if (device.Disabled)
                {
                    continue;
                }

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

                foreach (var execution in command.Execution)
                {
                    // Check if device supports the requested command class
                    if (deviceSupportedCommands.ContainsKey(execution.Command))
                    {
                        // Find the specific commands supported parameters it can handle
                        var deviceSupportedCommandParams = deviceSupportedCommands[execution.Command];

                        // Handle command delegation
                        if (deviceSupportedCommandParams.ContainsKey("_"))
                        {
                            // Build the MQTT message
                            var topic   = deviceSupportedCommandParams["_"];
                            var payload = execution.Params != null?JsonConvert.SerializeObject(execution.Params) : string.Empty;

                            await MqttClient.PublishAsync(new MqttApplicationMessageBuilder()
                                                          .WithTopic(topic)
                                                          .WithPayload(payload)
                                                          .WithAtLeastOnceQoS()
                                                          .Build())
                            .ConfigureAwait(false);
                        }

                        // Handle remaining command state param negotiation
                        if (execution.Params != null)
                        {
                            // Flatten the parameters
                            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 Modes, Toggles and FanSpeed
                                    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))
                                                      .SelectMany(x => x.State)
                                                      .Where(x => x.Key == stateKey)
                                                      .Select(x => x.Value)
                                                      .FirstOrDefault();

                                    // Build the MQTT message
                                    var    topic   = deviceSupportedCommandParams[parameter.Key];
                                    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);
                                }
                            }
                        }
                    }
                }
            }
        }
        /// <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);
                            }
                        }
                    }
                }
            }
        }
Beispiel #4
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);
        }
        /// <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();

            foreach (var command in intent.Payload.Commands)
            {
                // Convert command to a event to publish
                _messageHub.Publish(command);

                // Build response payload
                var commandResponse = new Models.Response.Command
                {
                    Status = Models.Response.CommandStatus.Success,
                    Ids    = command.Devices.Select(x => x.Id).ToList()
                };

                // Generate states
                var states = new Dictionary <string, object>();
                foreach (var execution in command.Execution)
                {
                    // Handle camera stream commands
                    if (execution.Command == "action.devices.commands.GetCameraStream")
                    {
                        // Only allow a single cast command at once
                        if (command.Devices.Count() == 1)
                        {
                            // Get the first trait for the camera, as this should be the only trait available
                            var trait = _deviceRepository.Get(command.Devices[0].Id).Traits.FirstOrDefault();
                            if (trait != null)
                            {
                                foreach (var state in trait.State)
                                {
                                    states.Add(state.Key, state.Value.MapValueToGoogle(null));
                                }
                            }
                        }
                    }
                    else
                    {
                        // Copy the incoming state values, rather than getting current as they won't be updated yet
                        var replacedParams = execution.Params
                                             .ToFlatDictionary()
                                             .ToDictionary(kvp => CommandToStateKeyMapper.Map(kvp.Key), kvp => kvp.Value)
                                             .ToNestedDictionary();

                        foreach (var param in replacedParams)
                        {
                            states.Add(param.Key, param.Value);
                        }
                    }
                }

                commandResponse.States = states;

                executionResponsePayload.Commands.Add(commandResponse);
            }

            return(executionResponsePayload);
        }
Beispiel #6
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();

            foreach (var command in intent.Payload.Commands)
            {
                // Build response payload
                var commandResponse = new Models.Response.Command
                {
                    Status = Models.Response.CommandStatus.Success,
                    Ids    = command.Devices.Select(x => x.Id).ToList()
                };

                // Generate states
                var states = new Dictionary <string, object>();
                foreach (var execution in command.Execution)
                {
                    // Validate challenges
                    var challengeResult = ValidateChallenges(command, execution);
                    if (challengeResult != null)
                    {
                        commandResponse.Status = Models.Response.CommandStatus.Error;

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

                        break;
                    }

                    // Dont bother with states for command delegation
                    if (IsDelegatedCommand(command, execution))
                    {
                        break;
                    }

                    // Handle camera stream commands
                    if (execution.Command == "action.devices.commands.GetCameraStream")
                    {
                        // Only allow a single cast command at once
                        if (command.Devices.Count() == 1)
                        {
                            // Get the first trait for the camera, as this should be the only trait available
                            var trait = _deviceRepository.Get(command.Devices[0].Id).Traits.FirstOrDefault();
                            if (trait != null)
                            {
                                foreach (var state in trait.State)
                                {
                                    states.Add(state.Key, state.Value.MapValueToGoogle(null));
                                }
                            }
                        }
                    }
                    else
                    {
                        // Copy the incoming state values, rather than getting current as they won't be updated yet
                        var replacedParams = execution.Params
                                             .ToFlatDictionary()
                                             .ToDictionary(kvp => CommandToStateKeyMapper.Map(kvp.Key), kvp => kvp.Value)
                                             .ToNestedDictionary();

                        foreach (var param in replacedParams)
                        {
                            states.Add(param.Key, param.Value);
                        }
                    }
                }

                if (commandResponse.Status == Models.Response.CommandStatus.Success)
                {
                    // Only add any processed states if there were no challenge or validation errors
                    commandResponse.States = states;

                    // Convert command to a event to publish now that its passed all verifications
                    _messageHub.Publish(command);
                }

                executionResponsePayload.Commands.Add(commandResponse);
            }

            return(executionResponsePayload);
        }