Skip to content
Open
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
151 changes: 85 additions & 66 deletions sonoff/sonoff.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# The domain of your component. Should be equal to the name of your component.
import logging, time, hmac, hashlib, random, base64, json, socket, requests, re, uuid
import logging, time, hmac, hashlib, random, base64, json, socket, requests, re, string
from datetime import timedelta

SCAN_INTERVAL = timedelta(seconds=60)
HTTP_MOVED_PERMANENTLY, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_NOT_FOUND = 301,400,401,404

_LOGGER = logging.getLogger(__name__)
#_LOGGER = logging.getLogger(__name__)


def gen_nonce(length=8):
Expand All @@ -28,14 +28,7 @@ def __init__(self, username, password, api_region, user_apikey=None, bearer_toke
self._bearer_token = bearer_token
self._devices = []
self._ws = None

# app details
self._app_version = '3.5.3'
self._appid = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
self._model = 'iPhone10,6'
self._os = 'iOS'
self._rom_version = '11.1.2'
self._version = '6'
self.appid = 'R8Oq3y0eSZSYdKccHlrQzT1ACCOUT9Gv'

if user_apikey and bearer_token:
self.do_reconnect()
Expand All @@ -58,29 +51,42 @@ def do_reconnect(self):
self.do_login()

def do_login(self):
import uuid

# reset the grace period
self._skipped_login = 0

app_details = {
'password' : self._password,
'version' : self._version,
'version' : '8',
'ts' : int(time.time()),
'nonce' : gen_nonce(15),
'appid' : self._appid,
#'appid' : 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq',
'appid' : 'R8Oq3y0eSZSYdKccHlrQzT1ACCOUT9Gv',
'imei' : str(uuid.uuid4()),
'os' : self._os,
'model' : self._model,
'romVersion': self._rom_version,
'appVersion': self._app_version
'os' : 'iOS',
'model' : 'iPhone10,6',
'romVersion': '11.1.2',
'appVersion': '3.5.3'
}

self._model = 'iPhone' + random.choice(['6,1', '6,2', '7,1', '7,2', '8,1', '8,2', '8,4', '9,1', '9,2', '9,3', '9,4', '10,1', '10,2', '10,3', '10,4', '10,5', '10,6', '11,2', '11,4', '11,6', '11,8'])
self._romVersion = random.choice([
'10.0', '10.0.2', '10.0.3', '10.1', '10.1.1', '10.2', '10.2.1', '10.3', '10.3.1', '10.3.2', '10.3.3', '10.3.4',
'11.0', '11.0.1', '11.0.2', '11.0.3', '11.1', '11.1.1', '11.1.2', '11.2', '11.2.1', '11.2.2', '11.2.3', '11.2.4', '11.2.5', '11.2.6', '11.3', '11.3.1', '11.4', '11.4.1',
'12.0', '12.0.1', '12.1', '12.1.1', '12.1.2', '12.1.3', '12.1.4', '12.2', '12.3', '12.3.1', '12.3.2', '12.4', '12.4.1', '12.4.2',
'13.0', '13.1', '13.1.1', '13.1.2', '13.2'
])
self._appVersion = random.choice(['3.5.3', '3.5.4', '3.5.6', '3.5.8', '3.5.10', '3.5.12', '3.6.0', '3.6.1', '3.7.0', '3.8.0', '3.9.0', '3.9.1', '3.10.0', '3.11.0'])
self._imei = str(uuid.uuid4())

if re.match(r'[^@]+@[^@]+\.[^@]+', self._username):
app_details['email'] = self._username
else:
app_details['phoneNumber'] = self._username

decryptedAppSecret = b'6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
#decryptedAppSecret = b'6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
decryptedAppSecret = b'1ve5Qk9GXfUhKAn1svnKwpAlxXkMarru'

hex_dig = hmac.new(
decryptedAppSecret,
Expand All @@ -94,16 +100,19 @@ def do_login(self):
'Content-Type' : 'application/json;charset=UTF-8'
}

r = requests.post('https://{}-api.coolkit.cc:8080/api/user/login'.format(self._api_region),
# r = requests.post('https://{}-api.coolkit.cc:8080/api/user/login'.format(self._api_region),
# headers=self._headers, json=app_details, verify=False)
r = requests.post('https://{}-api.coolkit.cc:8080/api/user/login'.format(self._api_region),
headers=self._headers, json=app_details)


resp = r.json()

# get a new region to login
if 'error' in resp and 'region' in resp and resp['error'] == HTTP_MOVED_PERMANENTLY:
self._api_region = resp['region']

_LOGGER.warning("found new region: >>> %s <<< (you should change api_region option to this value in configuration.yaml)", self._api_region)
print("found new region: >>> %s <<< (you should change api_region option to this value in configuration.yaml)", self._api_region)

# re-login using the new localized endpoint
self.do_login()
Expand All @@ -116,7 +125,7 @@ def do_login(self):
self.do_login()

else:
_LOGGER.error("Couldn't authenticate using the provided credentials!")
print("Couldn't authenticate using the provided credentials!")

return

Expand All @@ -131,12 +140,13 @@ def do_login(self):
self.update_devices() # to get the devices list

def set_wshost(self):
r = requests.post('https://%s-disp.coolkit.cc:8080/dispatch/app' % self._api_region, headers=self._headers)
# r = requests.post('https://%s-disp.coolkit.cc:8080/dispatch/app' % self._api_region, headers=self._headers, verify=False)
r = requests.post('https://%s-disp.coolkit.cc:8080/dispatch/app' % self._api_region, headers=self._headers)
resp = r.json()

if 'error' in resp and resp['error'] == 0 and 'domain' in resp:
self._wshost = resp['domain']
_LOGGER.info("Found websocket address: %s", self._wshost)
#print("Found websocket address: %s", self._wshost)
else:
raise Exception('No websocket domain')

Expand All @@ -157,39 +167,31 @@ def update_devices(self):

# we are in the grace period, no updates to the devices
if self._skipped_login and self.is_grace_period():
_LOGGER.info("Grace period active")
print("Grace period active, no updates to the devices")
return self._devices

query_params = {
'lang': 'en',
'version': self._version,
'ts': int(time.time()),
'nonce': gen_nonce(15),
'appid': self._appid,
'imei': str(uuid.uuid4()),
'os': self._os,
'model': self._model,
'romVersion': self._rom_version,
'appVersion': self._app_version
}
r = requests.get('https://{}-api.coolkit.cc:8080/api/user/device'.format(self._api_region),
params=query_params,
headers=self._headers)

#r = requests.get('https://{}-api.coolkit.cc:8080/api/user/device'.format(self._api_region),
# headers=self._headers)
r = requests.get('https://{}-api.coolkit.cc:8080/api/user/device?lang=en&apiKey={}&getTags=1&version=6&ts=%s&nonce=%s&appid=Uw83EKZFxdif7XFXEsrpduz5YyjP7nTl&imei=%s&os=iOS&model=%s&romVersion=%s&appVersion=%s'.format(
self._api_region, self.get_user_apikey(), str(int(time.time())), ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8)), self._imei, self._model, self._romVersion, self._appVersion
), headers=self._headers)
resp = r.json()
#print ("response :",resp)
#print (r.status_code)
#print (r.content)
if 'error' in resp and resp['error'] in [HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED]:
# @IMPROVE add maybe a service call / switch to deactivate sonoff component
if self.is_grace_period():
_LOGGER.warning("Grace period activated!")
print("Grace period activated!")

# return the current (and possible old) state of devices
# in this period any change made with the mobile app (on/off) won't be shown in HA
return self._devices

_LOGGER.info("Re-login component")
print("Re-login component")
self.do_login()

self._devices = resp.get('devicelist', [])
devices = r.json()
self._devices = devices['devicelist']
return self._devices

def get_devices(self, force_update = False):
Expand All @@ -213,6 +215,8 @@ def get_user_apikey(self):
return self._user_apikey

def _get_ws(self):
import ssl
import websocket
"""Check if the websocket is setup and connected."""
try:
create_connection
Expand All @@ -221,29 +225,44 @@ def _get_ws(self):

if self._ws is None:
try:
#self._ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
#self._ws.connect('wss://{}:8080/api/ws'.format(self._wshost))
self._ws = create_connection(('wss://{}:8080/api/ws'.format(self._wshost)), timeout=10)

ts = time.time()
# payload = {
# 'action' : "userOnline",
# 'userAgent' : 'app',
# 'version' : 8,
# 'nonce' : gen_nonce(15),
# 'apkVesrion': "1.8",
# 'os' : 'ios',
# 'at' : self.get_bearer_token(),
# 'apikey' : self.get_user_apikey(),
# 'ts' : str(int(time.time())),
# 'model' : 'iPhone10,6',
# 'romVersion': '11.1.2',
# 'sequence' : str(time.time()).replace('.','')
# }
payload = {
'action' : "userOnline",
'userAgent' : 'app',
'version' : 6,
'nonce' : gen_nonce(15),
'apkVesrion': "1.8",
'os' : 'ios',
'at' : self.get_bearer_token(),
'apikey' : self.get_user_apikey(),
'ts' : str(int(time.time())),
'model' : 'iPhone10,6',
'romVersion': '11.1.2',
'sequence' : str(time.time()).replace('.','')
'action': 'userOnline',
'at': self.get_bearer_token(),
'apikey': self.get_user_apikey(),
'userAgent': 'app',
'appid': self.appid,
'nonce': str(int(ts / 100)),
'ts': int(ts),
'version': 8,
'sequence': str(int(ts * 1000))
}

self._ws.send(json.dumps(payload))
wsresp = self._ws.recv()
# _LOGGER.error("open socket: %s", wsresp)
#print (wsresp)
#("open socket: %s", wsresp)

except (socket.timeout, ConnectionRefusedError, ConnectionResetError):
_LOGGER.error('failed to create the websocket')
# except (socket.timeout, ConnectionRefusedError, ConnectionResetError):
except (socket.timeout):
print('failed to create the websocket')
self._ws = None

return self._ws
Expand All @@ -253,13 +272,13 @@ def switch(self, new_state, deviceid, outlet=None):

# we're in the grace period, no state change
if self._skipped_login:
_LOGGER.info("Grace period, no state change")
print("Grace period, no state change")
return (not new_state)

self._ws = self._get_ws()

if not self._ws:
_LOGGER.warning('invalid websocket, state cannot be changed')
print('invalid websocket, state cannot be changed')
return (not new_state)

# convert from True/False to on/off
Expand All @@ -269,13 +288,13 @@ def switch(self, new_state, deviceid, outlet=None):
device = self.get_device(deviceid)

if outlet is not None:
_LOGGER.debug("Switching `%s - %s` on outlet %d to state: %s", \
print("Switching `%s - %s` on outlet %d to state: %s", \
device['deviceid'], device['name'] , (outlet+1) , new_state)
else:
_LOGGER.debug("Switching `%s` to state: %s", deviceid, new_state)
print ("Switching ", device['name'], " to state: ", new_state)

if not device:
_LOGGER.error('unknown device to be updated')
print('unknown device to be updated')
return False

# the payload rule is like this:
Expand Down Expand Up @@ -310,8 +329,8 @@ def switch(self, new_state, deviceid, outlet=None):

self._ws.send(json.dumps(payload))
wsresp = self._ws.recv()
# _LOGGER.debug("switch socket: %s", wsresp)

#_LOGGER.debug("switch socket: %s", wsresp)
#print (wsresp)
self._ws.close() # no need to keep websocket open (for now)
self._ws = None

Expand Down