diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index b99a4f320c9..cf11c9bb393 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -10,110 +10,125 @@ import logging import datetime import time -from homeassistant.util import Throttle +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component -from homeassistant.components.http import HomeAssistantView +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["fitbit==0.2.3"] -DEPENDENCIES = ["http"] - -ICON = "mdi:walk" +REQUIREMENTS = ['fitbit==0.2.3'] _CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +ATTR_ACCESS_TOKEN = 'access_token' +ATTR_REFRESH_TOKEN = 'refresh_token' +ATTR_CLIENT_ID = 'client_id' +ATTR_CLIENT_SECRET = 'client_secret' +ATTR_LAST_SAVED_AT = 'last_saved_at' + +CONF_MONITORED_RESOURCES = 'monitored_resources' + +DEPENDENCIES = ['http'] + +FITBIT_AUTH_CALLBACK_PATH = '/auth/fitbit/callback' +FITBIT_AUTH_START = '/auth/fitbit' +FITBIT_CONFIG_FILE = 'fitbit.conf' +FITBIT_DEFAULT_RESOURCES = ['activities/steps'] + +ICON = 'mdi:walk' -# Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=30) -FITBIT_AUTH_START = "/auth/fitbit" -FITBIT_AUTH_CALLBACK_PATH = "/auth/fitbit/callback" - DEFAULT_CONFIG = { - "client_id": "CLIENT_ID_HERE", - "client_secret": "CLIENT_SECRET_HERE" + 'client_id': 'CLIENT_ID_HERE', + 'client_secret': 'CLIENT_SECRET_HERE' } -FITBIT_CONFIG_FILE = "fitbit.conf" - FITBIT_RESOURCES_LIST = { - "activities/activityCalories": "cal", - "activities/calories": "cal", - "activities/caloriesBMR": "cal", - "activities/distance": "", - "activities/elevation": "", - "activities/floors": "floors", - "activities/heart": "bpm", - "activities/minutesFairlyActive": "minutes", - "activities/minutesLightlyActive": "minutes", - "activities/minutesSedentary": "minutes", - "activities/minutesVeryActive": "minutes", - "activities/steps": "steps", - "activities/tracker/activityCalories": "cal", - "activities/tracker/calories": "cal", - "activities/tracker/distance": "", - "activities/tracker/elevation": "", - "activities/tracker/floors": "floors", - "activities/tracker/minutesFairlyActive": "minutes", - "activities/tracker/minutesLightlyActive": "minutes", - "activities/tracker/minutesSedentary": "minutes", - "activities/tracker/minutesVeryActive": "minutes", - "activities/tracker/steps": "steps", - "body/bmi": "BMI", - "body/fat": "%", - "sleep/awakeningsCount": "times awaken", - "sleep/efficiency": "%", - "sleep/minutesAfterWakeup": "minutes", - "sleep/minutesAsleep": "minutes", - "sleep/minutesAwake": "minutes", - "sleep/minutesToFallAsleep": "minutes", - "sleep/startTime": "start time", - "sleep/timeInBed": "time in bed", - "body/weight": "" + 'activities/activityCalories': 'cal', + 'activities/calories': 'cal', + 'activities/caloriesBMR': 'cal', + 'activities/distance': '', + 'activities/elevation': '', + 'activities/floors': 'floors', + 'activities/heart': 'bpm', + 'activities/minutesFairlyActive': 'minutes', + 'activities/minutesLightlyActive': 'minutes', + 'activities/minutesSedentary': 'minutes', + 'activities/minutesVeryActive': 'minutes', + 'activities/steps': 'steps', + 'activities/tracker/activityCalories': 'cal', + 'activities/tracker/calories': 'cal', + 'activities/tracker/distance': '', + 'activities/tracker/elevation': '', + 'activities/tracker/floors': 'floors', + 'activities/tracker/minutesFairlyActive': 'minutes', + 'activities/tracker/minutesLightlyActive': 'minutes', + 'activities/tracker/minutesSedentary': 'minutes', + 'activities/tracker/minutesVeryActive': 'minutes', + 'activities/tracker/steps': 'steps', + 'body/bmi': 'BMI', + 'body/fat': '%', + 'sleep/awakeningsCount': 'times awaken', + 'sleep/efficiency': '%', + 'sleep/minutesAfterWakeup': 'minutes', + 'sleep/minutesAsleep': 'minutes', + 'sleep/minutesAwake': 'minutes', + 'sleep/minutesToFallAsleep': 'minutes', + 'sleep/startTime': 'start time', + 'sleep/timeInBed': 'time in bed', + 'body/weight': '' } -FITBIT_DEFAULT_RESOURCE_LIST = ["activities/steps"] - FITBIT_MEASUREMENTS = { - "en_US": { - "duration": "ms", - "distance": "mi", - "elevation": "ft", - "height": "in", - "weight": "lbs", - "body": "in", - "liquids": "fl. oz.", - "blood glucose": "mg/dL", + 'en_US': { + 'duration': 'ms', + 'distance': 'mi', + 'elevation': 'ft', + 'height': 'in', + 'weight': 'lbs', + 'body': 'in', + 'liquids': 'fl. oz.', + 'blood glucose': 'mg/dL', }, - "en_GB": { - "duration": "milliseconds", - "distance": "kilometers", - "elevation": "meters", - "height": "centimeters", - "weight": "stone", - "body": "centimeters", - "liquids": "milliliters", - "blood glucose": "mmol/L" + 'en_GB': { + 'duration': 'milliseconds', + 'distance': 'kilometers', + 'elevation': 'meters', + 'height': 'centimeters', + 'weight': 'stone', + 'body': 'centimeters', + 'liquids': 'milliliters', + 'blood glucose': 'mmol/L' }, - "metric": { - "duration": "milliseconds", - "distance": "kilometers", - "elevation": "meters", - "height": "centimeters", - "weight": "kilograms", - "body": "centimeters", - "liquids": "milliliters", - "blood glucose": "mmol/L" + 'metric': { + 'duration': 'milliseconds', + 'distance': 'kilometers', + 'elevation': 'meters', + 'height': 'centimeters', + 'weight': 'kilograms', + 'body': 'centimeters', + 'liquids': 'milliliters', + 'blood glucose': 'mmol/L' } } +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES): + vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]), +}) + def config_from_file(filename, config=None): """Small configuration file management function.""" if config: # We"re writing configuration try: - with open(filename, "w") as fdesc: + with open(filename, 'w') as fdesc: fdesc.write(json.dumps(config)) except IOError as error: _LOGGER.error("Saving config file failed: %s", error) @@ -123,7 +138,7 @@ def config_from_file(filename, config=None): # We"re reading config if os.path.isfile(filename): try: - with open(filename, "r") as fdesc: + with open(filename, 'r') as fdesc: return json.loads(fdesc.read()) except IOError as error: _LOGGER.error("Reading config file failed: %s", error) @@ -136,7 +151,7 @@ def config_from_file(filename, config=None): def request_app_setup(hass, config, add_devices, config_path, discovery_info=None): """Assist user with configuring the Fitbit dev application.""" - configurator = get_component("configurator") + configurator = get_component('configurator') # pylint: disable=unused-argument def fitbit_configuration_callback(callback_data): @@ -147,7 +162,7 @@ def request_app_setup(hass, config, add_devices, config_path, if config_file == DEFAULT_CONFIG: error_msg = ("You didn't correctly modify fitbit.conf", " please try again") - configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) + configurator.notify_errors(_CONFIGURING['fitbit'], error_msg) else: setup_platform(hass, config, add_devices, discovery_info) else: @@ -167,8 +182,8 @@ def request_app_setup(hass, config, add_devices, config_path, submit = "I have saved my Client ID and Client Secret into fitbit.conf." - _CONFIGURING["fitbit"] = configurator.request_config( - hass, "Fitbit", fitbit_configuration_callback, + _CONFIGURING['fitbit'] = configurator.request_config( + hass, 'Fitbit', fitbit_configuration_callback, description=description, submit_caption=submit, description_image="/static/images/config_fitbit_app.png" ) @@ -176,10 +191,10 @@ def request_app_setup(hass, config, add_devices, config_path, def request_oauth_completion(hass): """Request user complete Fitbit OAuth2 flow.""" - configurator = get_component("configurator") + configurator = get_component('configurator') if "fitbit" in _CONFIGURING: configurator.notify_errors( - _CONFIGURING["fitbit"], "Failed to register, please try again.") + _CONFIGURING['fitbit'], "Failed to register, please try again.") return @@ -187,12 +202,12 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """The actions to do when our configuration callback is called.""" - start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START) + start_url = '{}{}'.format(hass.config.api.base_url, FITBIT_AUTH_START) description = "Please authorize Fitbit by visiting {}".format(start_url) - _CONFIGURING["fitbit"] = configurator.request_config( - hass, "Fitbit", fitbit_configuration_callback, + _CONFIGURING['fitbit'] = configurator.request_config( + hass, 'Fitbit', fitbit_configuration_callback, description=description, submit_caption="I have authorized Fitbit." ) @@ -206,60 +221,61 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if os.path.isfile(config_path): config_file = config_from_file(config_path) if config_file == DEFAULT_CONFIG: - request_app_setup(hass, config, add_devices, config_path, - discovery_info=None) + request_app_setup( + hass, config, add_devices, config_path, discovery_info=None) return False else: config_file = config_from_file(config_path, DEFAULT_CONFIG) - request_app_setup(hass, config, add_devices, config_path, - discovery_info=None) + request_app_setup( + hass, config, add_devices, config_path, discovery_info=None) return False if "fitbit" in _CONFIGURING: - get_component("configurator").request_done(_CONFIGURING.pop("fitbit")) + get_component('configurator').request_done(_CONFIGURING.pop("fitbit")) import fitbit - access_token = config_file.get("access_token") - refresh_token = config_file.get("refresh_token") + access_token = config_file.get(ATTR_ACCESS_TOKEN) + refresh_token = config_file.get(ATTR_REFRESH_TOKEN) if None not in (access_token, refresh_token): - authd_client = fitbit.Fitbit(config_file.get("client_id"), - config_file.get("client_secret"), + authd_client = fitbit.Fitbit(config_file.get(ATTR_CLIENT_ID), + config_file.get(ATTR_CLIENT_SECRET), access_token=access_token, refresh_token=refresh_token) - if int(time.time()) - config_file.get("last_saved_at", 0) > 3600: + if int(time.time()) - config_file.get(ATTR_LAST_SAVED_AT, 0) > 3600: authd_client.client.refresh_token() authd_client.system = authd_client.user_profile_get()["user"]["locale"] if authd_client.system != 'en_GB': if hass.config.units.is_metric: - authd_client.system = "metric" + authd_client.system = 'metric' else: - authd_client.system = "en_US" + authd_client.system = 'en_US' dev = [] - for resource in config.get("monitored_resources", - FITBIT_DEFAULT_RESOURCE_LIST): - dev.append(FitbitSensor(authd_client, config_path, resource, - hass.config.units.is_metric)) + for resource in config.get(CONF_MONITORED_RESOURCES): + dev.append(FitbitSensor( + authd_client, config_path, resource, + hass.config.units.is_metric)) add_devices(dev) else: - oauth = fitbit.api.FitbitOauth2Client(config_file.get("client_id"), - config_file.get("client_secret")) + oauth = fitbit.api.FitbitOauth2Client( + config_file.get(ATTR_CLIENT_ID), + config_file.get(ATTR_CLIENT_SECRET)) - redirect_uri = "{}{}".format(hass.config.api.base_url, + redirect_uri = '{}{}'.format(hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, - scope=["activity", "heartrate", "nutrition", "profile", - "settings", "sleep", "weight"]) + scope=['activity', 'heartrate', 'nutrition', 'profile', + 'settings', 'sleep', 'weight']) hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) - hass.wsgi.register_view(FitbitAuthCallbackView(hass, config, - add_devices, oauth)) + hass.wsgi.register_view(FitbitAuthCallbackView( + hass, config, add_devices, oauth)) request_oauth_completion(hass) @@ -288,12 +304,12 @@ class FitbitAuthCallbackView(HomeAssistantView): response_message = """Fitbit has been successfully authorized! You can close this window now!""" - if data.get("code") is not None: - redirect_uri = "{}{}".format(self.hass.config.api.base_url, - FITBIT_AUTH_CALLBACK_PATH) + if data.get('code') is not None: + redirect_uri = '{}{}'.format( + self.hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) try: - self.oauth.fetch_access_token(data.get("code"), redirect_uri) + self.oauth.fetch_access_token(data.get('code'), redirect_uri) except MissingTokenError as error: _LOGGER.error("Missing token: %s", error) response_message = """Something went wrong when @@ -315,14 +331,14 @@ class FitbitAuthCallbackView(HomeAssistantView):