/// <summary> /// Gets device state as a Google device state object. /// </summary> /// <param name="stateCache">Current state cache.</param> /// <returns>A Google device state object.</returns> public IDictionary <string, object> GetGoogleState(IDictionary <string, string> stateCache) { var results = new Dictionary <string, object>(); var schemas = TraitSchemaProvider.GetTraitSchemas(); foreach (var trait in Traits) { // Dont include "stateless" traits var schema = schemas.FirstOrDefault(x => x.Trait == trait.Trait); if (schema?.StateSchema == null) { continue; } var validState = trait.GetGoogleStateFlattened(stateCache, schema) .Where(kvp => schema.StateSchema.Validator.FlattenedPathExists(kvp.Key)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); foreach (var googleState in validState.ToNestedDictionary()) { // Note: there explicitly shouldnt be overlap here requiring TryAdd results.Add(googleState.Key, googleState.Value); } } // Add explicit online if not specified by state mappings if (!results.ContainsKey("online")) { results.Add("online", true); } return(results); }
/// <summary> /// Get trait examples. /// </summary> /// <param name="traitId">Trait id.</param> /// <returns>Response.</returns> public IActionResult Examples([Required] string traitId) { var schemas = TraitSchemaProvider.GetTraitSchemas(); var targetSchema = schemas.FirstOrDefault(x => x.Trait == Enum.Parse <TraitType>(traitId)); if (targetSchema == null) { return(NotFound()); } var stateExamples = new List <SchemaExample>(); if (targetSchema.StateSchema != null) { stateExamples.AddRange(targetSchema.StateSchema.Examples); } // Flatten out command examples var commandExamples = new List <SchemaExample>(); foreach (var commandSchema in targetSchema.CommandSchemas) { var commandName = commandSchema.Command.ToEnumString(); // Add a "command delegation mode" example commandExamples.Add(new SchemaExample { Comment = $"{commandName}<br/>Pure command delegation mode.", Example = GetWrappedCommandExample(commandName) }); foreach (var commandExample in commandSchema.Examples) { // Transform on the way out to keep pure examples in schemas commandExamples.Add(new SchemaExample { Comment = $"{commandName}<br/>{commandExample.Comment}", Example = GetWrappedCommandExample(commandName, commandExample.Example) }); } // Add result examples to states if (commandSchema.ResultsExamples != null) { stateExamples.AddRange(commandSchema.ResultsExamples); } } var examples = new { AttributeExamples = targetSchema.AttributeSchema?.Examples, StateExamples = stateExamples.Any() ? stateExamples : null, CommandExamples = commandExamples }; return(Json(examples)); }
public void ValidateFlattenedPathReturnsFalse(TraitType traitType, string target) { // Arrange var schemas = TraitSchemaProvider.GetTraitSchemas(); var schema = schemas.FirstOrDefault(x => x.Trait == traitType); // Act var result = schema.StateSchema.Validator.FlattenedPathExists(target); // Assert Assert.False(result); }
public void GetGoogleTypeForFlattenedPathReturns(TraitType traitType, string target, GoogleType googleType) { // Arrange var schemas = TraitSchemaProvider.GetTraitSchemas(); var schema = schemas.FirstOrDefault(x => x.Trait == traitType); // Act var result = schema.StateSchema.Validator.GetGoogleTypeForFlattenedPath(target); // Assert Assert.Equal(googleType, result); }
/// <summary> /// Validates a <see cref="DeviceTrait"/>. /// </summary> /// <param name="deviceTrait">The <see cref="DeviceTrait"/> to validate.</param> /// <returns>Validation errors.</returns> public static IEnumerable <string> Validate(DeviceTrait deviceTrait) { var validationErrors = new List <string>(); var traitSchemas = TraitSchemaProvider.GetTraitSchemas(); var traitSchema = traitSchemas.FirstOrDefault(x => x.Trait == deviceTrait.Trait); if (traitSchema != null) { // Attribute validation if (deviceTrait.Attributes != null && traitSchema.AttributeSchema?.Validator != null) { var attributeJson = JsonConvert.SerializeObject(deviceTrait.Attributes); var attributeErrors = traitSchema.AttributeSchema.Validator.Validate(attributeJson); validationErrors.AddRange(attributeErrors.Select(x => $"Attributes: {x.Path}: {x.Kind}")); } //// State validation ////if (deviceTrait.State != null && traitSchema.StateSchema?.Validator != null) ////{ //// var stateJson = JsonConvert.SerializeObject(GetFakeGoogleState(deviceTrait.State, traitSchema)); //// var stateErrors = traitSchema.StateSchema.Validator.Validate(stateJson); //// validationErrors.AddRange(stateErrors.Select(x => $"State: {x.Path}: {x.Kind}")); ////} //// Command validations ////var deviceCommands = deviceTrait.Commands.ToDictionary( //// k => k.Key, //// v => v.Value?.ToDictionary( //// x => x.Key, //// x => (object)x.Value).ToNestedDictionary()); ////foreach (var command in deviceCommands) ////{ //// var commandType = command.Key.ToEnum<CommandType>(); //// if (command.Value != null && traitSchema.CommandSchemas.Any(x => x.Command == commandType)) //// { //// var commandValidator = traitSchema.CommandSchemas.First(x => x.Command == commandType).Validator; //// var commandJson = JsonConvert.SerializeObject(command.Value); //// var commandErrors = commandValidator.Validate(commandJson); //// validationErrors.AddRange(commandErrors.Select(x => $"Commands ({command.Key}): {x.Path}: {x.Kind}")); //// } ////} } return(validationErrors); }
/// <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); }