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