Skip to content

Commit

Permalink
Merge pull request #187 from YorkshireIoT/36-reduce-polling-of-less-f…
Browse files Browse the repository at this point in the history
…requently-updated-sensors

Add new configuration value to reduce polling of infrequently updated sensors
  • Loading branch information
YorkshireIoT authored Feb 8, 2024
2 parents a32e476 + 117cae1 commit 189efa7
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 57 deletions.
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,30 @@ data within Home Assistant.

**This integration will set up the following platforms.**

Platform | Name |Description
-- | -- | --
`sensor` | `active_minutes_daily` | [Active Minutes][active-minutes]. Reset daily.
`sensor` | `calories_burnt_daily` | [Calories burnt][calories-burnt] (kcal). Reset daily.
`sensor` | `basal_metabolic_rate` | [Base Metabolic Rate][basal-metabolic-rate] (kcal). Calories per day based on weight and activity.
`sensor` | `distance_travelled_daily` | [Distance travelled][distance-travelled] (metres). Reset daily.
`sensor` | `heart_points_daily` | [Heart Points][heart-points] earned. Reset daily.
`sensor` | `height` | [Height][height] (metres).
`sensor` | `weight` | [Weight][weight] (kilograms).
`sensor` | `body_fat` | [Body Fat][fat] (percentage).
`sensor` | `body_temperature` | [Body Temperature][temperature] (celsius).
`sensor` | `steps` | [Number of steps][steps] taken. Reset daily.
`sensor` | `deep_sleep` | [Deep sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider.
`sensor` | `light_sleep` | [Light sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider.
`sensor` | `rem_sleep` | [REM sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider.
`sensor` | `awake_time` | [Awake][sleep] time during a sleep session over the past 24 hours. Not overall daily awake time. May not be available depending on sleep data provider.
`sensor` | `sleep` | [Overall sleep][sleep] time over the past 24 hours.
`sensor` | `blood_pressure_diastolic` | Most recent Diastolic [blood pressure][blood-pressure] reading.
`sensor` | `blood_pressure_systolic` | Most recent Systolic [blood pressure][blood-pressure] reading.
`sensor` | `heart_rate` | Most recent [heart rate][heart-rate] measurement.
`sensor` | `resting_heart_rate` | Most recent resting [heart rate][heart-rate] measurement.
`sensor` | `blood_glucose` | Latest [blood_glucose][blood-glucose] measurement (mmol/L).
`sensor` | `hydration` | Total [water][hydration] consumed. Reset daily.
`sensor` | `oxygen_saturation` | The most recent [blood oxygen][blood-oxygen] saturation measurement.
Platform | Name | Description | Infrequent Update |
-- | -- | -- | -- |
`sensor` | `active_minutes_daily` | [Active Minutes][active-minutes]. Reset daily. | ☐ |
`sensor` | `calories_burnt_daily` | [Calories burnt][calories-burnt] (kcal). Reset daily. | ☐ |
`sensor` | `basal_metabolic_rate` | [Base Metabolic Rate][basal-metabolic-rate] (kcal). Calories per day based on weight and activity. | ☐ |
`sensor` | `distance_travelled_daily` | [Distance travelled][distance-travelled] (metres). Reset daily. | ☐ |
`sensor` | `heart_points_daily` | [Heart Points][heart-points] earned. Reset daily. | ☐ |
`sensor` | `height` | [Height][height] (metres). | ☑ |
`sensor` | `weight` | [Weight][weight] (kilograms). | ☑ |
`sensor` | `body_fat` | [Body Fat][fat] (percentage). | ☑ |
`sensor` | `body_temperature` | [Body Temperature][temperature] (celsius). | ☑ |
`sensor` | `steps` | [Number of steps][steps] taken. Reset daily. | ☐ |
`sensor` | `deep_sleep` | [Deep sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider. | ☐ |
`sensor` | `light_sleep` | [Light sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider. | ☐ |
`sensor` | `rem_sleep` | [REM sleep][sleep] time over the past 24 hours. May not be available depending on sleep data provider. | ☐ |
`sensor` | `awake_time` | [Awake][sleep] time during a sleep session over the past 24 hours. Not overall daily awake time. May not be available depending on sleep data provider. | ☐ |
`sensor` | `sleep` | [Overall sleep][sleep] time over the past 24 hours. | ☐ |
`sensor` | `blood_pressure_diastolic` | Most recent Diastolic [blood pressure][blood-pressure] reading. | ☐ |
`sensor` | `blood_pressure_systolic` | Most recent Systolic [blood pressure][blood-pressure] reading. | ☐ |
`sensor` | `heart_rate` | Most recent [heart rate][heart-rate] measurement. | ☐ |
`sensor` | `resting_heart_rate` | Most recent resting [heart rate][heart-rate] measurement. | ☐ |
`sensor` | `blood_glucose` | Latest [blood_glucose][blood-glucose] measurement (mmol/L). | ☐ |
`sensor` | `hydration` | Total [water][hydration] consumed. Reset daily. | ☐ |
`sensor` | `oxygen_saturation` | The most recent [blood oxygen][blood-oxygen] saturation measurement. | ☐ |

> Please note, there is a delay (roughly 30-60 minutes) between sensor measurements being recorded on the Google Fit
> app and the data then being available to query of the rest API. As such, although this integration polls the API
Expand Down Expand Up @@ -114,6 +114,7 @@ The following options can be tweaked after setting up the integration:
Option | Description | Default
------------ | ------------- | -------------
Update interval | Minutes between REST API queries. Can be increased if you're exceeding API quota | 5 (minutes) |
Infrequent Sensor Multiplier | Multiply the update interval by this for less frequently updated sensors, e.g. height. This reduces unnecessary API queries. | 12 (so default 5 mins update interval changes to an hour) |

## Unknown Sensor Behaviour

Expand Down
1 change: 1 addition & 0 deletions custom_components/google_fit/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class GoogleFitSensorDescription(SensorEntityDescription):
data_key: str = "undefined"
source: str = "undefined"
is_int: bool = False # If true, data is an integer. Otherwise, data is a float
infrequent_update: bool = False


@dataclass
Expand Down
9 changes: 9 additions & 0 deletions custom_components/google_fit/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from .const import (
DEFAULT_ACCESS,
DOMAIN,
CONF_INFREQUENT_INTERVAL_MULTIPLIER,
DEFAULT_SCAN_INTERVAL,
DEFAULT_INFREQUENT_INTERVAL,
)


Expand Down Expand Up @@ -155,6 +157,13 @@ async def async_step_init(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): config_validation.positive_int,
vol.Required(
CONF_INFREQUENT_INTERVAL_MULTIPLIER,
default=self.config_entry.options.get(
CONF_INFREQUENT_INTERVAL_MULTIPLIER,
DEFAULT_INFREQUENT_INTERVAL,
),
): config_validation.positive_int,
}
),
)
10 changes: 9 additions & 1 deletion custom_components/google_fit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@
DOMAIN: Final = "google_fit"
MANUFACTURER: Final = "Google, Inc."

# Configuration schema
CONF_INFREQUENT_INTERVAL_MULTIPLIER: Final = "infrequent_interval"

# Default Configuration Values
DEFAULT_SCAN_INTERVAL = 5
DEFAULT_SCAN_INTERVAL: Final = 5
DEFAULT_INFREQUENT_INTERVAL: Final = 12

# Useful constants
NANOSECONDS_SECONDS_CONVERSION: Final = 1000000000
Expand Down Expand Up @@ -124,6 +128,7 @@
device_class=SensorDeviceClass.DISTANCE,
source="derived:com.google.height:com.google.android.gms:merge_height",
data_key="height",
infrequent_update=True,
),
LastPointSensorDescription(
key="google_fit",
Expand All @@ -134,6 +139,7 @@
device_class=SensorDeviceClass.WEIGHT,
source="derived:com.google.weight:com.google.android.gms:merge_weight",
data_key="weight",
infrequent_update=True,
),
LastPointSensorDescription(
key="google_fit",
Expand All @@ -144,6 +150,7 @@
device_class=None,
source="derived:com.google.body.fat.percentage:com.google.android.gms:merged",
data_key="bodyFat",
infrequent_update=True,
),
LastPointSensorDescription(
key="google_fit",
Expand All @@ -154,6 +161,7 @@
device_class=SensorDeviceClass.TEMPERATURE,
source="derived:com.google.body.temperature:com.google.android.gms:merged",
data_key="bodyTemperature",
infrequent_update=True,
),
SumPointsSensorDescription(
key="google_fit",
Expand Down
107 changes: 77 additions & 30 deletions custom_components/google_fit/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
FitnessObject,
FitnessDataPoint,
FitnessSessionResponse,
GoogleFitSensorDescription,
SumPointsSensorDescription,
LastPointSensorDescription,
SumSessionSensorDescription,
)
from .const import (
CONF_INFREQUENT_INTERVAL_MULTIPLIER,
DEFAULT_INFREQUENT_INTERVAL,
DOMAIN,
LOGGER,
ENTITY_DESCRIPTIONS,
Expand All @@ -40,6 +43,8 @@ class Coordinator(DataUpdateCoordinator):
_auth: AsyncConfigEntryAuth
_config: ConfigEntry
fitness_data: FitnessData | None = None
sensor_update_counter: int
_infrequent_interval_multiplier: int

def __init__(
self,
Expand All @@ -50,10 +55,16 @@ def __init__(
"""Initialise."""
self._auth = auth
self._config = config
self.sensor_update_counter = 0
self._infrequent_interval_multiplier = config.options.get(
CONF_INFREQUENT_INTERVAL_MULTIPLIER, DEFAULT_INFREQUENT_INTERVAL
)
update_time = config.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
LOGGER.debug(
"Setting up Google Fit Coordinator. Updating every %u minutes",
"Setting up Google Fit Coordinator. Querying every %u minutes"
+ " (every %u minutes for less frequently used sensors).",
update_time,
(self._infrequent_interval_multiplier * update_time),
)
super().__init__(
hass=hass,
Expand All @@ -74,6 +85,11 @@ def current_data(self) -> FitnessData | None:
"""Return the current data, or None is data is not available."""
return self.fitness_data

@property
def infrequent_interval_multiplier(self) -> int:
"""Return the config option on what factor the interval should be for infrequent sensors."""
return self._infrequent_interval_multiplier

def _get_interval(self, interval_period: int = 0) -> str:
"""Return the necessary interval for API queries, with start and end time in nanoseconds.
Expand Down Expand Up @@ -109,6 +125,34 @@ async def _async_update_data(self) -> FitnessData | None:
async with async_timeout.timeout(30):
service = await self._auth.get_resource(self.hass)
parser = GoogleFitParse()
# Tracks whether we have retrieved sleep data for this update call
fetched_sleep = False

def _do_update(entity: GoogleFitSensorDescription) -> bool:
# Default is to update
do_update = True

if entity.infrequent_update:
if self.sensor_update_counter == 0:
LOGGER.debug(
"Querying infrequently updated sensor '%s'", entity.name
)
else:
LOGGER.debug(
"Skipping API query for infrequently updated sensor '%s'",
entity.name,
)
do_update = False

if (
isinstance(entity, SumPointsSensorDescription)
and entity.is_sleep
):
if fetched_sleep:
# Only need to call API once to get all different sleep segments
do_update = False

return do_update

def _get_data(source: str, dataset: str) -> FitnessObject:
return (
Expand Down Expand Up @@ -146,39 +190,42 @@ def _get_session(activity_id: int) -> FitnessSessionResponse:
.execute()
)

fetched_sleep = False
for entity in ENTITY_DESCRIPTIONS:
if isinstance(entity, SumPointsSensorDescription):
# Only need to call once to get all different sleep segments
if entity.is_sleep and fetched_sleep:
continue

dataset = self._get_interval(entity.period_seconds)
response = await self.hass.async_add_executor_job(
_get_data, entity.source, dataset
)

if entity.is_sleep:
fetched_sleep = True

parser.parse(entity, fit_object=response)
elif isinstance(entity, LastPointSensorDescription):
response = await self.hass.async_add_executor_job(
_get_data_changes, entity.source
)
parser.parse(entity, fit_point=response)
elif isinstance(entity, SumSessionSensorDescription):
response = await self.hass.async_add_executor_job(
_get_session, entity.activity_id
)
parser.parse(entity, fit_session=response)
else:
raise UpdateFailed(
f"Unknown sensor type for {entity.data_key}. Got: {type(entity)}"
)
if _do_update(entity):
if isinstance(entity, SumPointsSensorDescription):
dataset = self._get_interval(entity.period_seconds)
response = await self.hass.async_add_executor_job(
_get_data, entity.source, dataset
)

if entity.is_sleep:
fetched_sleep = True

parser.parse(entity, fit_object=response)
elif isinstance(entity, LastPointSensorDescription):
response = await self.hass.async_add_executor_job(
_get_data_changes, entity.source
)
parser.parse(entity, fit_point=response)
elif isinstance(entity, SumSessionSensorDescription):
response = await self.hass.async_add_executor_job(
_get_session, entity.activity_id
)
parser.parse(entity, fit_session=response)
# Single data point fetches
else:
raise UpdateFailed(
f"Unknown sensor type for {entity.data_key}. Got: {type(entity)}"
)

# Update globally stored data with fetched and parsed data
self.fitness_data = parser.fit_data

# Increment and modulo the counter
self.sensor_update_counter = (
self.sensor_update_counter + 1
) % self.infrequent_interval_multiplier

except HttpError as err:
if 400 <= err.status_code < 500:
raise ConfigEntryAuthFailed(
Expand Down
3 changes: 2 additions & 1 deletion custom_components/google_fit/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"title": "Google Fit Settings",
"description": "For help with settings, see [Configuration Options](https://github.com/YorkshireIoT/ha-google-fit#configuration)",
"data": {
"scan_interval": "Minutes between REST API queries."
"scan_interval": "Minutes between REST API queries.",
"infrequent_interval": "Infrequent Sensor Multiplier. Reduces API queries."
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion custom_components/google_fit/translations/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"title": "Google Fit nastavenia",
"description": "Pomoc s nastaveniami nájdete v časti [Configuration Options](https://github.com/YorkshireIoT/ha-google-fit#configuration)",
"data": {
"scan_interval": "Minúty medzi dopytmi REST API."
"scan_interval": "Minúty medzi dopytmi REST API.",
"infrequent_interval": "Zriedkavý násobiteľ senzorov. Znižuje počet dopytov API."
}
}
}
Expand Down

0 comments on commit 189efa7

Please sign in to comment.