65.9K
CodeProject is changing. Read more.
Home

Smart Home – Controlling Shelly® Devices (Part 2)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (4 votes)

Dec 6, 2023

CPOL

3 min read

viewsIcon

4150

downloadIcon

92

This article is a continuation of a series on controlling Shelly® devices in a smart home. It introduces a component that works with standard controls without requiring adaptation.

The basis for this article is the description in my previous article Smart Home – Controlling Shelly® Devices – Part 1

In this article I present a component that works with the standard controls without having to adapt them. Here the functions are assigned and the controls are animated via the component that I present in this article.

All or just some of the controls of the form can be selected and the desired action can be assigned to them:

In this article I assume that property handling, especially for collection objects, is known.

Basics

In contrast to a control, a component does not know which form it belongs to. This information can only be obtained using a trick. I found the basics on StackOverFlow in this post: get components parent-form

Now I know the form to which my component belongs and therefore have access to all the controls it contains:

   Private Sub GetParentForm()
        Dim myHost As IDesignerHost = Nothing
        If MyBase.Site IsNot Nothing Then myHost = CType(MyBase.Site.GetService(GetType(IDesignerHost)), IDesignerHost)
        If myHost IsNot Nothing Then
            myParentForm = CType(myHost.RootComponent, Form)
            Exit Sub
        End If
    End Sub
 
    Public Overrides Property Site() As System.ComponentModel.ISite
        Get
            Return MyBase.Site
        End Get
        Set(ByVal value As System.ComponentModel.ISite)
            MyBase.Site = value
            GetParentForm()
        End Set
    End Property
 
    <Category("Info-Data"), Description("the Parentform of this Component")>
    Property ParentForm() As Form
        Get
            Return myParentForm
        End Get
        Set(value As Form)
            myParentForm = value
        End Set
    End Property
    Private myParentForm As Form
  private void GetParentForm()
    {
        IDesignerHost myHost = null;
        if (base.Site != null)
            myHost = (IDesignerHost)base.Site.GetService(typeof(IDesignerHost));
        if (myHost != null)
        {
            myParentForm = (System.Windows.Forms.Form)myHost.RootComponent;
            return;
        }
    }
 
    public override System.ComponentModel.ISite Site
    {
        Get { return base.Site; }
        set
        {
            base.Site = value;
            GetParentForm();
        }
    }
 
    [Category("Info-Data")]
    [Description("the Parentform of this Component")]
    public Form ParentForm
    {
        get { return myParentForm; }
        set { myParentForm = value; }
    }
    private Form myParentForm;

With my own properties I now can specify with which colors a selected control should be animated and at what time interval a possible value change should be detected.

How it works

This class ControlAssignmentDefinition contains the basis for the function assignment to the controls:

    <TypeConverter(GetType(ExpandableObjectConverter))>
    Public Class ControlAssignmentDefinition
 
        <Category("Control"), Description("the Control which should do the Action")>
        Property SelectedControl As Control
            Get
                Return mySelectedControl
            End Get
            Set(value As Control)
                mySelectedControl = value
                savedBackColor = value.BackColor
                savedForeColor = value.ForeColor
            End Set
        End Property
        Private mySelectedControl As Control = Nothing
 
        Public savedBackColor As Color
        Public savedForeColor As Color
 
        <Category("Shelly"), Description("IpAdress of the Shelly-Device to work with")>
        <RefreshProperties(RefreshProperties.All)>
        Property IpAdress As String
            Get
                Return my_IPAdress
            End Get
            Set(value As String)
                my_ShellyType = ShellyCom.Shelly_GetType(value)
                If my_ShellyType <> ShellyCom.ShellyType.None Then my_IPAdress = value
                If my_ShellyType = ShellyCom.ShellyType.Shelly_Dimmer2 Then myOutput = 0
            End Set
        End Property
        Private my_IPAdress As String = ""
 
        <Category("Shelly"), Description("shows the Type of the connected Shelly-Device")>
        ReadOnly Property ShellyType As String
            Get
                Return my_ShellyType.ToString
            End Get
        End Property
        Private my_ShellyType As ShellyCom.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(ShellyActionComponent.ActionDefinition), "none")>
        Property Action As ShellyActionComponent.ActionDefinition
            Get
                Return myAction
            End Get
            Set(value As ShellyActionComponent.ActionDefinition)
                myAction = value
            End Set
        End Property
        Private myAction As ShellyActionComponent.ActionDefinition = ShellyActionComponent.ActionDefinition.none
 
        Public Sub New()
        End Sub
 
        Public Overrides Function toString() As String
            If mySelectedControl IsNot Nothing Then Return mySelectedControl.Name + " => " + myAction.ToString
            Return "[-]"
        End Function
 
    End Class
   [TypeConverter(typeof(ExpandableObjectConverter))]
    public class ControlAssignmentDefinition
    {
        [Category("Control")]
        [Description("the Control which should do the Action")]
        public Control SelectedControl
        {
            get { return mySelectedControl; }
            set
            {
                mySelectedControl = value;
                savedBackColor = value.BackColor;
                savedForeColor = value.ForeColor;
            }
        }
        private Control mySelectedControl = null;
 
        public Color savedBackColor;
        public Color savedForeColor;
 
        [Category("Shelly")]
        [Description("IpAdress of the Shelly-Device to work with")]
        [RefreshProperties(RefreshProperties.All)]
        public string IpAdress
        {
            get { return my_IPAdress; }
            set
            {
                my_ShellyType = ShellyCom.Shelly_GetType(value);
                if (my_ShellyType != ShellyCom.ShellyType.None)
                    my_IPAdress = value;
                if (my_ShellyType == ShellyCom.ShellyType.Shelly_Dimmer2)
                    myOutput = 0;
            }
        }
        private string my_IPAdress = "";
 
        [Category("Shelly")]
        [Description("shows the Type of the connected Shelly-Device")]
        public string ShellyType
        {
            get { return Convert.ToString(my_ShellyType); }
        }
        private ShellyCom.ShellyType my_ShellyType;
 
        [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(ShellyActionComponent.ActionDefinition), "none")]
        public ShellyActionComponent.ActionDefinition Action
        {
            get { return myAction; }
            set { myAction = value; }
        }
        private ShellyActionComponent.ActionDefinition myAction = ShellyActionComponent.ActionDefinition.none;
 
        public ControlAssignmentDefinition()
        {
        }
 
        public override string ToString()
        {
            if (mySelectedControl != null)
                return mySelectedControl.Name + " => " + Convert.ToString(myAction);
            return "[-]";
        }
    }

In order to be able to address several controls of the form with this class, it is embedded in the ActionCollection as a List (of ControlAssignmentDefinition).

This collection provides the designer script of the form with the assignments to the controls via the ShellyActions property:

    <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    ReadOnly Property ShellyActions As ActionCollection
        Get
            Return my_ShellyActions
        End Get
    End Property
    Private my_ShellyActions As New ActionCollection
    [Category("Shelly")]
    [Description("Assignment to the Controls")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public ActionCollection ShellyActions
    {
        get { return my_ShellyActions; }
    }
    private ActionCollection my_ShellyActions = new ActionCollection();

The properties of the ControlAssignmentDefinition class already mentioned have the following meaning:

  • SelectedControl - the control that should execute the assigned function
  • IpAdress – the IPAdress of the Shelly device to work with
  • ShellyType – shows the Type of the Shelly Device of the selected IpAdress
  • Action – which action should happen? (SetOut, ToggleOut, SetDimmer, SetRoller, ToggleRoller)
  • Output number – the number of the output of the Shelly device to work with
  • Value – the Value which is assigned to the Output – if the selected Action is Dimmer or Roller the Value is their percentage

For each control defined here, the click event is evaluated in the collection and assigned to the selected action.

For the sake of simplicity, I embedded a timer in the component to animate the controls used, the interval of which calls the ShellyRequest method, in which the elements of the collection are then gone through and checked for status changes.

The controls are then animated accordingly - although there is different logic for this, depending on the function selected.

    Private myTimer As New Timer With {.Enabled = False, .Interval = 1000}
 
    Private Sub ShellyRequest(sender As Object, e As System.EventArgs) 'Handles myTimer.Tick
        If myParentForm IsNot Nothing And Not DesignMode Then
            Dim ShellyStatus As ShellyCom.Shelly_IOStatus
            Dim myItem As ControlAssignmentDefinition
            Dim outActive1, outActive2, outActive3, notActive As Boolean
            Dim ControlType As Type
 
            For i As Integer = 0 To my_ShellyActions.Count - 1
                myItem = my_ShellyActions.Item(i)
                If ShellyCom.Shelly_GetType(myItem.IpAdress) <> ShellyCom.ShellyType.None Then
                    ShellyStatus = ShellyCom.Shelly_GetStatus(myItem.IpAdress)
                    notActive = ((myItem.Action = ActionDefinition.SetOut) Or (myItem.Action = ActionDefinition.SetDimmer)) And (myItem.Value = 0)
                    outActive1 = (myItem.Action = ActionDefinition.SetOut) And (myItem.Value <> 0)
                    outActive2 = (myItem.Value <> 0) And (((myItem.OutputNr = 0) And ShellyStatus.Out0) Or ((myItem.OutputNr = 1) And ShellyStatus.Out1))
                    outActive3 = ShellyStatus.RollerState = ShellyCom.ShellyRollerState.Opening Or ShellyStatus.RollerState = ShellyCom.ShellyRollerState.Closing
                    ControlType = myItem.SelectedControl.GetType
 
                    Select Case ControlType
                        Case GetType(Button)
                            If (outActive1 Or outActive2 Or outActive3) And Not notActive Then
                                myItem.SelectedControl.BackColor = my_AnimationBackColor
                                myItem.SelectedControl.ForeColor = my_AnimationForeColor
                            Else
                                myItem.SelectedControl.BackColor = myItem.savedBackColor
                                myItem.SelectedControl.ForeColor = myItem.savedForeColor
                            End If
                        Case GetType(Label), GetType(TextBox)
                            myItem.SelectedControl.Text = ShellyStatus.OutValue.ToString + " %"
                            If ShellyStatus.Mode = ShellyCom.ShellyMode.Roller Then myItem.SelectedControl.Text += " - " + ShellyStatus.RollerState.ToString
                    End Select
 
                End If
            Next
        End If
    End Sub
    private Timer myTimer = new Timer() { Enabled = false, Interval = 1000 };
 
    private void ShellyRequest(object sender, System.EventArgs e) // Handles myTimer.Tick
    {
        if (myParentForm != null & !DesignMode)
        {
            ShellyCom.Shelly_IOStatus ShellyStatus;
            ControlAssignmentDefinition myItem;
            bool outActive1, outActive2, outActive3, notActive;
            Type ControlType;
 
            for (int i = 0; i <= my_ShellyActions.Count - 1; i++)
            {
                myItem = my_ShellyActions.Item(i);
                if (ShellyCom.Shelly_GetType(myItem.IpAdress) != ShellyCom.ShellyType.None)
                {
                    ShellyStatus = ShellyCom.Shelly_GetStatus(myItem.IpAdress);
                    notActive = ((myItem.Action == ActionDefinition.SetOut) | (myItem.Action == ActionDefinition.SetDimmer)) & (myItem.Value == 0);
                    outActive1 = (myItem.Action == ActionDefinition.SetOut) & (myItem.Value != 0);
                    outActive2 = ((myItem.OutputNr == 0) & ShellyStatus.Out0) | ((myItem.OutputNr == 1) & ShellyStatus.Out1);
                    outActive3 = ShellyStatus.RollerState == ShellyCom.ShellyRollerState.Opening | ShellyStatus.RollerState == ShellyCom.ShellyRollerState.Closing; ControlType = myItem.SelectedControl.GetType();
 
                    if (ControlType == typeof(Button))
                    {
                        if ((outActive1 | outActive2 | outActive3) & !notActive)
                        {
                            myItem.SelectedControl.BackColor = my_AnimationBackColor;
                            myItem.SelectedControl.ForeColor = my_AnimationForeColor;
                        }
                        else
                        {
                            myItem.SelectedControl.BackColor = myItem.savedBackColor;
                            myItem.SelectedControl.ForeColor = myItem.savedForeColor;
                        }
                    }
                    else if(ControlType == typeof(Label) | ControlType == typeof(TextBox))
                        myItem.SelectedControl.Text = Convert.ToString(ShellyStatus.OutValue)+ " %";
                        if (ShellyStatus.Mode == ShellyCom.ShellyMode.Roller)
                            myItem.SelectedControl.Text += " - " + Convert.ToString(ShellyStatus.RollerState);
                }
            }
        }
    }

However, if the actual value of a blind or dimmer is to be displayed, this can only be done on a Label or a Textbox. In this case, these elements would also have to be included in the ShellyActions.

An assignment could look like this:

Finally – last words

I have planned the Button control for executing the actions (use of the click event) and the Label and Textbox controls for displaying values ​​(use of the text property). For other controls, a connection did not seem to make sense to me - except you would want to work with customized controls here

I would like to thank @sean-ewington for the help in creating this and the previous article.