Skip to content

Commit

Permalink
Merge pull request #23 from kueblc/smart-link-py
Browse files Browse the repository at this point in the history
SmartConfig in Python
  • Loading branch information
kueblc authored Mar 23, 2019
2 parents 42cce77 + 2309e03 commit b2211be
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 78 deletions.
19 changes: 1 addition & 18 deletions install_prereq.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,10 @@

set -e

which curl &> /dev/null || {
sudo apt-get update
sudo apt-get install -y curl
}

which node &> /dev/null || {
curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -
}

sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y dnsmasq hostapd screen curl python3-pip python3-setuptools python3-wheel mosquitto nodejs haveged
sudo apt-get install -y dnsmasq hostapd screen curl python3-pip python3-setuptools python3-wheel mosquitto haveged

sudo pip3 install paho-mqtt pyaes tornado

which npm &> /dev/null || {
echo "cannot find npm. please install nodejs manually."
exit 1
}
pushd scripts/smartconfig
npm i
popd

echo "Ready to start upgrade"
41 changes: 41 additions & 0 deletions scripts/smartconfig/broadcast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# encoding: utf-8
"""
broadcast.py
Created by kueblc on 2019-01-25.
Encode data for Tuya smartconfig via broadcast
broadcast strategy ported from https://github.com/tuyapi/link
"""

from crc import crc_8

broadcast_head = [1, 3, 6, 10]

def encode_broadcast_body( password, ssid, token_group ):
r = []
r.append( len(password) )
r.extend([ ord(l) for l in password ])
r.append( len(token_group) )
r.extend([ ord(l) for l in token_group ])
r.extend([ ord(l) for l in ssid ])
e = []
length = len(r)
length_crc = crc_8([ length ])
e.append( length >> 4 | 16 )
e.append( length & 0xF | 32 )
e.append( length_crc >> 4 | 48 )
e.append( length_crc & 0xF | 64 )
sequence = 0
for i in range(0, length, 4):
group = []
group.append( sequence )
group.extend( r[ i : i+4 ] )
group.extend( [0] * (5 - len(group)) )
group_crc = crc_8(group)
e.append( group_crc & 0x7F | 128 )
e.append( sequence | 128 )
e.extend([ b | 256 for b in r[ i : i+4 ] ])
sequence += 1
e.extend( [256] * (length - i) )
return e

35 changes: 35 additions & 0 deletions scripts/smartconfig/crc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# encoding: utf-8
"""
crc.py
Created by kueblc on 2019-01-25.
Compute 8bit and 32bit CRCs for Tuya communication
based in part on https://github.com/tuyapi/link
"""

def crc_8_byte( b ):
r = 0
for i in range(8):
if (r ^ b) & 1:
r ^= 0x18
r >>= 1
r |= 0x80
else:
r >>= 1
b >>= 1
return r

def crc_8( a ):
r = 0
for b in a:
r = crc_8_byte( r ^ b )
return r

crc_32_table = [0, 1996959894, -301047508, -1727442502, 124634137, 1886057615, -379345611, -1637575261, 249268274, 2044508324, -522852066, -1747789432, 162941995, 2125561021, -407360249, -1866523247, 498536548, 1789927666, -205950648, -2067906082, 450548861, 1843258603, -187386543, -2083289657, 325883990, 1684777152, -43845254, -1973040660, 335633487, 1661365465, -99664541, -1928851979, 997073096, 1281953886, -715111964, -1570279054, 1006888145, 1258607687, -770865667, -1526024853, 901097722, 1119000684, -608450090, -1396901568, 853044451, 1172266101, -589951537, -1412350631, 651767980, 1373503546, -925412992, -1076862698, 565507253, 1454621731, -809855591, -1195530993, 671266974, 1594198024, -972236366, -1324619484, 795835527, 1483230225, -1050600021, -1234817731, 1994146192, 31158534, -1731059524, -271249366, 1907459465, 112637215, -1614814043, -390540237, 2013776290, 251722036, -1777751922, -519137256, 2137656763, 141376813, -1855689577, -429695999, 1802195444, 476864866, -2056965928, -228458418, 1812370925, 453092731, -2113342271, -183516073, 1706088902, 314042704, -1950435094, -54949764, 1658658271, 366619977, -1932296973, -69972891, 1303535960, 984961486, -1547960204, -725929758, 1256170817, 1037604311, -1529756563, -740887301, 1131014506, 879679996, -1385723834, -631195440, 1141124467, 855842277, -1442165665, -586318647, 1342533948, 654459306, -1106571248, -921952122, 1466479909, 544179635, -1184443383, -832445281, 1591671054, 702138776, -1328506846, -942167884, 1504918807, 783551873, -1212326853, -1061524307, -306674912, -1698712650, 62317068, 1957810842, -355121351, -1647151185, 81470997, 1943803523, -480048366, -1805370492, 225274430, 2053790376, -468791541, -1828061283, 167816743, 2097651377, -267414716, -2029476910, 503444072, 1762050814, -144550051, -2140837941, 426522225, 1852507879, -19653770, -1982649376, 282753626, 1742555852, -105259153, -1900089351, 397917763, 1622183637, -690576408, -1580100738, 953729732, 1340076626, -776247311, -1497606297, 1068828381, 1219638859, -670225446, -1358292148, 906185462, 1090812512, -547295293, -1469587627, 829329135, 1181335161, -882789492, -1134132454, 628085408, 1382605366, -871598187, -1156888829, 570562233, 1426400815, -977650754, -1296233688, 733239954, 1555261956, -1026031705, -1244606671, 752459403, 1541320221, -1687895376, -328994266, 1969922972, 40735498, -1677130071, -351390145, 1913087877, 83908371, -1782625662, -491226604, 2075208622, 213261112, -1831694693, -438977011, 2094854071, 198958881, -2032938284, -237706686, 1759359992, 534414190, -2118248755, -155638181, 1873836001, 414664567, -2012718362, -15766928, 1711684554, 285281116, -1889165569, -127750551, 1634467795, 376229701, -1609899400, -686959890, 1308918612, 956543938, -1486412191, -799009033, 1231636301, 1047427035, -1362007478, -640263460, 1088359270, 936918000, -1447252397, -558129467, 1202900863, 817233897, -1111625188, -893730166, 1404277552, 615818150, -1160759803, -841546093, 1423857449, 601450431, -1285129682, -1000256840, 1567103746, 711928724, -1274298825, -1022587231, 1510334235, 755167117]

def crc_32( a ):
r = -1
for b in a:
r = ( (r & 0xFFFFFFFF) >> 8 ) ^ crc_32_table[(r ^ b) & 255]
return r ^ -1

27 changes: 27 additions & 0 deletions scripts/smartconfig/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
# encoding: utf-8
"""
main.py
Created by kueblc on 2019-01-25.
Configure Tuya devices via smartconfig for tuya-convert
"""

ssid = "vtrust-flash"
passwd = "flashmeifyoucan"
region = "US"
token = "00000000"
secret = "0101"

from smartconfig import smartconfig

print('Put Device in Learn Mode! Sending SmartConfig Packets now')

print('Sending SSID '+ssid)
print('Sending wifiPassword '+passwd)
print('SmartConfig in progress')

smartconfig( passwd, ssid, region, token, secret )

print()
print('SmartConfig complete.')

65 changes: 65 additions & 0 deletions scripts/smartconfig/multicast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# encoding: utf-8
"""
multicast.py
Created by kueblc on 2019-01-25.
Encode data for Tuya smartconfig via multicast
multicast strategy reverse engineered by kueblc
"""

from crc import crc_32

from Crypto.Cipher import AES
pad = lambda data, block_size : data + ('\0' * ( (block_size - len(data)) % block_size ) )
aes = AES.new( "a3c6794oiu876t54", AES.MODE_ECB, '' )
encrypt = lambda data : aes.encrypt( pad(data, 16) )

def encode_pw( pw ):
r = []
pw_bytes = [ ord(c) for c in pw ]
encrypted_pw = encrypt(pw)
# CRC and length are from plaintext
crc = crc_32(pw_bytes)
# length, twice
r.append( len(pw) )
r.append( len(pw) )
# encode CRC as LE
r.extend([ (crc >> i) & 255 for i in range(0,32,8) ])
# payload, AES encrypted
r.extend( encrypted_pw )
return r

def encode_plain( s ):
r = []
s_bytes = [ ord(c) for c in s ]
crc = crc_32(s_bytes)
# length, twice
r.append( len(s) )
r.append( len(s) )
# encode CRC as LE
r.extend([ (crc >> i) & 255 for i in range(0,32,8) ])
# payload, plaintext
r.extend( s_bytes )
return r

def bytes_to_ips( data, sequence ):
r = []
if len(data) & 1:
data.append(0)
for i in range(0, len(data), 2):
r.append( "226." + str(sequence) + "." + str(data[i+1]) + "." + str(data[i]) )
sequence += 1
return r

multicast_head = bytes_to_ips([ ord(c) for c in "TYST01" ], 120)

def encode_multicast_body( password, ssid, token_group ):
r = []
ssid_encoded = encode_plain(ssid)
r.extend( bytes_to_ips( ssid_encoded, 64 ) )
password_encoded = encode_pw(password)
r.extend( bytes_to_ips( password_encoded, 0 ) )
token_group_encoded = encode_plain(token_group)
r.extend( bytes_to_ips( token_group_encoded, 32 ) )
return r

25 changes: 0 additions & 25 deletions scripts/smartconfig/package.json

This file was deleted.

34 changes: 0 additions & 34 deletions scripts/smartconfig/smartconfig.js

This file was deleted.

62 changes: 62 additions & 0 deletions scripts/smartconfig/smartconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# encoding: utf-8
"""
smartconfig.py
Created by kueblc on 2019-01-25.
Configure Tuya devices via smartconfig without the Tuya cloud or app
broadcast strategy ported from https://github.com/tuyapi/link
multicast strategy reverse engineered by kueblc
"""

# Defaults

# time to sleep inbetween packets, 5ms
GAP = 5 / 1000.

BIND_ADDRESS = '10.42.42.1'

MULTICAST_TTL = 1

from socket import *
from time import sleep

class SmartConfigSocket(object):
def __init__( self, address = BIND_ADDRESS, gap = GAP ):
self._socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
self._socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self._socket.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
self._socket.setsockopt(IPPROTO_IP, IP_MULTICAST_TTL, MULTICAST_TTL)
self._socket.bind((address, 0))
self._gap = gap

def send_broadcast( self, data ):
for length in data:
self._socket.sendto( b'\0' * length, ('255.255.255.255', 30011))
sleep(self._gap)

def send_multicast( self, data ):
for ip in data:
self._socket.sendto( b'\0', (ip, 30012))
sleep(self._gap)

from broadcast import broadcast_head, encode_broadcast_body
from multicast import multicast_head, encode_multicast_body

def smartconfig( password, ssid, region, token, secret ):
sock = SmartConfigSocket()
token_group = region + token + secret
broadcast_body = encode_broadcast_body( password, ssid, token_group )
# print(broadcast_body)
multicast_body = encode_multicast_body( password, ssid, token_group )
# print(multicast_body)
# print("sending header")
for i in range(40): # originally 143, that's more than we really need
sock.send_multicast(multicast_head)
sock.send_broadcast(broadcast_head)
for i in range(10): # originally 30, again, more than necessary
# print("sending body iteration " + str(i))
print('.', end='', flush=True) # quick and dirty progress meter
sock.send_multicast(multicast_head)
sock.send_multicast(multicast_body)
sock.send_broadcast(broadcast_body)

2 changes: 1 addition & 1 deletion start_flash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ echo ""
echo "======================================================"
echo "Starting pairing procedure in screen"
sudo ip route add 255.255.255.255 dev $WLAN
$screen_with_log smarthack-smartconfig.log -S smarthack-smartconfig -m -d ./smartconfig/smartconfig.js
$screen_with_log smarthack-smartconfig.log -S smarthack-smartconfig -m -d ./smartconfig/main.py

popd

Expand Down

0 comments on commit b2211be

Please sign in to comment.