Hogar inteligente controlando dispositivos Shelly®.

En este artículo demuestro cómo integrar los relés Shelly® en mi hogar inteligente, controlando los dispositivos a través de rutinas en VB.NET.

Después de convertir la casa de mi yerno en una casa inteligente, también quería obtener los relés Shelly® para mi propia casa.

Principalmente controlo estos dispositivos con el Amazon Echo Dot® (“Alexa“) – pero por interés y porque no se puede encontrar buena documentación, decidí crear rutinas basadas en .NET para controlar o consultar los dispositivos.

En este artículo presento las rutinas que creé para este propósito y menciono las peculiaridades que tienen los módulos que utilizo.

En mi casa utilizo los módulos Shelly 2.5, Shelly Dimmer2, Shelly 1PM y Shelly 2PM. He creado las rutinas para estos módulos. Por supuesto, hay algunos otros módulos – pero el apreciado lector tendría que crear las rutinas para ellos, posiblemente utilizando esta plantilla.

Dado que solo tengo Visual Studio 2010® a mi disposición, el marco utilizado aquí es .NET 4.0

Conceptos básicos

Básicamente, la comunicación con los dispositivos se realiza mediante comandos HTML. La respuesta de los propios dispositivos se proporciona como una cadena JSON, para la cual he almacenado la información que me interesa en subclases apropiadas. Desafortunadamente, los comandos parciales varían de un dispositivo a otro, por lo que tuve que crear rutinas específicas para cada dispositivo.

Aquí asumo conocimientos básicos sobre la deserialización de cadenas JSON. No entraré en el uso de WebClient en detalle.

¿Con qué dispositivo estoy “hablando”?

Private Class ShellyType
        Public type As String
        Public app As String
 
        ReadOnly Property Type() 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(IpAddress As String) As ShellyType
 
        Request = "http://" + IpAddress + "/shelly"
        Dim myType As ShellyType = ShellyType.None
 
        Try
            Dim result As String = webClient.DownloadString(Request)
            Dim JSON_Packet As ShellyType = JsonConvert.DeserializeObject(Of ShellyType)(result)
 
            Select Case JSON_Packet.Type
                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

Como se puede ver aquí, hay un comando común para la consulta de tipo para todos los dispositivos. La respuesta de tipo se almacena nuevamente en diferentes propiedades JSON dependiendo del dispositivo – para algunos dispositivos en el elemento “type” y para otros dispositivos en el elemento “app”. La deserialización de JSON luego llena uno u otro elemento en mi clase.

La función mostrada me devuelve el tipo respectivo. Utilizo esta consulta en todas las consultas o comandos posteriores.

Solicitud del estado del dispositivo

 Function Shelly_GetStatus(IpAddress As String) As IO_Status

     Dim myType As ShellyType = Shelly_GetType(IpAddress)

     Select Case myType
         Case ShellyType.Shelly_25
             Return Shelly_25_GetStatus(IpAddress)
         Case ShellyType.Shelly_Dimmer2
             Return Shelly_Dimmer2_GetStatus(IpAddress)
         Case ShellyType.Shelly_1PM
             Return Shelly_1PM_GetStatus(IpAddress)
         Case ShellyType.Shelly_2PM
             Return Shelly_2PM_GetStatus(IpAddress)
         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

La función Shelly_GetStatus aquí mostrada devuelve el estado del dispositivo Shelly en la dirección IP especificada. La función se ramifica a la subfunción correspondiente según el tipo de Shelly respectivo.

Con el fin de lograr una estandarización, se utiliza el mismo estado de E/S para todos los dispositivos, solo que las áreas no existentes no se asignan en las subfunciones.

Subfunción de estado del dispositivo

Aquí describiré la subfunción en sí utilizando el ejemplo de uno de los dispositivos. Todos los demás dispositivos solo difieren en el comando y la cadena JSON recibida en respuesta.

En el siguiente ejemplo uso la consulta de un Shelly-1PM:

Clase Privada 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

    Clase Parcial cRelay
        Public output As Boolean
    End Class

    Clase Parcial cCover
        Public state As String
        Public last_direction As String
        Public current_pos As Integer
    End Class

    Clase Parcial cInput
        Public state As Object
    End Class

    Propiedad de solo lectura RelayState As Boolean()
        Obtener
            Dim myState(1) As Boolean
            Si Switch0 No Es Nada Entonces myState(0) = Switch0.output
            Si Switch1 No Es Nada Entonces myState(1) = Switch1.output
            Si Cover0 No Es Nada Entonces
                Seleccionar Caso Cover0.state
                    Caso "stopped"
                        myState(0) = False
                        myState(1) = False
                    Caso "opening"
                        myState(0) = True
                        myState(1) = False
                    Caso "closing"
                        myState(0) = False
                        myState(1) = True
                Fin Seleccionar
            Fin Si
            Devolver myState
        Fin Obtener
    End Property

    Propiedad de solo lectura InputState As Boolean()
        Obtener
            Dim myState(1) As Boolean
            Si No Boolean.TryParse(Input0.state, myState(0)) Entonces myState(0) = False
            Si No Boolean.TryParse(Input1.state, myState(1)) Entonces myState(1) = False
            Devolver myState
        Fin Obtener
    End Property

    Propiedad de solo lectura Mode As ShellyMode
        Obtener
            Si Switch0 No Es Nada Entonces Devolver ShellyMode.Relay
            Si Cover0 No Es Nada Entonces Devolver ShellyMode.Roller
            Devolver ShellyMode.none
        Fin Obtener
    End Property

    Propiedad de solo lectura RollerState As ShellyRollerState
        Obtener
            Si Cover0 No Es Nada Entonces
                Si (Cover0.state = "stop") Y (Cover0.last_direction = "opening") Entonces Devolver ShellyRollerState.Stop_AfterOpening
                Si (Cover0.state = "closing") Entonces Devolver ShellyRollerState.Closing
                Si (Cover0.state = "stop") Y (Cover0.last_direction = "closing") Entonces Devolver ShellyRollerState.Stop_AfterClosing
                Si (Cover0.state = "opening") Entonces Devolver ShellyRollerState.Opening
            Fin Si
            Devolver ShellyRollerState.none
        Fin Obtener
    End Property
End Class

Función Shelly_1PM_GetStatus(DirecciónIp As String) Como IO_Status

    Dim myStatus Como New IO_Status
    Solicitud = "http://" + DirecciónIp + "/rpc/Shelly.GetStatus"

    Intentar
        Dim resultado Como String = webClient.DownloadString(Solicitud)
        Dim PaqueteJSON Como JSON_Shelly12PM_Status = JsonConvert.DeserializeObject(Of JSON_Shelly12PM_Status)(resultado)

        myStatus.Out0 = PaqueteJSON.RelayState(0)
        myStatus.Out0 = False
        myStatus.OutValue = -1
        myStatus.Mode = "Relay"
        myStatus.In0 = PaqueteJSON.InputState(0)
        myStatus.In1 = False

        myStatus.Connection = ShellyResult.Conectado
        Devolver myStatus

    Catch ex As Exception
        myStatus.Connection = ShellyResult.ErrorDeConexión
        Devolver myStatus
    End Try

End Function

El Shelly-1PM es un relé de 1 canal que solo tiene una entrada. Sin embargo, la cadena JSON devuelta por el propio dispositivo no es diferente a la de un Shelly-2PM – por eso uso la misma clase para la deserialización de la cadena JSON para ambos dispositivos.

Controlar un dispositivo / enviar un comando

Como ejemplo, presento la función para controlar un relé en Shelly. También existe la opción de controlar el brillo de un regulador y mover una persiana a una posición específica. Sin embargo, ninguna de estas funciones difiere en lo básico.

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

Integración en un control de botón

El siguiente código muestra la integración de los métodos en un control de botón. En este caso, extiendo el botón estándar con algunas propiedades e integro las funciones en consecuencia.

El botón ahora llama al método Shelly_ToggleOutput en el evento de clic y cambia sus colores según el estado de salida del dispositivo Shelly seleccionado.

Imports System.ComponentModel
 
Public Class ShellyButton
    Inherits Button
 
    Sub New()
        MyBase.BackColor = my_DefaultBackColor
        MyBase.ForeColor = my_DefaultForeColor
    End Sub
 
#Region "Propiedades"
 
    ' hace que la propiedad estándar no sea visible dentro de PropertyGrid
    <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
    Shadows Property ForeColor As Color
 
    ' Reemplazo de la propiedad estándar dentro de PropertyGrid
    <Category("Shelly"), Description("ForeColor predeterminado del 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 del control cuando está animado")>
   <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
 
    ' hace que la propiedad estándar no sea visible dentro de PropertyGrid
    <Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
    Shadows Property BackColor As Color
 
    ' Reemplazo de la propiedad estándar dentro de PropertyGrid
    <Category("Shelly"), Description("BackColor predeterminado del 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 del control cuando está animado")>
    <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("Intervalo de actualización de la animación")>
    <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("Habilita la actualización de la animación")>
    <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("Dirección IP del dispositivo Shelly con el que trabajar")>
    <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("Número de salida del dispositivo Shelly con el que trabajar")>
     <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("muestra el tipo de dispositivo Shelly conectado")>
    ReadOnly Property ShellyType As String
        Get
            Return my_ShellyType
        End Get
    End Property
    Private my_ShellyType As String
 
#End Region
 
#Region "Métodos"
 
    ' llama al método ToggleButton con el evento Click del botón
    Protected Overrides Sub OnClick(e As System.EventArgs)
        Dim result As ShellyResult = Shelly_ToggleOutput(my_IPAdress, my_ShellyOutputNr)
    End Sub
 
 
    ' el Tick del temporizador hace que se active la animación del botón
    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

Puntos de Interés

En general, se incluyen los siguientes métodos:

Shelly_GetStatusString pasa la cadena de resultado completa y formateada a la solicitud seleccionada
Shelly_GetType pasa el tipo de dispositivo Shelly en la dirección IP seleccionada
Shelly_GetStatus transfiere el estado actual del dispositivo Shelly a la dirección IP seleccionada. Las características correspondientes se devuelven en Shelly_IOStatus. Dependiendo del tipo de dispositivo, se utilizan los submétodos:
Shelly_25_GetStatus: obtener el estado de un Shelly 2.5 Shelly_25_convertJSON: convertir la cadena JSON de la solicitud de Shelly 2.5
Shelly_Dimmer2_GetStatus: obtener el estado de un Shelly Dimmer2 Shelly_Dimmer2_convertJSON: convertir la cadena JSON de la solicitud de Shelly Dimmer2
Shelly_1PM_GetStatus: obtener el estado de un Shelly 1PM Shelly_1PM_convertJSON: convertir la cadena JSON de la solicitud de Shelly 1PM
Shelly_2PM_GetStatus: obtener el estado de un Shelly 2PM Shelly_2PM_convertJSON: convertir la cadena JSON de la solicitud de Shelly 2PM
Shelly_SetOutput establece la salida seleccionada en el dispositivo Shelly en la dirección IP seleccionada al estado seleccionado
Shelly_ToggleOutput alterna el estado de la salida seleccionada en el dispositivo Shelly en la dirección IP seleccionada
Shelly_SetRoller establece las persianas / estores en la posición seleccionada en el dispositivo Shelly en la dirección IP seleccionada
Shelly_ToggleRoller alterna el estado de conducción de la persiana / estor a la posición seleccionada en el dispositivo Shelly en la dirección IP seleccionada
Shelly_SetDimmer controla el regulador en el dispositivo Shelly en la dirección IP seleccionada al valor de brillo seleccionado

con los siguientes tipos de devolución:

Enum ShellyType los posibles tipos de Shelly
Enum ShellyResult los posibles resultados de una solicitud
Enum ShellyMode los posibles modos de operación del dispositivo Shelly
Enum ShellyRollerState los posibles estados del accionamiento de la persiana / estor
Class Shelly IOStatus el estado de IO del dispositivo Shelly solicitado

Finalmente, últimas palabras

Quiero agradecer a @RichardDeeming y a @Andre Oosthuizen por su ayuda con algunos detalles que desconocía.

Obtuve información básica sobre los propios dispositivos de la página Shelly Support.

Tuve que determinar los nombres de los elementos de las consultas utilizando la ingeniería inversa.


Leave a Reply

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