Philips Airfryer & NutriU Integration (Alexa only)?

We bought a new Airfryer, and apparently it comes with Wifi and an App. Because, why not?

The app is called ‘NutriU’ and it only offers support to a smart-home using Alexa. I do not use Amazon’s assistant, but could not help but see if I can somehow add it to Home Assistant. Not that I have a real usecase other then letting HA broadcast when the fries are done or something :slight_smile:

I could not find anything about this in HA, HACS or on these forums. Did I miss something, am I the first or has anyone already have experience trying to get this into HA?

5 Likes

Dumb question probably. Can you read devices setup in Alexa into HA? I know with google’s Assistant you cannot (it’s one way) but recently its become possible to send voice-commands to Google. Anything like that possible with Alexa or even read the devices and expose them using Alexa?

I’m also looking for NutriU Integration.

3 Likes

I have the same Philips Airfryer and it’s awsome for 2 floor houses, I can check my food from my office on 2nd floor. Im trying to find a way to connect it on HA and until now I don’t find… It will be perfect with a “oven card” and a circle shape timer would be the best.

1 Like

I’m looking for this as well. The traffic seems to go via a cloud service from Phillips rather than from app to the device. Just checked and it worked over 5G.

Maybe it’s possible to snoop on the traffic? Obviously an official integration would be the best.

I have the same one, and i was searching for that. The alexa integration is so bad, i have to tell alexa to open kitchen plus, then say turn on the airfryer, then what i want, like 20 min at 200celcius…
I see that this uses port 8883 for MQTT w/https, so is encrypted, i dont think we can do something with sniffing in that case.
Do anyone knows how we can make this happen? I dont see any way to force the trafic to go by a non-secured way… Maybe set up a mqtt with https local server and redirect everything there? i dont think is going to work, but maybe someone with more experience can say if there is a chance there

I have a Philips Airfryer for quite some time now, but sadly don’t have a solution either. While the API is very simple - the authentication is not. You can sniff the traffic with tools like Fiddler and get all the commands and an authorization token. But the problem is, that this token gets invalid after power loss and after also after a day.
I think the app gets the token from www.backend.ka.philips.com:443 but I can’t sniff that traffic.

Example rest_command to start the Airfryer (not working without fresh token)
airfryer_start:
  url: 'https://192.168.55.88:443/di/v1/products/1/airfryer'
  method: PUT
  verify_ssl: false
  headers:
    Authorization: 'PHILIPS-Condor GggOpz+PvGi0wVhpNTyFTmdr0dGOGKQkLIh7kP2xHIkPSFZKylH2H3Sws9E7l1Xb'
  payload: '{"status":"cooking", "time": 1500, "cur_time": 1500, "temp": 180 }'

The URL seems to be different per product. The first “smart” airfryer uses the URL above. My new one uses /di/v1/products/1/venusaf

Can you elaborate on how you decoded these messages? I am using Wireshark to sniff and I am getting traffic via UDP protocol and as a application/octet-stream. Do you happen to know the proper decoding/formatting?

PUT /di/v1/products/1/airfryer HTTP/1.1
Content-Type:application/octet-stream
Content-Transfer-Encoding:base64
Accept-Encoding:identity
x-di-udp-count:171
Host:192.168.2.16

uPLgY5hzmhYvM3hA243PULoeJUryNtqHD1cYJfs2sDwhaS211WMlso3pC9uaTpIDuaGV3uBp0ZxhN7xUmMbcIboyYMHfrmKfPjGlBZ2EOZImqIiFRqva2gFc/+3dufjE

I used the HTTPS decryption feature of Fiddler Classic. To do this, you have to install the Fidder root certificate on the smartphone with the NutriU app and then define the PC with Fiddler as a proxy. In Fiddler, you then need to enable the remote function and set it to only decrypt connections to the AirFryer (otherwise connections to the NutriU server will also be decrypted - and the server will block the connection completely, so the app can’t be launched). I think this could also be possible in WireShark, but I’m not familiar with that.
The app will complain when you connect to the AirFryer that there might be a man-in-the-middle attack going on - but you can just confirm that.

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

6 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