I don’t think so
Too bad. Closed source stuff, thanks Philips. So I’ll get the version without WiFi… if not “local only”… I don’t want my data to be routed over Philips’ servers anyway.
Okay, we got an Philips Airfryer from Santa and I’m more than curious to find a way to integrate it into my HA environment.
I live on the iPhone world and used Charles proxy to decode the traffic.
What I found out so far is, that the app is using UPNP to find the Airfryer on the local network.
curl http://192.168.42.96/upnp/description.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>2</major>
<minor>1</minor>
</specVersion>
<device>
<deviceType>urn:philips-com:device:DiProduct:1</deviceType>
<friendlyName>Cosmos</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<modelName>AirfryerConnected</modelName>
<modelNumber>HD9285/00</modelNumber>
<UDN>uuid:12345678-1234-1234-1234-e4bc9601cb4c</UDN>
<cppId>e4:bc:96:01:cb:4c</cppId>
</device>
</root>
Not sure if we need the “ccpid” for generating the key (that’s why I posted it here).
But I’m not looking into the UPNP implementation for now, this should be kind of easy to implement.
The next step is a POST request to the following URL:
curl -X POST -i --insecure https://192.168.42.96/di/v1/products/0/firmware
HTTP/1.1 401 Unauthorized
Content-Length: 0
Content-Type: text/plain
WWW-Authenticate: PHILIPS-Condor VnX/m5uWc6CV/H38ztbzog==
So the Authenticate header is giving you a base64 encoded Challenge.
This string changes from time to time and can be forced to change after a power-cycle.
If you have the correct Aithorizartion header, you can use it to call the Airfryer for a status:
curl -X GET -i --insecure --header "User-Agent: cml" --header "Content-Type: application/json" --header "Authorization: PHILIPS-Condor ssyjLmLPQSeIh/0nGNJYjgXeKfz66Px5QFU5PFwAc7/5G47//zT7CXhDoLhzL+lH" https://192.168.42.96/di/v1/products/1/airfryer
HTTP/1.1 200 OK
Content-Length: 229
Content-Type: application/json
Strict-Transport-Security: max-age=31536000
X-Condor-Features: changeindication-port
{"time":60,"cur_time":60,"timestamp":"2024-01-02T12:48:51Z","temp":180,"temp_unit":false,"drawer_open":false,"preset":0,"error":0,"prev_status":"idle","status":"standby","step_id":"","recipe_id":"","shaker_reminder_active":false}
I collected a couple of challenges and keys:
C: VnX/m5uWc6CV/H38ztbzog==
K: ssyjLmLPQSeIh/0nGNJYji/s2TonjTGzlRaCw/aKbVC50sqMbgxUffIjZhAijTSn
C: H1dhEZly5ek24AgRjpO8Kg==
K: ssyjLmLPQSeIh/0nGNJYjkVncSZUm2a0mTaYZ5eBnUc7SSWdFalGsdyqRm3ZMFst
C: tLpTA68q+rl2mhTBKtV7LA==
K: ssyjLmLPQSeIh/0nGNJYjgXeKfz66Px5QFU5PFwAc7/5G47//zT7CXhDoLhzL+lH
eismac:~/Downloads/airfryer $ echo "ssyjLmLPQSeIh/0nGNJYji/s2TonjTGzlRaCw/aKbVC50sqMbgxUffIjZhAijTSn" | base64 -d | od -x
0000000 ccb2 2ea3 cf62 2741 8788 27fd d218 8e58
0000020 ec2f 3ad9 8d27 b331 1695 c382 8af6 506d
0000040 d2b9 8cca 0c6e 7d54 23f2 1066 8d22 a734
eismac:~/Downloads/airfryer $ echo "ssyjLmLPQSeIh/0nGNJYjkVncSZUm2a0mTaYZ5eBnUc7SSWdFalGsdyqRm3ZMFst" | base64 -d | od -x
0000000 ccb2 2ea3 cf62 2741 8788 27fd d218 8e58
0000020 6745 2671 9b54 b466 3699 6798 8197 479d
0000040 493b 9d25 a915 b146 aadc 6d46 30d9 2d5b
eismac:~/Downloads/airfryer $ echo "ssyjLmLPQSeIh/0nGNJYjgXeKfz66Px5QFU5PFwAc7/5G47//zT7CXhDoLhzL+lH" | base64 -d | od -x
0000000 ccb2 2ea3 cf62 2741 8788 27fd d218 8e58
0000020 de05 fc29 e8fa 79fc 5540 3c39 005c bf73
0000040 1bf9 ff8e 34ff 09fb 4378 b8a0 2f73 47e9```
So the interesting part is, that the first couple of characters are always the same.
Continuing my research I found an reposity on GitHub which has a decompiled version of the NutriU App:
I think this interesting code can be found in this file:
smali_classes3/com/philips/connectivity/condor/lan/authentication/AuthenticationCredentials.smali
There is a Java methode defined:
.method private createAuthenticationCredentials()Ljava/lang/String;
[...]
NetworkNode;->getClientId()Ljava/lang/String;
[...]
NetworkNode;->getClientSecret()
[...]
getSHA256Hash
[...]
ByteUtil;->concatenate([B[B)[B
[...]
ByteUtil;->encodeToBase64
So it combines some values, concatenate them, build a sha256 checksum and then do a base64 encode.
Maybe someone with better Java skills can have a look at the decoded application linked above.
The algorithm is in this code, we “just” need to understand and extract it…
Okay, some more findings…you need to get the “clientId” and the “clientSecret”. They are stored in a preference file with the App. I assume that they are also delivered from the server when you login. But I haven’t gone through this step yet.
So get access to the following file:
com.philips.cl.nutriu.plist
Inside you’ll find the following lines:
<key>authenticationSecret</key>
<string>wAmopx0ldviQsArFKHEE2Q==</string>
<key>clientIdentifier</key>
<string>ssyjLmLPQSeIh/0nGNJYjg==</string>
This is the required clientId and the clientSecret.
Once we know this, we can calculate the Authorization string based on the given challange from the aitfryer.
I created a little python program which is running a request to the airfryer:
import base64
import hashlib
import requests
import json
airfryerIP = '192.168.42.96'
clientId = 'ssyjLmLPQSeIh/0nGNJYjg=='
clientSecret = 'wAmopx0ldviQsArFKHEE2Q=='
def decode(txt):
return base64.standard_b64decode(txt)
def getAuth(clientId, clientSecret, challange):
vvv = decode(challange) + decode(clientId) + decode(clientSecret)
myhash = hashlib.sha256(vvv).hexdigest()
myhashhex = bytes.fromhex(myhash)
res = decode(clientId) + myhashhex
encoded = base64.b64encode(res)
return encoded.decode("ascii")
requests.packages.urllib3.disable_warnings()
print('>>> Send Request without Authorization')
headers = { "User-Agent": "cml", "Content-Type": "application/json" }
response = requests.get("https://" + airfryerIP + "/di/v1/products/1/airfryer", headers=headers, verify=False)
# {'Content-Length': '0', 'Content-Type': 'text/plain', 'WWW-Authenticate': 'PHILIPS-Condor 7xqkAL19tyj/XqQeiWbdPw=='}
challange = response.headers.get('WWW-Authenticate')
challange = challange.replace('PHILIPS-Condor ', '')
print('>>> Received challange: ' + challange)
auth = getAuth(clientId, clientSecret, challange)
print('>>> Generate Authorization string: ' + auth)
print('>>> Sending Request with Authorization')
headers = { "User-Agent": "cml", "Content-Type": "application/json", "Authorization": "PHILIPS-Condor " + auth }
response = requests.get("https://" + airfryerIP + "/di/v1/products/1/airfryer", headers=headers, verify=False)
data = json.loads(response.content)
print(data)
(no, the code is not beautiful, it’s just a proof of concept )
Running this will give you the following result:
>>> Send Request without Authorization
>>> Received challange: 7xqkAL19tyj/XqQeiWbdPw==
>>> Generate Authorization string: ssyjLmLPQSeIh/0nGNJYjrd48vUBVQjp/6zHG1GOHoZDtv3f6jtW9Wwo29UrYzVT
>>> Sending Request with Authorization
{'time': 60, 'cur_time': 60, 'timestamp': '2024-01-03T10:34:01Z', 'temp': 180, 'temp_unit': False, 'drawer_open': False, 'preset': 0, 'error': 0, 'prev_status': 'idle', 'status': 'standby', 'step_id': '', 'recipe_id': '', 'shaker_reminder_active': False}
So the next steps would be to see how we can find the clientId and the clientSecret using the login
@tschach Wow, you’ve already made really good progress - thank you for your efforts!
One question: Where can I find the file Ok, I’m stupid … you are using an iPhone. The decoded Android app threw me off.com.philips.cl.nutriu.plist
? Can I access it directly or via ADB or do I need a rooted Android?
I tried to find something similar with a rooted Android VM, but no success. I only could find a file with an encrypted preference file (where I assume the authentification secrets are stored).
I have now also made some progress and found the client secrets on Android. Fortunately, they are not stored in the encrypted settings file, but in a simple sqlite database.
The database is located in /data/user/0/com.philips.ka.oneka.app/databases/
and is called network_node.db
. I assume that this directory can only be accessed with root. I used Android x86 with Proxmox (you can alternatively also boot it from a USB stick) which comes pre-rooted, installed NutriU, logged in, activated root in the developer settings of Android x86 and copied the file in question to a normally readable directory using the “FX File Explorer” app. You can then open the database with an SQlite viewer/editor and display the network_node table.
Install SQLite Database Editor (https://play.google.com/store/apps/details?id=com.tomminosoftware.sqliteeditor&hl=en_US) open it and allow root.
In the SQLite Database Editor you can directly select NutriU > network_node.db > network_node
The second last two columns are the ones we are interested in (swipe left to get there).
I was then able to successfully test @tschach’s code in AppDaemon - with the small difference that my airfryer model (HD9880) is accessed via /di/v1/products/1/venusaf
instead of /di/v1/products/1/airfryer
I did some additional researches. When you delete the App including all data, reinstall it and signup with your existing account, the ClientId and ClientSecret is send back from the NutriU-Server.
Unfortunately this login and signup process is pretty annoying. It’s jumping from one server to the other. I was able to replicate the first steps of the login via the home.id-servers. But after more than 5 POST-requests to different URLs I gave up (for now).
I was able to reproduce some of the authorisation strings starting from requesting the login-code via eMail, pushing the code to the server again. It’s a constant exchange between authorisation headers and additional cookies.
So if we would like to get the ClientId and ClientSecret without digging into the phone data, this is probably the way to continue.
curl -i --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkOWQ3OTgwNS1mYWZlLTQ2YmQtYTY3NC1lNTJlZjBmYmRmOWYiLCJzcGFjZUlkIjoiNzZhZDkyNGUtOTgyYy00MzZlLWEzYjEtNTdkYzcxZjczY2EyIiwiaXNzIjoid3d3LmJhY2tlbmQudmJzLnZlcnN1bmkuY29tIiwiZXhwIjoxNzA0MzMzMTI0LCJ0eXBlIjoiY29uc3VtZXIiLCJpYXQiOjE3MDQyODk5MjQsImp0aSI6ImRiN2VhNGM0LTY4ODYtNGFjMS04OWRjLTZjOTExMTNmODMwYiIsInVzZXJuYW1lIjoiNGVmYzQ0MDUtMDFkNC00YTJlLWIzMWItMzA2NjdhMDY3NzI5In0.qY3CxHwrS2aAgKRGNyyISvsvX3ansMuR7TQ_foQUlj3nlFib5Go_ZGXlGcbWXPH90aNm4p5CicW4xwdY5-G2LgkXwH3CYAEIwVHa-GNqq9wnhQcsu-gdON8ftAaLjkBRb2RZhiEIy13973uX3cL_7gLaqEAq6oobwez2uVTYpS9yW2hfF89iolJ5xHkx8tSRQmm4y8I9XMn1HY4IgZ7mni6kbqeV9hb2VSBkHZUNLSL2F5BaqgWhKzIZxsBO6TUXAY3T-vJTZ97cVj7rDyyMPxsCnJM5uU2Z6260dLy29HveredDvO3vgYbniuLRP3RPJNoYyFKd_1_dSQ_j-1wczg" https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd62985/Profile/self/Appliance
HTTP/2 200
content-type: application/vnd.oneka.v2.0+json;charset=UTF-8
content-length: 1132
date: Wed, 03 Jan 2024 16:58:12 GMT
apigw-requestid: Q-PFsgByDoEEM-w=
x-session-id: d5b9ba58900e26ad7e922a801851c55131420dfe68aae36a5653317e13631f82
etag: "012e02c908acb36ddbaf18a835ebb8adb"
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
cache-control: max-age=10, private
vary: Origin
x-cache: Miss from cloudfront
via: 1.1 1e1b63f715ae11e79ed87d9679a26800.cloudfront.net (CloudFront)
x-amz-cf-pop: WAW51-P2
x-amz-cf-id: 57XUsxjdwcCx8uFzCa_0TXzyLMBMNFHQ1ueJV0cG61IUwUnnSaKIaQ==
{"_embedded":{"item":[{"macAddress":"e4:bc:96:01:cb:4c","firmwareVersion":"1.0.0","externalDeviceId":"d25c4505-71f4-4128-a1e5-2cdd95733d9b","clientId":"ssyjLmLPQSeIh/0nGNJYjg==","clientSecret":"wAmopx0ldviQsArFKHEE2Q==","registeredIn":"HSDP","hsdpDeviceId":"d25c4505-71f4-4128-a1e5-2cdd95784d9b","_links":{"self":{"href":"https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd63785/Profile/self/Appliance/fa0a144d-29e0-46a4-b00a-1ced85b3258c"},"collection":{"href":"https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd63785/Profile/self/Appliance"},"device":{"href":"https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd63785/Device/e135865f-1994-4818-b6f3-d357a8589b5e{?country,unitSystem}","templated":true}}}]},"_links":{"self":{"href":"https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd63785/Profile/self/Appliance?page=1&size=20"},"item":[{"href":"https://www.backend.vbs.versuni.com/api/0921897c-a457-443b-b555-5bbc7cd63785/Profile/self/Appliance/fa0a144d-29e0-46a4-b00a-1ced85b4458c"}]},"page":{"size":20,"totalElements":1,"totalPages":1,"number":1}}
This is the initial login-request with the eMail address used to register NutriU:
curl -i -X POST --data "email=my%40emailaddress.com&lang=de&APIKey=4_JGZWlP8eQHpEqkvQElolbA&sdk=js_latest&authMode=cookie&pageURL=https%3A%2F%2Fwww.accounts.home.id%2Fauthui%2Fclient%2Flogin%3Fgig_ui_locales%3Dde-DE%26country%3Dde&sdkBuild=15627&format=json" https://cdc.accounts.home.id/accounts.otp.sendCode
HTTP/2 200
content-type: text/javascript; charset=utf-8
content-length: 324
date: Wed, 03 Jan 2024 15:58:43 GMT
cache-control: private
p3p: CP="IDC COR PSA DEV ADM OUR IND ONL"
x-error-code: 0
x-soa: true, Gator
x-server: eu1a-nomad-t4
x-callid: 57757e9029d84fd78a77a1bf2ee52a7a
x-robots-tag: none
x-cache: Miss from cloudfront
via: 1.1 7eb9eadda041aaab1056a6a0f8080462.cloudfront.net (CloudFront)
x-amz-cf-pop: ZRH55-P1
x-amz-cf-id: ShDXX4as_lzPv0kaSU7voYyhMhiIJDg465itXNqu8CDxr9WYG7XLJQ==
{
"callId": "57757e9029d84fd78a77a1bf2ee52a7a",
"errorCode": 0,
"apiVersion": 2,
"statusCode": 200,
"statusReason": "OK",
"time": "2024-01-03T15:58:43.452Z",
"vToken": "st2.s.AcbHuJlg8A.I6lVw0J6m8OJ9-kQdBYqEQ.BnX9PnbD-5evLupmSSzyR3upDcl464YDlu6WD8JurqbSsjOCJd8fQgvRsGzgqX8SxrpKOBLAikUGNfi2MO2UQg.sc3"
}
This will send the code to your email address. Now initiate the next call. You need to use the vToken from the above request:
curl -X POST --header "Content-Type: application/x-www-form-urlencoded" --data "vToken=st2.s.AcbH1zsTUA.vJpNZ5dTbJP2k9X3UAiOAA.LRuGLu1n3UgSfzdGK39N2yZS-A73WGnxNEVtBJRmj3uzdz_64gmaw0Le3cnFJNz4M0Ad6ZADkOJWjJACnvLuPw.sc3&code=534895&targetEnv=jssdk&includeUserInfo=true&include=profile%2Cid_token%2Cdata%2C&sessionExpiration=0&APIKey=4_JGZWlP8eQHpEqkvQElolbA&sdk=js_latest&authMode=cookie&pageURL=https%3A%2F%2Fwww.accounts.home.id%2Fauthui%2Fclient%2Flogin%3Fgig_ui_locales%3Dde-DE%26country%3Dde&sdkBuild=15627&format=json" https://cdc.accounts.home.id/accounts.otp.login
This call will give you another token:
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJFUTBNVVE1TjBOQ1JUSkVNemszTTBVMVJrTkRRMFUwUTBNMVJFRkJSamhETWpkRU5VRkJRZyIsImRjIjoiZXUxIn0.eyJpc3MiOiJodHRwczovL2ZpZG0uZ2lneWEuY29tL2p3dC80X0pHWldsUDhlUUhwRXFrdlFFbG9sYkEvIiwic3ViIjoiZmMyNzdiMjRlMmEyNDE0YmEyYjlmNDU5NjRjODZkN2QiLCJpYXQiOjE3MDQyOTk0NTcsImV4cCI6MTcwNDI5OTUxNywiaXNMb2dnZWRJbiI6dHJ1ZX0.f-eL7YroXB9WdgaRBWrmgiO1g_t18CcvRnYJQQoKQB2J7ATEh-10gXZTO_kw4c8O4KLEKueWC39OMrOf9FkZarZE4cP9bptzH9e06GMPUTaCK0DbhlT-vHYLq48nBTClWBCTJMUK0y0LlDiLuASnE_sxYGN8YXHsucD7F9fBXiqFDPR9aO91ASXS0kYlVMO8l-pBTPOlf0WegZLchl_APfy_nrTL7RR7HTlMqV-uZn_ktk06GiU4q7b19mlb-lmQS6AG0CBYKDZJDpp_9p21GhMFnJ-8HbS63TPYM9YRFd_dxRRmArPfYmwp3V7M73slvRFp2NEQDQ3v_gQmqNDyYA",
The token itself seems to be multiple strings concat with a “.”. Some parts are base64 encoded strings, except for the last part.
Due to the leak of time I currently have to stop here…
Just wanted to add that the script works for the Philips 5000 HD9255 model. I didn’t even have to change url path. Sorry i can’t really add anything, but wanna say what you’ve made is really amazing already!
For those who are able to get clientId & clientSecret (I described my approach a few posts above), I have uploaded my current integration to use via pyscript.
You have to adapt the code to other model versions if you don’t have the HD9880/90.
Since I barely know what I am doing in terms of programming, please read the disclaimer and the information carefully.
Here you go:
As I was shopping for an AirFryer, I checked Home Assistant at the same time, glad to see so fresh posts!
By any chance, do you plan on releasing your code as a HACS friendly package?
I don’t think that it is possible to distribute scripts for Pyscript via HACS and my coding skills aren’t good enough to create a real integration.
Also, please be aware that this is a highly experimental project - more of a gap filler until someone comes along and creates a proper integration
By the way: In the next few days I will probably release a new version with some optimizations and new service calls.
in which way you obtain an api key for this request??
i have tryed the way with android x86 but in ths way Nutriu dont show login page.
Your project is amazing, any idea for the api key?
thanks
p.s.: now i can login with nutriu on androird x86, i see my airfryer, but i cant connect, and i try to open db file clientid an secret is empty
You can see the airfryer in NutriU because it’s registered in your account - it doesn’t mean that the app can really “see” it.
- Are you sure that both your Android x86 and the airfryer have internet connection?
- Are they running in the same network/subnet, so they can see each other locally? (don’t know if this is needed, but it could be needed for the first connection)
- Did you use a “Man-in-the-middle attack” to capture the traffic before? After that you might need to go in the device settings in NutriU (possibly multiple times) until you get a warning that something has changed and if you really want to connect the device.
Android x86 can ping the airfryer and are in the same network connected to internet
i dont understand the last point, i dont use a “Man-in-the-middle attack” , which is the scope of this procedure? how i can try this?
That’s weird. Sadly I don’t have a solution for that.
You don’t need to do a man-in-the-middle attack - I just mentioned it, because if you did sniff the traffic of the air fryer before, then you might have temporary connection problems because of that. If you didn’t sniff the traffic before, you can ignore that.
i can try the other way, i need to know how obtain the apikey to use in the curl request
I have the same problem as you @italoc, ended up with the app showing airfryer in the appliances but it’s not connected (I have the android VM and airfryer in different networks but both have Internet access). At the same time, the airfryer is connected when I check on my phone (connected only to mobile Internet). @nicohirsch which exact version of philips app were you using to obtain the secrets? Maybe you would be willing to try it again just to double check it still works for you?
I’ve tried again now: Before that I’ve updated to the newest version 7.28.1 via Google Playstore. Then I deleted cache and storage for the app, logged in again and connected to the airfryer. Everything worked fine for me - even though I’ve blocked the internet connection for the Airfryer since I don’t want it to install updates which might break things.
I also found another SQLite Editor which makes finding the secrets easier (after connecting, of course). I’ve updated my post above: Philips Airfryer & NutriU Integration (Alexa only)? - #17 by nicohirsch
I’ve just updated the pyscript code and the frontend card - in the hope that someone else will get hold of the client secrets, too.
Some of the changes:
- More settings that should make it easier to use this projects with other devices (for example you can now disable airspeed and temp probe if you don’t have these)
- More services (look for “@service” in the code to get a list)
- Updated frontend card with better countdown timer (maybe not compatible with devices without airspeed / temp probe. Another update will follow)
Everything is tested on my HD9880/90 - I don’t have any other airfryer to test.
Known problem: If you disable Internet access for the Airfryer, it loses the time (at least if it has had no power in the meantime). In this case, the time calculation for the frontend card no longer works.
i have the same versione of the app Nutriu 7.28.1, but nothing always say no connection