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