/// <summary> /// Creates an entire Client Device from the provided metadata tree. Useful when access to all of the properties are needed. /// </summary> public static ClientDevice CreateClientDevice(ClientDeviceMetadata deviceMetadata) { if (ValidateTopicLevel(deviceMetadata.Id, out var validationMessage) == false) { throw new ArgumentException(validationMessage, nameof(deviceMetadata.Id)); } var returnDevice = new ClientDevice(BaseTopic, deviceMetadata); return(returnDevice); }
/// <summary> /// Tries parsing a whole tree of a single device. /// </summary> public static bool TryParse(ArrayList topicList, string baseTopic, string deviceId, out ClientDeviceMetadata parsedClientDeviceMetadata, ref ArrayList errorList, ref ArrayList warningList) { var isParsedWell = false; var candidateDevice = new ClientDeviceMetadata() { Id = deviceId }; var parsedTopicList = new ArrayList(); // It will be reassigned on successful parsing. parsedClientDeviceMetadata = null; var attributesGood = TryParseAttributes(ref topicList, ref parsedTopicList, baseTopic, ref candidateDevice, ref errorList, ref warningList); if (attributesGood) { var nodesGood = TryParseNodes(ref topicList, ref parsedTopicList, baseTopic, ref candidateDevice, ref errorList, ref warningList); if (nodesGood) { var propertiesGood = TryParseProperties(ref topicList, ref parsedTopicList, baseTopic, ref candidateDevice, ref errorList, ref warningList); if (propertiesGood) { TrimRottenBranches(ref candidateDevice, ref errorList, ref warningList); isParsedWell = true; parsedClientDeviceMetadata = candidateDevice; } else { errorList.Add($"Device '{candidateDevice.Id}' was detected, but it does not have a single valid property. This device subtree will be skipped."); } } else { errorList.Add($"Device '{candidateDevice.Id}' was detected, but it does not have a single valid node. This device subtree will be skipped."); } } else { errorList.Add($"Device '{candidateDevice.Id}' was detected, but it is missing important attributes. This device subtree will be skipped."); } return(isParsedWell); }
/// <summary> /// Parses Homie tree from provided topic and value dump. /// </summary> /// <param name="input">Each line should follow this format: {topic}:{value}. For example, homie/lightbulb/$homie:4.0.0</param> public static ClientDeviceMetadata[] Parse(string[] input, string baseTopic, out string[] errorList, out string[] warningList) { var tempErrorList = new ArrayList(); var tempWarningList = new ArrayList(); // First, need to figure out ho many devices are in the input dump. Looking for $homie attributes. var foundDeviceIds = new ArrayList(); var distinctDevicesRegex = new Regex($@"^({baseTopic})\/([a-z0-9][a-z0-9-]+)\/(\$homie):(\S+)$"); foreach (var inputString in input) { var regexMatch = distinctDevicesRegex.Match(inputString); if (regexMatch.Success) { foundDeviceIds.Add(regexMatch.Groups[2].Value); } } // Grouping topics by device, so we don't have to reiterate over full list over and over again. var sortedTopics = new Hashtable(); foreach (var inputString in input) { for (var d = 0; d < foundDeviceIds.Count; d++) { var deviceId = (string)foundDeviceIds[d]; // Adding a new device to hashtable, if it is not there yet. if (sortedTopics.Contains(deviceId) == false) { sortedTopics.Add(deviceId, new ArrayList()); } // Adding a relevant topic for that device. if (inputString.StartsWith($@"{baseTopic}/{deviceId}/")) { ((ArrayList)(sortedTopics[deviceId])).Add(inputString); } } } // Now, iterating over devices we have just found and trying our best to parse as much as possible. var goodDevices = new ArrayList(); for (var d = 0; d < foundDeviceIds.Count; d++) { var candidateId = (string)foundDeviceIds[d]; var candidateTopics = (ArrayList)sortedTopics[candidateId]; if (ClientDeviceMetadata.TryParse(candidateTopics, baseTopic, candidateId, out var candidateDevice, ref tempErrorList, ref tempWarningList)) { goodDevices.Add(candidateDevice); } } // Converting local temporary lists to final arrays and returning. errorList = new string[tempErrorList.Count]; for (var i = 0; i < tempErrorList.Count; i++) { errorList[i] = (string)tempErrorList[i]; } warningList = new string[tempWarningList.Count]; for (var i = 0; i < tempWarningList.Count; i++) { warningList[i] = (string)tempWarningList[i]; } var deviceTree = new ClientDeviceMetadata[goodDevices.Count]; for (var i = 0; i < goodDevices.Count; i++) { deviceTree[i] = (ClientDeviceMetadata)goodDevices[i]; } return(deviceTree); }
internal ClientDevice(string baseTopic, ClientDeviceMetadata deviceMetadata) { _baseTopic = baseTopic; DeviceId = deviceMetadata.Id; Name = deviceMetadata.NameAttribute; if (Helpers.TryParseHomieState(deviceMetadata.StateAttribute, out var parsedState)) { State = parsedState; } else { State = HomieState.Lost; } Nodes = new ClientNode[deviceMetadata.Nodes.Length]; for (var n = 0; n < deviceMetadata.Nodes.Length; n++) { var nodeMetaData = deviceMetadata.Nodes[n]; var node = new ClientNode(); Nodes[n] = node; node.Name = nodeMetaData.NameAttribute; node.Type = nodeMetaData.TypeAttribute; node.Properties = new ClientPropertyBase[nodeMetaData.Properties.Length]; node.NodeId = nodeMetaData.Id; for (var p = 0; p < nodeMetaData.Properties.Length; p++) { var propertyMetadata = nodeMetaData.Properties[p]; switch (propertyMetadata.DataType) { case DataType.String: var newStringProperty = CreateClientTextProperty(propertyMetadata); node.Properties[p] = newStringProperty; break; case DataType.Integer: case DataType.Float: var newNumberProperty = CreateClientNumberProperty(propertyMetadata); node.Properties[p] = newNumberProperty; break; case DataType.Boolean: case DataType.Enum: var newEnumProperty = CreateClientChoiceProperty(propertyMetadata); node.Properties[p] = newEnumProperty; break; case DataType.Color: var newColorProperty = CreateClientColorProperty(propertyMetadata); node.Properties[p] = newColorProperty; break; case DataType.DateTime: var newDateTimeProperty = CreateClientDateTimeProperty(propertyMetadata); node.Properties[p] = newDateTimeProperty; break; case DataType.Duration: // Duration is not supported by design. It's too complicated to support and provides no real value. break; } } } }
private static bool TryParseAttributes(ref ArrayList unparsedTopicList, ref ArrayList parsedTopicList, string baseTopic, ref ClientDeviceMetadata candidateDevice, ref ArrayList errorList, ref ArrayList warningList) { var isParseSuccessful = false; // Filtering out device attributes. We'll get nodes from them. var deviceAttributesRegex = new Regex($@"^({baseTopic})\/({candidateDevice.Id})\/(\$[a-z0-9][a-z0-9-]+):(.+)$"); var isHomieReceived = false; var isDeviceNameReceived = false; var isNodesReceived = false; var isStateReceived = false; foreach (string inputString in unparsedTopicList) { var regexMatch = deviceAttributesRegex.Match(inputString); if (regexMatch.Success) { var key = regexMatch.Groups[3].Value; var value = regexMatch.Groups[4].Value; if (key == "$homie") { isHomieReceived = true; candidateDevice.HomieAttribute = value; } if (key == "$name") { isDeviceNameReceived = true; candidateDevice.NameAttribute = value; } if (key == "$state") { isStateReceived = true; candidateDevice.StateAttribute = value; } if (key == "$nodes") { isNodesReceived = true; } candidateDevice.AllAttributes.Add(key, value); parsedTopicList.Add(inputString); } } foreach (var parsedTopic in parsedTopicList) { unparsedTopicList.Remove(parsedTopic); } var minimumDeviceSetReceived = isHomieReceived & isDeviceNameReceived & isNodesReceived & isStateReceived; if (minimumDeviceSetReceived) { isParseSuccessful = true; } else { isParseSuccessful = false; if (isHomieReceived == false) { errorList.Add($"Device '{candidateDevice.Id}': mandatory attribute $homie was not found, parsing cannot continue."); } if (isDeviceNameReceived == false) { errorList.Add($"Device '{candidateDevice.Id}': mandatory attribute $name was not found, parsing cannot continue."); } if (isNodesReceived == false) { errorList.Add($"Device '{candidateDevice.Id}': mandatory attribute $nodes was not found, parsing cannot continue."); } if (isStateReceived == false) { errorList.Add($"Device '{candidateDevice.Id}': mandatory attribute $state was not found, parsing cannot continue."); } } return(isParseSuccessful); }
private static void TrimRottenBranches(ref ClientDeviceMetadata candidateDevice, ref ArrayList problemList, ref ArrayList warningList) { var goodNodes = new ArrayList(); var newNodesValue = ""; var updateNeeded = false; // Nodes have $properties attribute, where all the properties are listed. This list must be synchronized with actual properties. // The count may differ because some properties may have been rejected if there's some crucial data missing. foreach (var node in candidateDevice.Nodes) { if (node.Properties.Length > 0) { goodNodes.Add(node); var shouldBeThatManyProperties = ((string)node.AllAttributes["$properties"]).Split(',').Length; if (node.Properties.Length != shouldBeThatManyProperties) { var newAttributeValue = node.Properties[0].PropertyId; for (var i = 1; i < node.Properties.Length; i++) { newAttributeValue += "," + node.Properties[i].PropertyId; } node.AllAttributes["$properties"] = newAttributeValue; updateNeeded = true; } } else { updateNeeded = true; } } // Same with device, it has $nodes synced with actual node count. if (goodNodes.Count > 0) { var shouldBeThatManyNodes = ((string)candidateDevice.AllAttributes["$nodes"]).Split(',').Length; if (goodNodes.Count != shouldBeThatManyNodes) { newNodesValue = ((ClientNodeMetadata)goodNodes[0]).Id; for (var i = 1; i < goodNodes.Count; i++) { newNodesValue += "," + ((ClientNodeMetadata)goodNodes[i]).Id; } updateNeeded = true; } } else { throw new InvalidOperationException("There should be at least a single good property at this point. Something is internally wrong with parser."); } // If needed, trimming the actual structures. if (updateNeeded) { candidateDevice.AllAttributes["$nodes"] = newNodesValue; candidateDevice.Nodes = new ClientNodeMetadata[goodNodes.Count]; for (var i = 0; i < goodNodes.Count; i++) { candidateDevice.Nodes[i] = (ClientNodeMetadata)goodNodes[i]; } warningList.Add($"Device {candidateDevice.Id} has been trimmed. Values of the fields will not be identical to what's on the broker topics!"); } }
private static bool TryParseProperties(ref ArrayList unparsedTopicList, ref ArrayList parsedTopicList, string baseTopic, ref ClientDeviceMetadata candidateDevice, ref ArrayList errorList, ref ArrayList warningList) { var isParseSuccessful = false; for (var n = 0; n < candidateDevice.Nodes.Length; n++) { var candidatePropertyIds = ((string)candidateDevice.Nodes[n].AllAttributes["$properties"]).Split(','); var goodProperties = new ArrayList(); for (var p = 0; p < candidatePropertyIds.Length; p++) { var candidateProperty = new ClientPropertyMetadata() { NodeId = candidateDevice.Nodes[n].Id, PropertyId = candidatePropertyIds[p] }; // Parsing property attributes and value. var attributeRegex = new Regex($@"^({baseTopic})\/({candidateDevice.Id})\/({candidateDevice.Nodes[n].Id})\/({candidateProperty.PropertyId})(\/\$[a-z0-9][a-z0-9-]+)?(:)(.+)$"); var setRegex = new Regex($@"^({baseTopic})\/({candidateDevice.Id})\/({candidateDevice.Nodes[n].Id})\/({candidateProperty.PropertyId})(\/set)(:)(.+)$"); var valueRegex = new Regex($@"^({baseTopic})\/({candidateDevice.Id})\/({candidateDevice.Nodes[n].Id})\/({candidateProperty.PropertyId})()(:)(.+)$"); var isSettable = false; var isRetained = false; var isNameReceived = false; var isDataTypeReceived = false; var isSettableReceived = false; var isRetainedReceived = false; foreach (string inputString in unparsedTopicList) { var attributeMatch = attributeRegex.Match(inputString); if (attributeMatch.Success) { var key = attributeMatch.Groups[5].Value; var value = attributeMatch.Groups[7].Value; if (key == "/$name") { candidateProperty.Name = value; isNameReceived = true; } if (key == "/$datatype") { if (Helpers.TryParseHomieDataType(value, out var parsedType)) { candidateProperty.DataType = parsedType; isDataTypeReceived = true; } ; } if (key == "/$format") { candidateProperty.Format = value; } if (key == "/$settable") { if (Helpers.TryParseBool(value, out isSettable)) { isSettableReceived = true; } } if (key == "/$retained") { if (Helpers.TryParseBool(value, out isRetained)) { isRetainedReceived = true; } ; } if (key == "/$unit") { candidateProperty.Unit = value; } parsedTopicList.Add(inputString); } var setMatch = setRegex.Match(inputString); if (setMatch.Success) { // Discarding this one. This is a historically cached command, and these should not execute during initialization. Besides, it shouldn't be retained, // so the fact that we're here means something is wrong with the host side. warningList.Add($"{candidateDevice.Id}/{candidateProperty} has /set topic assigned, which means /set message is published as retained. This is against Homie convention."); } var valueMatch = valueRegex.Match(inputString); if (valueMatch.Success) { var value = attributeMatch.Groups[7].Value; candidateProperty.InitialValue = value; } } foreach (var parsedTopic in parsedTopicList) { unparsedTopicList.Remove(parsedTopic); } // Basic data extraction is done. Now we'll validate if values of the fields are compatible with each other and also YAHI itself. var isOk = isNameReceived & isDataTypeReceived & isSettableReceived & isRetainedReceived; if (isOk) { // Setting property type. if ((isSettable == false) && (isRetained == true)) { candidateProperty.PropertyType = PropertyType.State; } if ((isSettable == true) && (isRetained == false)) { candidateProperty.PropertyType = PropertyType.Command; } if ((isSettable == true) && (isRetained == true)) { candidateProperty.PropertyType = PropertyType.Parameter; } if ((isSettable == false) && (isRetained == false)) { errorList.Add($"{candidateDevice.Id}/{candidateProperty.NodeId}/{candidateProperty.PropertyId} has all mandatory fields set, but this retainability and settability configuration is not supported by YAHI. Skipping this property entirely."); isOk = false; } } else { // Some of the mandatory topic were not received. Can't let this property through. errorList.Add($"{candidateDevice.Id}/{candidateProperty.NodeId}/{candidateProperty.PropertyId} is defined, but mandatory attributes are missing. Skipping this property entirely."); isOk = false; } // Validating by property data type, because rules are very different for each of those. if (isOk) { var tempErrorList = new ArrayList(); var tempWarningList = new ArrayList(); // ValidateAndFix method does not know anythin about device it is parsing, thus doing some wrapping around error and warning lists (because I want device info in those). isOk = candidateProperty.ValidateAndFix(ref tempErrorList, ref tempWarningList); foreach (var error in tempErrorList) { errorList.Add($"{candidateDevice.Id}/{error}"); } foreach (var warning in tempWarningList) { warningList.Add($"{candidateDevice.Id}/{warning}"); } } if (isOk) { goodProperties.Add(candidateProperty); } } // Converting local temporary property lists to final arrays. candidateDevice.Nodes[n].Properties = new ClientPropertyMetadata[goodProperties.Count]; for (var i = 0; i < goodProperties.Count; i++) { candidateDevice.Nodes[n].Properties[i] = (ClientPropertyMetadata)goodProperties[i]; } } // Converting local temporary node lists to final arrays and returning. foreach (var node in candidateDevice.Nodes) { if (node.Properties.Length > 0) { isParseSuccessful = true; } } return(isParseSuccessful); }
private static bool TryParseNodes(ref ArrayList unparsedTopicList, ref ArrayList parsedTopicList, string baseTopic, ref ClientDeviceMetadata candidateDevice, ref ArrayList errorList, ref ArrayList warningList) { var isParseSuccessful = true; var candidateNodeIds = ((string)candidateDevice.AllAttributes["$nodes"]).Split(','); var goodNodes = new ArrayList(); for (var n = 0; n < candidateNodeIds.Length; n++) { var candidateNode = new ClientNodeMetadata() { Id = candidateNodeIds[n] }; // Filtering out attributes for this node. We'll get properties from them. var nodeAttributesRegex = new Regex($@"^({baseTopic})\/({candidateDevice.Id})\/({candidateNode.Id})\/(\$[a-z0-9][a-z0-9-]+):(.+)$"); foreach (string inputString in unparsedTopicList) { var regexMatch = nodeAttributesRegex.Match(inputString); if (regexMatch.Success) { var key = regexMatch.Groups[4].Value; var value = regexMatch.Groups[5].Value; if (key == "$name") { candidateNode.NameAttribute = value; } if (key == "$type") { candidateNode.TypeAttribute = value; } candidateNode.AllAttributes.Add(regexMatch.Groups[4].Value, regexMatch.Groups[5].Value); parsedTopicList.Add(inputString); } } foreach (var parsedTopic in parsedTopicList) { unparsedTopicList.Remove(parsedTopic); } // Figuring out properties we have for this node. if (candidateNode.AllAttributes.Contains("$properties")) { goodNodes.Add(candidateNode); } else { // Something is wrong, an essential topic is missing. errorList.Add($"{candidateDevice.Id}/{candidateNode.Id} is defined, but $properties attribute is missing. This node subtree will be skipped entirely."); } } // Should be at least one valid node. If not - problem. if (goodNodes.Count == 0) { isParseSuccessful = false; } // Converting local temporary lists to final arrays and returning. candidateDevice.Nodes = new ClientNodeMetadata[goodNodes.Count]; for (var i = 0; i < goodNodes.Count; i++) { candidateDevice.Nodes[i] = (ClientNodeMetadata)goodNodes[i]; } return(isParseSuccessful); }