iCloud device data integration in openHAB

Great work with this script, this will be useful for me :slight_smile:
I have added two devices which I’m polling every minute. The script ran fine but after a few minutes I ended up with high memory consumption and alot of open connections on my RaspPi 3. The fix for this is to disconnect the HttpsURLConnection object and to close the BufferedInputStream and BufferedReader objects after they have been used.

...    
val jsonResponse = sb.toString()
          
inputStream.close()
br.close()
connection.disconnect()
2 Likes

thank you for your improvement; I´ve added it to the original post. I´ll check, maybe we can use something like the using pattern in C# in java as well … I´ll check if the streams implement “AutoCloseable” interface.

with kind regards,
Patrik

1 Like

Sure, just started to try some simplifications to avoid code redundancy.
I will post the results asap.

Adding a function to retrieve the json response for [apple id, device id] might already be a huge improvement, wdyt?

That’s exactly what I am trying…

1 Like

I got stuck with the functions, perhaps someone could point me to the right direction:

items:

//My work iPhone 7
String iPhone7_Battery "iPhone7 Batterie [%s]" <battery> (IOS)
String iPhone7_Battery_Status "iPhone7 Batterie Status [%s]" <battery> (IOS)
Number iPhone7_Battery_Level "iPhone7 Batterie Level [%.0f]" <battery> (IOS)
String iPhone7_Location "iPhone7 Location [%s]" <suitcase> (IOS)
DateTime iPhone7_Location_Timestamp "iPhone7 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPhone7_Coordinates_Accuracy "iPhone7 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPhone7_Coordinates "iPhone7 Koordinaten" <suitcase> (IOS)
String iPhone7_Location_Address "iPhone7 Location Address [%s]" (IOS)

//My work iPad Pro
String iPadPro_Battery "iPadPro Batterie [%s]" <battery> (IOS)
String iPadPro_Battery_Status "iPadPro Batterie Status [%s]" <battery> (IOS)
Number iPadPro_Battery_Level "iPadPro Batterie Level [%.0f]" <battery> (IOS)
String iPadPro_Location "iPadPro Location [%s]" <suitcase> (IOS)
DateTime iPadPro_Location_Timestamp "iPadPro Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPadPro_Coordinates_Accuracy "iPadPro Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPadPro_Coordinates "iPadPro Koordinaten" <suitcase> (IOS)
String iPadPro_Location_Address "iPadPro Location Address [%s]" (IOS)

//My private iPhone 5
String iPhone5_Battery "iPhone5 Batterie [%s]" <battery> (IOS)
String iPhone5_Battery_Status "iPhone5 Batterie Status [%s]" <battery> (IOS)
Number iPhone5_Battery_Level "iPhone5 Batterie Level [%.0f]" <battery> (IOS)
String iPhone5_Location "iPhone5 Location [%s]" <suitcase> (IOS)
DateTime iPhone5_Location_Timestamp "iPhone5 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPhone5_Coordinates_Accuracy "iPhone5 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPhone5_Coordinates "iPhone5 Koordinaten" <suitcase> (IOS)
String iPhone5_Location_Address "iPhone5 Location Address [%s]" (IOS)

//My private iPad Air2
String iPadAir2_Battery "iPadAir2 Batterie [%s]" <battery> (IOS)
String iPadAir2_Battery_Status "iPadAir2 Batterie Status [%s]" <battery> (IOS)
Number iPadAir2_Battery_Level "iPadAir2 Batterie Level [%.0f]" <battery> (IOS)
String iPadAir2_Location "iPadAir2 Location [%s]" <suitcase> (IOS)
DateTime iPadAir2_Location_Timestamp "iPadAir2 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPadAir2_Coordinates_Accuracy "iPadAir2 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPadAir2_Coordinates "iPadAir2 Koordinaten" <suitcase> (IOS)
String iPadAir2_Location_Address "iPadAir2 Location Address [%s]" (IOS)

Global imports and definitions:

import java.net.URL
import java.nio.charset.StandardCharsets
import javax.net.ssl.HttpsURLConnection
import org.eclipse.smarthome.core.library.types.DateTimeType
import org.eclipse.smarthome.core.library.types.PointType
import org.eclipse.xtext.xbase.lib.Functions
import com.sun.org.apache.xml.internal.security.utils.Base64
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Date

val String filename = "icloud.rules"
val PointType home = new PointType(new DecimalType(51.xxxxxxxxxxxxxx), new DecimalType(6.xxxxxxxxxxxxxxx))

Function to retrieve data

// Function called to retrieve iCloud data    
val Functions$Function2<String, String, String> iCloudRetrieve= [ appleId, password |
                                            
  logInfo(filename, "Lambda to retrieve iCloud data is called")

  val iCloudUrl = "https://www.icloud.com"
  val iCloudLoginUrl = "https://fmipmobile.icloud.com/fmipservice/device/" + appleId + "/initClient"

  val loginUrl = new URL(iCloudLoginUrl);
  val HttpsURLConnection connection = loginUrl.openConnection() as HttpsURLConnection
  val request = '{"clientContext":{"appName":"iCloud Find (Web)","appVersion":"2.0","timezone":"US/Eastern","inactiveTime":2255,"apiVersion":"3.0","webStats":"0:15"}}'
  val byte[] postData = request.getBytes(StandardCharsets.UTF_8)

  var String basicAuth = "Basic " + Base64.encode((appleId + ":" + password).getBytes())

  // prepare post request headers for login ...
  connection.setRequestProperty("Authorization", basicAuth)
  connection.requestMethod = "POST"
  connection.setRequestProperty("User-Agent", "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)")
  connection.setRequestProperty("Origin", iCloudUrl)
  connection.setRequestProperty("Content-Type", "application/json")
  connection.setRequestProperty("charset", "utf-8")
  connection.setRequestProperty("Accept-language", "en-us")
  connection.setRequestProperty("Connection", "keep-alive")
  connection.setRequestProperty("X-Apple-Find-Api-Ver", "2.0")
  connection.setRequestProperty("X-Apple-Authscheme", "UserIdGuest")
  connection.setRequestProperty("X-Apple-Realm-Support", "1.0")
  connection.setRequestProperty("X-Client-Name", "iPad")
  connection.setRequestProperty("Content-Length", Integer.toString(postData.length))

  connection.doOutput = true
  connection.setDoInput = true
  connection.outputStream.write(postData)

  val responseCode = connection.responseCode

  logInfo(filename, "HTTP Code: " + responseCode)
  logInfo(filename, "Message: " + connection.responseMessage)

  var StringBuffer sb = new StringBuffer()
  var inputStream = new BufferedInputStream(connection.getInputStream())
  var BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))
  var String inputLine = ""
  while ((inputLine = br.readLine()) != null) {
    sb.append(inputLine)
  }
  val jsonResponse = sb.toString()
  
  inputStream.close()
  br.close()
  connection.disconnect()
  
  return jsonResponse
]

Function to transform jsonResponse

// Function to transform iCloud jsonResponse    
val Functions$Function6<String,Integer,GenericItem,GenericItem,GenericItem,GenericItem,DateTimeType> jsonResponseTransform= [ jsonResponse, deviceIndex, Battery_Status, Battery_Level, Coordinates_Accuracy, Coordinates |

  logInfo(filename, "Function to transform iCloud jsonResponse is called")

  val String owner = transform("JSONPATH", "$.userInfo.firstName", jsonResponse) + " " + transform("JSONPATH", "$.userInfo.lastName", jsonResponse)
  logInfo(filename, "Owner:         " + owner)
  val String deviceName = (transform("JSONPATH", String.format("$.content[%d].name", deviceIndex), jsonResponse))
  logInfo(filename, "Name:          " + deviceName)
  val String deviceId = (transform("JSONPATH", String.format("$.content[%d].id", deviceIndex), jsonResponse))
  logInfo(filename, "Unique ID:     " + deviceId)
  val String batteryStatus = (transform("JSONPATH", String.format("$.content[%d].batteryStatus", deviceIndex), jsonResponse))
  logInfo(filename, "BatteryStatus: " + batteryStatus)
  val Double batteryLevel = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].batteryLevel", deviceIndex), jsonResponse))
  logInfo(filename, "BatteryLevel:  " + batteryLevel)
  val Double latitude = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.latitude", deviceIndex), jsonResponse))
  logInfo(filename, "Latitude:      " + latitude)
  val Double longitude = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.longitude", deviceIndex), jsonResponse))
  logInfo(filename, "Longitude:     " + longitude)
  val Double accuracy = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.horizontalAccuracy", deviceIndex), jsonResponse))
  logInfo(filename, "Accuracy:      " + accuracy)
  val Long timestampEpoch = Long.parseLong(transform("JSONPATH", String.format("$.content[%d].location.timeStamp", deviceIndex), jsonResponse))
  val location = new PointType(new DecimalType(latitude), new DecimalType(longitude))
  logInfo(filename, "Location:      " + location)
  val SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
  val String timestampString = sdf.format(new Date(timestampEpoch))
  val DateTimeType timestamp = DateTimeType.valueOf(timestampString)

  Battery_Status.postUpdate(batteryStatus)
  Battery_Level.postUpdate(batteryLevel * 100)
  Coordinates_Accuracy.postUpdate(accuracy)
  Coordinates.postUpdate(location)
	
  return timestamp
]

Rule

rule "iCloud Data Retrieval"
when
  Time cron "0 0/1 * * * ? *"
then

var String jsonResponse = ""
var String appleId = ""
var String password = ""
var Integer deviceIndex = 0
var DateTimeType timestamp

// Example for first AppleID
  logInfo(filename, "Retrieving My Work iCloud Data ...")
  appleId = "MyFirstAppleID"
  password = "secret"

  logInfo(filename, "AppleID  : " + appleId)
  logInfo(filename, "Password : " + password)

  jsonResponse = iCloudRetrieve.apply(appleId, password)
  logInfo(filename, jsonResponse)

//Example for first device in first ApppleID
  deviceIndex = 0
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPhone7_Battery_Status, iPhone7_Battery_Level, iPhone7_Coordinates_Accuracy, iPhone7_Coordinates) 
  logInfo(filename, "TimeStamp:     " + timestamp)
  iPhone7_Location_Timestamp.postUpdate(timestamp)
//End Example for first device in first AppleID  

//Example for second device in first ApppleID
  deviceIndex = 1
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPadPro_Battery_Status, iPadPro_Battery_Level, iPadPro_Coordinates_Accuracy, iPadPro_Coordinates) 
  logInfo(filename, "TimeStamp:     " + timestamp)
  iPadPro_Location_Timestamp.postUpdate(timestamp)
//End Example for second device in first AppleID  
  
  logInfo(filename, "My Work iCloud Retrieval Finished")
// End Example for first AppleID

// Example for second AppleID
  logInfo(filename, "Retrieving My Private iCloud Data ...")
  appleId = "MySecondAppleID"
  password = "secret"

  //jsonResponse = iCloudRetrieve.apply(appleId, password)
  logInfo(filename, jsonResponse)

//Example for first device in second ApppleID
  deviceIndex = 0
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPhone5_Battery_Status, iPhone5_Battery_Level, iPhone5_Coordinates_Accuracy, iPhone5_Coordinates) 
  logInfo(filename, "TimeStamp:     " + timestamp)
  iPhone5_Location_Timestamp.postUpdate(timestamp)
//End Example for first device in second AppleID  

//Example for second device in second ApppleID
  deviceIndex = 1
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPadAir2_Battery_Status, iPadAir2_Battery_Level, iPadAir2_Coordinates_Accuracy, iPadAir2_Coordinates) 
  logInfo(filename, "TimeStamp:     " + timestamp)
  iPadAir2_Location_Timestamp.postUpdate(timestamp)
//End Example for second device in second AppleID  
  
  logInfo(filename, "My Private iCloud Retrieval Finished")
// End Example for second AppleID

end

Designer ist showing no errors, appleID and password are set and logged correct, but all I see in the log is

2017-08-27 23:09:00.569 [INFO ] [.smarthome.model.script.icloud.rules] - Retrieving My Work iCloud Data ...
2017-08-27 23:09:00.572 [INFO ] [.smarthome.model.script.icloud.rules] - AppleID  : MyFirstAppleID
2017-08-27 23:09:00.579 [INFO ] [.smarthome.model.script.icloud.rules] - Password : secret
2017-08-27 23:09:00.580 [ERROR] [ntime.internal.engine.ExecuteRuleJob] - Error during the execution of rule iCloud Data Retrieval: null

Anyone any idea what could cause this issue ???

1 Like

You shouldn’t have to import anything from org.eclipse.smarthome. Those are imported by default now.

I hate the way the Rules engine reports errors from lambdas. No matter what the error it seems to always report them as NullPointerExceptions.

Here is what I would do to debug the problem.

In your lambdas surround everything with try/catch and catch all exceptions. For example:

val Functions$Function2<String, String, String> iCloudRetrieve= [ appleId, password |
    val String jsonResponse = null
    try{
        // existing code goes here, remember to take the val off of the jsonResponse line
    }
    catch (Exception e){
        logError("iCloudRetrieve", "Error in iCloudRetrieve: " + e.toString)
    }
    jsonResponse
]

This should help you get a little more informative error.

Beyond that, you will need to litter the functions with log statements to discover which line it failed on and then hopefully that will give you or someone here a clue as to the problem.

I don’t see anything obviously wrong with the code above.

3 Likes

Thanks Rich, I will follow your suggestions and report any progress.

1 Like

Some Progress made.
NPE was thrown, because

val String filename = "icloud.rules"

was not reachable within the lambda, so no logging was possible.
Now I get a 403 - Forbidden from Apple’s servers.

We are coming closer …

1 Like

Aha, yes. Everything and I mean everything has to be passed to the lambda. Glad you made progress.

Further Progress made :
403 error was a dumb typo, solved it.
I am adding some more NPE checks and clean the code a bit more.

Will publish my results latest end of the week.

2 Likes

Hi folks,
below please find the result of trying to reuse as much code as possible within 5 lambdas/functions. You can copy all parts into one rules file.
Thanks to @patrik_gfeller for creating the first version without finctions, which was my baseline and of course all other contibutors to this discussion.

@ThomDietrich, if you like my result, please feel free to add it to the official documentation, as I will not have much time to do so in the next couple of weeks.

Item Definitions

//My work iPhone 7
String iPhone7_Battery "iPhone7 Batterie [%s]" <battery> (IOS)
String iPhone7_Battery_Status "iPhone7 Batterie Status [%s]" <battery> (IOS)
Number iPhone7_Battery_Level "iPhone7 Batterie Level [%.0f]" <battery> (IOS)
String iPhone7_Location "iPhone7 Location [%s]" <suitcase> (IOS)
DateTime iPhone7_Location_Timestamp "iPhone7 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPhone7_Coordinates_Accuracy "iPhone7 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPhone7_Coordinates "iPhone7 Koordinaten" <suitcase> (IOS)
String iPhone7_Location_Address "iPhone7 Location Address [%s]" (IOS)

//My work iPad Pro
String iPadPro_Battery "iPadPro Batterie [%s]" <battery> (IOS)
String iPadPro_Battery_Status "iPadPro Batterie Status [%s]" <battery> (IOS)
Number iPadPro_Battery_Level "iPadPro Batterie Level [%.0f]" <battery> (IOS)
String iPadPro_Location "iPadPro Location [%s]" <suitcase> (IOS)
DateTime iPadPro_Location_Timestamp "iPadPro Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPadPro_Coordinates_Accuracy "iPadPro Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPadPro_Coordinates "iPadPro Koordinaten" <suitcase> (IOS)
String iPadPro_Location_Address "iPadPro Location Address [%s]" (IOS)

//My private iPhone 5
String iPhone5_Battery "iPhone5 Batterie [%s]" <battery> (IOS)
String iPhone5_Battery_Status "iPhone5 Batterie Status [%s]" <battery> (IOS)
Number iPhone5_Battery_Level "iPhone5 Batterie Level [%.0f]" <battery> (IOS)
String iPhone5_Location "iPhone5 Location [%s]" <suitcase> (IOS)
DateTime iPhone5_Location_Timestamp "iPhone5 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPhone5_Coordinates_Accuracy "iPhone5 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPhone5_Coordinates "iPhone5 Koordinaten" <suitcase> (IOS)
String iPhone5_Location_Address "iPhone5 Location Address [%s]" (IOS)

//My private iPad Air2
String iPadAir2_Battery "iPadAir2 Batterie [%s]" <battery> (IOS)
String iPadAir2_Battery_Status "iPadAir2 Batterie Status [%s]" <battery> (IOS)
Number iPadAir2_Battery_Level "iPadAir2 Batterie Level [%.0f]" <battery> (IOS)
String iPadAir2_Location "iPadAir2 Location [%s]" <suitcase> (IOS)
DateTime iPadAir2_Location_Timestamp "iPadAir2 Location Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <suitcase> (IOS)
Number iPadAir2_Coordinates_Accuracy "iPadAir2 Koordinaten HDOP [%.0f]" <suitcase> (IOS)
Location iPadAir2_Coordinates "iPadAir2 Koordinaten" <suitcase> (IOS)
String iPadAir2_Location_Address "iPadAir2 Location Address [%s]" (IOS)

Global Imports and Definitions

import org.eclipse.xtext.xbase.lib.Functions
import java.net.URL
import java.nio.charset.StandardCharsets
import javax.net.ssl.HttpsURLConnection
import com.sun.org.apache.xml.internal.security.utils.Base64
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Date

val String filename = "icloud.rules"
//my home location
val PointType home = new PointType(new DecimalType(51.xxxxxxxxxxxxxx), new DecimalType(6.xxxxxxxxxxxxxxx))
//my work location
val PointType work = new PointType(new DecimalType(51.xxxxxxxxxxxxxx), new DecimalType(6.xxxxxxxxxxxxxxx))

Function to retrieve iCloud date

// Function called to retrieve iCloud data    
val Functions$Function2<String, String, String> iCloudRetrieve= [ appleId, password |
  val String filename = "icloud.rules"
  var String jsonResponse = null
                                            
  logInfo(filename, "Function to retrieve iCloud data is called")
  
  try{
     var iCloudUrl = "https://www.icloud.com"
     var iCloudLoginUrl = "https://fmipmobile.icloud.com/fmipservice/device/" + appleId + "/initClient"

     var loginUrl = new URL(iCloudLoginUrl);
     var HttpsURLConnection connection = loginUrl.openConnection() as HttpsURLConnection
     var request = '{"clientContext":{"appName":"iCloud Find (Web)","appVersion":"2.0","timezone":"US/Eastern","inactiveTime":2255,"apiVersion":"3.0","webStats":"0:15"}}'
     var byte[] postData = request.getBytes(StandardCharsets.UTF_8)

     var String basicAuth = "Basic " + Base64.encode((appleId + ":" + password).getBytes())

     // prepare post request headers for login ...
     connection.setRequestProperty("Authorization", basicAuth)
     connection.requestMethod = "POST"
     connection.setRequestProperty("User-Agent", "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)")
     connection.setRequestProperty("Origin", iCloudUrl)
     connection.setRequestProperty("Content-Type", "application/json")
     connection.setRequestProperty("charset", "utf-8")
     connection.setRequestProperty("Accept-language", "en-us")
     connection.setRequestProperty("Connection", "keep-alive")
     connection.setRequestProperty("X-Apple-Find-Api-Ver", "2.0")
     connection.setRequestProperty("X-Apple-Authscheme", "UserIdGuest")
     connection.setRequestProperty("X-Apple-Realm-Support", "1.0")
     connection.setRequestProperty("X-Client-Name", "iPad")
     connection.setRequestProperty("Content-Length", Integer.toString(postData.length))

     connection.doOutput = true
     connection.setDoInput = true
     connection.outputStream.write(postData)

     var responseCode = connection.responseCode

     logInfo(filename, "HTTP Code: " + responseCode)
     logInfo(filename, "Message: " + connection.responseMessage)

     var StringBuffer sb = new StringBuffer()
     var inputStream = new BufferedInputStream(connection.getInputStream())
     var BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))
     var String inputLine = ""
     while ((inputLine = br.readLine()) != null) {
        sb.append(inputLine)
     }
     jsonResponse = sb.toString()
  
     inputStream.close()
     br.close()
     connection.disconnect()
  }
  catch (Exception e){
      logError("iCloudRetrieve", "Error in iCloudRetrieve: " + e.toString)
  }
  return jsonResponse
]

Function to transform iCloud jsonResponse

// Function to transform iCloud jsonResponse
val Functions$Function6<String,Integer,GenericItem,GenericItem,GenericItem,GenericItem,DateTimeType> jsonResponseTransform= [ jsonResponse, deviceIndex, Battery_Status, Battery_Level, Coordinates_Accuracy, Coordinates |
  val String filename = "icloud.rules"
  var DateTimeType timestamp

  logInfo(filename, "Function to transform iCloud jsonResponse is called")

  val String owner = transform("JSONPATH", "$.userInfo.firstName", jsonResponse) + " " + transform("JSONPATH", "$.userInfo.lastName", jsonResponse)
  logInfo(filename, "Owner:         " + owner)
  val String deviceName = (transform("JSONPATH", String.format("$.content[%d].name", deviceIndex), jsonResponse))
  logInfo(filename, "Name:          " + deviceName)
  val String deviceId = (transform("JSONPATH", String.format("$.content[%d].id", deviceIndex), jsonResponse))
  logInfo(filename, "Unique ID:     " + deviceId)
  val String batteryStatus = (transform("JSONPATH", String.format("$.content[%d].batteryStatus", deviceIndex), jsonResponse))
  logInfo(filename, "BatteryStatus: " + batteryStatus)
  val Double batteryLevel = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].batteryLevel", deviceIndex), jsonResponse))
  logInfo(filename, "BatteryLevel:  " + batteryLevel)

  Battery_Status.postUpdate(batteryStatus)
  Battery_Level.postUpdate(batteryLevel * 100)

  if (transform("JSONPATH", String.format("$.content[%d].location.positionType", deviceIndex), jsonResponse) != null) {
    val Long timestampEpoch = Long.parseLong(transform("JSONPATH", String.format("$.content[%d].location.timeStamp", deviceIndex), jsonResponse))
    val SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
    val String timestampString = sdf.format(new Date(timestampEpoch))
    timestamp = DateTimeType.valueOf(timestampString)
    logInfo(filename, "Timestamp:      " + timestamp)
    val Double latitude = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.latitude", deviceIndex), jsonResponse))
    logInfo(filename, "Latitude:      " + latitude)
    val Double longitude = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.longitude", deviceIndex), jsonResponse))
    logInfo(filename, "Longitude:     " + longitude)
    val Double accuracy = Double.parseDouble(transform("JSONPATH", String.format("$.content[%d].location.horizontalAccuracy", deviceIndex), jsonResponse))
    logInfo(filename, "Accuracy:      " + accuracy)
    val location = new PointType(new DecimalType(latitude), new DecimalType(longitude))
    logInfo(filename, "Location:      " + location)
  
    Coordinates_Accuracy.postUpdate(accuracy)
    Coordinates.postUpdate(location)
    } 
  return timestamp
]

Function to generate battery state summary

// Function called to generate battery state summary    
val Functions$Function2<GenericItem, GenericItem, String> batterySummary= [ Battery_Status, Battery_Level |
  val String status =
    if (Battery_Status.state == "Charging") "(charging...)"
    else if (Battery_Status.state == "Charged") "(charging completed)"
    else ""
  val level = (Battery_Level.state as DecimalType).intValue()
  val summary = String::format("%s %d%%", status, level)
 
  return summary
]

Function to calculate distance from location

// Function called to calculate location distance    
val Functions$Function4<GenericItem, PointType, String, Number, String> locationDistance= [ Coordinates, place, placeName, distance2 |
  val PointType location = Coordinates.state as PointType
  var int distance
  var String message
  // my home location
  distance = location.distanceFrom(place).intValue()
  if (distance < distance2) {
    message = (String::format("%s (%dm)", placeName, distance))
  } else {
    message = "(unknown location)"
    }
  return message	
]

Function to translate location to address

// Function to transform location coordinates to address
val Functions$Function1<GenericItem, String> locationAddress= [ Coordinates |
  val geocodeURL = "https://maps.googleapis.com/maps/api/geocode/json?latlng=" + iPhone7_Coordinates.state.toString + "&language=german&sensor=true"
  val String geocodeJson = sendHttpGetRequest(geocodeURL)
  var String formattedAddress = transform("JSONPATH", "$.results[0].formatted_address", geocodeJson)
  formattedAddress = formattedAddress.replace(", Germany", "")
  return formattedAddress
]

Main rule to retrieve iCloud data
The following example queries two Apple ID’s with two devices each. This can be handled in one rule and even be extended to more ID’s or devices in one ID.
ATTENTION : You need to have all items defined like shown above and used the in correct order as shown below, otherwise you might get false results or even errors.

rule "iCloud Data Retrieval"
when
  Time cron "0 0/5 * * * ? *"
then

var String jsonResponse = null
var String appleId = null
var String password = null
var Integer deviceIndex = 0
var DateTimeType timestamp

// Example for first AppleID
  logInfo(filename, "Retrieving My Work iCloud Data ...")
  appleId = "My first AppleId"
  password = "secret"

    try{
       jsonResponse = iCloudRetrieve.apply(appleId, password)
       //logInfo(filename, jsonResponse)
    }
    catch (Exception e){
        logError("iCloudRetrieve", "Error in calling iCloudRetrieve: " + e.toString)
    }

//Example for first device in first ApppleID
  deviceIndex = 1
//following item order is important !!!!!
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPhone7_Battery_Status, iPhone7_Battery_Level, iPhone7_Coordinates_Accuracy, iPhone7_Coordinates) 
  //logInfo(filename, "TimeStamp:     " + timestamp)
  if (timestamp != null) {
  iPhone7_Location_Timestamp.postUpdate(timestamp)
  }
//End Example for first device in first AppleID  

//Example for second device in first ApppleID
  deviceIndex = 0
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPadPro_Battery_Status, iPadPro_Battery_Level, iPadPro_Coordinates_Accuracy, iPadPro_Coordinates) 
  //logInfo(filename, "TimeStamp:     " + timestamp)
  if (timestamp != null) {
  iPadPro_Location_Timestamp.postUpdate(timestamp)
  }
//End Example for second device in first AppleID  
  
  logInfo(filename, "My Work iCloud Retrieval Finished")
// End Example for first AppleID

// Example for second AppleID
  logInfo(filename, "Retrieving My Private iCloud Data ...")
  appleId = "My second AppleID"
  password = "secret"

    try{
       jsonResponse = iCloudRetrieve.apply(appleId, password)
       //logInfo(filename, jsonResponse)
    }
    catch (Exception e){
        logError("iCloudRetrieve", "Error in calling iCloudRetrieve: " + e.toString)
    }

//Example for first device in second ApppleID
  deviceIndex = 0
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPhone5_Battery_Status, iPhone5_Battery_Level, iPhone5_Coordinates_Accuracy, iPhone5_Coordinates) 
  //logInfo(filename, "TimeStamp:     " + timestamp)
  if (timestamp != null) {
  iPhone5_Location_Timestamp.postUpdate(timestamp)
  }
//End Example for first device in second AppleID  

//Example for second device in second ApppleID
  deviceIndex = 1
  timestamp = jsonResponseTransform.apply(jsonResponse, deviceIndex, iPadAir2_Battery_Status, iPadAir2_Battery_Level, iPadAir2_Coordinates_Accuracy, iPadAir2_Coordinates) 
  //logInfo(filename, "TimeStamp:     " + timestamp)
  if (timestamp != null) {
  iPadAir2_Location_Timestamp.postUpdate(timestamp)
  }
//End Example for second device in second AppleID  
  
  logInfo(filename, "My Private iCloud Retrieval Finished")
// End Example for second AppleID

end

Rule for battery summary
This rule can be copied for every single device you want to be monitored, just change the item names.

rule "My Work iPhone Battery Summary"
when
  Item iPhone7_Battery_Level changed
then
  logInfo(filename, "My Work iPhone Battery Update")
  iPhone7_Battery.postUpdate(batterySummary.apply(iPhone7_Battery_Status, iPhone7_Battery_Level))
end

Rule for location matching and address translation
This rule can be copied for every single device and location you want to be monitored, just change the item names.
Syntax for distance checking:
device_Location_item.postUpdate(locationDistance.apply(device_Coordinates_item, location_PointType, String_PlaceName, Number_Distance_to_check)

rule "My Work iPhone Location"
when
  Item iPhone7_Coordinates changed
then
  logInfo(filename, "My Work iPhone Location")
  iPhone7_Location.postUpdate(locationDistance.apply(iPhone7_Coordinates, home, "Home" 200))
// If you don't want to translate your GPS coordinates to an address, just remove the following line
  iPhone7_Location_Address.postUpdate(locationAddress.apply(iPhone7_Coordinates))
end
3 Likes

Excellent work! Thanks for posting.

Should this be one of the sticky tutorials that are linked to from the Docs? I think so.

Also, for someone who wants to cut their teeth on writing a new binding, this looks like a great candidate.

1 Like

Very nice! Thanks for enhancing my version. I wonder what the next guy will add :smiley:

@hmerk If that’s what you were asking for I’m happy to say that I’ve already done that :wink: On of the links at http://docs.openhab.org/tutorials already points to this thread.

@patrik_gfeller would you update the first posting with @hmerk’s result?

Exactly :wink:

@hmerk sems like you’ve compressed everything nicely, even the address resolution. :1st_place_medal:

Two small details:

  • You are comparing with a “home” and “work” place. I guess most user will have more than these two places they want to compare the location with. My solution was a bit more flexible in that sense, also because it allowed to define the distance per place. Maybe a lambda with [location, place, distance] would be better. The end user can then call it as often as he wants.
  • In a lambda I like to use “return” for readability…

Done

I will try to implement this over the weekend.

1 Like

Implemented as well and post updated :slight_smile:

… done :slight_smile: - hope I did no copy-paste error(s).

1 Like

Thanks @patrik_gfeller, lgtm.