Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SmartConfig in Python #23

Merged
merged 9 commits into from
Mar 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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