Philips Airfryer & NutriU Integration (Alexa only)?

Thanks for sharing the insight! I’ll try it out sometime.

I’m planning on buying an Airfryer but I wanted to check if there is an integration before I buy one. Has anyone made any progress on integrating it with Home Assistant?

1 Like

I don’t think so :frowning:

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.

1 Like

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…

5 Likes

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 :slight_smile: )

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

7 Likes

@tschach Wow, you’ve already made really good progress - thank you for your efforts!
One question: Where can I find the file com.philips.cl.nutriu.plist? Can I access it directly or via ADB or do I need a rooted Android? Ok, I’m stupid … you are using an iPhone. The decoded Android app threw me off.

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 :tada::tada::tada:

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…

1 Like

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.
image

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:

4 Likes

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 :wink:

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

1 Like