Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / IoT

Smart Home – Controlling Shelly® Devices (Part 3)

5.00/5 (1 vote)
13 Dec 2023CPOL4 min read 3.1K   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.

Image 1

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

Image 2

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

Image 3

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:

VB.NET
<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
C#
[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:

VB.NET
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
C#
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)

Image 4

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

VB.NET
<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
C#
[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.

VB.NET
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
C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)