智能家居-控制Shelly®设备

在本文中,我将演示如何将Shelly®继电器集成到我的智能家居中,通过VB.NET的例行程序来控制设备

在我帮我的女婿让他的新房智能化之后,我也想为我自己的房子购买Shelly®继电器。

我主要使用亚马逊Dot®(“Alexa”)来控制这些设备 – 但由于没有找到好的文档,并且出于兴趣,我决定基于.NET创建例行程序,以便控制或查询这些设备。

本文将介绍我为此目的创建的例行程序,并列出我使用的模块的特点。

我在我的房子里使用Shelly 2.5、Shelly Dimmer2、Shelly 1PM和Shelly 2PM模块。我为这些模块创建了例行程序。当然,还有其他一些模块 – 但是尊贵的读者将不得不自己创建这些模块的过程,可能使用这个模板。

由于我只有Visual Studio 2010®,所以这里使用的框架是.NET 4.0

基本原理

基本上,与设备的通信是通过HTML命令进行的。设备本身的反馈以JSON字符串的形式提供,我将感兴趣的信息存储在适当的子类中。不幸的是,部分命令因设备而异,这就是我不得不为每个设备创建特定的例行程序的原因。

这里我假设大家对JSON字符串的反序列化有基本的了解。我不会进一步讨论WebClient的使用。

我正在“与哪个设备交流?”

Private Class ShellyTyp
        Public type As String
        Public app As String
 
        ReadOnly Property Typ() As String
            Get
                If type IsNot Nothing Then Return type
                If app IsNot Nothing Then Return app
                Return ""
            End Get
        End Property
 
    End Class
 
    Function Shelly_GetType(IpAdress As String) As ShellyType
 
        Request = "http://" + IpAdress + "/shelly"
        Dim myType As ShellyType = ShellyType.None
 
        Try
            Dim result As String = webClient.DownloadString(Request)
            Dim JSON_Packet As ShellyTyp = JsonConvert.DeserializeObject(Of ShellyTyp)(result)
 
            Select Case JSON_Packet.Typ
                Case "SHSW-25" : myType = ShellyType.Shelly_25
                Case "SHDM-2" : myType = ShellyType.Shelly_Dimmer2
                Case "Plus1PM", "Plus1Mini" : myType = ShellyType.Shelly_1PM
                Case "Plus2PM" : myType = ShellyType.Shelly_2PM
            End Select
 
            Return myType
 
        Catch ex As Exception
            Return ShellyType.None
        End Try
 
    End Function

正如这里所看到的,对于所有设备,有一个通用的类型查询命令。根据设备的不同,类型响应再次存储在不同的JSON属性中 – 对于某些设备是在“type”项中,对于某些设备是在“app”项中。然后,JSON反序列化将填充我的类中的一项或另一项。

所示的函数返回了对应的类型。我在所有后续的查询/命令中使用此查询。

设备状态请求

 Function Shelly_GetStatus(IpAdress As String) As IO_Status

     Dim myType As ShellyType = Shelly_GetType(IpAdress)

     Select Case myType
         Case ShellyType.Shelly_25
             Return Shelly_25_GetStatus(IpAdress)
         Case ShellyType.Shelly_Dimmer2
             Return Shelly_Dimmer2_GetStatus(IpAdress)
         Case ShellyType.Shelly_1PM
             Return Shelly_1PM_GetStatus(IpAdress)
         Case ShellyType.Shelly_2PM
             Return Shelly_2PM_GetStatus(IpAdress)
         Case ShellyType.None
             Return New IO_Status
     End Select

     Return New IO_Status
 End Function

Class IO_Status
     Public Connection As ShellyResult = ShellyResult.None

     Public In0 As Boolean = False
     Public In1 As Boolean = False
     Public Out0 As Boolean = False
     Public Out1 As Boolean = False
     Public Mode As ShellyMode = ShellyMode.none
     Public OutValue As Integer = -1

     Overrides Function toString() As String
         Dim s As String = Connection.ToString

         Dim inActive As String = ""
         If In0 Then inActive += "0"
         If In1 Then inActive += "1"
         If inActive <> "" Then s += ", in:" + inActive

         Dim outActive As String = ""
         If Out0 Then outActive += "0"
         If Out1 Then outActive += "1"
         If outActive <> "" Then s += ", out:" + outActive
         If OutValue >= 0 Then s += ", " + Str(OutValue).Trim + "%"

         If Mode <> ShellyMode.none Then s += ", mode:" + Mode.ToString
         Return s
     End Function
 End Class

这里显示的Shelly_GetStatus函数返回指定IP地址的Shelly设备的状态。该函数根据相应的Shelly类型分支到相应的子函数。

为了在这里实现标准化,所有设备都具有相同的IO状态,只有不存在的区域在子函数中未被分配。

设备状态子函数

我将在此处使用一个设备的示例来描述子函数本身。所有其他设备只在命令和收到的JSON字符串上有所不同。

以下示例中我使用了一个Shelly-1PM的查询:

Private Class JSON_Shelly12PM_Status

  <Newtonsoft.Json.JsonProperty("switch:0")>
    Public Switch0 As cRelay
    <Newtonsoft.Json.JsonProperty("switch:1")>
    Public Switch1 As cRelay
    <Newtonsoft.Json.JsonProperty("cover:0")>
    Public Cover0 As cCover
    <Newtonsoft.Json.JsonProperty("input:0")>
    Public Input0 As cInput
    <Newtonsoft.Json.JsonProperty("input:1")>
    Public Input1 As cInput

    Partial Public Class cRelay
        Public output As Boolean
    End Class

    Partial Public Class cCover
        Public state As String
        Public last_direction As String
        Public current_pos As Integer
    End Class

    Partial Public Class cInput
        Public state As Object
    End Class

    ReadOnly Property RelayState As Boolean()
        Get
            Dim myState(1) As Boolean
            If Switch0 IsNot Nothing Then myState(0) = Switch0.output
            If Switch1 IsNot Nothing Then myState(1) = Switch1.output
            If Cover0 IsNot Nothing Then
                Select Case Cover0.state
                    Case "stopped"
                        myState(0) = False
                        myState(1) = False
                    Case "opening"
                        myState(0) = True
                        myState(1) = False
                    Case "closing"
                        myState(0) = False
                        myState(1) = True
                End Select
            End If
            Return myState
        End Get
    End Property

    ReadOnly Property InputState As Boolean()
        Get
            Dim myState(1) As Boolean
            If Not Boolean.TryParse(Input0.state, myState(0)) Then myState(0) = False
            If Not Boolean.TryParse(Input1.state, myState(1)) Then myState(1) = False
            Return myState
        End Get
    End Property

    ReadOnly Property Mode As ShellyMode
        Get
            If Switch0 IsNot Nothing Then Return ShellyMode.Relay
            If Cover0 IsNot Nothing Then Return ShellyMode.Roller
            Return ShellyMode.none
        End Get
    End Property

    ReadOnly Property RollerState As ShellyRollerState
        Get
            If Cover0 IsNot Nothing Then
                If (Cover0.state = "stop") And (Cover0.last_direction = "opening") Then Return ShellyRollerState.Stop_AfterOpening
                If (Cover0.state = "closing") Then Return ShellyRollerState.Closing
                If (Cover0.state = "stop") And (Cover0.last_direction = "closing") Then Return ShellyRollerState.Stop_AfterClosing
                If (Cover0.state = "opening") Then Return ShellyRollerState.Opening
            End If
            Return ShellyRollerState.none
        End Get
    End Property
End Class

Function Shelly_1PM_GetStatus(IpAdress As String) As IO_Status

    Dim myStatus As New IO_Status
    Request = "http://" + IpAdress + "/rpc/Shelly.GetStatus"

    Try
        Dim result As String = webClient.DownloadString(Request)
        Dim JSON_Packet As JSON_Shelly12PM_Status = JsonConvert.DeserializeObject(Of JSON_Shelly12PM_Status)(result)

        myStatus.Out0 = JSON_Packet.RelayState(0)
        myStatus.Out0 = False
        myStatus.OutValue = -1
        myStatus.Mode = "继电器"
        myStatus.In0 = JSON_Packet.InputState(0)
        myStatus.In1 = False

        myStatus.Connection = ShellyResult.已连接
        Return myStatus

    Catch ex As Exception
        myStatus.Connection = ShellyResult.连接错误
        Return myStatus
    End Try

End Function

Shelly-1PM 是一个具有一个输入的单通道继电器。然而,设备本身返回的 JSON 字符串与 Shelly-2PM 没有区别 – 这就是为什么我在对这两个设备的 JSON 字符串进行反序列化时使用同一个类的原因。

控制设备 / 发送命令

作为示例,我演示了控制 Shelly 继电器的函数。还有一个控制调光器亮度并将百叶窗移动到特定位置的选项。然而,这些功能在基本上没有区别。

Function Shelly_SetOutput(IpAdress As String, OutNr As Integer, State As Boolean) As ShellyResult

    Dim myType As ShellyType = Shelly_GetType(IpAdress)
    Request = "http://" + IpAdress + "/relay/"

    Select Case myType
        Case ShellyType.Shelly_1PM
            Request += "0?turn="

            If Not State Then
                Request += "off"
            Else
                Request += "on"
            End If

        Case ShellyType.Shelly_2PM, ShellyType.Shelly_25
            Select Case OutNr
                Case 0, 1
                    Request += Str(OutNr).Trim
                Case Else
                    Return ShellyResult.ErrorShellyType
            End Select

            Request += "?turn="

            If Not State Then
                Request += "off"
            Else
                Request += "on"
            End If

        Case ShellyType.Shelly_Dimmer2
            Request = "http://" + IpAdress + "/light/0?turn="

            If Not State Then
                Request += "off"
            Else
                Request += "on"
            End If

        Case Else
            Return ShellyResult.NoAction

    End Select

    Try

        Dim result As String = webClient.DownloadString(Request)
        Return ShellyResult.Done

    Catch ex As Exception
        Return ShellyResult.ErrorConnection
    End Try

    Return ShellyResult.NoAction
End Function

在按钮控件中集成

以下代码展示了将方法集成到按钮控件中的方式。在这种情况下,我使用一些属性扩展了标准按钮,并相应地集成了这些函数。

按钮现在在点击事件中调用 Shelly_ToggleOutput 方法,并根据所选 Shelly 设备的输出状态改变其颜色。

Imports System.ComponentModel
 
Public Class ShellyButton
    Inherits Button
 
    Sub New()
        MyBase.BackColor = my_DefaultBackColor
        MyBase.ForeColor = my_DefaultForeColor
    End Sub
 
#Region "Properties"
 
    ' makes the Standard-Property unvisible inside the PropertyGrid
    <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
    Shadows Property ForeColor As Color
 
    ' Replacement for the Standard-Property inside the PropertyGrid
    <Category("Shelly"), Description("Default ForeColor of the Control")>
   <DefaultValue(GetType(System.Drawing.Color), "Black")>
    Shadows Property DefaultForeColor As Color
        Get
            Return my_DefaultForeColor
        End Get
        Set(ByVal value As Color)
            my_DefaultForeColor = value
            MyBase.BackColor = value
        End Set
    End Property
    Private my_DefaultForeColor As Color = Color.Black
 
    <Category("Shelly"), Description("ForeColor of the Control when animated")>
   <DefaultValue(GetType(System.Drawing.Color), "White")>
    Shadows Property AnimationForeColor As Color
        Get
            Return my_AnimationForeColor
        End Get
        Set(ByVal value As Color)
            my_AnimationForeColor = value
        End Set
    End Property
    Private my_AnimationForeColor As Color = Color.White
 
    ' makes the Standard-Property unvisible inside the PropertyGrid
    <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
    Shadows Property BackColor As Color
 
    ' Replacement for the Standard-Property inside the PropertyGrid
    <Category("Shelly"), Description("Default BackColor of the Control")>
    <DefaultValue(GetType(System.Drawing.Color), "LightGray")>
    Shadows Property DefaultBackColor As Color
        Get
            Return my_DefaultBackColor
        End Get
        Set(ByVal value As Color)
            my_DefaultBackColor = value
            MyBase.BackColor = value
            Me.Invalidate()
        End Set
    End Property
    Private my_DefaultBackColor As Color = Color.LightGray
 
    <Category("Shelly"), Description("BackColor of the Control when animated")>
    <DefaultValue(GetType(System.Drawing.Color), "Green")>
    Property AnimationBackColor As Color
        Get
            Return my_AnimationBackColor
        End Get
        Set(ByVal value As Color)
            my_AnimationBackColor = value
            Me.Invalidate()
        End Set
    End Property
    Private my_AnimationBackColor As Color = Color.Green
 
    <Category("Shelly"), Description("Refresh-Interval for the Animation")>
    <DefaultValue(1000)>
    Property RefreshInterval As Integer
        Get
            Return my_Timer.Interval
        End Get
        Set(value As Integer)
            If value > 500 Then
                my_Timer.Interval = value
            End If
        End Set
    End Property
 
    <Category("Shelly"), Description("Enables the Refresh of the Animation")>
    <DefaultValue(False)>
    Property RefreshEnabled As Boolean
        Get
            Return my_RefreshEnabled
        End Get
        Set(value As Boolean)
            my_RefreshEnabled = value
            If Not DesignMode Then my_Timer.Enabled = value
        End Set
    End Property
    Private my_RefreshEnabled As Boolean = False
 
    <Category("Shelly"), Description("IpAdress of the Shelly-Device to work with")>
    <RefreshProperties(RefreshProperties.All)>
     <DefaultValue(1000)>
    Property IpAdress As String
        Get
            Return my_IPAdress
        End Get
        Set(value As String)
            my_ShellyType = Shelly_GetType(value).ToString
            If my_ShellyType <> "None" Then my_IPAdress = value
        End Set
    End Property
    Private my_IPAdress As String = ""
 
    <Category("Shelly"), Description("Output-Number of the Shelly-Device to work with")>
     <DefaultValue(0)>
    Property ShellyOutputNr As Integer
        Get
            Return my_ShellyOutputNr
        End Get
        Set(value As Integer)
            If (value >= 0) And (value <= 1) Then my_ShellyOutputNr = value
        End Set
    End Property
    Private my_ShellyOutputNr As Integer = 0
 
    <Category("Shelly"), Description("shows the Type of the connected Shelly-Device")>
    ReadOnly Property ShellyType As String
        Get
            Return my_ShellyType
        End Get
    End Property
    Private my_ShellyType As String
 
#End Region
 
#Region "Methods"
 
    ' call the ToggleButton-Method with the Button-Click
    Protected Overrides Sub OnClick(e As System.EventArgs)
        Dim result As ShellyResult = Shelly_ToggleOutput(my_IPAdress, my_ShellyOutputNr)
    End Sub
 
 
    ' the Timer-Tick does when activated the Animation of the Button
    Sub Timer_Tick() Handles my_Timer.Tick
        my_Status = Shelly_GetStatus(my_IPAdress)
        my_OutActive = (my_ShellyOutputNr = 0 And my_Status.Out0) Or (my_ShellyOutputNr = 1 And my_Status.Out1)
        If my_OutActive Then
            MyBase.BackColor = my_AnimationBackColor
            MyBase.ForeColor = my_AnimationForeColor
        Else
            MyBase.BackColor = my_DefaultBackColor
            MyBase.ForeColor = my_DefaultForeColor
        End If
    End Sub
    Private my_Status As Shelly_IOStatus
    Private my_OutActive As Boolean = False
    Private WithEvents my_Timer As New Timer With {.Enabled = False, .Interval = 1000}
 
#End Region
 
End Class

兴趣点

总的来说,下面的方法包括:

Shelly_GetStatusString 将完整和格式化的结果字符串传递给选定的请求
Shelly_GetType 传递选定IP地址的Shelly设备类型
Shelly_GetStatus 将当前Shelly设备的设备状态传输到选定的IP地址,对应的特性在 Shelly_IOStatus 中返回根据设备类型,使用子方法:

  • Shelly_25_GetStatus:获取Shelly 2.5的状态
  • Shelly_25_convertJSON:转换Shelly 2.5请求的JSON字符串
  • Shelly_Dimmer2_GetStatus:获取Shelly Dimmer2的状态
  • Shelly_Dimmer2_convertJSON:转换Shelly Dimmer2请求的JSON字符串
  • Shelly_1PM_GetStatus:获取Shelly 1PM的状态
  • Shelly_1PM_convertJSON:转换Shelly 1PM请求的JSON字符串
  • Shelly_2PM_GetStatus:获取Shelly 2PM的状态
  • Shelly_2PM_convertJSON:转换Shelly 2PM请求的JSON字符串
Shelly_SetOutput 将选定IP地址的Shelly设备上的选定输出设置为选定状态
Shelly_ToggleOutput 切换选定IP地址的Shelly设备上选定输出的状态
Shelly_SetRoller 将选定IP地址的百叶窗/卷帘设置为选定位置的Shelly设备
Shelly_ToggleRoller 切换选定IP地址的百叶窗/卷帘的驱动状态到选定位置的Shelly设备
Shelly_SetDimmer 将选定IP地址的Shelly设备上的调光器控制为选定的亮度值

返回类型如下:

Enum ShellyType 可能的Shelly类型
Enum ShellyResult 请求的可能结果
Enum ShellyMode Shelly设备的可能操作模式
Enum ShellyRollerState 百叶窗/卷帘驱动的可能状态
Class Shelly IOStatus 所请求的Shelly设备的IO状态

最后 – 最后一些话

我要感谢 @RichardDeeming 和 @Andre Oosthuizen 对我不了解的某些细节提供的帮助。

关于设备本身的基本信息,我从页面 Shelly Support 获取到。

我不得不自行通过逆向工程确定查询的项目名称。


Leave a Reply

Your email address will not be published. Required fields are marked *