Skip to content
This repository has been archived by the owner on Oct 31, 2023. It is now read-only.

Fixes #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 89 additions & 72 deletions custom_components/google-fit/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@

# Sensor details.
SENSOR = 'google_fit'
SENSOR_NAME = '{} {}'

# Sensor base attributes.
ATTR_LAST_UPDATED = 'last_updated'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
DEFAULT_NAME = 'Google Fit'
DEFAULT_CREDENTIALS_FILE = '.google_fit.credentials.json'
ICON = 'mdi:heart-pulse'
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=10)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
TOKEN_FILE = '.{}.token'.format(SENSOR)
SENSOR_NAME = '{} {}'


# # Define schema of sensor.
# Define schema of sensor.
PLATFORM_SCHEMA = config_validation.PLATFORM_SCHEMA.extend({
voluptuous.Required(CONF_CLIENT_ID): config_validation.string,
voluptuous.Required(CONF_CLIENT_SECRET): config_validation.string,
Expand All @@ -57,16 +59,15 @@
MOVE_TIME = 'move time'
CALORIES = 'calories'
SLEEP = 'sleep'
TOKEN_FILE = ''
#name = config.get(const.CONF_NAME)
HEARTRATE = 'heart rate'

# Endpoint scopes required for the sensor.
# Read more: https://developers.google.com/fit/rest/v1/authorization

SCOPES = ['https://www.googleapis.com/auth/fitness.body.read',
'https://www.googleapis.com/auth/fitness.body.write',
'https://www.googleapis.com/auth/fitness.activity.read',
'https://www.googleapis.com/auth/fitness.location.read']
'https://www.googleapis.com/auth/fitness.body.write',
'https://www.googleapis.com/auth/fitness.activity.read',
'https://www.googleapis.com/auth/fitness.location.read']

def _today_dataset_start():
today = datetime.today().date()
Expand All @@ -78,12 +79,11 @@ def _today_dataset_end():

def _get_client(token_file):
"""Get the Google Fit service with the storage file token.

Args:
token_file: str, File path for API token.
token_file: str, File path for API token.

Return:
Google Fit API client.
Google Fit API client.
"""
import httplib2
from googleapiclient import discovery as google_discovery
Expand All @@ -100,19 +100,15 @@ def _get_client(token_file):

def setup(hass, config):
"""Set up the Google Fit platform."""
#token_file = hass.config.path(TOKEN_FILE)
name = config.get(const.CONF_NAME)
TOKEN_FILE = '.{}_{}.token'.format(name,SENSOR)
token_file = hass.config.path(TOKEN_FILE)
if not os.path.exists(token_file):
if not os.path.isfile(token_file):
return do_authentication(hass, config)

return True


def do_authentication(hass, config):
"""Notify user of actions and authenticate.

Notify user of user_code and verification_url then poll until we have an
access token.
"""
Expand Down Expand Up @@ -140,8 +136,8 @@ def do_authentication(hass, config):
'In order to authorize Home-Assistant to view your Google Fit data '
'you must visit: <a href="{}" target="_blank">{}</a> and enter '
'code: {}'.format(dev_flow.verification_url,
dev_flow.verification_url,
dev_flow.user_code),
dev_flow.verification_url,
dev_flow.user_code),
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
)

Expand All @@ -160,33 +156,31 @@ def step2_exchange(now):
except oauth2client.FlowExchangeError:
# not ready yet, call again
return
name = config.get(const.CONF_NAME)
TOKEN_FILE = '.{}_{}.token'.format(name,SENSOR)

storage = oauth2file.Storage(hass.config.path(TOKEN_FILE))
storage.put(credentials)
listener()

listener = track_time_change(hass, step2_exchange,
second=range(0, 60, dev_flow.interval))
second=range(0, 60, dev_flow.interval))
return True


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Adds sensor platform to the list of platforms."""
setup(hass, config)
name = config.get(const.CONF_NAME)
TOKEN_FILE = '.{}_{}.token'.format(name,SENSOR)

token_file = hass.config.path(TOKEN_FILE)
client = _get_client(token_file)

name = config.get(const.CONF_NAME)
add_devices([GoogleFitWeightSensor(client, name),
GoogleFitHeartRateSensor(client, name),
GoogleFitHeightSensor(client, name),
GoogleFitStepsSensor(client, name),
GoogleFitSleepSensor(client, name),
GoogleFitMoveTimeSensor(client, name),
GoogleFitCaloriesSensor(client, name),
GoogleFitDistanceSensor(client, name)], True)
GoogleFitHeartRateSensor(client, name),
GoogleFitHeightSensor(client, name),
GoogleFitStepsSensor(client, name),
GoogleFitSleepSensor(client, name),
GoogleFitMoveTimeSensor(client, name),
GoogleFitCaloriesSensor(client, name),
GoogleFitDistanceSensor(client, name)], True)


class GoogleFitSensor(entity.Entity):
Expand Down Expand Up @@ -219,8 +213,9 @@ def state(self):
@property
def last_updated(self):
"""Returns date when it was last updated."""
if isinstance(self._last_updated, int):
return utc_from_timestamp(self._last_updated)
if self._last_updated != 'unknown':
stamp = float(self._last_updated)
return utc_from_timestamp(int(stamp))

@property
def name(self):
Expand Down Expand Up @@ -269,10 +264,10 @@ def _get_datasources(self, data_type_name):
"""Gets data sources information for weight data.

Args:
data_type_name: str, Type of data sources to retrieve.
data_type_name: str, Type of data sources to retrieve.

Returns:
Dictionary containing all available data sources.
Dictionary containing all available data sources.
"""
datasources_request = self._client.users().dataSources().list(
userId=API_USER_ID,
Expand Down Expand Up @@ -346,7 +341,7 @@ def update(self):

self._last_updated = round(last_time_update / 1000)
self._state = last_weight
#print(self.name, last_weight)
_LOGGER.debug("Last weight %s", last_weight)
self._attributes = {}


Expand Down Expand Up @@ -404,8 +399,7 @@ def update(self):

self._last_updated = round(last_time_update / 1000)
self._state = last_height
#print(self.name, last_height)

_LOGGER.debug("Last height %s", last_height)
self._attributes = {}


Expand All @@ -426,33 +420,50 @@ def icon(self):
@property
def _name_suffix(self):
"""Returns the name suffix of the sensor."""
return 'HEARTRATE'
return HEARTRATE

@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Extracts the relevant data points for from the Fitness API."""
heartrate_datasources = self._get_datasources('com.google.heart_rate.bpm')

values = {}
for datapoint in self._get_dataset(self.DATA_SOURCE)["point"]:
datapoint_value = datapoint["value"][0]["fpVal"]
datapoint_value_ts= datapoint["startTimeNanos"]
values[datapoint_value_ts] = datapoint_value

time_updates = list(values.keys())
time_updates.sort(reverse=True)
if time_updates is None: return None
last_time_update = time_updates[0]
last_heartrate = values[last_time_update]

self._last_updated = round(int(last_time_update) / 1000000000)
self._state = last_heartrate
#print(self.name, sum(values))
heart_datapoints = {}
for datasource in heartrate_datasources:
datasource_id = datasource.get('dataStreamId')
heart_request = self._client.users().dataSources().\
dataPointChanges().list(
userId=API_USER_ID,
dataSourceId=datasource_id,
)
heart_data = heart_request.execute()
heart_inserted_datapoints = heart_data.get('insertedDataPoint')
for datapoint in heart_inserted_datapoints:
point_value = datapoint.get('value')
if not point_value:
continue
heartrate = point_value[0].get('fpVal')
if not heartrate:
continue
last_update_milis = int(datapoint.get('modifiedTimeMillis', 0))
if not last_update_milis:
continue
heart_datapoints[last_update_milis] = heartrate

if heart_datapoints:
time_updates = list(heart_datapoints.keys())
time_updates.sort(reverse=True)

last_time_update = time_updates[0]
last_heartrate = heart_datapoints[last_time_update]

self._last_updated = round(last_time_update / 1000)
self._state = last_heartrate
self._attributes = {}


class GoogleFitStepsSensor(GoogleFitSensor):
DATA_SOURCE = "derived:com.google.step_count.delta:" \
"com.google.android.gms:estimated_steps"
"com.google.android.gms:estimated_steps"

@property
def _name_suffix(self):
Expand Down Expand Up @@ -480,13 +491,13 @@ def update(self):

self._last_updated = time.time()
self._state = sum(values)
#print(self.name, sum(values))
_LOGGER.debug("Steps %s", self._state)
self._attributes = {}


class GoogleFitMoveTimeSensor(GoogleFitSensor):
DATA_SOURCE = "derived:com.google.active_minutes:" \
"com.google.android.gms:merge_active_minutes"
"com.google.android.gms:merge_active_minutes"

@property
def _name_suffix(self):
Expand Down Expand Up @@ -514,13 +525,13 @@ def update(self):

self._last_updated = time.time()
self._state = sum(values)
#print(self.name, sum(values))
_LOGGER.debug("Move time %s", self._state)
self._attributes = {}


class GoogleFitCaloriesSensor(GoogleFitSensor):
DATA_SOURCE = "derived:com.google.calories.expended:" \
"com.google.android.gms:merge_calories_expended"
"com.google.android.gms:merge_calories_expended"

@property
def _name_suffix(self):
Expand All @@ -547,13 +558,13 @@ def update(self):

self._last_updated = time.time()
self._state = round(sum(values))
#print(self.name, round(sum(values)))
_LOGGER.debug("Calories %s", self._state)
self._attributes = {}


class GoogleFitDistanceSensor(GoogleFitSensor):
DATA_SOURCE = "derived:com.google.distance.delta:" \
"com.google.android.gms:merge_distance_delta"
"com.google.android.gms:merge_distance_delta"

@property
def _name_suffix(self):
Expand All @@ -580,11 +591,13 @@ def update(self):

self._last_updated = time.time()
self._state = round(sum(values) / 1000, 2)
#print(self.name, round(sum(values) / 1000, 2))
_LOGGER.debug("Distance %s", self._state)
self._attributes = {}

class GoogleFitSleepSensor(GoogleFitSensor):

DATA_SOURCE = "derived:com.google.step_count.delta:" \
"com.google.android.gms:estimated_steps"

@property
def _name_suffix(self):
"""Returns the name suffix of the sensor."""
Expand All @@ -603,41 +616,45 @@ def icon(self):
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Extracts the relevant data points for from the Fitness API."""

yesterday = datetime.now().replace(hour=17,minute=0,second=0,microsecond=0)
yesterday = yesterday - timedelta(days=1)
starttime = yesterday.isoformat("T") + "Z"
today = datetime.now().replace(hour=11,minute=0,second=0,microsecond=0)
endtime = today.isoformat("T") + "Z"
_LOGGER.debug("Starttime %s, Endtime %s", starttime, endtime)
sleep_dataset = self._client.users().sessions().list(userId='me',fields='session',startTime=starttime,endTime=endtime).execute()
starts = []
ends = []
deep_sleep = []
light_sleep = []
_LOGGER.debug("Sleep dataset %s", sleep_dataset)
starttime
for point in sleep_dataset["session"]:
if int(point["activityType"]) == 72 :
starts.append(int(point["startTimeMillis"]))
ends.append(int(point["endTimeMillis"]))
if point["name"].startswith('Deep'):
deep_sleep_start = datetime.fromtimestamp(int(point["startTimeMillis"]) / 1000)
deep_sleep_end = datetime.fromtimestamp(int(point["endTimeMillis"]) / 1000)
deep_sleep.append(deep_sleep_end - deep_sleep_start)
deep_sleep_start = datetime.fromtimestamp(int(point["startTimeMillis"]) / 1000)
deep_sleep_end = datetime.fromtimestamp(int(point["endTimeMillis"]) / 1000)
_LOGGER.debug("Deep Sleep dataset Total %s", (deep_sleep_end - deep_sleep_start))
deep_sleep.append(deep_sleep_end - deep_sleep_start)
elif point["name"].startswith('Light'):
light_sleep_start = datetime.fromtimestamp(int(point["startTimeMillis"]) / 1000)
light_sleep_end = datetime.fromtimestamp(int(point["endTimeMillis"]) / 1000)
light_sleep.append(light_sleep_end - light_sleep_start)

light_sleep_start = datetime.fromtimestamp(int(point["startTimeMillis"]) / 1000)
light_sleep_end = datetime.fromtimestamp(int(point["endTimeMillis"]) / 1000)
_LOGGER.debug("Light Sleep dataset Total %s", (light_sleep_end - light_sleep_start))
light_sleep.append(light_sleep_end - light_sleep_start)
if len(starts) != 0 or len(ends) != 0:
bed_time = datetime.fromtimestamp(round(min(starts) / 1000))
wake_up_time = datetime.fromtimestamp(round(max(ends) / 1000))
total_sleep = wake_up_time - bed_time
total_deep_sleep = sum(deep_sleep,timedelta())
total_light_sleep = sum(light_sleep, timedelta())
state_dict = dict({'bed_time': str(bed_time), 'wake_up_time': str(wake_up_time), 'sleep': str(total_sleep), 'deep_sleep': str(total_deep_sleep), 'light_sleep': str(total_light_sleep)})
#print(state_dict)
self._state = str(total_sleep)
self._attributes = state_dict
self._last_updated = time.time()
else:
self._state = ""
self._attributes = {}
self._last_updated = time.time()
self._last_updated = time.time()
5 changes: 5 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Google Fit",
"render_readme": true,
"domains": ["sensor"]
}