Hi all.
go get rid of my last dependency to MiOS/Vera I´ve started to create a rule to get the location of an iPhone directly from the iCloud.
With the feedback from @ThomDietrich. @hmerk & the community the example evolved and is now able to retrieve quite some device specific information for multiple devices:
- Name: John’s iPhone
- Owner: John Doe
- Unique ID: VIRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- BatteryStatus: NotCharging
- BatteryLevel: 0.8100000023841858
- Latitude: 51.xxxxxxxxx
- Longitude: 11.xxxxxxxxxx
- Accuracy: 16.0
- TimeStamp: 2017-07-29T15:50:51.372+0200
NOTES:
- Feel free to contribute & directly edit this post to add your enhancements/corrections. If unsure, use the thread below to discuss your changes first.
- Since OH 2.2 device data can also be retrieved using the iCloud Binding.
Item(s)
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)
Rule(s)
imports & 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 data
// 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.
iCloud Data Retrieval
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
Other Examples
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.
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
Syntax for distance checking:
device_Location_item.postUpdate(locationDistance.apply(device_Coordinates_item, location_PointType, String_PlaceName, Number_Distance_to_check)