Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
156ab7d
commit 1c13638
Showing
12 changed files
with
684 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"config": { | ||
"title": "Life360", | ||
"step": { | ||
"user": { | ||
"title": "Life360 Account Info", | ||
"data": { | ||
"username": "Username", | ||
"password": "Password" | ||
}, | ||
"description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts." | ||
} | ||
}, | ||
"error": { | ||
"invalid_username": "Invalid username", | ||
"invalid_credentials": "Invalid credentials", | ||
"user_already_configured": "Account has already been configured" | ||
}, | ||
"create_entry": { | ||
"default": "To set advanced options, see [Life360 documentation]({docs_url})." | ||
}, | ||
"abort": { | ||
"invalid_credentials": "Invalid credentials", | ||
"user_already_configured": "Account has already been configured" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"""Life360 integration.""" | ||
import logging | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.device_tracker import ( | ||
CONF_SCAN_INTERVAL, DOMAIN as DEVICE_TRACKER) | ||
from homeassistant.components.device_tracker.const import ( | ||
SCAN_INTERVAL as DEFAULT_SCAN_INTERVAL) | ||
from homeassistant.const import ( | ||
CONF_EXCLUDE, CONF_INCLUDE, CONF_PASSWORD, CONF_PREFIX, CONF_USERNAME) | ||
from homeassistant.helpers import discovery | ||
import homeassistant.helpers.config_validation as cv | ||
|
||
from .const import ( | ||
CONF_AUTHORIZATION, CONF_CIRCLES, CONF_DRIVING_SPEED, CONF_ERROR_THRESHOLD, | ||
CONF_MAX_GPS_ACCURACY, CONF_MAX_UPDATE_WAIT, CONF_MEMBERS, | ||
CONF_WARNING_THRESHOLD, DOMAIN) | ||
from .helpers import get_api | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DEFAULT_PREFIX = DOMAIN | ||
|
||
CONF_ACCOUNTS = 'accounts' | ||
|
||
|
||
def _excl_incl_list_to_filter_dict(value): | ||
return { | ||
'include': CONF_INCLUDE in value, | ||
'list': value.get(CONF_EXCLUDE) or value.get(CONF_INCLUDE) | ||
} | ||
|
||
|
||
def _prefix(value): | ||
if not value: | ||
return '' | ||
if not value.endswith('_'): | ||
return value + '_' | ||
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: | ||
raise vol.Invalid('{} must be larger than {}'.format( | ||
CONF_ERROR_THRESHOLD, CONF_WARNING_THRESHOLD)) | ||
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 | ||
|
||
|
||
ACCOUNT_SCHEMA = vol.Schema({ | ||
vol.Required(CONF_USERNAME): cv.string, | ||
vol.Required(CONF_PASSWORD): cv.string, | ||
}) | ||
|
||
_SLUG_LIST = vol.All( | ||
cv.ensure_list, [cv.slugify], | ||
vol.Length(min=1, msg='List cannot be empty')) | ||
|
||
_LOWER_STRING_LIST = vol.All( | ||
cv.ensure_list, [vol.All(cv.string, vol.Lower)], | ||
vol.Length(min=1, msg='List cannot be empty')) | ||
|
||
_EXCL_INCL_SLUG_LIST = vol.All( | ||
vol.Schema({ | ||
vol.Exclusive(CONF_EXCLUDE, 'incl_excl'): _SLUG_LIST, | ||
vol.Exclusive(CONF_INCLUDE, 'incl_excl'): _SLUG_LIST, | ||
}), | ||
cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE), | ||
_excl_incl_list_to_filter_dict, | ||
) | ||
|
||
_EXCL_INCL_LOWER_STRING_LIST = vol.All( | ||
vol.Schema({ | ||
vol.Exclusive(CONF_EXCLUDE, 'incl_excl'): _LOWER_STRING_LIST, | ||
vol.Exclusive(CONF_INCLUDE, 'incl_excl'): _LOWER_STRING_LIST, | ||
}), | ||
cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE), | ||
_excl_incl_list_to_filter_dict | ||
) | ||
|
||
_THRESHOLD = vol.All(vol.Coerce(int), vol.Range(min=1)) | ||
|
||
LIFE360_SCHEMA = vol.All( | ||
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_WARNING_THRESHOLD): _THRESHOLD, | ||
}), | ||
_thresholds | ||
) | ||
|
||
CONFIG_SCHEMA = vol.Schema({ | ||
DOMAIN: LIFE360_SCHEMA | ||
}, extra=vol.ALLOW_EXTRA) | ||
|
||
|
||
def setup(hass, config): | ||
"""Set up integration.""" | ||
conf = config.get(DOMAIN, LIFE360_SCHEMA({})) | ||
hass.data[DOMAIN] = {'config': conf, 'apis': []} | ||
discovery.load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config) | ||
|
||
if CONF_ACCOUNTS in conf: | ||
for account in conf[CONF_ACCOUNTS]: | ||
hass.async_create_task(hass.config_entries.flow.async_init( | ||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, | ||
data=account)) | ||
return True | ||
|
||
|
||
async def async_setup_entry(hass, entry): | ||
"""Set up config entry.""" | ||
hass.data[DOMAIN]['apis'].append( | ||
get_api(entry.data[CONF_AUTHORIZATION])) | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
"""Config flow to configure Life360 integration.""" | ||
from collections import OrderedDict | ||
import logging | ||
|
||
from life360 import LoginError | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
|
||
from .const import CONF_AUTHORIZATION, DOMAIN | ||
from .helpers import get_api | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DOCS_URL = 'https://www.home-assistant.io/components/life360' | ||
|
||
|
||
@config_entries.HANDLERS.register(DOMAIN) | ||
class Life360ConfigFlow(config_entries.ConfigFlow): | ||
"""Life360 integration config flow.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL | ||
|
||
def __init__(self): | ||
"""Initialize.""" | ||
self._api = get_api() | ||
self._username = vol.UNDEFINED | ||
self._password = vol.UNDEFINED | ||
|
||
@property | ||
def configured_usernames(self): | ||
"""Return tuple of configured usernames.""" | ||
entries = self.hass.config_entries.async_entries(DOMAIN) | ||
if entries: | ||
return (entry.data[CONF_USERNAME] for entry in entries) | ||
return () | ||
|
||
async def async_step_user(self, user_input=None): | ||
"""Handle a user initiated config flow.""" | ||
errors = {} | ||
|
||
if user_input is not None: | ||
self._username = user_input[CONF_USERNAME] | ||
self._password = user_input[CONF_PASSWORD] | ||
try: | ||
# pylint: disable=no-value-for-parameter | ||
vol.Email()(self._username) | ||
authorization = self._api.get_authorization( | ||
self._username, self._password) | ||
except vol.Invalid: | ||
errors[CONF_USERNAME] = 'invalid_username' | ||
except LoginError: | ||
errors['base'] = 'invalid_credentials' | ||
else: | ||
if self._username in self.configured_usernames: | ||
errors['base'] = 'user_already_configured' | ||
else: | ||
return self.async_create_entry( | ||
title=self._username, | ||
data={ | ||
CONF_USERNAME: self._username, | ||
CONF_PASSWORD: self._password, | ||
CONF_AUTHORIZATION: authorization | ||
}, | ||
description_placeholders={'docs_url': DOCS_URL} | ||
) | ||
|
||
data_schema = OrderedDict() | ||
data_schema[vol.Required(CONF_USERNAME, default=self._username)] = str | ||
data_schema[vol.Required(CONF_PASSWORD, default=self._password)] = str | ||
|
||
return self.async_show_form( | ||
step_id='user', | ||
data_schema=vol.Schema(data_schema), | ||
errors=errors, | ||
description_placeholders={'docs_url': DOCS_URL} | ||
) | ||
|
||
async def async_step_import(self, user_input): | ||
"""Import a config flow from configuration.""" | ||
username = user_input[CONF_USERNAME] | ||
password = user_input[CONF_PASSWORD] | ||
if username in self.configured_usernames: | ||
_LOGGER.warning('%s already configured', username) | ||
return self.async_abort(reason='user_already_configured') | ||
try: | ||
authorization = self._api.get_authorization(username, password) | ||
except LoginError: | ||
_LOGGER.error('Invalid credentials for %s', username) | ||
return self.async_abort(reason='invalid_credentials') | ||
return self.async_create_entry( | ||
title='{} (from configuration)'.format(username), | ||
data={ | ||
CONF_USERNAME: username, | ||
CONF_PASSWORD: password, | ||
CONF_AUTHORIZATION: authorization | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
"""Constants for Life360 integration.""" | ||
DOMAIN = 'life360' | ||
|
||
CONF_AUTHORIZATION = 'authorization' | ||
CONF_CIRCLES = 'circles' | ||
CONF_DRIVING_SPEED = 'driving_speed' | ||
CONF_ERROR_THRESHOLD = 'error_threshold' | ||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' | ||
CONF_MAX_UPDATE_WAIT = 'max_update_wait' | ||
CONF_MEMBERS = 'members' | ||
CONF_WARNING_THRESHOLD = 'warning_threshold' |
Oops, something went wrong.