/// <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); } } } } } }
/// <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); }