/// <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);
        }
示例#2
0
        /// <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);
        }
示例#4
0
        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;
                    }
                }
            }
        }
示例#5
0
        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);
        }
示例#6
0
        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!");
            }
        }
示例#7
0
        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);
        }
示例#8
0
        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);
        }