Smart Home – управление устройствами Shelly®

В этой статье я покажу, как интегрировать реле Shelly® в мой умный дом, управляя устройствами через рутины в VB.NET.

После того, как я умный дом сделал для своего зятя, я также захотел получить модули реле Shelly® для своего дома.

Я в основном управляю этими устройствами с помощью Amazon Echo 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, показанная здесь, возвращает статус Shelly устройства по указанному IP-адресу. Сама функция ветвится на соответствующую подфункцию в зависимости от типа Shelly.

Для обеспечения стандартизации здесь используется одинаковое состояние ввода-вывода для всех устройств, только несуществующие области в подфункциях не назначаются.

Подфункция состояния устройства

Я описываю саму подфункцию здесь, используя пример для одного из устройств. Остальные устройства отличаются только командой и полученной 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 = "Relay"
        myStatus.In0 = JSON_Packet.InputState(0)
        myStatus.In1 = False

        myStatus.Connection = ShellyResult.Connected
        Return myStatus

    Catch ex As Exception
        myStatus.Connection = ShellyResult.ErrorConnection
        Return myStatus
    End Try

End Function

Shelly-1PM – это 1-канальное реле с одним только вводом. Однако JSON-строка, возвращаемая самим устройством, ничем не отличается от 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

Интеграция в элемент управления Button

Следующий код демонстрирует интеграцию методов в элемент управления Button. В этом случае я расширяю стандартную кнопку с некоторыми свойствами и соответствующими функциями.

Кнопка теперь вызывает метод Shelly_ToggleOutput при событии нажатия и меняет свои цвета в соответствии с состоянием вывода выбранного устройства Shelly.

Imports System.ComponentModel

Public Class ShellyButton
Inherits Button

Sub New()
MyBase.BackColor = my_DefaultBackColor
MyBase.ForeColor = my_DefaultForeColor
End Sub

#Region "Свойства"

' делает стандартное свойство невидимым внутри PropertyGrid
<Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
Shadows Property ForeColor As Color

' Замена стандартного свойства внутри 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

' делает стандартное свойство невидимым внутри PropertyGrid
<Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
Shadows Property BackColor As Color

' Замена стандартного свойства внутри 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 "Методы"

' вызывает метод ToggleButton при нажатии на кнопку
Protected Overrides Sub OnClick(e As System.EventArgs)
Dim result As ShellyResult = Shelly_ToggleOutput(my_IPAdress, my_ShellyOutputNr)
End Sub

' событие Timer-Tick производит анимацию кнопки при активации
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

Интересные моменты

В общем, включены следующие методы:

Shelly_GetStatusString передает полную и форматированную строку с результатами выбранному запросу
Shelly_GetType передает тип устройства Shelly по выбранному IP-адресу
Shelly_GetStatus передает текущий статус устройства Shelly по выбранному IP-адресу, соответствующие характеристики возвращаются в Shelly_IOStatus. В зависимости от типа устройства используются подметоды:

  • Shelly_25_GetStatus: получить состояние Shelly 2.5
  • Shelly_25_convertJSON: преобразовать JSON-строку запроса для Shelly 2.5
  • Shelly_Dimmer2_GetStatus: получить состояние Shelly Dimmer2
  • Shelly_Dimmer2_convertJSON: преобразовать JSON-строку запроса для Shelly Dimmer2
  • Shelly_1PM_GetStatus: получить состояние Shelly 1PM
  • Shelly_1PM_convertJSON: преобразовать JSON-строку запроса для Shelly 1PM
  • Shelly_2PM_GetStatus: получить состояние Shelly 2PM
  • Shelly_2PM_convertJSON: преобразовать JSON-строку запроса для Shelly 2PM
Shelly_SetOutput устанавливает выбранный выход на устройстве Shelly для выбранного IP-адреса в выбранный статус
Shelly_ToggleOutput переключает состояние выбранного выхода на устройстве Shelly по выбранному IP-адресу
Shelly_SetRoller устанавливает жалюзи / роллеты в выбранную позицию на устройстве Shelly по выбранному IP-адресу
Shelly_ToggleRoller переключает состояние движения жалюзи / роллеты в выбранную позицию на устройстве Shelly по выбранному IP-адресу
Shelly_SetDimmer управляет диммером на устройстве Shelly по выбранному IP-адресу, устанавливая выбранное значение яркости

с возвращаемыми типами:

Enum ShellyType возможные типы устройств Shelly
Enum ShellyResult Возможные результаты запроса
Enum ShellyMode возможные режимы работы устройства Shelly
Enum ShellyRollerState возможные состояния двигателя жалюзи / роллеты
Class Shelly IOStatus IO-статус запрашиваемого устройства Shelly

И, наконец, последние слова

Я хотел бы поблагодарить @RichardDeeming и @Andre Oosthuizen за помощь в некоторых деталях, которые я не знал.

Основную информацию о самих устройствах я получил с сайта Shelly Support.

Я самостоятельно определял имена элементов запросов с помощью метода обратного проектирования.


Leave a Reply

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