Marcus-L/m4rcus.TuyaCore

TuyaCore in vb.net

Closed this issue · 1 comments

Hi Marcus,

A couple of year ago I wrote an UWP program that controls lights and sensors based on Zwave. Recently I bought some Tuya lights and sockets and want to implement those in my program. I found your TuyaCore and downloaded it. As I write my program in vb.net I rewrote your C# version. I also cannot use Windows Core because my program has to run on a Raspberry Pi and therefor uses Windows UWP. After some struggle I finaly got the program error-free. Unfortunatelly, I get error messages when I try to address any of my Tuya devices. I did manage to find the DevID and the LocalKey for my devices. The errormessage I get is: Message = "Unable to read data from the transport connection: The external host has broken the connection.". Stacktrace: StackTrace = " at System.Net.Sockets.NetworkStream.EndRead(IAsyncResult asyncResult)" & vbCrLf & " at System.Threading.Tasks.TaskFactory1.FromAsyncCoreLogic(IAsyncResult iar, Func2 endFunction, Action1 endAction, Task1 promise, Boolean requiresSynchronization)" ...

I'm not familiar with programming a WiFi call so I have no clue what this error means. Would you be so kind to help me please? I have added my vb.net tuyacode code below. I had to look for some alternative coding here and there because some statements are not supported in vb.net/UWP. The routines below are called from a main program by this code:

Protected Async Sub SetTuyaStatus(ByVal OnOff As Boolean)
Dim device As New TuyaCore.TuyaPlug()
device.IP = "192.168.2.222"
device.LocalKey = "" // deleted the localkey
device.Id = "" // deleted the device ID

    Try
        Dim status As TuyaCore.TuyaPlug.TuyaStatus = Await device.GetStatus()
        Await device.SetStatus(Not status.Powered)
    Catch ex As Exception
        LogEventSource.Log.Error("Error in routine SetTuyaStatus:" & ex.Message.ToString)
    End Try

End Sub

The error occurs in the GetStatus routine.

Tuya Classes:

Imports System.Net.Sockets
Imports System.Security.Cryptography
Imports System.Text
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Imports Windows.Security.Cryptography.Core
Imports Windows.Storage.Streams

Namespace TuyaCore
Public Class TuyaDevice
Public Sub New()
End Sub

    Public Property IP As String
    Public Property Id As String
    Public Property LocalKey As String
    Public Property Port As Integer = 6668
    Public Property Version As String = "3.1"

    Protected Function CreatePayload(ByVal data As String, ByVal Optional encrypt As Boolean = True) As Byte()
        If Not encrypt Then Return Encoding.UTF8.GetBytes(data)
        Dim bytes = Encoding.UTF8.GetBytes(data)

        'Dim aes = New AesManaged() With {
        '    .Mode = CipherMode.ECB,
        '    .Key = Encoding.UTF8.GetBytes(LocalKey)
        '}

        Dim Key As Byte() = Encoding.UTF8.GetBytes(LocalKey)

        Using ms = New MemoryStream()

            Using cs = New CryptoStream(ms, Aes.Create.CreateEncryptor(), CryptoStreamMode.Write)
                cs.Write(bytes, 0, bytes.Length)
                'cs.Close()
                bytes = ms.ToArray()
            End Using
        End Using

        Dim data64 As String = Convert.ToBase64String(bytes)
        Dim payload As Byte() = Encoding.UTF8.GetBytes($"data={data64}||lpv={Version}||")

        Using md5 As MD5 = MD5.Create()

            Using ms = New MemoryStream()
                ms.Write(payload, 0, payload.Length)
                'ms.Write(aes.Key, 0, aes.Key.Length)
                ms.Write(Key, 0, Key.Length)
                Dim md5s As String = ArrayToHex(md5.ComputeHash(ms.ToArray())).Substring(8, 16)
                Return Encoding.UTF8.GetBytes($"{Version}{md5s}{data64}")
            End Using
        End Using
    End Function

    Protected Async Function Send(ByVal data As Byte()) As Task(Of Byte())
        Dim tries As Integer = 2
        Dim lastException As Exception = Nothing

        While Math.Max(System.Threading.Interlocked.Decrement(tries), tries + 1) > 0

            Try

                Using client = New TcpClient()

                    Await client.ConnectAsync(IP, Port)

                    Using stream = client.GetStream()


                        Using ms = New MemoryStream()
                            Dim buffer As Byte() = New Byte(1023) {}
                            Await stream.WriteAsync(data, 0, data.Length)
                            Dim bytes As Integer = Await stream.ReadAsync(buffer, 0, buffer.Length)
                            'stream.Close()
                            ms.Write(buffer, 0, bytes)
                            Return ms.ToArray()
                        End Using
                    End Using
                End Using

            Catch ex As IOException
                lastException = ex
            End Try

            Await Task.Delay(500)
        End While

        Throw lastException
    End Function

    Protected Function ReadBuffer(ByVal data As Byte()) As String
        Dim spec = New With {
            .prefix = New Byte() {0, 0, 85, 170, 0, 0, 0, 0, 0, 0, 0},
            .suffix = New Byte() {0, 0, 170, 85}
        }

        If data.Length < 24 OrElse Not data.Take(11).SequenceEqual(spec.prefix) Then
            Throw New Exception("invalid read buffer/prefix")
        End If

        Dim length As Integer = BitConverter.ToInt32(data.Skip(12).Take(4).Reverse().ToArray(), 0)

        If data.Length <> 16 + length Then
            Throw New Exception("invalid read buffer length")
        End If

        Dim payload As String = Encoding.UTF8.GetString(data.Skip(20).Take(length - 12).ToArray())

        If Not data.Skip(16 + length - 4).Take(4).SequenceEqual(spec.suffix) Then
            Throw New Exception("invalid read buffer/suffix")
        End If

        Return payload
    End Function

    Protected Function ConstructBuffer(ByVal data As Byte(), ByVal command As Byte) As Byte()
        Dim spec = New With {
            .prefix = New Byte() {0, 0, 85, 170, 0, 0, 0, 0, 0, 0, 0},
            .suffix = New Byte() {0, 0, 0, 0, 0, 0, 170, 85}
        }
        Dim dataLength As Byte() = BitConverter.GetBytes(data.Length + spec.suffix.Length)
        If BitConverter.IsLittleEndian Then Array.Reverse(dataLength)

        Using ms = New MemoryStream()
            ms.Write(spec.prefix, 0, spec.prefix.Length)
            ms.Write(New Byte() {command}, 0, 1)
            ms.Write(dataLength, 0, 4)
            ms.Write(data, 0, data.Length)
            ms.Write(spec.suffix, 0, spec.suffix.Length)
            Return ms.ToArray()
        End Using
    End Function

    Private Shared Function ArrayToHex(ByVal arr As Byte()) As String
        Return BitConverter.ToString(arr).Replace("-", String.Empty).ToLower()
    End Function

    Private Shared Function HexToArray(ByVal hex As String) As Byte()
        Return Enumerable.Range(0, hex.Length / 2).[Select](Function(x) Convert.ToByte(hex.Substring(x * 2, 2), 16)).ToArray()
    End Function

    Public Structure TuyaStatus
        Public Powered As Boolean
        Public Delay As Integer
        Public Current_mA As Double
        Public Power_W As Double
        Public Voltage_V As Double
    End Structure

End Class

Public Class TuyaPlug
    Inherits TuyaDevice

    Public Async Function GetStatus() As Task(Of TuyaStatus)

        Try
            Dim cmd As New Dictionary(Of String, String)
            cmd.Add("gwId", LocalKey)
            cmd.Add("devId", Id)

            Dim json As String = JsonConvert.SerializeObject(cmd)
            Dim payload = CreatePayload(json, False)
            Dim buffer = ConstructBuffer(payload, 10)
            json = ReadBuffer(Await Send(buffer))

            Dim tuyaStatus As TuyaStatus = New TuyaStatus()
            Dim result As JObject = JObject.Parse(json)

            If result("dps") IsNot Nothing Then
                If result("dps")("1") IsNot Nothing Then tuyaStatus.Powered = CBool(result("dps")("1").ToObject(GetType(Boolean)))
                If result("dps")("2") IsNot Nothing Then tuyaStatus.Delay = CInt(result("dps")("2").ToObject(GetType(Integer)))
                If result("dps")("4") IsNot Nothing Then tuyaStatus.Current_mA = CDbl(result("dps")("4").ToObject(GetType(Double)))
                If result("dps")("5") IsNot Nothing Then tuyaStatus.Power_W = CDbl(result("dps")("5").ToObject(GetType(Double)))
                If result("dps")("6") IsNot Nothing Then tuyaStatus.Voltage_V = CDbl(result("dps")("6").ToObject(GetType(Double))) / 10.0
                If result("dps")("18") IsNot Nothing Then tuyaStatus.Current_mA = CDbl(result("dps")("18").ToObject(GetType(Double)))
                If result("dps")("19") IsNot Nothing Then tuyaStatus.Power_W = CDbl(result("dps")("19").ToObject(GetType(Double))) / 10.0
                If result("dps")("20") IsNot Nothing Then tuyaStatus.Voltage_V = CDbl(result("dps")("20").ToObject(GetType(Double))) / 10.0
            End If

            Return (tuyaStatus)
        Catch ex As Exception
            'Throw New Exception($"Error reading buffer: {json}", ex)
        End Try
    End Function

    Public Async Function SetStatus(ByVal status As Boolean, ByVal Optional delay As Integer = 0) As Task

        Dim dps As New Dictionary(Of String, Object)
        dps.Add("1", status)
        dps.Add("2", delay)

        Dim cmd As New Dictionary(Of String, Object)
        cmd.Add("t", (DateTime.Now - New DateTime(1970, 1, 1)).TotalSeconds.ToString("0"))
        cmd.Add("devId", Id)
        cmd.Add("dps", dps)
        cmd.Add("uid", "")

        Dim json As String = JsonConvert.SerializeObject(cmd)
        Dim payload = CreatePayload(json)
        Dim buffer = ConstructBuffer(payload, 7)
        Dim result = ReadBuffer(Await Send(buffer))

        If result <> "" Then
            Throw New Exception($"Unexpected result: {result}")
        End If
    End Function
End Class

End Namespace

Raspberry Pi should be able to run .Net Core, check out https://github.com/dotnet/core/blob/master/samples/RaspberryPiInstructions.md