[Domoticz插件]查找我的iPhone
发表于 : 周一 11月 27, 2017 15:12
beta版本的有系统崩溃风险
beta版本的有系统崩溃风险
beta版本的有系统崩溃风险
请尽量在release版本下尝试,文件夹拷贝到domoticz/plugins下,重启系统即可
系统设置中的经纬度要填,用来取做家的位置坐标
高德和百度的key需要在相关网站申请,其他应该不需要解释
插件中用到短地址服务,请按需再酸酸乳上添加tinyurl,否则不能成功更新设备
最后解释下,区别于homeassistant中icloud组件,此方式使用的是Find My iPhone入口,无视两步验证,因此不会有间隔数月重新验证的情况。对于不同充电状态也可以自设刷新时间,可调性更强。
其中声音提醒无视手机静音,请谨慎使用。
其他功能入防盗模式和抹除设置等风险太大暂不写入。
beta版本的有系统崩溃风险
beta版本的有系统崩溃风险
请尽量在release版本下尝试,文件夹拷贝到domoticz/plugins下,重启系统即可
系统设置中的经纬度要填,用来取做家的位置坐标
高德和百度的key需要在相关网站申请,其他应该不需要解释
插件中用到短地址服务,请按需再酸酸乳上添加tinyurl,否则不能成功更新设备
最后解释下,区别于homeassistant中icloud组件,此方式使用的是Find My iPhone入口,无视两步验证,因此不会有间隔数月重新验证的情况。对于不同充电状态也可以自设刷新时间,可调性更强。
其中声音提醒无视手机静音,请谨慎使用。
其他功能入防盗模式和抹除设置等风险太大暂不写入。
代码: 全选
# Find My iPhone Plugin for Domoticz
#
# Author: Duke, 2017
# Below is what will be displayed in Domoticz GUI under HW
"""
<plugin key="FMIP" name="Find My iPhone" author="Duke" version="1.0.0">
<params>
<param field="Username" label="Apple ID" width="200px" required="true"/>
<param field="Password" label="Password" width="200px" required="true"/>
<param field="Mode1" label="高德Key" width="200px" required="true"/>
<param field="Mode2" label="百度Key" width="200px" required="true"/>
<param field="Mode3" label="设备名称" width="200px" required="true"/>
<param field="Mode4" label="更新间隔(默认/充电/小于20/小于10" width="200px" required="true" default="15/5/30/60"/>
<param field="Mode5" label="公司 lon/lat:rad" width="300px" required="true" default="121/31:0.5"/>
<param field="Mode6" label="Debug" width="75px">
<options>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="True" />
</options>
</param>
</params>
</plugin>
"""
import Domoticz
import sys
sys.path.append('/usr/local/lib/python3.5/dist-packages')
import datetime
import time
import base64
import json
import re
import urllib.request
from haversine import haversine
from urllib.parse import quote, quote_plus
icons = {"batterylevelfull": "batterylevelfull icons.zip",
"batterylevelok": "batterylevelok icons.zip",
"batterylevellow": "batterylevellow icons.zip",
"batterylevelempty": "batterylevelempty icons.zip"}
class BasePlugin:
enabled = True
def __init__(self):
return
def onStart(self):
if Parameters["Mode6"] != "Normal":
Domoticz.Debugging(1)
# load custom battery images
for key, value in icons.items():
if key not in Images:
Domoticz.Image(value).Create()
Domoticz.Debug("Added icon: " + key + " from file " + value)
Domoticz.Debug("Number of icons loaded = " + str(len(Images)))
for image in Images:
Domoticz.Debug("Icon " + str(Images[image].ID) + " " + Images[image].Name)
if ( 1 not in Devices):
Domoticz.Device(Name="位置", Unit=1, TypeName="Text", Used=1).Create()
if ( 2 not in Devices):
Domoticz.Device(Name="位于", Unit=2, TypeName="Text", Used=1).Create()
if ( 3 not in Devices):
Domoticz.Device(Name="电量", Unit=3, TypeName="Custom", Options={"Custom": "1;%"}, Used=1).Create()
if ( 4 not in Devices):
Domoticz.Device(Name="声音提醒", Unit=4, TypeName="Switch", Switchtype=9, Used=1).Create()
Domoticz.Heartbeat(60)
Domoticz.Log("onStart called")
def onStop(self):
Domoticz.Log("onStop called")
def onConnect(self, Connection, Status, Description):
Domoticz.Log("onConnect called")
def onMessage(self, Connection, Data, Status, Extra):
Domoticz.Log("onMessage called")
def onCommand(self, Unit, Command, Level, Hue):
Domoticz.Log("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
self.username = Parameters["Username"]
self.password = Parameters["Password"]
self.device_name = Parameters["Mode3"]
icloud_info, is_succ = self.FMIP(self.username, self.password)
if is_succ:
self.device_info = [device for i, device in enumerate(icloud_info['content']) if icloud_info['content'][i]['name'] == self.device_name][0]
self.play_sound(self.device_info, self.username, self.password)
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
Domoticz.Log("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onDisconnect(self, Connection):
Domoticz.Log("onDisconnect called")
def onHeartbeat(self):
self.default_interval = int(Parameters["Mode4"].split('/')[0])
self.charging_interval = int(Parameters["Mode4"].split('/')[1])
self.lt20_interval = int(Parameters["Mode4"].split('/')[2])
self.lt10_interval = int(Parameters["Mode4"].split('/')[3])
if Parameters["Mode6"] != "Normal":
self.interval =1
else:
if Devices[1].sValue.find('未知') == -1:
if Devices[1].sValue.find('充电中') != -1 or Devices[1].sValue.find('已充满') != -1:
self.interval = self.charging_interval
else:
if int(Devices[3].sValue) <= 10:
self.interval = self.lt10_interval
elif int(Devices[3].sValue) <= 20:
self.interval = self.lt20_interval
else:
self.interval = self.default_interval
else:
self.interval = 1
Domoticz.Debug(str(self.interval))
if int(time.strftime("%M", time.localtime())) % self.interval == 0:
self.username = Parameters["Username"]
self.password = Parameters["Password"]
self.device_name = Parameters["Mode3"]
self.amapkey = Parameters["Mode1"]
self.baiduAk = Parameters["Mode2"]
self.work_radius = Parameters["Mode5"].split(':')[1]
self.work_lon = Parameters["Mode5"].split(':')[0].split('/')[0]
self.work_lat = Parameters["Mode5"].split(':')[0].split('/')[1]
loc = Settings["Location"].split(";")
self.home_lat = float(loc[0])
self.home_lon = float(loc[1])
icloud_info, is_succ = self.FMIP(self.username, self.password)
if is_succ:
self.device_info = [device for i, device in enumerate(icloud_info['content']) if icloud_info['content'][i]['name'] == self.device_name][0]
lon, lat, accu, conv_lon, conv_lat, address, is_china = self.convert_geo(self.device_info)
address = address[address.find('市')+1:]
home_latlon = (float(self.home_lat), float(self.home_lon))
distance2home = haversine((float(lat), float(lon)), home_latlon)
if distance2home < float(accu)/1000:
address = '家'
work_latlon = (float(self.work_lat), float(self.work_lon))
distance2work = haversine((float(lat), float(lon)), work_latlon)
if distance2work < float(self.work_radius):
address = '公司'
Domoticz.Debug('离家'+str(distance2home)+'公里')
Domoticz.Debug('离公司'+str(distance2work)+'公里')
powerlevel, powerstatus = self.batt_info(self.device_info)
fixedtime = self.info_timediff(self.device_info)
position_text, location_pic = self.show_info(lon, lat, conv_lon, conv_lat, address, is_china, powerlevel, powerstatus, fixedtime)
UpdateDevice(1, 0, position_text)
UpdateDevice(2, 0, location_pic)
# UpdateDevice(3, 0, str(powerlevel))
UpdateBattery(3, str(powerlevel))
else:
patt = re.compile(r"电池状态:(.*?)<", re.I|re.X)
powerstatus = re.search(patt, Devices[1].sValue).group()[5:-1]
position_text = Devices[1].sValue.replace(powerstatus, '未知')
UpdateDevice(1, 0, position_text)
Domoticz.Log("onHeartbeat called")
def FMIP(self, username, password):
try: #if we are given a FMIP token, change auth Type
int(username)
auth_type = "Forever"
except ValueError: #else apple id use useridguest
auth_type = "UserIDGuest"
userAndPass = (username + ":" + password).encode('utf-8')
userAndPass = base64.b64encode(userAndPass).decode('utf-8')
url = 'https://fmipmobile.icloud.com/fmipservice/device/%s/initClient' % username
headers = {
'X-Apple-Realm-Support': '1.0',
'Authorization': 'Basic %s' % userAndPass,
'X-Apple-Find-API-Ver': '3.0',
'X-Apple-AuthScheme': '%s' % auth_type,
'User-Agent': 'FindMyiPhone/500 CFNetwork/758.4.3 Darwin/15.5.0',
}
is_succ = False
try:
request = urllib.request.Request(url=url, headers=headers, method='POST')
result = urllib.request.urlopen(request, timeout=5)
result = json.loads(result.read().decode("utf-8"))
except:
result = {}
if 'statusCode' in result and result['statusCode'] == '200':
is_succ = True
Domoticz.Debug('Successfully authenticated')
return result, is_succ
def convert_geo(self, device_info):
lon = device_info['location']['longitude']
lat = device_info['location']['latitude']
accu = device_info['location']['horizontalAccuracy']
conv_url = 'http://restapi.amap.com/v3/assistant/coordinate/convert?key=%s&coordsys=gps&locations=%s,%s&output=json' % (self.amapkey, lon, lat)
conv_reqest = urllib.request.Request(url=conv_url, method='GET')
conv_result = urllib.request.urlopen(conv_reqest, timeout=5)
conv_result = json.loads(conv_result.read().decode("utf-8"))
conv_lon = conv_result['locations'].split(',')[0]
conv_lat = conv_result['locations'].split(',')[1]
regeo_url = 'http://restapi.amap.com/v3/geocode/regeo?key=%s&location=%s,%s&output=json' % (self.amapkey, conv_lon, conv_lat)
regeo_request = urllib.request.Request(url=regeo_url, method='GET')
regeo_result = urllib.request.urlopen(regeo_request, timeout=5)
regeo_result = json.loads(regeo_result.read().decode("utf-8"))
is_china = False
if regeo_result['regeocode']['formatted_address'] != []: #国内
address = regeo_result['regeocode']['formatted_address']
is_china = True
else: #国外
regeo_url = 'http://api.map.baidu.com/geocoder/v2/?callback=renderReverse&coordtype=wgs84ll&ak=%s&coordsys=gps&location=%s,%s&output=json' % (self.baiduAk, lat, lon)
regeo_request = urllib.request.Request(url=regeo_url, method='GET')
regeo_result = urllib.request.urlopen(regeo_request, timeout=5)
regeo_result = regeo_result.read().decode("utf-8")
patt = re.compile(r"\((.*?)\)", re.I|re.X)
address = json.loads(re.search(patt, regeo_result.text).group()[1:-1])['result']['formatted_address']
return lon, lat, accu, conv_lon, conv_lat, address, is_china
def batt_info(self, device_info):
powerlevel = int(device_info['batteryLevel']*100)
batt_status = device_info['batteryStatus']
powerstatus = '未知'
if batt_status == 'NotCharging':
powerstatus = '使用中'
elif batt_status == 'Charging':
powerstatus = '充电中'
elif batt_status == 'Charged':
powerstatus = '已充满'
return powerlevel, powerstatus
def info_timediff(self, device_info):
info_time = device_info['location']['timeStamp']
timediff = datetime.datetime.now()-datetime.datetime.fromtimestamp(info_time/1000)
seconds = timediff.total_seconds()
if timediff >=datetime.timedelta(hours=1):
fixedtime = '一小时前'
elif timediff>=datetime.timedelta(minutes=1):
fixedtime = '%d分%d秒前' % ((seconds % 3600) // 60, (seconds % 3600) % 60)
elif timediff<datetime.timedelta(minutes=1):
fixedtime = '%d秒前' % seconds
return fixedtime
def show_info(self, lon, lat, conv_lon, conv_lat, address, is_china, powerlevel, powerstatus, fixedtime):
if is_china:
long_url = 'http://uri.amap.com/marker?position=%s,%s' % (conv_lon, conv_lat)
long_pic = 'http://restapi.amap.com/v3/staticmap?markers=mid,0xFFFF00,A:%s,%s&key=%s&size=300*130&zoom=14' % (conv_lon, conv_lat, self.amapkey)
else:
long_url = 'http://api.map.baidu.com/marker?title=%s&content=这&output=html&location=%s,%s' % (self.device_name, lat, lon)
long_pic = 'http://api.map.baidu.com/staticimage/v2?ak=%s&markers=%s,%s&zoom=16&markerStyles=m,A,0xFFFF00&width=300&height=130' % (self.baiduAk, lon, lat)
short_url = self.shortenurl(long_url)
short_pic = self.shortenurl(long_pic)
position_text = '<a style="color:black" target="blank" href="%s">%s(%s)</a><br>电池状态:%s</br>' % (short_url, address, fixedtime, powerstatus)
location_pic = '<iframe width="300" height="130" frameborder="0" style="border:0" src="%s" allowfullscreen></iframe>' % short_pic
return position_text, location_pic
def shortenurl(self, text):
tinyurl = 'http://tinyurl.com/api-create.php?url=%s' % text
i = 0
while i<2:
try:
tiny_request = urllib.request.Request(url=tinyurl, method='GET')
tiny_result = urllib.request.urlopen(tiny_request, timeout=5)
shortenurl = tiny_result.read().decode("utf-8")
break
except:
i = i+1
return shortenurl
def play_sound(self, device_info, username, password):
try: #if we are given a FMIP token, change auth Type
int(username)
auth_type = "Forever"
except ValueError: #else apple id use useridguest
auth_type = "UserIDGuest"
userAndPass = (username + ":" + password).encode('utf-8')
userAndPass = base64.b64encode(userAndPass).decode('utf-8')
url = 'https://fmipmobile.icloud.com/fmipservice/device/%s/playSound' % username
headers = {
'Accept':'*/*',
'Authorization':'Basic %s' % userAndPass,
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'en-us',
'Content-Type':'application/json; charset=utf-8',
'X-Apple-AuthScheme':auth_type,
}
data = {
'device': device_info['id'],
'subject': 'FMIP',
}
jsondata = json.dumps(data).encode('utf-8')
request = urllib.request.Request(url=url, headers=headers, data=jsondata, method='POST')
result = urllib.request.urlopen(request, timeout=10)
return
global _plugin
_plugin = BasePlugin()
def onStart():
global _plugin
_plugin.onStart()
def onStop():
global _plugin
_plugin.onStop()
def onConnect(Connection, Status, Description):
global _plugin
_plugin.onConnect(Connection, Status, Description)
def onMessage(Connection, Data, Status, Extra):
global _plugin
_plugin.onMessage(Connection, Data, Status, Extra)
def onCommand(Unit, Command, Level, Hue):
global _plugin
_plugin.onCommand(Unit, Command, Level, Hue)
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
global _plugin
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
def onDisconnect(Connection):
global _plugin
_plugin.onDisconnect(Connection)
def onHeartbeat():
global _plugin
_plugin.onHeartbeat()
# Update Device into database
def UpdateDevice(Unit, nValue, sValue, AlwaysUpdate=False):
# Make sure that the Domoticz device still exists (they can be deleted) before updating it
if Unit in Devices:
if Devices[Unit].nValue != nValue or Devices[Unit].sValue != sValue or AlwaysUpdate == True:
Devices[Unit].Update(nValue, str(sValue))
return
def UpdateBattery(Unit, Percent):
# Make sure that the Domoticz device still exists (they can be deleted) before updating it
if Unit in Devices:
levelBatt = int(Percent)
if levelBatt >= 75:
icon = "batterylevelfull"
elif levelBatt >= 50:
icon = "batterylevelok"
elif levelBatt >= 25:
icon = "batterylevellow"
else:
icon = "batterylevelempty"
try:
Devices[Unit].Update(nValue=0, sValue=Percent, Image=Images[icon].ID)
except:
Domoticz.Error("Failed to update device unit " + str(Unit))
return
# Generic helper functions
def DumpConfigToLog():
for x in Parameters:
if Parameters[x] != "":
Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Debug("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'")
Domoticz.Debug("Device Name: '" + Devices[x].Name + "'")
Domoticz.Debug("Device nValue: " + str(Devices[x].nValue))
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
return