2019-04-03 15:40:03 +00:00
|
|
|
"""Platform that supports scanning iCloud."""
|
2015-11-13 06:37:15 +00:00
|
|
|
import logging
|
2016-11-03 04:07:23 +00:00
|
|
|
import os
|
2019-11-26 16:59:42 +00:00
|
|
|
import random
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2019-11-26 16:59:42 +00:00
|
|
|
from pyicloud import PyiCloudService
|
|
|
|
from pyicloud.exceptions import (
|
|
|
|
PyiCloudException,
|
|
|
|
PyiCloudFailedLoginException,
|
|
|
|
PyiCloudNoDevicesException,
|
|
|
|
)
|
2016-07-26 21:53:31 +00:00
|
|
|
import voluptuous as vol
|
2016-02-19 05:27:50 +00:00
|
|
|
|
2019-05-15 21:43:45 +00:00
|
|
|
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
|
|
|
from homeassistant.components.device_tracker.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ATTRIBUTES,
|
2019-11-26 16:59:42 +00:00
|
|
|
DOMAIN,
|
2019-07-31 19:25:30 +00:00
|
|
|
ENTITY_ID_FORMAT,
|
|
|
|
)
|
2019-05-15 21:43:45 +00:00
|
|
|
from homeassistant.components.device_tracker.legacy import DeviceScanner
|
2019-05-25 20:34:53 +00:00
|
|
|
from homeassistant.components.zone import async_active_zone
|
2019-11-26 16:59:42 +00:00
|
|
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
2016-11-03 04:07:23 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2019-11-26 16:59:42 +00:00
|
|
|
from homeassistant.helpers.event import track_utc_time_change
|
2016-07-26 21:53:31 +00:00
|
|
|
from homeassistant.util import slugify
|
2019-11-26 16:59:42 +00:00
|
|
|
from homeassistant.util.async_ import run_callback_threadsafe
|
2016-11-03 04:07:23 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
from homeassistant.util.location import distance
|
2015-11-13 06:37:15 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ACCOUNTNAME = "account_name"
|
|
|
|
CONF_MAX_INTERVAL = "max_interval"
|
|
|
|
CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold"
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
# entity attributes
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ACCOUNTNAME = "account_name"
|
|
|
|
ATTR_INTERVAL = "interval"
|
|
|
|
ATTR_DEVICENAME = "device_name"
|
|
|
|
ATTR_BATTERY = "battery"
|
|
|
|
ATTR_DISTANCE = "distance"
|
|
|
|
ATTR_DEVICESTATUS = "device_status"
|
|
|
|
ATTR_LOWPOWERMODE = "low_power_mode"
|
|
|
|
ATTR_BATTERYSTATUS = "battery_status"
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
ICLOUDTRACKERS = {}
|
|
|
|
|
|
|
|
_CONFIGURING = {}
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEVICESTATUSSET = [
|
|
|
|
"features",
|
|
|
|
"maxMsgChar",
|
|
|
|
"darkWake",
|
|
|
|
"fmlyShare",
|
|
|
|
"deviceStatus",
|
|
|
|
"remoteLock",
|
|
|
|
"activationLocked",
|
|
|
|
"deviceClass",
|
|
|
|
"id",
|
|
|
|
"deviceModel",
|
|
|
|
"rawDeviceModel",
|
|
|
|
"passcodeLength",
|
|
|
|
"canWipeAfterLock",
|
|
|
|
"trackingInfo",
|
|
|
|
"location",
|
|
|
|
"msg",
|
|
|
|
"batteryLevel",
|
|
|
|
"remoteWipe",
|
|
|
|
"thisDevice",
|
|
|
|
"snd",
|
|
|
|
"prsId",
|
|
|
|
"wipeInProgress",
|
|
|
|
"lowPowerMode",
|
|
|
|
"lostModeEnabled",
|
|
|
|
"isLocating",
|
|
|
|
"lostModeCapable",
|
|
|
|
"mesg",
|
|
|
|
"name",
|
|
|
|
"batteryStatus",
|
|
|
|
"lockedTimestamp",
|
|
|
|
"lostTimestamp",
|
|
|
|
"locationCapable",
|
|
|
|
"deviceDisplayName",
|
|
|
|
"lostDevice",
|
|
|
|
"deviceColor",
|
|
|
|
"wipedTimestamp",
|
|
|
|
"modelDisplayName",
|
|
|
|
"locationEnabled",
|
|
|
|
"isMac",
|
|
|
|
"locFoundEnabled",
|
|
|
|
]
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2017-04-30 05:04:49 +00:00
|
|
|
DEVICESTATUSCODES = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"200": "online",
|
|
|
|
"201": "offline",
|
|
|
|
"203": "pending",
|
|
|
|
"204": "unregistered",
|
2017-04-30 05:04:49 +00:00
|
|
|
}
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
|
|
|
|
vol.Optional(ATTR_DEVICENAME): cv.slugify,
|
|
|
|
vol.Optional(ATTR_INTERVAL): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
|
|
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
|
|
|
|
vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int,
|
|
|
|
vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
|
2017-02-07 19:47:11 +00:00
|
|
|
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
2016-11-03 04:07:23 +00:00
|
|
|
"""Set up the iCloud Scanner."""
|
|
|
|
username = config.get(CONF_USERNAME)
|
|
|
|
password = config.get(CONF_PASSWORD)
|
2019-07-31 19:25:30 +00:00
|
|
|
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition("@")[0]))
|
2018-05-08 21:42:57 +00:00
|
|
|
max_interval = config.get(CONF_MAX_INTERVAL)
|
|
|
|
gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
icloudaccount = Icloud(
|
|
|
|
hass, username, password, account, max_interval, gps_accuracy_threshold, see
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
if icloudaccount.api is not None:
|
|
|
|
ICLOUDTRACKERS[account] = icloudaccount
|
|
|
|
|
|
|
|
else:
|
|
|
|
_LOGGER.error("No ICLOUDTRACKERS added")
|
2015-12-17 20:31:33 +00:00
|
|
|
return False
|
2015-11-13 06:37:15 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def lost_iphone(call):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Call the lost iPhone function if the device is found."""
|
2016-11-03 04:07:23 +00:00
|
|
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
|
|
|
devicename = call.data.get(ATTR_DEVICENAME)
|
|
|
|
for account in accounts:
|
|
|
|
if account in ICLOUDTRACKERS:
|
|
|
|
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
2018-05-08 21:42:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, "icloud_lost_iphone", lost_iphone, schema=SERVICE_SCHEMA
|
|
|
|
)
|
2016-07-26 21:53:31 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def update_icloud(call):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Call the update function of an iCloud account."""
|
2016-11-03 04:07:23 +00:00
|
|
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
|
|
|
devicename = call.data.get(ATTR_DEVICENAME)
|
|
|
|
for account in accounts:
|
|
|
|
if account in ICLOUDTRACKERS:
|
|
|
|
ICLOUDTRACKERS[account].update_icloud(devicename)
|
2018-05-08 21:42:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, "icloud_update", update_icloud, schema=SERVICE_SCHEMA
|
|
|
|
)
|
2015-12-15 05:39:48 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def reset_account_icloud(call):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Reset an iCloud account."""
|
2016-11-03 04:07:23 +00:00
|
|
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
|
|
|
for account in accounts:
|
|
|
|
if account in ICLOUDTRACKERS:
|
|
|
|
ICLOUDTRACKERS[account].reset_account_icloud()
|
2018-05-08 21:42:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, "icloud_reset_account", reset_account_icloud, schema=SERVICE_SCHEMA
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def setinterval(call):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Call the update function of an iCloud account."""
|
2016-11-03 04:07:23 +00:00
|
|
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
|
|
|
interval = call.data.get(ATTR_INTERVAL)
|
|
|
|
devicename = call.data.get(ATTR_DEVICENAME)
|
|
|
|
for account in accounts:
|
|
|
|
if account in ICLOUDTRACKERS:
|
|
|
|
ICLOUDTRACKERS[account].setinterval(interval, devicename)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, "icloud_set_interval", setinterval, schema=SERVICE_SCHEMA
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
# Tells the bootstrapper that the component was successfully initialized
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2017-01-02 19:50:42 +00:00
|
|
|
class Icloud(DeviceScanner):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Representation of an iCloud account."""
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self, hass, username, password, name, max_interval, gps_accuracy_threshold, see
|
|
|
|
):
|
2016-11-03 04:07:23 +00:00
|
|
|
"""Initialize an iCloud account."""
|
|
|
|
self.hass = hass
|
|
|
|
self.username = username
|
|
|
|
self.password = password
|
|
|
|
self.api = None
|
|
|
|
self.accountname = name
|
|
|
|
self.devices = {}
|
|
|
|
self.seen_devices = {}
|
|
|
|
self._overridestates = {}
|
|
|
|
self._intervals = {}
|
2018-05-08 21:42:57 +00:00
|
|
|
self._max_interval = max_interval
|
|
|
|
self._gps_accuracy_threshold = gps_accuracy_threshold
|
2016-11-03 04:07:23 +00:00
|
|
|
self.see = see
|
|
|
|
|
|
|
|
self._trusted_device = None
|
|
|
|
self._verification_code = None
|
|
|
|
|
|
|
|
self._attrs = {}
|
|
|
|
self._attrs[ATTR_ACCOUNTNAME] = name
|
|
|
|
|
|
|
|
self.reset_account_icloud()
|
|
|
|
|
|
|
|
randomseconds = random.randint(10, 59)
|
2019-07-31 19:25:30 +00:00
|
|
|
track_utc_time_change(self.hass, self.keep_alive, second=randomseconds)
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def reset_account_icloud(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Reset an iCloud account."""
|
2019-07-31 19:25:30 +00:00
|
|
|
icloud_dir = self.hass.config.path("icloud")
|
2016-11-03 04:07:23 +00:00
|
|
|
if not os.path.exists(icloud_dir):
|
|
|
|
os.makedirs(icloud_dir)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.api = PyiCloudService(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.username, self.password, cookie_directory=icloud_dir, verify=True
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
except PyiCloudFailedLoginException as error:
|
|
|
|
self.api = None
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Error logging into iCloud Service: %s", error)
|
2016-11-03 04:07:23 +00:00
|
|
|
return
|
2016-07-28 03:38:55 +00:00
|
|
|
|
2015-11-13 06:37:15 +00:00
|
|
|
try:
|
2016-11-03 04:07:23 +00:00
|
|
|
self.devices = {}
|
|
|
|
self._overridestates = {}
|
|
|
|
self._intervals = {}
|
|
|
|
for device in self.api.devices:
|
|
|
|
status = device.status(DEVICESTATUSSET)
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Device Status is %s", status)
|
|
|
|
devicename = slugify(status["name"].replace(" ", "", 99))
|
|
|
|
_LOGGER.info("Adding icloud device: %s", devicename)
|
2018-03-01 00:54:19 +00:00
|
|
|
if devicename in self.devices:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Multiple devices with name: %s", devicename)
|
2018-03-01 00:54:19 +00:00
|
|
|
continue
|
|
|
|
self.devices[devicename] = device
|
|
|
|
self._intervals[devicename] = 1
|
|
|
|
self._overridestates[devicename] = None
|
2016-11-03 04:07:23 +00:00
|
|
|
except PyiCloudNoDevicesException:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("No iCloud Devices found!")
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def icloud_trusted_device_callback(self, callback_data):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Handle chosen trusted devices."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._trusted_device = int(callback_data.get("trusted_device"))
|
2016-11-03 04:07:23 +00:00
|
|
|
self._trusted_device = self.api.trusted_devices[self._trusted_device]
|
2017-03-08 11:07:34 +00:00
|
|
|
|
|
|
|
if not self.api.send_verification_code(self._trusted_device):
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Failed to send verification code")
|
2017-03-08 11:07:34 +00:00
|
|
|
self._trusted_device = None
|
|
|
|
return
|
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.accountname in _CONFIGURING:
|
|
|
|
request_id = _CONFIGURING.pop(self.accountname)
|
2017-08-14 05:37:50 +00:00
|
|
|
configurator = self.hass.components.configurator
|
2016-11-03 04:07:23 +00:00
|
|
|
configurator.request_done(request_id)
|
|
|
|
|
2017-03-08 11:07:34 +00:00
|
|
|
# Trigger the next step immediately
|
|
|
|
self.icloud_need_verification_code()
|
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def icloud_need_trusted_device(self):
|
|
|
|
"""We need a trusted device."""
|
2017-08-14 05:37:50 +00:00
|
|
|
configurator = self.hass.components.configurator
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.accountname in _CONFIGURING:
|
|
|
|
return
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
devicesstring = ""
|
2016-11-03 04:07:23 +00:00
|
|
|
devices = self.api.trusted_devices
|
|
|
|
for i, device in enumerate(devices):
|
2017-03-08 11:07:34 +00:00
|
|
|
devicename = device.get(
|
2019-07-31 19:25:30 +00:00
|
|
|
"deviceName", "SMS to %s" % device.get("phoneNumber")
|
|
|
|
)
|
2019-09-03 15:27:14 +00:00
|
|
|
devicesstring += f"{i}: {devicename};"
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
_CONFIGURING[self.accountname] = configurator.request_config(
|
2019-09-03 15:27:14 +00:00
|
|
|
f"iCloud {self.accountname}",
|
2016-11-03 04:07:23 +00:00
|
|
|
self.icloud_trusted_device_callback,
|
|
|
|
description=(
|
2019-07-31 19:25:30 +00:00
|
|
|
"Please choose your trusted device by entering"
|
|
|
|
" the index from this list: " + devicesstring
|
|
|
|
),
|
2016-11-03 04:07:23 +00:00
|
|
|
entity_picture="/static/images/config_icloud.png",
|
2019-07-31 19:25:30 +00:00
|
|
|
submit_caption="Confirm",
|
|
|
|
fields=[{"id": "trusted_device", "name": "Trusted Device"}],
|
2016-11-03 04:07:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def icloud_verification_callback(self, callback_data):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Handle the chosen trusted device."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._verification_code = callback_data.get("code")
|
2017-03-08 11:07:34 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
if not self.api.validate_verification_code(
|
2019-07-31 19:25:30 +00:00
|
|
|
self._trusted_device, self._verification_code
|
|
|
|
):
|
|
|
|
raise PyiCloudException("Unknown failure")
|
2017-03-08 11:07:34 +00:00
|
|
|
except PyiCloudException as error:
|
2017-09-23 15:15:46 +00:00
|
|
|
# Reset to the initial 2FA state to allow the user to retry
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Failed to verify verification code: %s", error)
|
2017-03-08 11:07:34 +00:00
|
|
|
self._trusted_device = None
|
|
|
|
self._verification_code = None
|
|
|
|
|
|
|
|
# Trigger the next step immediately
|
|
|
|
self.icloud_need_trusted_device()
|
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.accountname in _CONFIGURING:
|
|
|
|
request_id = _CONFIGURING.pop(self.accountname)
|
2017-08-14 05:37:50 +00:00
|
|
|
configurator = self.hass.components.configurator
|
2016-11-03 04:07:23 +00:00
|
|
|
configurator.request_done(request_id)
|
|
|
|
|
|
|
|
def icloud_need_verification_code(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the verification code."""
|
2017-08-14 05:37:50 +00:00
|
|
|
configurator = self.hass.components.configurator
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.accountname in _CONFIGURING:
|
|
|
|
return
|
|
|
|
|
|
|
|
_CONFIGURING[self.accountname] = configurator.request_config(
|
2019-09-03 15:27:14 +00:00
|
|
|
f"iCloud {self.accountname}",
|
2016-11-03 04:07:23 +00:00
|
|
|
self.icloud_verification_callback,
|
2019-07-31 19:25:30 +00:00
|
|
|
description=("Please enter the validation code:"),
|
2016-11-03 04:07:23 +00:00
|
|
|
entity_picture="/static/images/config_icloud.png",
|
2019-07-31 19:25:30 +00:00
|
|
|
submit_caption="Confirm",
|
|
|
|
fields=[{"id": "code", "name": "code"}],
|
2016-11-03 04:07:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def keep_alive(self, now):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Keep the API alive."""
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.api is None:
|
|
|
|
self.reset_account_icloud()
|
|
|
|
|
|
|
|
if self.api is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.api.requires_2fa:
|
|
|
|
try:
|
|
|
|
if self._trusted_device is None:
|
|
|
|
self.icloud_need_trusted_device()
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._verification_code is None:
|
|
|
|
self.icloud_need_verification_code()
|
|
|
|
return
|
|
|
|
|
2017-03-08 11:07:34 +00:00
|
|
|
self.api.authenticate()
|
|
|
|
if self.api.requires_2fa:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise Exception("Unknown failure")
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2017-03-08 11:07:34 +00:00
|
|
|
self._trusted_device = None
|
|
|
|
self._verification_code = None
|
|
|
|
except PyiCloudException as error:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Error setting up 2FA: %s", error)
|
2016-11-03 04:07:23 +00:00
|
|
|
else:
|
|
|
|
self.api.authenticate()
|
|
|
|
|
|
|
|
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
|
2017-08-28 16:09:36 +00:00
|
|
|
try:
|
|
|
|
for devicename in self.devices:
|
|
|
|
interval = self._intervals.get(devicename, 1)
|
2019-07-31 19:25:30 +00:00
|
|
|
if (currentminutes % interval == 0) or (
|
|
|
|
interval > 10 and currentminutes % interval in [2, 4]
|
|
|
|
):
|
2017-08-28 16:09:36 +00:00
|
|
|
self.update_device(devicename)
|
|
|
|
except ValueError:
|
|
|
|
_LOGGER.debug("iCloud API returned an error")
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def determine_interval(self, devicename, latitude, longitude, battery):
|
|
|
|
"""Calculate new interval."""
|
2019-05-25 20:34:53 +00:00
|
|
|
currentzone = run_callback_threadsafe(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.loop, async_active_zone, self.hass, latitude, longitude
|
2019-05-25 20:34:53 +00:00
|
|
|
).result()
|
2016-11-03 04:07:23 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
currentzone is not None
|
|
|
|
and currentzone == self._overridestates.get(devicename)
|
|
|
|
) or (currentzone is None and self._overridestates.get(devicename) == "away"):
|
2016-11-03 04:07:23 +00:00
|
|
|
return
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
zones = (
|
|
|
|
self.hass.states.get(entity_id)
|
|
|
|
for entity_id in sorted(self.hass.states.entity_ids("zone"))
|
|
|
|
)
|
2018-03-01 00:54:19 +00:00
|
|
|
|
|
|
|
distances = []
|
|
|
|
for zone_state in zones:
|
2019-07-31 19:25:30 +00:00
|
|
|
zone_state_lat = zone_state.attributes["latitude"]
|
|
|
|
zone_state_long = zone_state.attributes["longitude"]
|
2018-03-01 00:54:19 +00:00
|
|
|
zone_distance = distance(
|
2019-07-31 19:25:30 +00:00
|
|
|
latitude, longitude, zone_state_lat, zone_state_long
|
|
|
|
)
|
2018-03-01 00:54:19 +00:00
|
|
|
distances.append(round(zone_distance / 1000, 1))
|
|
|
|
|
|
|
|
if distances:
|
|
|
|
mindistance = min(distances)
|
|
|
|
else:
|
|
|
|
mindistance = None
|
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
self._overridestates[devicename] = None
|
|
|
|
|
|
|
|
if currentzone is not None:
|
2018-05-08 21:42:57 +00:00
|
|
|
self._intervals[devicename] = self._max_interval
|
2016-11-03 04:07:23 +00:00
|
|
|
return
|
|
|
|
|
2018-03-01 00:54:19 +00:00
|
|
|
if mindistance is None:
|
2016-11-03 04:07:23 +00:00
|
|
|
return
|
2018-03-01 00:54:19 +00:00
|
|
|
|
|
|
|
# Calculate out how long it would take for the device to drive to the
|
|
|
|
# nearest zone at 120 km/h:
|
|
|
|
interval = round(mindistance / 2, 0)
|
|
|
|
|
|
|
|
# Never poll more than once per minute
|
|
|
|
interval = max(interval, 1)
|
|
|
|
|
|
|
|
if interval > 180:
|
|
|
|
# Three hour drive? This is far enough that they might be flying
|
|
|
|
interval = 30
|
|
|
|
|
|
|
|
if battery is not None and battery <= 33 and mindistance > 3:
|
|
|
|
# Low battery - let's check half as often
|
|
|
|
interval = interval * 2
|
|
|
|
|
|
|
|
self._intervals[devicename] = interval
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def update_device(self, devicename):
|
|
|
|
"""Update the device_tracker entity."""
|
|
|
|
# An entity will not be created by see() when track=false in
|
|
|
|
# 'known_devices.yaml', but we need to see() it at least once
|
|
|
|
entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename))
|
|
|
|
if entity is None and devicename in self.seen_devices:
|
|
|
|
return
|
|
|
|
attrs = {}
|
|
|
|
kwargs = {}
|
|
|
|
|
|
|
|
if self.api is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
for device in self.api.devices:
|
|
|
|
if str(device) != str(self.devices[devicename]):
|
2016-07-28 03:38:55 +00:00
|
|
|
continue
|
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
status = device.status(DEVICESTATUSSET)
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Device Status is %s", status)
|
|
|
|
dev_id = status["name"].replace(" ", "", 99)
|
2016-11-03 04:07:23 +00:00
|
|
|
dev_id = slugify(dev_id)
|
|
|
|
attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(
|
2019-07-31 19:25:30 +00:00
|
|
|
status["deviceStatus"], "error"
|
|
|
|
)
|
|
|
|
attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"]
|
|
|
|
attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"]
|
2016-11-03 04:07:23 +00:00
|
|
|
attrs[ATTR_ACCOUNTNAME] = self.accountname
|
|
|
|
status = device.status(DEVICESTATUSSET)
|
2019-07-31 19:25:30 +00:00
|
|
|
battery = status.get("batteryLevel", 0) * 100
|
|
|
|
location = status["location"]
|
|
|
|
if location and location["horizontalAccuracy"]:
|
|
|
|
horizontal_accuracy = int(location["horizontalAccuracy"])
|
2018-05-08 21:42:57 +00:00
|
|
|
if horizontal_accuracy < self._gps_accuracy_threshold:
|
|
|
|
self.determine_interval(
|
2019-07-31 19:25:30 +00:00
|
|
|
devicename,
|
|
|
|
location["latitude"],
|
|
|
|
location["longitude"],
|
|
|
|
battery,
|
|
|
|
)
|
2018-05-08 21:42:57 +00:00
|
|
|
interval = self._intervals.get(devicename, 1)
|
|
|
|
attrs[ATTR_INTERVAL] = interval
|
2019-07-31 19:25:30 +00:00
|
|
|
accuracy = location["horizontalAccuracy"]
|
|
|
|
kwargs["dev_id"] = dev_id
|
|
|
|
kwargs["host_name"] = status["name"]
|
|
|
|
kwargs["gps"] = (location["latitude"], location["longitude"])
|
|
|
|
kwargs["battery"] = battery
|
|
|
|
kwargs["gps_accuracy"] = accuracy
|
2018-05-08 21:42:57 +00:00
|
|
|
kwargs[ATTR_ATTRIBUTES] = attrs
|
|
|
|
self.see(**kwargs)
|
|
|
|
self.seen_devices[devicename] = True
|
2015-11-22 04:12:41 +00:00
|
|
|
except PyiCloudNoDevicesException:
|
2017-08-28 16:09:36 +00:00
|
|
|
_LOGGER.error("No iCloud Devices found")
|
2015-11-22 04:04:28 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def lost_iphone(self, devicename):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Call the lost iPhone function if the device is found."""
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.api is None:
|
|
|
|
return
|
2016-07-26 21:53:31 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
self.api.authenticate()
|
|
|
|
for device in self.api.devices:
|
2018-12-23 12:35:39 +00:00
|
|
|
if str(device) == str(self.devices[devicename]):
|
|
|
|
_LOGGER.info("Playing Lost iPhone sound for %s", devicename)
|
2016-11-03 04:07:23 +00:00
|
|
|
device.play_sound()
|
2015-12-17 20:31:33 +00:00
|
|
|
|
2016-11-03 04:07:23 +00:00
|
|
|
def update_icloud(self, devicename=None):
|
2018-05-08 21:42:57 +00:00
|
|
|
"""Request device information from iCloud and update device_tracker."""
|
2016-11-03 04:07:23 +00:00
|
|
|
if self.api is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
if devicename is not None:
|
|
|
|
if devicename in self.devices:
|
2018-05-08 21:42:57 +00:00
|
|
|
self.update_device(devicename)
|
2016-11-03 04:07:23 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"devicename %s unknown for account %s",
|
|
|
|
devicename,
|
|
|
|
self._attrs[ATTR_ACCOUNTNAME],
|
|
|
|
)
|
2016-11-03 04:07:23 +00:00
|
|
|
else:
|
|
|
|
for device in self.devices:
|
2018-05-08 21:42:57 +00:00
|
|
|
self.update_device(device)
|
2016-11-03 04:07:23 +00:00
|
|
|
except PyiCloudNoDevicesException:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("No iCloud Devices found")
|
2016-11-03 04:07:23 +00:00
|
|
|
|
|
|
|
def setinterval(self, interval=None, devicename=None):
|
|
|
|
"""Set the interval of the given devices."""
|
|
|
|
devs = [devicename] if devicename else self.devices
|
|
|
|
for device in devs:
|
2019-09-03 15:27:14 +00:00
|
|
|
devid = f"{DOMAIN}.{device}"
|
2016-11-03 04:07:23 +00:00
|
|
|
devicestate = self.hass.states.get(devid)
|
|
|
|
if interval is not None:
|
|
|
|
if devicestate is not None:
|
2019-05-25 20:34:53 +00:00
|
|
|
self._overridestates[device] = run_callback_threadsafe(
|
|
|
|
self.hass.loop,
|
|
|
|
async_active_zone,
|
2016-11-03 04:07:23 +00:00
|
|
|
self.hass,
|
2019-07-31 19:25:30 +00:00
|
|
|
float(devicestate.attributes.get("latitude", 0)),
|
|
|
|
float(devicestate.attributes.get("longitude", 0)),
|
2019-05-25 20:34:53 +00:00
|
|
|
).result()
|
2016-11-03 04:07:23 +00:00
|
|
|
if self._overridestates[device] is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
self._overridestates[device] = "away"
|
2016-11-03 04:07:23 +00:00
|
|
|
self._intervals[device] = interval
|
|
|
|
else:
|
|
|
|
self._overridestates[device] = None
|
|
|
|
self.update_device(device)
|