Use voluptuous for FitBit (#3686)

* Migrate to voluptuous

* Fix default
This commit is contained in:
Fabian Affolter 2016-10-11 09:27:15 +02:00 committed by Paulus Schoutsen
parent 7cf9ff83bc
commit 7cf2c48175

View File

@ -10,110 +10,125 @@ import logging
import datetime import datetime
import time 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.helpers.entity import Entity
from homeassistant.loader import get_component 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']
REQUIREMENTS = ["fitbit==0.2.3"]
DEPENDENCIES = ["http"]
ICON = "mdi:walk"
_CONFIGURING = {} _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) MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=30)
FITBIT_AUTH_START = "/auth/fitbit"
FITBIT_AUTH_CALLBACK_PATH = "/auth/fitbit/callback"
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"client_id": "CLIENT_ID_HERE", 'client_id': 'CLIENT_ID_HERE',
"client_secret": "CLIENT_SECRET_HERE" 'client_secret': 'CLIENT_SECRET_HERE'
} }
FITBIT_CONFIG_FILE = "fitbit.conf"
FITBIT_RESOURCES_LIST = { FITBIT_RESOURCES_LIST = {
"activities/activityCalories": "cal", 'activities/activityCalories': 'cal',
"activities/calories": "cal", 'activities/calories': 'cal',
"activities/caloriesBMR": "cal", 'activities/caloriesBMR': 'cal',
"activities/distance": "", 'activities/distance': '',
"activities/elevation": "", 'activities/elevation': '',
"activities/floors": "floors", 'activities/floors': 'floors',
"activities/heart": "bpm", 'activities/heart': 'bpm',
"activities/minutesFairlyActive": "minutes", 'activities/minutesFairlyActive': 'minutes',
"activities/minutesLightlyActive": "minutes", 'activities/minutesLightlyActive': 'minutes',
"activities/minutesSedentary": "minutes", 'activities/minutesSedentary': 'minutes',
"activities/minutesVeryActive": "minutes", 'activities/minutesVeryActive': 'minutes',
"activities/steps": "steps", 'activities/steps': 'steps',
"activities/tracker/activityCalories": "cal", 'activities/tracker/activityCalories': 'cal',
"activities/tracker/calories": "cal", 'activities/tracker/calories': 'cal',
"activities/tracker/distance": "", 'activities/tracker/distance': '',
"activities/tracker/elevation": "", 'activities/tracker/elevation': '',
"activities/tracker/floors": "floors", 'activities/tracker/floors': 'floors',
"activities/tracker/minutesFairlyActive": "minutes", 'activities/tracker/minutesFairlyActive': 'minutes',
"activities/tracker/minutesLightlyActive": "minutes", 'activities/tracker/minutesLightlyActive': 'minutes',
"activities/tracker/minutesSedentary": "minutes", 'activities/tracker/minutesSedentary': 'minutes',
"activities/tracker/minutesVeryActive": "minutes", 'activities/tracker/minutesVeryActive': 'minutes',
"activities/tracker/steps": "steps", 'activities/tracker/steps': 'steps',
"body/bmi": "BMI", 'body/bmi': 'BMI',
"body/fat": "%", 'body/fat': '%',
"sleep/awakeningsCount": "times awaken", 'sleep/awakeningsCount': 'times awaken',
"sleep/efficiency": "%", 'sleep/efficiency': '%',
"sleep/minutesAfterWakeup": "minutes", 'sleep/minutesAfterWakeup': 'minutes',
"sleep/minutesAsleep": "minutes", 'sleep/minutesAsleep': 'minutes',
"sleep/minutesAwake": "minutes", 'sleep/minutesAwake': 'minutes',
"sleep/minutesToFallAsleep": "minutes", 'sleep/minutesToFallAsleep': 'minutes',
"sleep/startTime": "start time", 'sleep/startTime': 'start time',
"sleep/timeInBed": "time in bed", 'sleep/timeInBed': 'time in bed',
"body/weight": "" 'body/weight': ''
} }
FITBIT_DEFAULT_RESOURCE_LIST = ["activities/steps"]
FITBIT_MEASUREMENTS = { FITBIT_MEASUREMENTS = {
"en_US": { 'en_US': {
"duration": "ms", 'duration': 'ms',
"distance": "mi", 'distance': 'mi',
"elevation": "ft", 'elevation': 'ft',
"height": "in", 'height': 'in',
"weight": "lbs", 'weight': 'lbs',
"body": "in", 'body': 'in',
"liquids": "fl. oz.", 'liquids': 'fl. oz.',
"blood glucose": "mg/dL", 'blood glucose': 'mg/dL',
}, },
"en_GB": { 'en_GB': {
"duration": "milliseconds", 'duration': 'milliseconds',
"distance": "kilometers", 'distance': 'kilometers',
"elevation": "meters", 'elevation': 'meters',
"height": "centimeters", 'height': 'centimeters',
"weight": "stone", 'weight': 'stone',
"body": "centimeters", 'body': 'centimeters',
"liquids": "milliliters", 'liquids': 'milliliters',
"blood glucose": "mmol/L" 'blood glucose': 'mmol/L'
}, },
"metric": { 'metric': {
"duration": "milliseconds", 'duration': 'milliseconds',
"distance": "kilometers", 'distance': 'kilometers',
"elevation": "meters", 'elevation': 'meters',
"height": "centimeters", 'height': 'centimeters',
"weight": "kilograms", 'weight': 'kilograms',
"body": "centimeters", 'body': 'centimeters',
"liquids": "milliliters", 'liquids': 'milliliters',
"blood glucose": "mmol/L" '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): def config_from_file(filename, config=None):
"""Small configuration file management function.""" """Small configuration file management function."""
if config: if config:
# We"re writing configuration # We"re writing configuration
try: try:
with open(filename, "w") as fdesc: with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config)) fdesc.write(json.dumps(config))
except IOError as error: except IOError as error:
_LOGGER.error("Saving config file failed: %s", error) _LOGGER.error("Saving config file failed: %s", error)
@ -123,7 +138,7 @@ def config_from_file(filename, config=None):
# We"re reading config # We"re reading config
if os.path.isfile(filename): if os.path.isfile(filename):
try: try:
with open(filename, "r") as fdesc: with open(filename, 'r') as fdesc:
return json.loads(fdesc.read()) return json.loads(fdesc.read())
except IOError as error: except IOError as error:
_LOGGER.error("Reading config file failed: %s", 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, def request_app_setup(hass, config, add_devices, config_path,
discovery_info=None): discovery_info=None):
"""Assist user with configuring the Fitbit dev application.""" """Assist user with configuring the Fitbit dev application."""
configurator = get_component("configurator") configurator = get_component('configurator')
# pylint: disable=unused-argument # pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data): 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: if config_file == DEFAULT_CONFIG:
error_msg = ("You didn't correctly modify fitbit.conf", error_msg = ("You didn't correctly modify fitbit.conf",
" please try again") " please try again")
configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) configurator.notify_errors(_CONFIGURING['fitbit'], error_msg)
else: else:
setup_platform(hass, config, add_devices, discovery_info) setup_platform(hass, config, add_devices, discovery_info)
else: 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." submit = "I have saved my Client ID and Client Secret into fitbit.conf."
_CONFIGURING["fitbit"] = configurator.request_config( _CONFIGURING['fitbit'] = configurator.request_config(
hass, "Fitbit", fitbit_configuration_callback, hass, 'Fitbit', fitbit_configuration_callback,
description=description, submit_caption=submit, description=description, submit_caption=submit,
description_image="/static/images/config_fitbit_app.png" 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): def request_oauth_completion(hass):
"""Request user complete Fitbit OAuth2 flow.""" """Request user complete Fitbit OAuth2 flow."""
configurator = get_component("configurator") configurator = get_component('configurator')
if "fitbit" in _CONFIGURING: if "fitbit" in _CONFIGURING:
configurator.notify_errors( configurator.notify_errors(
_CONFIGURING["fitbit"], "Failed to register, please try again.") _CONFIGURING['fitbit'], "Failed to register, please try again.")
return return
@ -187,12 +202,12 @@ def request_oauth_completion(hass):
def fitbit_configuration_callback(callback_data): def fitbit_configuration_callback(callback_data):
"""The actions to do when our configuration callback is called.""" """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) description = "Please authorize Fitbit by visiting {}".format(start_url)
_CONFIGURING["fitbit"] = configurator.request_config( _CONFIGURING['fitbit'] = configurator.request_config(
hass, "Fitbit", fitbit_configuration_callback, hass, 'Fitbit', fitbit_configuration_callback,
description=description, description=description,
submit_caption="I have authorized Fitbit." 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): if os.path.isfile(config_path):
config_file = config_from_file(config_path) config_file = config_from_file(config_path)
if config_file == DEFAULT_CONFIG: if config_file == DEFAULT_CONFIG:
request_app_setup(hass, config, add_devices, config_path, request_app_setup(
discovery_info=None) hass, config, add_devices, config_path, discovery_info=None)
return False return False
else: else:
config_file = config_from_file(config_path, DEFAULT_CONFIG) config_file = config_from_file(config_path, DEFAULT_CONFIG)
request_app_setup(hass, config, add_devices, config_path, request_app_setup(
discovery_info=None) hass, config, add_devices, config_path, discovery_info=None)
return False return False
if "fitbit" in _CONFIGURING: if "fitbit" in _CONFIGURING:
get_component("configurator").request_done(_CONFIGURING.pop("fitbit")) get_component('configurator').request_done(_CONFIGURING.pop("fitbit"))
import fitbit import fitbit
access_token = config_file.get("access_token") access_token = config_file.get(ATTR_ACCESS_TOKEN)
refresh_token = config_file.get("refresh_token") refresh_token = config_file.get(ATTR_REFRESH_TOKEN)
if None not in (access_token, refresh_token): if None not in (access_token, refresh_token):
authd_client = fitbit.Fitbit(config_file.get("client_id"), authd_client = fitbit.Fitbit(config_file.get(ATTR_CLIENT_ID),
config_file.get("client_secret"), config_file.get(ATTR_CLIENT_SECRET),
access_token=access_token, access_token=access_token,
refresh_token=refresh_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.client.refresh_token()
authd_client.system = authd_client.user_profile_get()["user"]["locale"] authd_client.system = authd_client.user_profile_get()["user"]["locale"]
if authd_client.system != 'en_GB': if authd_client.system != 'en_GB':
if hass.config.units.is_metric: if hass.config.units.is_metric:
authd_client.system = "metric" authd_client.system = 'metric'
else: else:
authd_client.system = "en_US" authd_client.system = 'en_US'
dev = [] dev = []
for resource in config.get("monitored_resources", for resource in config.get(CONF_MONITORED_RESOURCES):
FITBIT_DEFAULT_RESOURCE_LIST): dev.append(FitbitSensor(
dev.append(FitbitSensor(authd_client, config_path, resource, authd_client, config_path, resource,
hass.config.units.is_metric)) hass.config.units.is_metric))
add_devices(dev) add_devices(dev)
else: else:
oauth = fitbit.api.FitbitOauth2Client(config_file.get("client_id"), oauth = fitbit.api.FitbitOauth2Client(
config_file.get("client_secret")) 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_CALLBACK_PATH)
fitbit_auth_start_url, _ = oauth.authorize_token_url( fitbit_auth_start_url, _ = oauth.authorize_token_url(
redirect_uri=redirect_uri, redirect_uri=redirect_uri,
scope=["activity", "heartrate", "nutrition", "profile", scope=['activity', 'heartrate', 'nutrition', 'profile',
"settings", "sleep", "weight"]) 'settings', 'sleep', 'weight'])
hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
hass.wsgi.register_view(FitbitAuthCallbackView(hass, config, hass.wsgi.register_view(FitbitAuthCallbackView(
add_devices, oauth)) hass, config, add_devices, oauth))
request_oauth_completion(hass) request_oauth_completion(hass)
@ -288,12 +304,12 @@ class FitbitAuthCallbackView(HomeAssistantView):
response_message = """Fitbit has been successfully authorized! response_message = """Fitbit has been successfully authorized!
You can close this window now!""" You can close this window now!"""
if data.get("code") is not None: if data.get('code') is not None:
redirect_uri = "{}{}".format(self.hass.config.api.base_url, redirect_uri = '{}{}'.format(
FITBIT_AUTH_CALLBACK_PATH) self.hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH)
try: 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: except MissingTokenError as error:
_LOGGER.error("Missing token: %s", error) _LOGGER.error("Missing token: %s", error)
response_message = """Something went wrong when response_message = """Something went wrong when
@ -315,14 +331,14 @@ class FitbitAuthCallbackView(HomeAssistantView):
<body><h1>{}</h1></body></html>""".format(response_message) <body><h1>{}</h1></body></html>""".format(response_message)
config_contents = { config_contents = {
"access_token": self.oauth.token["access_token"], ATTR_ACCESS_TOKEN: self.oauth.token['access_token'],
"refresh_token": self.oauth.token["refresh_token"], ATTR_REFRESH_TOKEN: self.oauth.token['refresh_token'],
"client_id": self.oauth.client_id, ATTR_CLIENT_ID: self.oauth.client_id,
"client_secret": self.oauth.client_secret ATTR_CLIENT_SECRET: self.oauth.client_secret
} }
if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE), if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE),
config_contents): config_contents):
_LOGGER.error("failed to save config file") _LOGGER.error("Failed to save config file")
setup_platform(self.hass, self.config, self.add_devices) setup_platform(self.hass, self.config, self.add_devices)
@ -338,22 +354,22 @@ class FitbitSensor(Entity):
self.client = client self.client = client
self.config_path = config_path self.config_path = config_path
self.resource_type = resource_type self.resource_type = resource_type
pretty_resource = self.resource_type.replace("activities/", "") pretty_resource = self.resource_type.replace('activities/', '')
pretty_resource = pretty_resource.replace("/", " ") pretty_resource = pretty_resource.replace('/', ' ')
pretty_resource = pretty_resource.title() pretty_resource = pretty_resource.title()
if pretty_resource == "Body Bmi": if pretty_resource == 'Body Bmi':
pretty_resource = "BMI" pretty_resource = 'BMI'
self._name = pretty_resource self._name = pretty_resource
unit_type = FITBIT_RESOURCES_LIST[self.resource_type] unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
if unit_type == "": if unit_type == "":
split_resource = self.resource_type.split("/") split_resource = self.resource_type.split('/')
try: try:
measurement_system = FITBIT_MEASUREMENTS[self.client.system] measurement_system = FITBIT_MEASUREMENTS[self.client.system]
except KeyError: except KeyError:
if is_metric: if is_metric:
measurement_system = FITBIT_MEASUREMENTS["metric"] measurement_system = FITBIT_MEASUREMENTS['metric']
else: else:
measurement_system = FITBIT_MEASUREMENTS["en_US"] measurement_system = FITBIT_MEASUREMENTS['en_US']
unit_type = measurement_system[split_resource[-1]] unit_type = measurement_system[split_resource[-1]]
self._unit_of_measurement = unit_type self._unit_of_measurement = unit_type
self._state = 0 self._state = 0
@ -384,16 +400,16 @@ class FitbitSensor(Entity):
def update(self): def update(self):
"""Get the latest data from the Fitbit API and update the states.""" """Get the latest data from the Fitbit API and update the states."""
container = self.resource_type.replace("/", "-") container = self.resource_type.replace("/", "-")
response = self.client.time_series(self.resource_type, period="7d") response = self.client.time_series(self.resource_type, period='7d')
self._state = response[container][-1].get("value") self._state = response[container][-1].get('value')
if self.resource_type == "activities/heart": if self.resource_type == 'activities/heart':
self._state = response[container][-1].get("restingHeartRate") self._state = response[container][-1].get('restingHeartRate')
config_contents = { config_contents = {
"access_token": self.client.client.token["access_token"], ATTR_ACCESS_TOKEN: self.client.client.token['access_token'],
"refresh_token": self.client.client.token["refresh_token"], ATTR_REFRESH_TOKEN: self.client.client.token['refresh_token'],
"client_id": self.client.client.client_id, ATTR_CLIENT_ID: self.client.client.client_id,
"client_secret": self.client.client.client_secret, ATTR_CLIENT_SECRET: self.client.client.client_secret,
"last_saved_at": int(time.time()) ATTR_LAST_SAVED_AT: int(time.time())
} }
if not config_from_file(self.config_path, config_contents): if not config_from_file(self.config_path, config_contents):
_LOGGER.error("failed to save config file") _LOGGER.error("Failed to save config file")