Authentication failed (ewelink-api)
Hi all,
I have been using the ewelink node api for more than a year now on my solar home automation system. Been working flawless, but now the api returns 'Authentication failed'. I noticed my Authorization Key expired on https://dev.ewelink.cc. I deleted the old app created a new one and started using the new APP_ID and APP_SECRET. Its been more than a week now and its still not working. Has there been changes to the API or something i'm not aware of?
Thanks for any help in advance. lou
Similar issue here but API key has not expired for me, but I get a 407 path request not allowed for any endpoint that was already working before. Created a ticket and they sent me to talk to [email protected], no reply from them for the last 5 days.
Same problem here. Created a new APP_ID and APP_SECRET (even though previous had not expired), but same message:
407 path request not allowed for appid:xxxxxxxxxxxxxxxxxxxxxx
Looks like something changed.
Yes, I got in touch with support and the only options are: -Pay for an enterprise license (2k/yr) to be able to call the /login endpoint directly -Login manually using the web interface to obtain an authorization token to interact with the rest of the endpoints
On Tue, 10 Dec 2024 at 8:24 PM Nick Waterton @.***> wrote:
Same problem here. Created a new APP_ID and APP_SECRET (even though previous had not expired), but same message:
407 path request not allowed for appid:xxxxxxxxxxxxxxxxxxxxxx
Looks like something changed.
— Reply to this email directly, view it on GitHub https://github.com/skydiver/ewelink-api/issues/242#issuecomment-2533205999, or unsubscribe https://github.com/notifications/unsubscribe-auth/A5R4D3RTOSNNQ3ID2NIM5TD2E5ZZFAVCNFSM6AAAAABR63CLYCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKMZTGIYDKOJZHE . You are receiving this because you commented.Message ID: @.***>
Well that’s super annoying.
when they say “login manually”, do they mean just send your login and password to get the token? I can’t see why you would have to specifically use the web interface.
I see they have a getToken endpoint, that expects a url to be passed - do you think this is what they are talking about?
Agree, they did this without any notice. This is what they replied.
You are using Oauth2.0 interface, you can not directly call the login interface, you need to obtain an authorization code (at) in the authorization page, you can use this at to call other interfaces.
https://coolkit-technologies.github.io/eWeLink-API/#/en/OAuth2.0?id=access-process
https://coolkit-technologies.github.io/eWeLink-API/#/en/OAuth2.0 You basically need to setup a web page (the authorization page) where you would enter the user/pass in an form to obtain the authorization token.
“After the user fills in the account password and clicks login successfully, the page will jump to the redirect address URL you added before and carries the parameters code, regin, state, and the request method is GET.”
On Tue, 10 Dec 2024 at 9:44 PM Nick Waterton @.***> wrote:
Well that’s super annoying.
when they say “login manually”, do they mean just send your login and password to get the token? I can’t see why you would have to specifically use the web interface.
I see they have at getToken endpoint, that expects a url to be passed - do you think this is what they are talking about?
— Reply to this email directly, view it on GitHub https://github.com/skydiver/ewelink-api/issues/242#issuecomment-2533361201, or unsubscribe https://github.com/notifications/unsubscribe-auth/A5R4D3XUBAQSCL7DKPL5JLT2E6DIPAVCNFSM6AAAAABR63CLYCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKMZTGM3DCMRQGE . You are receiving this because you commented.Message ID: @.***>
I have a feeling they don’t know what they are talking about. There is no need to have an actual web page for people to fill in.
I’ve read their docs, the problem is that most of them are out of date, or don’t actually work.
I’ll figure it out.
if you figure out how to do it without the webpage please let me know. thank you!
On Tue, Dec 10, 2024 at 10:44 PM Nick Waterton @.***> wrote:
I have a feeling they don’t know what they are talking about. There is no need to have an actual web page for people to fill in.
I’ve read their docs, the problem is that most of them are out of date, or don’t actually work.
I’ll figure it out.
— Reply to this email directly, view it on GitHub https://github.com/skydiver/ewelink-api/issues/242#issuecomment-2533431679, or unsubscribe https://github.com/notifications/unsubscribe-auth/A5R4D3RXIUJEJ7L6ZHUPP6T2E6KHJAVCNFSM6AAAAABR63CLYCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKMZTGQZTCNRXHE . You are receiving this because you commented.Message ID: @.***>
It seems the api only supports oauth2 now. I wrote an application in Python to fetch the oath token. Then with the token I'm able to get status/update/stop/start devices. I am by no mean a developer by trade, but if your stuck let me know and I can upload the code here....
@SpoedNick80 If you have some code that gets the token without using the /v2/user/login end point I would certainly be interested.
Feel free to upload a snippet.
@SpoedNick80 Would you be able to share the code? It's my first time using this library and I couldn't get even the basic example from the documentation to work as I am getting this error
@Husseinhassan1 This is the problem, this library won’t work anymore, as they have moved the login end point to be an enterprise feature.
Ok, so I figured out how to get the code (without the login page), I can pass that to get token and successfully get the access and refresh tokens.
Problem is when you use this access token to try to get the apikey you get the same error messagepath not allowed for app id:xxxxxxxxxxxxxxxxx.
So, I’m still stuck. I can query devices, homes etc, but you can’t connect to the websocket without the apikey.
So, I finally have it working again.
The process is:
- Obtain the access code
- exchange the access code for an access token
- get the family data to obtain the apikey
- open the websocket using the obtained apikey and access token
To get the access code (without using the login page) - sorry this is Python:
APP = [(appid, clientsecret)]
redirectUrl is the redirect url you defined in the developer console for the app - it is NOT USED, so it can be whatever you want, but it has to match. I used "https://127.0.0.1:8888"
helper methods:
def makeSign(self, key, message):
j = hmac.new(key.encode(), message.encode(), digestmod=hashlib.sha256)
return (base64.b64encode(j.digest())).decode()
def get_nonce(self):
ts = time.time()
return str(int(ts))[-8:]
def get_ts(self):
'''
gets timestamp in ms as string
'''
return str(int(time.time()* 1000))
async def get_code(login, password, app=0):
appid, appsecret = APP[app]
seq = self.get_ts()
nonce = self.get_nonce()
state = '12345'
sign = self.makeSign(appsecret, '{}_{}'.format(appid, seq))
payload = { 'authorization' : 'Sign ' + sign,
"email" : login,
"password" : password,
'seq': seq,
'clientId': appid,
"state" : state,
"grantType" : "authorization_code",
'redirectUrl':redirectUrl,
"nonce" : nonce
}
data = json.dumps(payload).encode()
headers = { "X-CK-Appid": appid,
"X-CK-Nonce": nonce,
"X-CK-Seq": seq,
"Authorization": "Sign " + sign,
"Content-Type": "application/json; charset=utf-8"
}
url = "https://apia.coolkit.cn/v2/user/oauth/code"
self.log.info('Logging in via: {} with {} and {}'.format(url, appid, appsecret))
r = await self.session.post(
url, data=data, headers=headers,
timeout=30
)
resp = await r.json()
return resp['data']
the access code is in resp. To get the access token from the access code:
async def get_token(self, code, app=0):
'''
get token from code
'''
appid, appsecret = APP[app]
payload = { "code": code,
"redirectUrl":redirectUrl,
"grantType":"authorization_code"
}
data = json.dumps(payload).encode()
sign = self.makeSign(appsecret, json.dumps(payload))
headers = { "X-CK-Appid": appid,
"X-CK-Nonce": self.get_nonce(),
"Authorization": "Sign " + sign,
"Content-Type": "application/json; charset=utf-8"
}
self.host = "https://us-apia.coolkit.cc" #depends on region (returned with code)
url = self.host + '/v2/user/oauth/token'
self.log.info('Getting token via: {} with code: {}'.format(url, code))
r = await self.session.post(
url, data=data, headers=headers,
timeout=30
)
resp = await r.json()
return resp['data']
The access token, refresh token and expiry dates (in timestamp milliseconds) are returned. To get the apikey using the access token:
async def _get(self, token, app = 0):
headers = { "Authorization": "Bearer " + token,
"X-CK-Appid": APP[app][0]}
r = await self.session.get(
self.host + '/v2/family', headers=headers, timeout=5
)
resp = await r.json()
return resp['data']
The apikey is contained in the returned json, and can be used to open and connect to the websocket.
Sorry this is a bit rough, and in Python, but hopefully you understand the sequence.
@NickWaterton that's extremely helpful, thank you. Can confirm that your approach works when implemented in Kotlin.
Using repo baugp/ewelink-api, which use a different APP_ID and APP_SECRET, it works for me
This is intended to use your own APP_ID and APP_SECRET, which you have created and have control over at the developer web site.
If you use someone else’s, you have no idea when they will expire, or what the scope is. So it may work for now, but it could stop working at any time.
Got it working with this
import aiohttp import asyncio import hmac import hashlib import base64 import json import time import random import string import websockets
=== User-configurable switch name and desired state ===
SWITCH_NAME = "Hhh" # <-- set the device name here SWITCH_STATE = "off" # <-- set "on" or "off" here
=== Your eWeLink App Credentials ===
APPID = "insert your key" APPSECRET = "insert your secret" REDIRECT_URL = "https://127.0.0.1:8888"
=== Your eWeLink Login Credentials ===
EMAIL = "email" PASSWORD = "password"
=== Helper functions ===
def make_sign(key, message): return base64.b64encode(hmac.new(key.encode(), message.encode(), digestmod=hashlib.sha256).digest()).decode()
def get_nonce(): return ''.join(random.choices(string.ascii_letters + string.digits, k=8))
def get_ts(): return str(int(time.time() * 1000))
def millis(): return str(int(time.time() * 1000))
=== Step 1: Get Access Code ===
async def get_code(session, email, password): seq = get_ts() nonce = get_nonce() sign = make_sign(APPSECRET, f"{APPID}_{seq}")
payload = {
"authorization": "Sign " + sign,
"email": email,
"password": password,
"seq": seq,
"clientId": APPID,
"state": "12345",
"grantType": "authorization_code",
"redirectUrl": REDIRECT_URL,
"nonce": nonce
}
headers = {
"X-CK-Appid": APPID,
"X-CK-Nonce": nonce,
"X-CK-Seq": seq,
"Authorization": "Sign " + sign,
"Content-Type": "application/json; charset=utf-8"
}
async with session.post("https://apia.coolkit.cn/v2/user/oauth/code",
data=json.dumps(payload), headers=headers) as resp:
data = await resp.json()
print("✅ Access Code Response:")
print(json.dumps(data, indent=2))
return data["data"]
=== Step 2: Get Access Token ===
async def get_token(session, code, region="us"): payload = { "code": code, "redirectUrl": REDIRECT_URL, "grantType": "authorization_code" } sign = make_sign(APPSECRET, json.dumps(payload)) nonce = get_nonce()
headers = {
"X-CK-Appid": APPID,
"X-CK-Nonce": nonce,
"Authorization": "Sign " + sign,
"Content-Type": "application/json; charset=utf-8"
}
host = f"https://{region}-apia.coolkit.cc"
async with session.post(f"{host}/v2/user/oauth/token",
data=json.dumps(payload), headers=headers) as resp:
data = await resp.json()
print("\n✅ Access Token Response:")
print(json.dumps(data, indent=2))
return data["data"], host, region
=== Step 3: Get Family List ===
async def get_family_list(session, token, host): headers = { "Authorization": "Bearer " + token, "X-CK-Appid": APPID } async with session.get(f"{host}/v2/family", headers=headers) as resp: data = await resp.json() print("\n✅ Family Data:") print(json.dumps(data, indent=2)) return data["data"].get("familyList", [])
=== Step 4: Get All Devices (Direct Query) ===
async def get_all_devices(session, token, host): headers = { "Authorization": "Bearer " + token, "X-CK-Appid": APPID } params = { "num": 100, "page": 1 } all_devices = [] while True: async with session.get(f"{host}/v2/device", headers=headers, params=params) as resp: data = await resp.json() devices = data["data"].get("deviceList", []) total = data["data"].get("total", 0) all_devices.extend(devices) print(f"\n📟 All Devices (Direct Query), page {params['page']}:") print(json.dumps(data, indent=2)) print(f"Devices fetched: {len(devices)}, Total expected: {total}") if len(devices) < params["num"] or len(all_devices) >= total: break params["page"] += 1 return all_devices
=== Step 5: Get Devices for Family or No Family ===
async def get_devices_for_family(session, token, host, family_apikey=None): headers = { "Authorization": "Bearer " + token, "X-CK-Appid": APPID } all_devices = [] for item_type in [1, 2, 3, 4]: for use_family in [True, False]: params = { "num": 100, "page": 1, "type": item_type } if use_family and family_apikey: params["familyApikey"] = family_apikey while True: async with session.get(f"{host}/v2/device/thing", headers=headers, params=params) as resp: data = await resp.json() devices = data["data"].get("thingList", []) total = data["data"].get("total", 0) all_devices.extend(devices) print(f"\n📟 Devices for {'family ' + family_apikey if use_family and family_apikey else 'no family'}, type {item_type}, page {params['page']}:") print(json.dumps(data, indent=2)) print(f"Devices fetched: {len(devices)}, Total expected: {total}") if len(devices) < params["num"] or len(all_devices) >= total: break params["page"] += 1 return all_devices
=== Get Dispatch Server Info ===
async def get_dispatch_server(session, token, region): url = f"https://{region}-dispa.coolkit.cc/dispatch/app" headers = {"Authorization": f"Bearer {token}"} async with session.get(url, headers=headers) as resp: data = await resp.json() if data["error"] != 0: raise Exception("Failed to get dispatch server") print("\n✅ Dispatch Server Info:") print(json.dumps(data, indent=2)) return data["domain"], data["port"]
=== WebSocket Control ===
async def websocket_control(device_id, access_token, user_apikey, region, state): async with aiohttp.ClientSession() as session: domain, port = await get_dispatch_server(session, access_token, region) ws_url = f"wss://{domain}:{port}/api/ws" print(f"\nConnecting to WebSocket: {ws_url}")
async with websockets.connect(ws_url) as ws:
# Handshake
handshake = {
"action": "userOnline",
"version": 8,
"ts": int(time.time()),
"at": access_token,
"userAgent": "app",
"apikey": user_apikey,
"appid": APPID,
"nonce": get_nonce(),
"sequence": millis()
}
await ws.send(json.dumps(handshake))
print("Handshake sent")
resp = await ws.recv()
print("Handshake response:", resp)
# Send device ON/OFF command
update_msg = {
"action": "update",
"deviceid": device_id,
"apikey": user_apikey,
"userAgent": "app",
"sequence": millis(),
"params": {
"switch": state
}
}
await ws.send(json.dumps(update_msg))
print(f"Sent toggle {state.upper()} command for device {device_id}")
toggle_resp = await ws.recv()
print("Toggle response:", toggle_resp)
=== Main Workflow ===
async def main(): async with aiohttp.ClientSession() as session: # Step 1: Get access code code_data = await get_code(session, EMAIL, PASSWORD) code = code_data["code"] region = code_data.get("region", "us")
# Step 2: Get access token
token_data, host, region = await get_token(session, code, region)
access_token = token_data["accessToken"]
user_apikey = token_data.get("apikey", None)
if not user_apikey:
# fallback: get family apikey if no user apikey in token response
families = await get_family_list(session, access_token, host)
if families:
user_apikey = families[0]["apikey"]
else:
print("Error: Could not find user apikey.")
return
# Step 3: Get families and devices
families = await get_family_list(session, access_token, host)
all_devices = []
direct_devices = await get_all_devices(session, access_token, host)
all_devices.extend(direct_devices)
for family in families + [None]:
family_apikey = family["apikey"] if family else None
devices = await get_devices_for_family(session, access_token, host, family_apikey)
all_devices.extend(devices)
print(f"\nTotal devices found: {len(all_devices)}\n")
# Step 4: Print devices and pick one to toggle
print("🧾 Your Devices:")
unique_ids = set()
device_to_toggle = None
for dev in all_devices:
item = dev.get("itemData", {})
deviceid = item.get("deviceid") or item.get("mainDeviceId") or "<unknown>"
if deviceid in unique_ids:
continue
unique_ids.add(deviceid)
name = item.get("name", "<no name>")
online = "Yes" if item.get("online") else "No"
print(f"- {name} (deviceid: {deviceid}) - Online: {online}")
# Choose device to toggle by user-defined name
if name == SWITCH_NAME:
device_to_toggle = deviceid
if not device_to_toggle:
print(f"Could not find device '{SWITCH_NAME}' to toggle.")
return
print(f"\nToggling device '{device_to_toggle}' {SWITCH_STATE.upper()} via WebSocket...")
# Step 5: WebSocket control
await websocket_control(device_to_toggle, access_token, user_apikey, region, SWITCH_STATE)
if name == "main": asyncio.run(main())
спасибо за исходный код. вы мне очень помогли ))))) я 2 дня потратил что бы найти endpoint https://eu-apia.coolkit.cc/v2/user/oauth/code для получения кода. Его не заметил в официальной документации хотя перечитал ее 10 раз ))) /v2/user/oauth/token нужно было писать а я все писали /v2/oauth/token ))) спасибо )