[Domoticz插件]查找我的iPhone

回复
blindlight
帖子: 98
注册时间: 周四 3月 30, 2017 00:03

[Domoticz插件]查找我的iPhone

帖子 blindlight »

beta版本的有系统崩溃风险
beta版本的有系统崩溃风险
beta版本的有系统崩溃风险

请尽量在release版本下尝试,文件夹拷贝到domoticz/plugins下,重启系统即可
系统设置中的经纬度要填,用来取做家的位置坐标

setting.png
setting.png (187.58 KiB) 查看 136140 次
高德和百度的key需要在相关网站申请,其他应该不需要解释
插件中用到短地址服务,请按需再酸酸乳上添加tinyurl,否则不能成功更新设备
show1.png
show1.png (118.85 KiB) 查看 136140 次
show2.png
show2.png (36.81 KiB) 查看 136140 次
最后解释下,区别于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
FMIP.rar
(11.88 KiB) 已下载 2227 次
头像
DT27
帖子: 345
注册时间: 周四 3月 30, 2017 08:54
Gender:

Re: [Domoticz插件]查找我的iPhone

帖子 DT27 »

手握三星围观 ;)
声音提醒这个方便啊,拿着平板找手机 :lol:
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

添加后数据并没有更新。
仔细看了看文章,「请在酸酸乳」中添加tinyurl 这是什么操作?
头像
DT27
帖子: 345
注册时间: 周四 3月 30, 2017 08:54
Gender:

Re: [Domoticz插件]查找我的iPhone

帖子 DT27 »

binai 写了: 周一 12月 11, 2017 22:48 添加后数据并没有更新。
仔细看了看文章,「请在酸酸乳」中添加tinyurl 这是什么操作?
翻墙,不设置应该不会影响数据更新。
blindlight
帖子: 98
注册时间: 周四 3月 30, 2017 00:03

Re: [Domoticz插件]查找我的iPhone

帖子 blindlight »

binai 写了: 周一 12月 11, 2017 22:48 添加后数据并没有更新。
仔细看了看文章,「请在酸酸乳」中添加tinyurl 这是什么操作?
开启debug 看log什么错贴出来
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

「酸酸乳」 我后面算是看懂了,我这里似乎可以用 https 。

原来是设备名称的设置问题。查了获取的信息才知道要设备成icloud上面的那个设备名称才行。
上次由 binai 在 周二 12月 12, 2017 16:46,总共编辑 1 次。
blindlight
帖子: 98
注册时间: 周四 3月 30, 2017 00:03

Re: [Domoticz插件]查找我的iPhone

帖子 blindlight »

binai 写了: 周二 12月 12, 2017 15:27 「酸酸乳」 我后面算是看懂了,我这里似乎可以用 https 。

不过设备还是没更新。还得看看日志。这个PROXY的日志不知道是啥意思。


2017-12-12 15:52:44.682 (FindPhone) Calling message handler 'onHeartbeat'.
2017-12-12 15:53:44.713 (FindPhone) Calling message handler 'onHeartbeat'.
2017-12-12 15:53:50.089 Error: PROXY: timeout occurred, reconnecting
2017-12-12 15:53:50.090 Error: PROXY: Connect failed, reconnecting: Network is unreachable

代码: 全选

icloud_info, is_succ = self.FMIP(self.username, self.password)
这句前后加log
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

已经解决了。建议 tinyurl 使用 https 可以直接使用。
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

屏幕快照 2017-12-12 16.57.30.jpg
屏幕快照 2017-12-12 16.57.30.jpg (21.55 KiB) 查看 135997 次
这个位置不知道为啥没显示完全。
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

公司的那个范围单位是公里,这个说明一下。
图片显示不出来,似乎和网络有关系。
找机会找个国内访问好一点的短地址更新试一下。
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

短地址更新了一下,用国内的百度的。
需要使用下面的模块
import requests

代码: 全选

    def shorturl(self, text):
        tinyurl = 'http://dwz.cn/create.php'
        headers = {'Host':'dwz.cn',
                   'Accept':'*/*',
                   'Origin':'http://dwz.cn',
                   'X-Requested-With':'XMLHttpRequest',
                   'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
                   'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
                   'DNT':'1',
                   'Referer':'http://dwz.cn/',
                   'Accept-Encoding':'gzip, deflate',
                   'Accept-Language':'zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7'}
        data={'url':text,
              'alias':'',
              'access_type':'web'}
        r=requests.post(tinyurl,data=data,headers=headers)
        #print(r.encoding)
        r.encoding='utf-8'
        #print(r.json())
        #print(r.json().get('tinyurl',''))
        return r.json().get('tinyurl', '')
blindlight
帖子: 98
注册时间: 周四 3月 30, 2017 00:03

Re: [Domoticz插件]查找我的iPhone

帖子 blindlight »

binai 写了: 周二 12月 12, 2017 22:06 短地址更新了一下,用国内的百度的。
需要使用下面的模块
import requests

代码: 全选

    def shorturl(self, text):
        tinyurl = 'http://dwz.cn/create.php'
        headers = {'Host':'dwz.cn',
                   'Accept':'*/*',
                   'Origin':'http://dwz.cn',
                   'X-Requested-With':'XMLHttpRequest',
                   'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
                   'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
                   'DNT':'1',
                   'Referer':'http://dwz.cn/',
                   'Accept-Encoding':'gzip, deflate',
                   'Accept-Language':'zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7'}
        data={'url':text,
              'alias':'',
              'access_type':'web'}
        r=requests.post(tinyurl,data=data,headers=headers)
        #print(r.encoding)
        r.encoding='utf-8'
        #print(r.json())
        #print(r.json().get('tinyurl',''))
        return r.json().get('tinyurl', '')
好嘞 我也换了试试看
binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

插件里用了 requests 模块 Domoticz 就崩了。做了改动。


from urllib.parse import quote, quote_plus, urlencode
import gzip

代码: 全选


    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
        #######################################
        tinyurl = 'http://dwz.cn/create.php'
        headers = {'Host':'dwz.cn',
                'Accept':'*/*',
                'Origin':'http://dwz.cn',
                'X-Requested-With':'XMLHttpRequest',
                'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
                'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
                'DNT':'1',
                'Referer':'http://dwz.cn/',
                'Accept-Encoding':'gzip, deflate',
                'Accept-Language':'zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7'}
        data={'url':text,
            'alias':'',
            'access_type':'web'}
        data = urlencode(data).encode('utf-8')
        # print(data)
        req = urllib.request.Request(tinyurl,data=data,headers=headers)
        # print(req.data)
        page = urllib.request.urlopen(req,timeout=5).read()
        page = gzip.decompress(page).decode('utf-8')
        page = json.loads(page)
        # print(page)
        return page.get('tinyurl','')

binai
帖子: 22
注册时间: 周一 6月 26, 2017 14:59

Re: [Domoticz插件]查找我的iPhone

帖子 binai »

在网上做个备份

代码: 全选

#           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.4/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
from urllib.parse import quote, quote_plus, urlencode
import gzip

icons = {"FMIPbatterylevelfull": "batterylevelfull icons.zip",
         "FMIPbatterylevelok": "batterylevelok icons.zip",
         "FMIPbatterylevellow": "batterylevellow icons.zip",
         "FMIPbatterylevelempty": "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)))

        if 'batterylevelfull'  not in Images: Domoticz.Image('batterylevelfull icons.zip').Create()
        if 'batterylevelok' not in Images: Domoticz.Image('batterylevelok icons.zip').Create()
        if 'batterylevellow' not in Images: Domoticz.Image('batterylevellow icons.zip').Create()
        if 'batterylevelempty' not in Images: Domoticz.Image('batterylevelempty icons.zip').Create()
        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
                elif Devices[1].sValue.find('省电中') != -1:
                    self.interval = 35
                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 = 8
        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)
                Domoticz.Debug(str(accu))
                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)/100:
                    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))
                if powerlevel != 0:
                    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 = '已充满'
        else:
            if device_info['lowPowerMode']==True:
                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*80&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=12&markerStyles=m,A,0xFFFF00&width=300&height=80' % (self.baiduAk, lon, lat)
        short_url = self.shortenurl(long_url)
        short_pic = self.shortenurl1(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="80" frameborder="0" style="border:0" src="%s" allowfullscreen></iframe>' % short_pic
        return position_text, location_pic

    def shortenurl(self, text):
        # tinyurl =  'https://tinyurl.com/api-create.php?url=%s' % text
        # tinyurl = "http://api.c7.gg/api.php?url={}".format(text)
        tinyurl = 'http://suo.im/api.php?url={}'.format(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)
                shorturl = tiny_result.read().decode("utf-8")
                break
            except:
                i = i+1
        return shorturl
        #####################
    def shortenurl0(self, text):
        tinyurl = text
        return tinyurl

    def shortenurl1(self, text):
        tinyurl = 'http://dwz.cn/create.php'
        headers = {'Host':'dwz.cn',
               'Accept':'*/*',
               'Origin':'http://dwz.cn',
               'X-Requested-With':'XMLHttpRequest',
               'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
               'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
               'DNT':'1',
               'Referer':'http://dwz.cn/',
               'Accept-Encoding':'gzip, deflate',
               'Accept-Language':'zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7'}
        data={'url':text,
            'alias':'',
            'access_type':'web'}
        i = 0
        while i<2:
            try:
                data = urlencode(data).encode('utf-8')
                # print(data)
                req = urllib.request.Request(tinyurl,data=data,headers=headers)
                # print(req.data)
                page = urllib.request.urlopen(req,timeout=6).read()
                page = gzip.decompress(page).decode('utf-8')
                page = json.loads(page)
                # print(page)
                # Domoticz.Debug(str(text))
                # Domoticz.Debug(str(page))
                shorturl = page['tinyurl']
                break
            except BaseException:
                i = i + 1
        return shorturl

    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 = "FMIPbatterylevelfull"
        elif levelBatt >= 50:
            icon = "FMIPbatterylevelok"
        elif levelBatt >= 25:
            icon = "FMIPbatterylevellow"
        else:
            icon = "FMIPbatterylevelempty"
        Domoticz.Debug(str(Images["FMIPbatterylevelfull"].ID))
        Domoticz.Debug(str(Images["FMIPbatterylevellow"].ID))
        Domoticz.Debug(str(Images["FMIPbatterylevelempty"].ID))
        Domoticz.Debug(str(Images["FMIPbatterylevelok"].ID))
        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

回复