IOS Battery Level

For anyone who is trying to do this, I have found a solution. Unfortunately the solution is a bit of a Frankenstein sort of setup. Part of its complexity is based on the fact that almost all of my setup is in docker instances devoted to a single application (Node-Red, MQTT, etc.). If you don't have this sort of setup it will likely be easier for you set up.

I use MQTT extensively so I knew if I could get a regular data feed into MQTT (like I have from Zigbee2MQTT for many zigbee devices) then all of the rest of my setup for logic would work easily. What I will show here is about getting the data into MQTT. I am sure that you could build a function node (if you do such things) that could parse out the key data and send it out as a Node-Red message, but because I use MQTT, I didn't build such a function node.

First I found this:

This is a python app that "Connects iCloud Accounts/Devices to Homie 4 MQTT convention."

My first thought on finding this was, "Perfect, the data will be in MQTT". Turns out that of course to run a python app, you need python installed. And it turns out that the Node-Red Docker container I run doesn't and seems to not really have any way to install it. I thought thats ok, I can get a Docker container that has python, run iCloud-Homie-4 there and then get what I need out of MQTT. So I got that container up and this part is probably common, however and wherever you run python, I installed iCloud-Homie-4 using the pip command from its webpage "pip install iCloud-Homie-4".

At this point, if you are familiar with python and or don't have a weird Docker setup you probably could just edit the yaml file decribed on the webpage, point it to you MQTT server and be off. Unfortunately while I could edit the yaml file and get it situated, I could not manage to actually start pip install iCloud-Homie-4.

While hacking around trying to get it to work, I learned that the pip install iCloud-Homie-4 install had installed various things needed to work and there was now an executable program on the system called icloud. I think this is yet another set of software developed by someone else that pip install iCloud-Homie-4 calls and then processes the output and sends it to MQTT. But since that program outputs the results to stdout, I didn't actually need the pip install iCloud-Homie-4 portion to work.

So I setup icloud. To do so, from the command line I ran the command icloud --username placeholder@nowhere.com --password AlaKazam . The next few steps are from memory, so sorry if they aren't exactly right, but it seemed pretty straightforward at the time. Apple wanted a 2FA verification that I should have access to the data and that I hadn't just stolen someones username and password. So it both popped up the 2FA on my IOS devices and gave me the option to have a code texted to me. Because the iCloud-Homie-4 website said to use the text code and not the popup code, that is what I did. I took the code from the text message and put it in to the command line where prompted. That completed the authentication. I think I then clicked the approve/that is me button on the popup on the IOS device, but did nothing with code it showed me. That code might have worked, but because the texted code worked, I didn't even try.

At this point, I could now run either of these commands (note that neither includes the password):
icloud --username placeholder@nowhere.com --llist
icloud --username placeholder@nowhere.com --list

list gave a short readout of all of the devices on my account, but not the battery level. llist give a lot of information about all of the devices on the account.

This was great, I got what I needed, except this is not the docker instance running Node-Red. How do I get it there? After a bunch of stops and starts, I gave in and just installed Node-Red on this Docker container. Inside this Node-Red instance, I created this flow that uses an exec node to run the icloud program, takes the stdout results, runs them through a function node that parses the input into a series of msg objects suitable for sending to my MQTT server. In case anyone wants it here is the flow:

[{"id":"b1461284.d3add","type":"exec","z":"fd3f9aee.282bf8","command":"icloud --username placeholder@nowhere.com --llist","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":490,"y":580,"wires":[["a2d35223.10dc1"],["c839104b.0fb98"],[]]},{"id":"c839104b.0fb98","type":"debug","z":"fd3f9aee.282bf8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":600,"wires":[]},{"id":"7bb3cc06.1e30b4","type":"mqtt out","z":"fd3f9aee.282bf8","name":"","topic":"","qos":"2","retain":"false","broker":"30ef67df.7df4a8","x":1090,"y":560,"wires":[]},{"id":"a2d35223.10dc1","type":"function","z":"fd3f9aee.282bf8","name":"Format icloud data for MQTT","func":"//split into array based on newline\ntopics =[]\nlet aOS = msg.payload.split('\\n')\n//set destination sub-object\nmsg.parsed = {}\n//loop through all lines\nfor( var i = 0; i < aOS.length; i++ ) {\n    //identify and then do diffent process for IOS device versus reading for that device\n    if ((aOS[i].slice(0,1) !== \" \")&&(aOS[i].slice(0,1) !== \"-\")&& (aOS[i] !== \"\")){\n        //save IOS device name and then create empty sub-object\n        deviceName =aOS[i]\n        msg.parsed[deviceName]={}\n    } else{\n        //ignore the header line that is all dashes and the footer line that is empty\n        if ((aOS[i].slice(0,1) !== \"-\")&&aOS[i] !== \"\"){\n            //parse out the name of the reading\n            dataType= aOS[i].slice(0,20).trim()\n            //if the reading starts with { then it almost JSON\n            if (aOS[i].substring(23).trim().slice(0,1) == \"{\"){\n                cleanedToJSON = aOS[i].substring(23).trim()\n                //reformat False, True, None and ' into false, true, \"None\" and '\n                cleanedToJSON = cleanedToJSON.replace(/False/g, \"false\")\n                cleanedToJSON = cleanedToJSON.replace(/True/g, \"true\")\n                cleanedToJSON = cleanedToJSON.replace(/None/g, '\"None\"')\n                cleanedToJSON = cleanedToJSON.replace(/'/g, '\"')\n                //parse string into object\n                cleanedToJSON = JSON.parse(cleanedToJSON)\n                //place all key:value pairs in the new object into the IOS sub-object\n                for (const [key, value] of Object.entries(cleanedToJSON)) {\n                    msg.parsed[deviceName][key] = value\n                }\n            } else {\n                //put the reading name (key) and the reading value as a key:value pair within the IOS device sub-Object\n                msg.parsed[deviceName][dataType] = aOS[i].substring(23).trim()\n            }\n        }\n    }\n}\n//remove the input\ndelete msg.payload\n//node.warn(msg)\nfor (var [key, value] of Object.entries(msg.parsed)) {\n    for (var [key2, value2] of Object.entries(value)) {\n        //node.warn(`${key}: ${key2}: ${value2}`);\n        key = key.replace(/\\+/g, \"plus\")\n        key2 = key2.replace(/\\+/g, \"plus\")\n        msg.topic = `IOS/${key}/${key2}`\n        topics.push(`IOS/${key}/${key2}`)\n        msg.payload = value2\n        node.send(msg)\n        //msg.parsed[deviceName][key] = value\n        }\n    //node.warn(`${key}: ${value}`);\n    //msg.parsed[deviceName][key] = value\n    }\n//node.warn(topics)\nreturn //msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":840,"y":560,"wires":[["7bb3cc06.1e30b4"]]},{"id":"a04412f2.a97c8","type":"inject","z":"fd3f9aee.282bf8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"600","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":580,"wires":[["b1461284.d3add"]]},{"id":"609e98c4.b31448","type":"catch","z":"fd3f9aee.282bf8","name":"","scope":null,"uncaught":false,"x":560,"y":640,"wires":[["c839104b.0fb98"]]},{"id":"30ef67df.7df4a8","type":"mqtt-broker","z":"","name":"Placeholder MQTT","broker":"myMQTTHost.com","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

I think I have anonymized everything, but would appreciate feedback if I left personal information in the sample flow.

So until Apple makes a change that breaks what the icloud app is doing, I have exactly what I need plus a bunch of other data that I may or may not end up using in some way.

Just to give you a fee. for the data provided, here is are several screenshots of the MQTT data in MQTT Explorer:


In particular, I am likely to use batteryLevel, batteryStatus, Latitude and Longitude. Again, if you see that I have left in any personal data, I would appreciate a heads up.

Oh, a few more notes:

  • This info is only as good as what iCloud knows about the devices.
  • I don't know how often IOS devices update iCloud.
  • I have this set to pull the information on a 10 minute interval. I am not sure if any shorter interval makes sense.
  • I started with a 15 minute interval and found that for some devices (including my iPhone), that with the longer interval, the battery reading would sometimes drop to 0 even when it was at 100%. That reading, however, seemed to have caused iCloud to query the device and so the next reading was always accurate. By decreasing this to 10 minutes it seems to have gone away as a problem.
  • I will warn, that you might be tempted to use the lat/long for presence sensing. This won't really work (at least for triggering things on arrival). By only querying every 10 minutes the latency for knowing your device is at home can be 10 minutes plus the latency of how often the device updates iCloud.
1 Like