Black
This commit is contained in:
parent
da05dfe708
commit
4de97abc3a
2676 changed files with 163166 additions and 140084 deletions
|
@ -7,11 +7,19 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.device_tracker import CONF_SCAN_INTERVAL
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT)
|
||||
ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT,
|
||||
)
|
||||
from homeassistant.components.zone import async_active_zone
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_CHARGING, ATTR_ENTITY_ID, CONF_PREFIX, LENGTH_FEET,
|
||||
LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, STATE_UNKNOWN)
|
||||
ATTR_BATTERY_CHARGING,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_PREFIX,
|
||||
LENGTH_FEET,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_METERS,
|
||||
LENGTH_MILES,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
@ -19,29 +27,37 @@ from homeassistant.util.distance import convert
|
|||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
CONF_CIRCLES, CONF_DRIVING_SPEED, CONF_ERROR_THRESHOLD,
|
||||
CONF_MAX_GPS_ACCURACY, CONF_MAX_UPDATE_WAIT, CONF_MEMBERS,
|
||||
CONF_SHOW_AS_STATE, CONF_WARNING_THRESHOLD, DOMAIN, SHOW_DRIVING,
|
||||
SHOW_MOVING)
|
||||
CONF_CIRCLES,
|
||||
CONF_DRIVING_SPEED,
|
||||
CONF_ERROR_THRESHOLD,
|
||||
CONF_MAX_GPS_ACCURACY,
|
||||
CONF_MAX_UPDATE_WAIT,
|
||||
CONF_MEMBERS,
|
||||
CONF_SHOW_AS_STATE,
|
||||
CONF_WARNING_THRESHOLD,
|
||||
DOMAIN,
|
||||
SHOW_DRIVING,
|
||||
SHOW_MOVING,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_FACTOR_MPH = 2.25
|
||||
EVENT_DELAY = timedelta(seconds=30)
|
||||
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_AT_LOC_SINCE = 'at_loc_since'
|
||||
ATTR_DRIVING = 'driving'
|
||||
ATTR_LAST_SEEN = 'last_seen'
|
||||
ATTR_MOVING = 'moving'
|
||||
ATTR_PLACE = 'place'
|
||||
ATTR_RAW_SPEED = 'raw_speed'
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_WAIT = 'wait'
|
||||
ATTR_WIFI_ON = 'wifi_on'
|
||||
ATTR_ADDRESS = "address"
|
||||
ATTR_AT_LOC_SINCE = "at_loc_since"
|
||||
ATTR_DRIVING = "driving"
|
||||
ATTR_LAST_SEEN = "last_seen"
|
||||
ATTR_MOVING = "moving"
|
||||
ATTR_PLACE = "place"
|
||||
ATTR_RAW_SPEED = "raw_speed"
|
||||
ATTR_SPEED = "speed"
|
||||
ATTR_WAIT = "wait"
|
||||
ATTR_WIFI_ON = "wifi_on"
|
||||
|
||||
EVENT_UPDATE_OVERDUE = 'life360_update_overdue'
|
||||
EVENT_UPDATE_RESTORED = 'life360_update_restored'
|
||||
EVENT_UPDATE_OVERDUE = "life360_update_overdue"
|
||||
EVENT_UPDATE_RESTORED = "life360_update_restored"
|
||||
|
||||
|
||||
def _include_name(filter_dict, name):
|
||||
|
@ -50,28 +66,30 @@ def _include_name(filter_dict, name):
|
|||
if not filter_dict:
|
||||
return True
|
||||
name = name.lower()
|
||||
if filter_dict['include']:
|
||||
return name in filter_dict['list']
|
||||
return name not in filter_dict['list']
|
||||
if filter_dict["include"]:
|
||||
return name in filter_dict["list"]
|
||||
return name not in filter_dict["list"]
|
||||
|
||||
|
||||
def _exc_msg(exc):
|
||||
return '{}: {}'.format(exc.__class__.__name__, str(exc))
|
||||
return "{}: {}".format(exc.__class__.__name__, str(exc))
|
||||
|
||||
|
||||
def _dump_filter(filter_dict, desc, func=lambda x: x):
|
||||
if not filter_dict:
|
||||
return
|
||||
_LOGGER.debug(
|
||||
'%scluding %s: %s',
|
||||
'In' if filter_dict['include'] else 'Ex', desc,
|
||||
', '.join([func(name) for name in filter_dict['list']]))
|
||||
"%scluding %s: %s",
|
||||
"In" if filter_dict["include"] else "Ex",
|
||||
desc,
|
||||
", ".join([func(name) for name in filter_dict["list"]]),
|
||||
)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up device scanner."""
|
||||
config = hass.data[DOMAIN]['config']
|
||||
apis = hass.data[DOMAIN]['apis']
|
||||
config = hass.data[DOMAIN]["config"]
|
||||
apis = hass.data[DOMAIN]["apis"]
|
||||
Life360Scanner(hass, config, see, apis)
|
||||
return True
|
||||
|
||||
|
@ -120,30 +138,31 @@ class Life360Scanner:
|
|||
self._circles_logged = set()
|
||||
self._members_logged = set()
|
||||
|
||||
_dump_filter(self._circles_filter, 'Circles')
|
||||
_dump_filter(self._members_filter, 'device IDs', self._dev_id)
|
||||
_dump_filter(self._circles_filter, "Circles")
|
||||
_dump_filter(self._members_filter, "device IDs", self._dev_id)
|
||||
|
||||
self._started = dt_util.utcnow()
|
||||
self._update_life360()
|
||||
track_time_interval(
|
||||
self._hass, self._update_life360, config[CONF_SCAN_INTERVAL])
|
||||
self._hass, self._update_life360, config[CONF_SCAN_INTERVAL]
|
||||
)
|
||||
|
||||
def _dev_id(self, name):
|
||||
return self._prefix + name
|
||||
|
||||
def _ok(self, key):
|
||||
if self._errs.get(key, 0) >= self._max_errs:
|
||||
_LOGGER.error('%s: OK again', key)
|
||||
_LOGGER.error("%s: OK again", key)
|
||||
self._errs[key] = 0
|
||||
|
||||
def _err(self, key, err_msg):
|
||||
_errs = self._errs.get(key, 0)
|
||||
if _errs < self._max_errs:
|
||||
self._errs[key] = _errs = _errs + 1
|
||||
msg = '{}: {}'.format(key, err_msg)
|
||||
msg = "{}: {}".format(key, err_msg)
|
||||
if _errs >= self._error_threshold:
|
||||
if _errs == self._max_errs:
|
||||
msg = 'Suppressing further errors until OK: ' + msg
|
||||
msg = "Suppressing further errors until OK: " + msg
|
||||
_LOGGER.error(msg)
|
||||
elif _errs >= self._warning_threshold:
|
||||
_LOGGER.warning(msg)
|
||||
|
@ -161,15 +180,19 @@ class Life360Scanner:
|
|||
if overdue and not reported and now - self._started > EVENT_DELAY:
|
||||
self._hass.bus.fire(
|
||||
EVENT_UPDATE_OVERDUE,
|
||||
{ATTR_ENTITY_ID: DT_ENTITY_ID_FORMAT.format(dev_id)})
|
||||
{ATTR_ENTITY_ID: DT_ENTITY_ID_FORMAT.format(dev_id)},
|
||||
)
|
||||
reported = True
|
||||
elif not overdue and reported:
|
||||
self._hass.bus.fire(
|
||||
EVENT_UPDATE_RESTORED, {
|
||||
EVENT_UPDATE_RESTORED,
|
||||
{
|
||||
ATTR_ENTITY_ID: DT_ENTITY_ID_FORMAT.format(dev_id),
|
||||
ATTR_WAIT:
|
||||
str(last_seen - (prev_seen or self._started))
|
||||
.split('.')[0]})
|
||||
ATTR_WAIT: str(last_seen - (prev_seen or self._started)).split(
|
||||
"."
|
||||
)[0],
|
||||
},
|
||||
)
|
||||
reported = False
|
||||
|
||||
self._dev_data[dev_id] = last_seen or prev_seen, reported
|
||||
|
@ -177,20 +200,20 @@ class Life360Scanner:
|
|||
return prev_seen
|
||||
|
||||
def _update_member(self, member, dev_id):
|
||||
loc = member.get('location')
|
||||
loc = member.get("location")
|
||||
try:
|
||||
last_seen = _utc_from_ts(loc.get('timestamp'))
|
||||
last_seen = _utc_from_ts(loc.get("timestamp"))
|
||||
except AttributeError:
|
||||
last_seen = None
|
||||
prev_seen = self._prev_seen(dev_id, last_seen)
|
||||
|
||||
if not loc:
|
||||
err_msg = member['issues']['title']
|
||||
err_msg = member["issues"]["title"]
|
||||
if err_msg:
|
||||
if member['issues']['dialog']:
|
||||
err_msg += ': ' + member['issues']['dialog']
|
||||
if member["issues"]["dialog"]:
|
||||
err_msg += ": " + member["issues"]["dialog"]
|
||||
else:
|
||||
err_msg = 'Location information missing'
|
||||
err_msg = "Location information missing"
|
||||
self._err(dev_id, err_msg)
|
||||
return
|
||||
|
||||
|
@ -198,49 +221,53 @@ class Life360Scanner:
|
|||
if not last_seen or prev_seen and last_seen <= prev_seen:
|
||||
return
|
||||
|
||||
lat = loc.get('latitude')
|
||||
lon = loc.get('longitude')
|
||||
gps_accuracy = loc.get('accuracy')
|
||||
lat = loc.get("latitude")
|
||||
lon = loc.get("longitude")
|
||||
gps_accuracy = loc.get("accuracy")
|
||||
try:
|
||||
lat = float(lat)
|
||||
lon = float(lon)
|
||||
# Life360 reports accuracy in feet, but Device Tracker expects
|
||||
# gps_accuracy in meters.
|
||||
gps_accuracy = round(
|
||||
convert(float(gps_accuracy), LENGTH_FEET, LENGTH_METERS))
|
||||
convert(float(gps_accuracy), LENGTH_FEET, LENGTH_METERS)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self._err(dev_id, 'GPS data invalid: {}, {}, {}'.format(
|
||||
lat, lon, gps_accuracy))
|
||||
self._err(
|
||||
dev_id, "GPS data invalid: {}, {}, {}".format(lat, lon, gps_accuracy)
|
||||
)
|
||||
return
|
||||
|
||||
self._ok(dev_id)
|
||||
|
||||
msg = 'Updating {}'.format(dev_id)
|
||||
msg = "Updating {}".format(dev_id)
|
||||
if prev_seen:
|
||||
msg += '; Time since last update: {}'.format(last_seen - prev_seen)
|
||||
msg += "; Time since last update: {}".format(last_seen - prev_seen)
|
||||
_LOGGER.debug(msg)
|
||||
|
||||
if (self._max_gps_accuracy is not None
|
||||
and gps_accuracy > self._max_gps_accuracy):
|
||||
if self._max_gps_accuracy is not None and gps_accuracy > self._max_gps_accuracy:
|
||||
_LOGGER.warning(
|
||||
'%s: Ignoring update because expected GPS '
|
||||
'accuracy (%.0f) is not met: %.0f',
|
||||
dev_id, self._max_gps_accuracy, gps_accuracy)
|
||||
"%s: Ignoring update because expected GPS "
|
||||
"accuracy (%.0f) is not met: %.0f",
|
||||
dev_id,
|
||||
self._max_gps_accuracy,
|
||||
gps_accuracy,
|
||||
)
|
||||
return
|
||||
|
||||
# Get raw attribute data, converting empty strings to None.
|
||||
place = loc.get('name') or None
|
||||
address1 = loc.get('address1') or None
|
||||
address2 = loc.get('address2') or None
|
||||
place = loc.get("name") or None
|
||||
address1 = loc.get("address1") or None
|
||||
address2 = loc.get("address2") or None
|
||||
if address1 and address2:
|
||||
address = ', '.join([address1, address2])
|
||||
address = ", ".join([address1, address2])
|
||||
else:
|
||||
address = address1 or address2
|
||||
raw_speed = loc.get('speed') or None
|
||||
driving = _bool_attr_from_int(loc.get('isDriving'))
|
||||
moving = _bool_attr_from_int(loc.get('inTransit'))
|
||||
raw_speed = loc.get("speed") or None
|
||||
driving = _bool_attr_from_int(loc.get("isDriving"))
|
||||
moving = _bool_attr_from_int(loc.get("inTransit"))
|
||||
try:
|
||||
battery = int(float(loc.get('battery')))
|
||||
battery = int(float(loc.get("battery")))
|
||||
except (TypeError, ValueError):
|
||||
battery = None
|
||||
|
||||
|
@ -255,51 +282,59 @@ class Life360Scanner:
|
|||
|
||||
# Make driving attribute True if it isn't and we can derive that it
|
||||
# should be True from other data.
|
||||
if (driving in (STATE_UNKNOWN, False)
|
||||
and self._driving_speed is not None
|
||||
and speed != STATE_UNKNOWN):
|
||||
if (
|
||||
driving in (STATE_UNKNOWN, False)
|
||||
and self._driving_speed is not None
|
||||
and speed != STATE_UNKNOWN
|
||||
):
|
||||
driving = speed >= self._driving_speed
|
||||
|
||||
attrs = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_AT_LOC_SINCE: _dt_attr_from_ts(loc.get('since')),
|
||||
ATTR_BATTERY_CHARGING: _bool_attr_from_int(loc.get('charge')),
|
||||
ATTR_AT_LOC_SINCE: _dt_attr_from_ts(loc.get("since")),
|
||||
ATTR_BATTERY_CHARGING: _bool_attr_from_int(loc.get("charge")),
|
||||
ATTR_DRIVING: driving,
|
||||
ATTR_LAST_SEEN: last_seen,
|
||||
ATTR_MOVING: moving,
|
||||
ATTR_PLACE: place,
|
||||
ATTR_RAW_SPEED: raw_speed,
|
||||
ATTR_SPEED: speed,
|
||||
ATTR_WIFI_ON: _bool_attr_from_int(loc.get('wifiState')),
|
||||
ATTR_WIFI_ON: _bool_attr_from_int(loc.get("wifiState")),
|
||||
}
|
||||
|
||||
# If user wants driving or moving to be shown as state, and current
|
||||
# location is not in a HA zone, then set location name accordingly.
|
||||
loc_name = None
|
||||
active_zone = run_callback_threadsafe(
|
||||
self._hass.loop, async_active_zone, self._hass, lat, lon,
|
||||
gps_accuracy).result()
|
||||
self._hass.loop, async_active_zone, self._hass, lat, lon, gps_accuracy
|
||||
).result()
|
||||
if not active_zone:
|
||||
if SHOW_DRIVING in self._show_as_state and driving is True:
|
||||
loc_name = SHOW_DRIVING
|
||||
elif SHOW_MOVING in self._show_as_state and moving is True:
|
||||
loc_name = SHOW_MOVING
|
||||
|
||||
self._see(dev_id=dev_id, location_name=loc_name, gps=(lat, lon),
|
||||
gps_accuracy=gps_accuracy, battery=battery, attributes=attrs,
|
||||
picture=member.get('avatar'))
|
||||
self._see(
|
||||
dev_id=dev_id,
|
||||
location_name=loc_name,
|
||||
gps=(lat, lon),
|
||||
gps_accuracy=gps_accuracy,
|
||||
battery=battery,
|
||||
attributes=attrs,
|
||||
picture=member.get("avatar"),
|
||||
)
|
||||
|
||||
def _update_members(self, members, members_updated):
|
||||
for member in members:
|
||||
member_id = member['id']
|
||||
member_id = member["id"]
|
||||
if member_id in members_updated:
|
||||
continue
|
||||
err_key = 'Member data'
|
||||
err_key = "Member data"
|
||||
try:
|
||||
first = member.get('firstName')
|
||||
last = member.get('lastName')
|
||||
first = member.get("firstName")
|
||||
last = member.get("lastName")
|
||||
if first and last:
|
||||
full_name = ' '.join([first, last])
|
||||
full_name = " ".join([first, last])
|
||||
else:
|
||||
full_name = first or last
|
||||
slug_name = cv.slugify(full_name)
|
||||
|
@ -308,9 +343,13 @@ class Life360Scanner:
|
|||
if member_id not in self._members_logged:
|
||||
self._members_logged.add(member_id)
|
||||
_LOGGER.debug(
|
||||
'%s -> %s: will%s be tracked, id=%s', full_name,
|
||||
dev_id, '' if include_member else ' NOT', member_id)
|
||||
sharing = bool(int(member['features']['shareLocation']))
|
||||
"%s -> %s: will%s be tracked, id=%s",
|
||||
full_name,
|
||||
dev_id,
|
||||
"" if include_member else " NOT",
|
||||
member_id,
|
||||
)
|
||||
sharing = bool(int(member["features"]["shareLocation"]))
|
||||
except (KeyError, TypeError, ValueError, vol.Invalid):
|
||||
self._err(err_key, member)
|
||||
continue
|
||||
|
@ -325,7 +364,7 @@ class Life360Scanner:
|
|||
members_updated = []
|
||||
|
||||
for api in self._apis.values():
|
||||
err_key = 'get_circles'
|
||||
err_key = "get_circles"
|
||||
try:
|
||||
circles = api.get_circles()
|
||||
except Life360Error as exc:
|
||||
|
@ -334,30 +373,30 @@ class Life360Scanner:
|
|||
self._ok(err_key)
|
||||
|
||||
for circle in circles:
|
||||
circle_id = circle['id']
|
||||
circle_id = circle["id"]
|
||||
if circle_id in circles_updated:
|
||||
continue
|
||||
circles_updated.append(circle_id)
|
||||
circle_name = circle['name']
|
||||
circle_name = circle["name"]
|
||||
incl_circle = _include_name(self._circles_filter, circle_name)
|
||||
if circle_id not in self._circles_logged:
|
||||
self._circles_logged.add(circle_id)
|
||||
_LOGGER.debug(
|
||||
'%s Circle: will%s be included, id=%s', circle_name,
|
||||
'' if incl_circle else ' NOT', circle_id)
|
||||
"%s Circle: will%s be included, id=%s",
|
||||
circle_name,
|
||||
"" if incl_circle else " NOT",
|
||||
circle_id,
|
||||
)
|
||||
try:
|
||||
places = api.get_circle_places(circle_id)
|
||||
place_data = "Circle's Places:"
|
||||
for place in places:
|
||||
place_data += '\n- name: {}'.format(place['name'])
|
||||
place_data += '\n latitude: {}'.format(
|
||||
place['latitude'])
|
||||
place_data += '\n longitude: {}'.format(
|
||||
place['longitude'])
|
||||
place_data += '\n radius: {}'.format(
|
||||
place['radius'])
|
||||
place_data += "\n- name: {}".format(place["name"])
|
||||
place_data += "\n latitude: {}".format(place["latitude"])
|
||||
place_data += "\n longitude: {}".format(place["longitude"])
|
||||
place_data += "\n radius: {}".format(place["radius"])
|
||||
if not places:
|
||||
place_data += ' None'
|
||||
place_data += " None"
|
||||
_LOGGER.debug(place_data)
|
||||
except (Life360Error, KeyError):
|
||||
pass
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue