65.9K
CodeProject is changing. Read more.
Home

Smart Home – Controlling Shelly® Devices (Part 3)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1 vote)

Dec 13, 2023

CPOL

4 min read

viewsIcon

3101

downloadIcon

66

The article walks through ShellySceneComponent and ShellyScenesComponent, which allow users to define scenes with multiple actions assigned to a control or controls for Shelly devices.

Introduction

The basis for this article is the description in my previous article: Controlling Shelly-Devices - Part 1.

This is a series of articles - this also includes: Controlling Shelly-Devices - Part 2.

In this article I present two more components, a few features that I think are useful, and then, remembering DRY (Don't Repeat Yourself), I modified my basic routines from the first two articles a little. So it makes sense (if you are interested) to download the archive of this article and replace the previous components. For reasons of compatibility, I leave the previous articles unchanged on this point.

The new components are:

  • ShellySceneComponent – here you can define a scene that can consist of many actions but is only assigned to one control (button).
  • ShellyScenesComponent – Here you can define many scenes, which can consist of many actions and are assigned to corresponding controls (buttons).

Both components work with the standard controls without having to be adjusted.

As a feature, I added a TypeConverter that allows you to use a list selection instead of an input for the IpAdress property, which accesses the addresses that are basically known and therefore predefined for your own project.

I have also revised the ShellyCom routines and added another modification of these routines as ShellyCom2. Personally, I think the ShellyCom2 variant is more elegant.

The ShellySceneComponent

This component allows you to assign any number of actions to a control, which can then be triggered with a click. The basic function of this component does not differ significantly from the ShellyActionComponent presented in the article Controlling Shelly-Devices - Part-2. Simply select a control and then assign any number of actions to it (see screenshot).

This component does not cause animation of the associated control.

The ShellyScenesComponent

This component allows you to assign any number of actions to many controls, which can then be triggered with a click of the respective control. The basic function of this component does not differ significantly from the previously mentioned components - except that another collection is called in a collection (see screenshots).

I present the structure of this constellation here as code:

    <TypeConverter(GetType(ExpandableObjectConverter))>
     Partial Public Class SceneAssignmentDefinition

        'if the code-line with the TypeConverter is activated this Property works with already defined IP addresses
        'provided inside the File Definitions. The Property "myIpAdresses" is part of this behaviour.
        'This makes the application (if you want) simpler and less error-prone
        <TypeConverter(GetType(DropDownConverter)), DropDownConverterData("myIpAdresses")>
        <Category("Shelly"), Description("IpAdress of the Shelly-Device to work with")>
        <RefreshProperties(RefreshProperties.All)>
        Property IpAdress As String
            Get
                Return myIPAdress
            End Get
            Set(value As String)
                If System.Net.IPAddress.TryParse(value, myIP) Then
                    myShellyType = ShellyCom.Shelly_GetType(value)
                    If myShellyType <> Shelly.ShellyType.None Then myIPAdress = value
                    If myShellyType = Shelly.ShellyType.Shelly_Dimmer2 Then myOutput = 0
                End If
            End Set
        End Property
        <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
        ReadOnly Property myIpAdresses As String()
            Get
                Return Shelly.My.IpAdresses ' availible adresses are defined in File: Definitions
            End Get
        End Property

        Private myIPAdress As String = ""
        Private myIP As System.Net.IPAddress

        <Category("Shelly"), Description("shows the Type of the connected Shelly-Device")>
        ReadOnly Property ShellyType As String
            Get
                Return myShellyType.ToString
            End Get
        End Property
        Private myShellyType As Shelly.ShellyType

        <Category("Shelly"), Description("Output-Number of the Shelly-Device to work with")>
        <DefaultValue(0)>
        Property OutputNr As Integer
            Get
                Return myOutput
            End Get
            Set(value As Integer)
                If (value >= 0) And (value <= 1) Then
                    myOutput = value
                End If
            End Set
        End Property
        Private myOutput As Integer = 0

        <Category("Shelly"), Description("the Value which is assigned to the Shelly-Device")>
        <DefaultValue(0)>
        Property Value As Integer
            Get
                Return myValue
            End Get
            Set(value As Integer)
                If (value >= 0) And (value <= 100) Then
                    myValue = value
                End If

            End Set
        End Property
        Private myValue As Integer = 0

        <Category("Shelly"), Description("the Action which happens with a Control-Click Event")>
        <DefaultValue(GetType(Shelly.ActionDefinition), "none")>
        Property Action As Shelly.ActionDefinition
            Get
                Return myAction
            End Get
            Set(value As Shelly.ActionDefinition)
                myAction = value
            End Set
        End Property
        Private myAction As Shelly.ActionDefinition = Shelly.ActionDefinition.none

        Public Sub New()
        End Sub

        Public Overrides Function toString() As String
            Return myIPAdress + ", " + myAction.ToString + ", O:" + myOutput.ToString.Trim + ", Val:" + myValue.ToString.Trim
        End Function

    End Class

    <TypeConverter(GetType(ExpandableObjectConverter))>
    Partial Public Class ControlAssignmentDefinition

        <Category("Control"), Description("the Control which should do the Action(s)")>
        Property SelectedControl As Control
            Get
                Return mySelectedControl
            End Get
            Set(value As Control)
                mySelectedControl = value
            End Set
        End Property
        Private mySelectedControl As Control = Nothing

        Public savedBackColor As Color
        Public savedForeColor As Color

        <Category("Control"), Description("says that this Scene was last activated")>
        <DefaultValue(False)>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
        Property isActivated As Boolean
            Get
                Return myActivated
            End Get
            Set(value As Boolean)
                myActivated = value
            End Set
        End Property
        Private myActivated As Boolean = False

        <Category("Control-Settings"), Description("Enables the Animation of this Control")>
        <DefaultValue(True)>
        Property EnableAnimation As Boolean
            Get
                Return my_EnableAnimation
            End Get
            Set(value As Boolean)
                my_EnableAnimation = value
            End Set
        End Property
        Private my_EnableAnimation As Boolean = True

        <Category("Control"), Description("Assignment to the Shelly-Devices")>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
        ReadOnly Property ShellyActions As ScenesCollection
            Get
                Return myShellyActions
            End Get
        End Property
        Private myShellyActions As New ScenesCollection

        Public Overrides Function toString() As String
            If mySelectedControl IsNot Nothing Then Return mySelectedControl.Name
            Return "[-]"
        End Function

    End Class
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public partial class SceneAssignmentDefinition
    {
        // if the code-line with the TypeConverter is activated this Property works with already defined IP addresses
        // provided inside the File Definitions. The Property "myIpAdresses" is part of this behaviour.
        // This makes the application (if you want) simpler and less error-prone
        [TypeConverter(typeof(DropDownConverter))]
        [DropDownConverterData("myIpAdresses")]
        [Category("Shelly")]
        [Description("IpAdress of the Shelly-Device to work with")]
        [RefreshProperties(RefreshProperties.All)]
        public string IpAdress
        {
            get { return myIPAdress; }
            set
            {
                if (System.Net.IPAddress.TryParse(value, out myIP))
                {
                    myShellyType = ShellyCom.Shelly_GetType(value);
                    if (myShellyType != Shelly.ShellyType.None)
                        myIPAdress = value;
                    if (myShellyType == Shelly.ShellyType.Shelly_Dimmer2)
                        myOutput = 0;
                }
            }
        }
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public string[] myIpAdresses
        {
            get { return Shelly.My.IpAdresses; } // availible adresses are defined in File: Definitions
        }        
        
        private string myIPAdress = "";
        private System.Net.IPAddress myIP;

        [Category("Shelly")]
        [Description("shows the Type of the connected Shelly-Device")]
        public string ShellyType
        {
            get { return Convert.ToString(myShellyType); }
        }
        private Shelly.ShellyType myShellyType;

        [Category("Shelly")]
        [Description("Output-Number of the Shelly-Device to work with")]
        [DefaultValue(0)]
        public int OutputNr
        {
            get { return myOutput; }
            set
            {
                if ((value >= 0) & (value <= 1))
                    myOutput = value;
            }
        }
        private int myOutput = 0;

        [Category("Shelly")]
        [Description("the Value which is assigned to the Shelly-Device")]
        [DefaultValue(0)]
        public int Value
        {
            get { return myValue; }
            set
            {
                if ((value >= 0) & (value <= 100))
                    myValue = value;
            }
        }
        private int myValue = 0;

        [Category("Shelly")]
        [Description("the Action which happens with a Control-Click Event")]
        [DefaultValue(typeof(Shelly.ActionDefinition), "none")]
        public Shelly.ActionDefinition Action
        {
            get { return myAction; }
            set { myAction = value; }
        }
        private Shelly.ActionDefinition myAction = Shelly.ActionDefinition.none;

        public SceneAssignmentDefinition()
        {
        }

        public override string ToString()
        {
            return myIPAdress + ", " + Convert.ToString(myAction) + ", O:" + myOutput.ToString().Trim() + ", Val:" + myValue.ToString().Trim();
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public partial class ControlAssignmentDefinition
    {
        [Category("Control")]
        [Description("the Control which should do the Action(s)")]
        public Control SelectedControl
        {
            get { return mySelectedControl; }
            set { mySelectedControl = value; }
        }
        private Control mySelectedControl = null/* TODO Change to default(_) if this is not a reference type */;

        public Color savedBackColor;
        public Color savedForeColor;

        [Category("Control")]
        [Description("says that this Scene was last activated")]
        [DefaultValue(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool isActivated
        {
            get { return myActivated; }
            set { myActivated = value; }
        }
        private bool myActivated = false;

        [Category("Control-Settings")]
        [Description("Enables the Animation of this Control")]
        [DefaultValue(true)]
        public bool EnableAnimation
        {
            get { return my_EnableAnimation; }
            set { my_EnableAnimation = value; }
        }
        private bool my_EnableAnimation = true;

        [Category("Control")]
        [Description("Assignment to the Shelly-Devices")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public ScenesCollection ShellyActions
        {
            get { return myShellyActions; }
        }
        private ScenesCollection myShellyActions = new ScenesCollection();

        public override string ToString()
        {
            if (mySelectedControl != null)
                return mySelectedControl.Name;
            return "[-]";
        }
    }

These were initially the two base classes for the two collections.

As you can see, the ScenesCollection is already integrated into the ControlAssignmentDefinition class.

These are the associated collections:

    Partial Public Class ActionCollection
        Inherits CollectionBase

        Public Sub Add(ByVal item As ControlAssignmentDefinition)
            Dim myControl As Control = item.SelectedControl
            If myControl IsNot Nothing Then AddHandler item.SelectedControl.Click, AddressOf ControlClickHandler
            List.Add(item)
        End Sub

        Public Sub Insert(ByVal item As ControlAssignmentDefinition)
            Dim myControl As Control = item.SelectedControl
            If myControl IsNot Nothing Then AddHandler item.SelectedControl.Click, AddressOf ControlClickHandler
            List.Add(item)
        End Sub

        Public Sub Remove(ByVal index As Integer)
            If (index > -1) Then
                Dim item As ControlAssignmentDefinition = List.Item(index)
                Dim myControl As Control = item.SelectedControl
                If myControl IsNot Nothing Then RemoveHandler item.SelectedControl.Click, AddressOf ControlClickHandler
                List.RemoveAt(index)
            End If
        End Sub

        Public Property Item(ByVal index As Integer) As ControlAssignmentDefinition
            Get
                Return List(index)
            End Get
            Set(ByVal value As ControlAssignmentDefinition)
                List(index) = value
            End Set
        End Property

        Public ReadOnly Property Item(ByVal ControlName As String) As ControlAssignmentDefinition
            Get
                For i As Integer = 0 To List.Count - 1
                    If Item(i).SelectedControl.Name = ControlName Then Return Item(i)
                Next

                Return Nothing
            End Get
        End Property


        Public Shadows Sub Clear()
            For i As Integer = 0 To List.Count - 1
                RemoveHandler Item(i).SelectedControl.Click, AddressOf ControlClickHandler
            Next
            List.Clear()
        End Sub

        Overrides Function ToString() As String
            Return "[...]"
        End Function

        Public Sub Dispose()
            Me.Clear()
        End Sub



        Property Enabled As Boolean
            Get
                Return my_Enabled
            End Get
            Set(value As Boolean)
                my_Enabled = value
            End Set
        End Property
        Private my_Enabled As Boolean = True



        Private Sub ControlClickHandler(sender As System.Object, e As System.EventArgs)
            If Not my_Enabled Then Exit Sub

            Dim myControl As Control = sender
            Dim myItem As ControlAssignmentDefinition = Item(myControl.Name)

            If myItem IsNot Nothing Then
                For i As Integer = 0 To List.Count - 1
                    Item(i).isActivated = False
                Next
                myItem.isActivated = True

                For i As Integer = 0 To myItem.ShellyActions.Count - 1
                    Dim myAction As SceneAssignmentDefinition = myItem.ShellyActions.Item(i)

                    If myAction IsNot Nothing Then
                        If myAction.Action <> Shelly.ActionDefinition.none Then
                            Select Case myAction.Action
                                Case Shelly.ActionDefinition.SetOut
                                    Dim myState As Boolean = myAction.Value > 0
                                    ShellyCom.Shelly_SetOutput(myAction.IpAdress, myAction.OutputNr, myState)
                                Case Shelly.ActionDefinition.ToggleOut
                                    ShellyCom.Shelly_ToggleOutput(myAction.IpAdress, myAction.OutputNr)
                                Case Shelly.ActionDefinition.SetDimmer
                                    ShellyCom.Shelly_SetDimmer(myAction.IpAdress, myAction.Value)
                                Case Shelly.ActionDefinition.SetRoller
                                    ShellyCom.Shelly_SetRoller(myAction.IpAdress, myAction.Value)
                                Case Shelly.ActionDefinition.ToggleRoller
                                    ShellyCom.Shelly_ToggleRoller(myAction.IpAdress)
                                Case Shelly.ActionDefinition.none
                                    ' do nothing - only for display Values
                            End Select
                        End If
                    End If

                Next
            End If
        End Sub

    End Class

    Partial Public Class ScenesCollection
        Inherits CollectionBase

        Public Sub Add(ByVal item As SceneAssignmentDefinition)
            List.Add(item)
        End Sub

        Public Sub Insert(ByVal item As SceneAssignmentDefinition)
            List.Add(item)
        End Sub

        Public Sub Remove(ByVal index As Integer)
            If (index > -1) Then
                Dim item As SceneAssignmentDefinition = List.Item(index)
                List.RemoveAt(index)
            End If
        End Sub

        Public Property Item(ByVal index As Integer) As SceneAssignmentDefinition
            Get
                Return List(index)
            End Get
            Set(ByVal value As SceneAssignmentDefinition)
                List(index) = value
            End Set
        End Property


        Public Shadows Sub Clear()
            List.Clear()
        End Sub

        Overrides Function ToString() As String
            Return "[...]"
        End Function

        Public Sub Dispose()
            List.Clear()
        End Sub

    End Class
   public class ActionCollection : System.Collections.Generic.List<ControlAssignmentDefinition>
    {
        public void Add(ControlAssignmentDefinition item)
        {
            Control myControl = item.SelectedControl;
            if (myControl != null)
                item.SelectedControl.Click += ControlClickHandler;
            base.Add(item);
        }

        public void Insert(ControlAssignmentDefinition item)
        {
            Control myControl = item.SelectedControl;
            if (myControl != null)
                item.SelectedControl.Click += ControlClickHandler;
            base.Add(item);
        }

        public void Remove(int index)
        {
            if ((index > -1))
            {
                ControlAssignmentDefinition item = (ControlAssignmentDefinition)this[index];
                Control myControl = item.SelectedControl;
                if (myControl != null)
                    item.SelectedControl.Click -= ControlClickHandler;
                base.RemoveAt(index);
            }
        }

        public ControlAssignmentDefinition Item(int index)
        { return (ControlAssignmentDefinition)this[index]; }

        public ControlAssignmentDefinition Item(String ControlName)
        {
            for (int i = 0; i <= Count - 1; i++)
            {
                ControlAssignmentDefinition myItem = (ControlAssignmentDefinition)this[i];
                if (myItem.SelectedControl.Name == ControlName)
                    return myItem;
            }
            return null;
        }


        public new void Clear()
        {
            for (int i = 0; i <=  Count - 1; i++)
                Item(i).SelectedControl.Click -= ControlClickHandler;
            base.Clear();
        }

        public override string ToString()
        { return "[...]"; }

        public void Dispose()
        { this.Clear(); }



        public bool Enabled
        {
            get { return my_Enabled; }
            set { my_Enabled = value; }
        }
        private bool my_Enabled = true;



        private void ControlClickHandler(System.Object sender, System.EventArgs e)
        {
            if (!my_Enabled)
                return;

            Control myControl = (Control)sender;
            ControlAssignmentDefinition myItem = Item(myControl.Name);

            if (myItem != null)
            {
                for (int i = 0; i <= Count - 1; i++)
                    Item(i).isActivated = false;
                myItem.isActivated = true;

                for (int i = 0; i <= myItem.ShellyActions.Count - 1; i++)
                {
                    SceneAssignmentDefinition myAction = myItem.ShellyActions.Item(i);

                    if (myAction != null)
                    {
                        if (myAction.Action != Shelly.ActionDefinition.none)
                        {
                            switch (myAction.Action)
                            {
                                case ActionDefinition.SetOut:
                                    {
                                        bool myState = myAction.Value > 0;
                                        ShellyCom.Shelly_SetOutput(myAction.IpAdress, myAction.OutputNr, myState);
                                        break;
                                    }

                                case ActionDefinition.ToggleOut:
                                    {
                                        ShellyCom.Shelly_ToggleOutput(myAction.IpAdress, myAction.OutputNr);
                                        break;
                                    }

                                case ActionDefinition.SetDimmer:
                                    {
                                        ShellyCom.Shelly_SetDimmer(myAction.IpAdress, myAction.Value);
                                        break;
                                    }

                                case ActionDefinition.SetRoller:
                                    {
                                        ShellyCom.Shelly_SetRoller(myAction.IpAdress, myAction.Value);
                                        break;
                                    }

                                case ActionDefinition.ToggleRoller:
                                    {
                                        ShellyCom.Shelly_ToggleRoller(myAction.IpAdress);
                                        break;
                                    }

                                case ActionDefinition.none:
                                    { break; }
                            }
                        }
                    }
                }
            }
        }
    }

    public partial class ScenesCollection : System.Collections.Generic.List<SceneAssignmentDefinition>
    {
        public void Add(SceneAssignmentDefinition item)
        { base.Add(item); }

        public void Insert(SceneAssignmentDefinition item)
        { base.Add(item); }

        public void Remove(int index)
        {
            if ((index > -1))
            { base.RemoveAt(index); }
        }

        public SceneAssignmentDefinition Item(int index)
        { return (SceneAssignmentDefinition)this[index]; }



        public new void Clear()
        { base.Clear(); }

        public override string ToString()
        { return "[...]"; }

        public void Dispose()
        { base.Clear(); }
    }

Inside the ActionCollection, the click of the respective control is evaluated (ControlClickHandler) and the assigned actions of the subordinate collection are executed.

Here, however, there is again the possibility of animating the triggering control. This is again done using a timer in the component itself.

The TypeConverter (DropDownConverter)

Here I show how to use the converter at the property:

        <TypeConverter(GetType(DropDownConverter)), DropDownConverterData("myIpAdresses")>
        <Category("Shelly"), Description("IpAdress of the Shelly-Device to work with")>
        <RefreshProperties(RefreshProperties.All)>
        Property IpAdress As String
            Get
                Return myIPAdress
            End Get
            Set(value As String)
                If System.Net.IPAddress.TryParse(value, myIP) Then
                    myShellyType = ShellyCom.Shelly_GetType(value)
                    If myShellyType <> Shelly.ShellyType.None Then myIPAdress = value
                    If myShellyType = Shelly.ShellyType.Shelly_Dimmer2 Then myOutput = 0
                End If
            End Set
        End Property
        <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
        ReadOnly Property myIpAdresses As String()
            Get
                Return Shelly.My.IpAdresses ' availible adresses are defined in File: Definitions
            End Get
        End Property

        Private myIPAdress As String = ""
        Private myIP As System.Net.IPAddress
        [TypeConverter(typeof(DropDownConverter, DropDownConverterData("myIpAdresses")]
        [Category("Shelly")]
        [Description("IpAdress of the Shelly-Device to work with")]
        [RefreshProperties(RefreshProperties.All)]
        public string IpAdress
        {
            get { return myIPAdress; }
            set
            {
                if (System.Net.IPAddress.TryParse(value, out myIP))
                {
                    myShellyType = ShellyCom.Shelly_GetType(value);
                    if (myShellyType != Shelly.ShellyType.None)
                        myIPAdress = value;
                    if (myShellyType == Shelly.ShellyType.Shelly_Dimmer2)
                        myOutput = 0;
                }
            }
        }
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public string[] myIpAdresses
        {
            get { return Shelly.My.IpAdresses; } // availible adresses are defined in File: Definitions
        }        
        
        private string myIPAdress = "";
        private System.Net.IPAddress myIP;

The line with the assignment of the TypeConverter determines whether a combo box function is activated when the property is selected in which the string array passed by the property myIpAddresses is offered for selection, or if the line is commented out the standard function of the property takes place.

The IP addresses that I want to display are specified in the Definitions file.

I've already posted the DropDownConverter as a solution once or twice in the Q&A section at CodeProject.

Imports System.ComponentModel

Public Class DropDownConverter
    Inherits StringConverter

    Public Overloads Overrides Function GetStandardValuesSupported(ByVal context As ITypeDescriptorContext) As Boolean
        Return True 'True - tells the PropertyGrid, that a Combobox shall be shown
    End Function

    Public Overloads Overrides Function GetStandardValuesExclusive(ByVal context As ITypeDescriptorContext) As Boolean
        Return True
        ' True  - allows only selection out of the Combobox
        ' False - allows free inputs also
    End Function

    Private EntriesArray(-1) As String  'saves the selectable entries for the DropDown

    Public Overrides Function CanConvertFrom(context As System.ComponentModel.ITypeDescriptorContext, sourceType As System.Type) As Boolean
        If sourceType Is GetType(String) Then Return True 'allows the converting from String 
        Return MyBase.CanConvertFrom(context, sourceType)
    End Function

    Public Overloads Overrides Function GetStandardValues(ByVal context As ITypeDescriptorContext) As StandardValuesCollection
         Dim myPD As PropertyDescriptor = Nothing

        Dim myAttribute As DropDownConverterDataAttribute = context.PropertyDescriptor.Attributes(GetType(DropDownConverterDataAttribute))
        If myAttribute IsNot Nothing Then  ' is the name of the array to be used passed as an Attribute ...?
            myPD = TypeDescriptor.GetProperties(context.Instance)(myAttribute.DataArray)

        Else    ' The name of the array to be used is derived from the name of the Property ...?
            ' identify Host-Property and related options 
            Dim HostPropertyName As String = context.PropertyDescriptor.Name
            Dim HostPropertyArrayName As String = HostPropertyName + "Array"
            myPD = TypeDescriptor.GetProperties(context.Instance)(HostPropertyArrayName)

        End If

        If myPD IsNot Nothing Then
            If myPD.PropertyType Is GetType(String()) Then
                EntriesArray = myPD.GetValue(context.Instance)
            ElseIf myPD.PropertyType Is GetType(List(Of String)) Then
                Dim myList As List(Of String) = myPD.GetValue(context.Instance)
                ReDim EntriesArray(myList.Count - 1)
                For i As Integer = 0 To myList.Count - 1
                    EntriesArray(i) = myList(i)
                Next
            ElseIf myPD.PropertyType Is GetType(Collection) Then
                Dim myCollection As Collection = myPD.GetValue(context.Instance)
                ReDim EntriesArray(myCollection.Count - 1)
                For i As Integer = 0 To myCollection.Count - 1
                    EntriesArray(i) = myCollection.Item(i + 1)
                Next
            End If
        End If

        Return New StandardValuesCollection(EntriesArray) ' exports our specified options
    End Function

End Class

<AttributeUsage(AttributeTargets.[Property] Or AttributeTargets.Method)> _
Public Class DropDownConverterDataAttribute
    Inherits Attribute

    Public Sub New(DataArray_PropertyName As String)
        my_DataArray = DataArray_PropertyName
    End Sub

    Public ReadOnly Property DataArray() As String
        Get
            Return my_DataArray
        End Get
    End Property
    Private my_DataArray As String

End Class
using System;
using System.Collections.Generic;
using System.ComponentModel;

public class DropDownConverter : StringConverter
{
    public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
    {
        return true; // True - tells the PropertyGrid, that a Combobox shall be shown
    }

    public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
    { return true; }

    private string[] EntriesArray = new string[0];  // saves the selectable entries for the DropDown

    public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Type sourceType)
    {
        if (sourceType == typeof(string))
            return true; // allows the converting from String 
        return base.CanConvertFrom(context, sourceType);
    }

    public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
    {
        PropertyDescriptor myPD = null;

        DropDownConverterDataAttribute myAttribute = (DropDownConverterDataAttribute)context.PropertyDescriptor.Attributes[typeof(DropDownConverterDataAttribute)];
        if (myAttribute != null)
            myPD = TypeDescriptor.GetProperties(context.Instance)[myAttribute.DataArray];
        else
        {
            // identify Host-Property and related options 
            string HostPropertyName = context.PropertyDescriptor.Name;
            string HostPropertyArrayName = HostPropertyName + "Array";
            myPD = TypeDescriptor.GetProperties(context.Instance)[HostPropertyArrayName];
        }

        if (myPD != null)
        {
            if (myPD.PropertyType == typeof(string[]))
                EntriesArray = (string[]) myPD.GetValue(context.Instance );
            else if (myPD.PropertyType == typeof(List<string>))
            {
                List<string> myList = (List<string>) myPD.GetValue(context.Instance);
                EntriesArray = new string[myList.Count - 1 + 1];
                for (int i = 0; i <= myList.Count - 1; i++)
                    EntriesArray[i] = myList[i];
            }
         }

        return new TypeConverter.StandardValuesCollection(EntriesArray); // exports our specified options
    }
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
public class DropDownConverterDataAttribute : Attribute
{
    public DropDownConverterDataAttribute(string DataArray_PropertyName)
    { my_DataArray = DataArray_PropertyName; }

    public string DataArray
    {
        get { return my_DataArray; }
    }
    private string my_DataArray;
}

Although I created the converter myself, I can only write a little about the internal functions.

In the lower part I created my own Attribute that allows the converter to specify the data source. The converter itself now accesses the content of this attribute and forwards it to the property by using the PropertyDescriptor. At the same time, the converter tells the PropertyGrid that a combobox should be selected for the output.

I can pass the StringArray and List(of String) types to the converter itself and, under VB.NET, the collection type as a data source.

ShellyCom und ShellyCom2

The two libraries do not differ in their basic functionality. The difference is in the status output.

With ShellyCom there are individual variables for the status of the inputs and outputs. For the ShellyCom2 I have defined arrays that are as large as the actual inputs and outputs.

DRY – Don’t Repeat Yourself

Due to the different functions of the components, there would have been a repetition of various ENums.

I have now moved this to the Definitions file in the Shelly namespace. This also changes the addressing when used in the components.

Finally – last words

This would be the end of my mini-series for now. However, I am open to suggestions or stimulations and maybe there will be a Part 4 after all.

If I use additional Shelly’s or gain access to them, I would expand/adapt the libraries accordingly.

I would like to thank (again) @sean-ewington for the help in creating this and the previous articles.