2019-06-06 23:07:15 +00:00
|
|
|
"""Life360 integration."""
|
|
|
|
import logging
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant import config_entries
|
|
|
|
from homeassistant.components.device_tracker import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SCAN_INTERVAL,
|
|
|
|
DOMAIN as DEVICE_TRACKER,
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
from homeassistant.components.device_tracker.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
SCAN_INTERVAL as DEFAULT_SCAN_INTERVAL,
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_EXCLUDE,
|
|
|
|
CONF_INCLUDE,
|
|
|
|
CONF_PASSWORD,
|
|
|
|
CONF_PREFIX,
|
|
|
|
CONF_USERNAME,
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
from homeassistant.helpers import discovery
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
|
|
|
|
from .const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_AUTHORIZATION,
|
|
|
|
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,
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
from .helpers import get_api
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEFAULT_PREFIX = DOMAIN
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ACCOUNTS = "accounts"
|
2019-06-06 23:07:15 +00:00
|
|
|
|
2019-06-24 15:05:34 +00:00
|
|
|
SHOW_AS_STATE_OPTS = [SHOW_DRIVING, SHOW_MOVING]
|
|
|
|
|
2019-06-06 23:07:15 +00:00
|
|
|
|
|
|
|
def _excl_incl_list_to_filter_dict(value):
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"include": CONF_INCLUDE in value,
|
|
|
|
"list": value.get(CONF_EXCLUDE) or value.get(CONF_INCLUDE),
|
2019-06-06 23:07:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _prefix(value):
|
|
|
|
if not value:
|
2019-07-31 19:25:30 +00:00
|
|
|
return ""
|
|
|
|
if not value.endswith("_"):
|
2020-04-04 23:32:58 +00:00
|
|
|
return f"{value}_"
|
2019-06-06 23:07:15 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def _thresholds(config):
|
|
|
|
error_threshold = config.get(CONF_ERROR_THRESHOLD)
|
|
|
|
warning_threshold = config.get(CONF_WARNING_THRESHOLD)
|
|
|
|
if error_threshold and warning_threshold:
|
|
|
|
if error_threshold <= warning_threshold:
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid(
|
2020-02-28 11:39:29 +00:00
|
|
|
f"{CONF_ERROR_THRESHOLD} must be larger than {CONF_WARNING_THRESHOLD}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
elif not error_threshold and warning_threshold:
|
|
|
|
config[CONF_ERROR_THRESHOLD] = warning_threshold + 1
|
|
|
|
elif error_threshold and not warning_threshold:
|
|
|
|
# Make them the same which effectively prevents warnings.
|
|
|
|
config[CONF_WARNING_THRESHOLD] = error_threshold
|
|
|
|
else:
|
|
|
|
# Log all errors as errors.
|
|
|
|
config[CONF_ERROR_THRESHOLD] = 1
|
|
|
|
config[CONF_WARNING_THRESHOLD] = 1
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ACCOUNT_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
|
|
|
|
_SLUG_LIST = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
cv.ensure_list, [cv.slugify], vol.Length(min=1, msg="List cannot be empty")
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
|
|
|
|
_LOWER_STRING_LIST = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
cv.ensure_list,
|
|
|
|
[vol.All(cv.string, vol.Lower)],
|
|
|
|
vol.Length(min=1, msg="List cannot be empty"),
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
|
|
|
|
_EXCL_INCL_SLUG_LIST = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Exclusive(CONF_EXCLUDE, "incl_excl"): _SLUG_LIST,
|
|
|
|
vol.Exclusive(CONF_INCLUDE, "incl_excl"): _SLUG_LIST,
|
|
|
|
}
|
|
|
|
),
|
2019-06-06 23:07:15 +00:00
|
|
|
cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE),
|
|
|
|
_excl_incl_list_to_filter_dict,
|
|
|
|
)
|
|
|
|
|
|
|
|
_EXCL_INCL_LOWER_STRING_LIST = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Exclusive(CONF_EXCLUDE, "incl_excl"): _LOWER_STRING_LIST,
|
|
|
|
vol.Exclusive(CONF_INCLUDE, "incl_excl"): _LOWER_STRING_LIST,
|
|
|
|
}
|
|
|
|
),
|
2019-06-06 23:07:15 +00:00
|
|
|
cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE),
|
2019-07-31 19:25:30 +00:00
|
|
|
_excl_incl_list_to_filter_dict,
|
2019-06-06 23:07:15 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
_THRESHOLD = vol.All(vol.Coerce(int), vol.Range(min=1))
|
|
|
|
|
|
|
|
LIFE360_SCHEMA = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_ACCOUNTS): vol.All(
|
|
|
|
cv.ensure_list, [ACCOUNT_SCHEMA], vol.Length(min=1)
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_CIRCLES): _EXCL_INCL_LOWER_STRING_LIST,
|
|
|
|
vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float),
|
|
|
|
vol.Optional(CONF_ERROR_THRESHOLD): _THRESHOLD,
|
|
|
|
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
|
|
|
vol.Optional(CONF_MAX_UPDATE_WAIT): vol.All(
|
|
|
|
cv.time_period, cv.positive_timedelta
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_MEMBERS): _EXCL_INCL_SLUG_LIST,
|
|
|
|
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): vol.All(
|
|
|
|
vol.Any(None, cv.string), _prefix
|
|
|
|
),
|
|
|
|
vol.Optional(
|
|
|
|
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
|
|
|
): cv.time_period,
|
|
|
|
vol.Optional(CONF_SHOW_AS_STATE, default=[]): vol.All(
|
|
|
|
cv.ensure_list, [vol.In(SHOW_AS_STATE_OPTS)]
|
|
|
|
),
|
|
|
|
vol.Optional(CONF_WARNING_THRESHOLD): _THRESHOLD,
|
|
|
|
}
|
|
|
|
),
|
|
|
|
_thresholds,
|
2019-06-06 23:07:15 +00:00
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: LIFE360_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
2019-06-06 23:07:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
def setup(hass, config):
|
|
|
|
"""Set up integration."""
|
|
|
|
conf = config.get(DOMAIN, LIFE360_SCHEMA({}))
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.data[DOMAIN] = {"config": conf, "apis": {}}
|
2019-06-06 23:07:15 +00:00
|
|
|
discovery.load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config)
|
|
|
|
|
2019-06-10 19:45:22 +00:00
|
|
|
if CONF_ACCOUNTS not in conf:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Check existing config entries. For any that correspond to an entry in
|
|
|
|
# configuration.yaml, and whose password has not changed, nothing needs to
|
|
|
|
# be done with that config entry or that account from configuration.yaml.
|
|
|
|
# But if the config entry was created by import and the account no longer
|
|
|
|
# exists in configuration.yaml, or if the password has changed, then delete
|
|
|
|
# that out-of-date config entry.
|
|
|
|
already_configured = []
|
|
|
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
|
|
|
# Find corresponding configuration.yaml entry and its password.
|
|
|
|
password = None
|
2019-06-06 23:07:15 +00:00
|
|
|
for account in conf[CONF_ACCOUNTS]:
|
2019-06-10 19:45:22 +00:00
|
|
|
if account[CONF_USERNAME] == entry.data[CONF_USERNAME]:
|
|
|
|
password = account[CONF_PASSWORD]
|
|
|
|
if password == entry.data[CONF_PASSWORD]:
|
|
|
|
already_configured.append(entry.data[CONF_USERNAME])
|
|
|
|
continue
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
not password
|
|
|
|
and entry.source == config_entries.SOURCE_IMPORT
|
|
|
|
or password
|
|
|
|
and password != entry.data[CONF_PASSWORD]
|
|
|
|
):
|
|
|
|
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
|
2019-06-10 19:45:22 +00:00
|
|
|
|
|
|
|
# Create config entries for accounts listed in configuration.
|
|
|
|
for account in conf[CONF_ACCOUNTS]:
|
|
|
|
if account[CONF_USERNAME] not in already_configured:
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
|
|
data=account,
|
|
|
|
)
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, entry):
|
|
|
|
"""Set up config entry."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.data[DOMAIN]["apis"][entry.data[CONF_USERNAME]] = get_api(
|
|
|
|
entry.data[CONF_AUTHORIZATION]
|
|
|
|
)
|
2019-06-06 23:07:15 +00:00
|
|
|
return True
|
2019-06-10 19:45:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
|
|
"""Unload config entry."""
|
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.data[DOMAIN]["apis"].pop(entry.data[CONF_USERNAME])
|
2019-06-10 19:45:22 +00:00
|
|
|
return True
|
|
|
|
except KeyError:
|
|
|
|
return False
|