diff --git a/custom_components/google_fit/sensor.py b/custom_components/google_fit/sensor.py index 9a97fe3..9764425 100644 --- a/custom_components/google_fit/sensor.py +++ b/custom_components/google_fit/sensor.py @@ -27,8 +27,8 @@ CONF_CLIENT_SECRET = 'client_secret' DEFAULT_NAME = 'Google Fit' ICON = 'mdi:heart-pulse' -MIN_TIME_BETWEEN_SCANS = timedelta(minutes=10) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_TIME_BETWEEN_SCANS = timedelta(minutes=8) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=8) SENSOR_NAME = '{} {}' @@ -51,13 +51,16 @@ # Google Fit API URL. API_VERSION = 'v1' API_USER_ID = 'me' -WEIGHT = 'weight' +WEIGHT_LBS = 'weight (lbs)' +WEIGHT_KG = 'weight (kg)' HEIGHT = 'height' -DISTANCE = 'distance' +DISTANCE_KM = 'distance (km)' +DISTANCE_MI = 'distance (mi)' STEPS = 'steps' MOVE_TIME = 'move time' CALORIES = 'calories' SLEEP = 'sleep' +MEDITATION = 'meditation' HEARTRATE = 'heart rate' TOKEN_FILE = '' @@ -69,13 +72,16 @@ 'https://www.googleapis.com/auth/fitness.activity.read', 'https://www.googleapis.com/auth/fitness.location.read'] + def _today_dataset_start(): today = datetime.today().date() return int(time.mktime(today.timetuple()) * 1000000000) + def _today_dataset_end(): now = datetime.today() - return int(time.mktime(now.timetuple()) * 1000000000) + return int(time.mktime(now.timetuple()) * 1000000000 + 1) + def _get_client(token_file): """Get the Google Fit service with the storage file token. @@ -99,6 +105,7 @@ def _get_client(token_file): 'fitness', API_VERSION, http=http, cache_discovery=False) return service + def setup(hass, config): """Set up the Google Fit platform.""" name = config.get(const.CONF_NAME) @@ -179,14 +186,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): token_file = hass.config.path(TOKEN_FILE) client = _get_client(token_file) - add_devices([GoogleFitWeightSensor(client, name), + add_devices([GoogleFitWeightLbsSensor(client,name), + GoogleFitWeightKGSensor(client, name), GoogleFitHeartRateSensor(client, name), GoogleFitHeightSensor(client, name), GoogleFitStepsSensor(client, name), GoogleFitSleepSensor(client, name), + GoogleFitMeditationSensor(client, name), GoogleFitMoveTimeSensor(client, name), GoogleFitCaloriesSensor(client, name), - GoogleFitDistanceSensor(client, name)], True) + GoogleFitDistanceKmSensor(client, name), + GoogleFitDistanceMiSensor(client, name)], True) class GoogleFitSensor(entity.Entity): @@ -300,7 +310,8 @@ def _get_dataset_from_last_update(self, source): get(userId=API_USER_ID, dataSourceId=source, datasetId=dataset). \ execute() -class GoogleFitWeightSensor(GoogleFitSensor): + +class GoogleFitWeightKGSensor(GoogleFitSensor): @property def unit_of_measurement(self): """Returns the unit of measurement.""" @@ -314,7 +325,7 @@ def icon(self): @property def _name_suffix(self): """Returns the name suffix of the sensor.""" - return WEIGHT + return WEIGHT_KG @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -361,6 +372,67 @@ def update(self): self._attributes = {} +class GoogleFitWeightLbsSensor(GoogleFitSensor): + @property + def unit_of_measurement(self): + """Returns the unit of measurement.""" + return const.MASS_POUNDS + + @property + def icon(self): + """Return the icon.""" + return 'mdi:weight-pound' + + @property + def _name_suffix(self): + """Returns the name suffix of the sensor.""" + return WEIGHT_LBS + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Extracts the relevant data points for from the Fitness API.""" + if not self._client: + return + + weight_datasources = self._get_datasources('com.google.weight') + + weight_datapoints = {} + for datasource in weight_datasources: + datasource_id = datasource.get('dataStreamId') + weight_request = self._client.users().dataSources().\ + dataPointChanges().list( + userId=API_USER_ID, + dataSourceId=datasource_id, + ) + weight_data = weight_request.execute() + weight_inserted_datapoints = weight_data.get('insertedDataPoint') + + for datapoint in weight_inserted_datapoints: + point_value = datapoint.get('value') + if not point_value: + continue + weight = point_value[0].get('fpVal') + if not weight: + continue + weight = round(weight * 2.20462262185, 2) # Convert to pounds while rounding + last_update_milis = int(datapoint.get('modifiedTimeMillis', 0)) + if not last_update_milis: + continue + weight_datapoints[last_update_milis] = weight + + if weight_datapoints: + time_updates = list(weight_datapoints.keys()) + time_updates.sort(reverse=True) + + last_time_update = time_updates[0] + last_weight = weight_datapoints[last_time_update] + + self._last_updated = round(last_time_update / 1000) + self._state = last_weight + _LOGGER.debug("Last weight %s", last_weight) + self._attributes = {} + + class GoogleFitHeightSensor(GoogleFitSensor): @property def unit_of_measurement(self): @@ -462,8 +534,6 @@ def update(self): self._attributes = {} - - class GoogleFitStepsSensor(GoogleFitSensor): DATA_SOURCE = "derived:com.google.step_count.delta:" \ "com.google.android.gms:estimated_steps" @@ -565,14 +635,14 @@ def update(self): self._attributes = {} -class GoogleFitDistanceSensor(GoogleFitSensor): +class GoogleFitDistanceKmSensor(GoogleFitSensor): DATA_SOURCE = "derived:com.google.distance.delta:" \ "com.google.android.gms:merge_distance_delta" @property def _name_suffix(self): """Returns the name suffix of the sensor.""" - return DISTANCE + return DISTANCE_KM @property def unit_of_measurement(self): @@ -597,8 +667,40 @@ def update(self): _LOGGER.debug("Distance %s", self._state) self._attributes = {} -class GoogleFitSleepSensor(GoogleFitSensor): - +class GoogleFitDistanceMiSensor(GoogleFitSensor): + DATA_SOURCE = "derived:com.google.distance.delta:" \ + "com.google.android.gms:merge_distance_delta" + + @property + def _name_suffix(self): + """Returns the name suffix of the sensor.""" + return DISTANCE_MI + + @property + def unit_of_measurement(self): + """Returns the unit of measurement.""" + return const.LENGTH_MILES + + @property + def icon(self): + """Return the icon.""" + return 'mdi:map-marker-distance' + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Extracts the relevant data points for from the Fitness API.""" + values = [] + for point in self._get_dataset(self.DATA_SOURCE)["point"]: + if int(point["startTimeNanos"]) > _today_dataset_start(): + values.append(point['value'][0]['fpVal']) + + self._last_updated = time.time() + self._state = round(sum(values) / 1000 * 0.62137119, 2) + _LOGGER.debug("Distance %s", self._state) + self._attributes = {} + + +class GoogleFitSleepSensor(GoogleFitSensor): @property def _name_suffix(self): """Returns the name suffix of the sensor.""" @@ -657,4 +759,57 @@ def update(self): else: self._state = "" self._attributes = {} - self._last_updated = time.time() \ No newline at end of file + self._last_updated = time.time() + + +class GoogleFitMeditationSensor(GoogleFitSensor): + @property + def _name_suffix(self): + """Returns the name suffix of the sensor.""" + return MEDITATION + + @property + def unit_of_measurement(self): + """Returns the unit of measurement.""" + return 'min' + + @property + def icon(self): + """Return the icon.""" + return 'mdi:meditation' + + @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) + meditation_dataset = self._client.users().sessions().list(userId='me',fields='session',startTime=starttime,endTime=endtime).execute() + starts = [] + ends = [] + meditation = [] + _LOGGER.debug("Meditation dataset %s", meditation_dataset) + for point in meditation_dataset["session"]: + if int(point["activityType"]) == 45 : # https://developers.google.com/fit/rest/v1/reference/activity-types + starts.append(int(point["startTimeMillis"])) + ends.append(int(point["endTimeMillis"])) + meditation_start = datetime.fromtimestamp(int(point["startTimeMillis"]) / 1000) + meditation_end = datetime.fromtimestamp(int(point["endTimeMillis"]) / 1000) + _LOGGER.debug("Meditation dataset Total %s", (meditation_end - meditation_start)) + meditation.append(meditation_end - meditation_start) + + if len(starts) != 0 or len(ends) != 0: + start_time = datetime.fromtimestamp(round(min(starts) / 1000)) + end_time = datetime.fromtimestamp(round(max(ends) / 1000)) + total_meditation = sum(meditation,timedelta()) + state_dict = dict({'first_start_time': str(start_time), 'last_end_time': str(end_time), 'total_meditation': str(total_meditation)}) + self._state = total_meditation.total_seconds() // 60 + self._attributes = state_dict + self._last_updated = time.time() + else: + self._state = "" + self._attributes = {} + self._last_updated = time.time()