Skip to content

Commit

Permalink
Remove use_zero configuration and use zero as base/default value for …
Browse files Browse the repository at this point in the history
…cumulative sensors (#139)

* Remove use_zero configuration and use zero as base/default value for cumulative sensors
* Behaviour for sensors that are cumulative, e.g. steps, will be to show 0 if no data exists in the account
* Behaviour for sensors that aren't cumulative, e.g. height, will be to return "Unknown" if no data exists the account
* Update docs to explain integration behaviour
  • Loading branch information
YorkshireIoT authored Nov 21, 2023
1 parent ff1d173 commit 6fbe42b
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 77 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,52 @@ 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) |
Unknown value | When there is no data available in your Google Fit account within the time period, set the sensor value to 0 or unknown | Set to 0 |

## Unknown Sensor Behaviour

All sensors in this integration can be grouped into two categories; cumulative or individual.
This is due to how data is reported in your Google Fit account. Every piece of data is only associated
with a time period and has no underlying logic for running totals.

For example, steps are reported like this in your account:

* 1st January 2023 9:32:01 - 495
* 2nd January 2023 11:54:03 - 34
* 2nd January 2023 13:02:40 - 1005
* 2nd January 2023 17:16:27 - 842

There is then some built-in logic in this integration to work out what
sensor we're dealing with, and to either sum up these values over a
logical time period, or to just take the latest known value, when the
sensor is something like height.

> If you're interested in all the underlying logic, it's contained in
> [api.py](/custom_components/google_fit/api.py).
The behaviour of this integration when there is *no* data available in your account differs
depending on the sensor category.

*Cumulative* sensors will use 0 as their base value and this will be their value in Home Assistant
if their is no data in your Google Fit account for that sensor.

*Individual* sensors will only report a value if they can find some data in your Google Fit account.
Otherwise, they will show `Unknown`.

### Unavailable

Besides `Unknown`, there is an additional Home Assistant sensor state; `Unavailable`.

This state has nothing to do with if there is or isn't data in your account. It indicates some error
in the fetching of the data. Your internet has stopped working, or maybe the Google servers are
down. In some cases, it may also indicate there is a bug with this integration. If that is the case,
please report it as a bug.

There are no plans to ignore these data fetching issues and retain the last known sensor
value. The reasoning for this is:

* This is the correct representation of the sensors state, and
* It indicates to the user that there is some underlying issue with the integration itself
and this should not be hidden.s

## Adding Multiple Accounts

Expand Down
77 changes: 36 additions & 41 deletions custom_components/google_fit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ def __init__(self):
bodyFat=None,
bodyTemperature=None,
steps=None,
awakeSeconds=None,
sleepSeconds=None,
lightSleepSeconds=None,
deepSleepSeconds=None,
remSleepSeconds=None,
awakeSeconds=0,
sleepSeconds=0,
lightSleepSeconds=0,
deepSleepSeconds=0,
remSleepSeconds=0,
heartRate=None,
heartRateResting=None,
bloodPressureSystolic=None,
Expand All @@ -123,37 +123,49 @@ def __init__(self):
)
self.unknown_sleep_warn = False

def _sum_points_int(self, response: FitnessObject) -> int | None:
def _sum_points_int(self, response: FitnessObject) -> int:
"""Get the most recent integer point value.
If no data points exist, return 0.
"""
counter = 0
found_value = False
for point in response.get("point"):
value = point.get("value")[0].get("intVal")
if value is not None:
found_value = True
counter += value
if found_value:
return counter
# If no value is found, return None to keep sensor value as "Unknown"
LOGGER.debug("No int data points found for %s", response.get("dataSourceId"))
return None

def _sum_points_float(self, response: FitnessObject) -> float | None:
if not found_value:
LOGGER.debug("No int data points found for %s", response.get("dataSourceId"))

return counter

def _sum_points_float(self, response: FitnessObject) -> float:
"""Get the most recent floating point value.
If no data points exist, return 0.
"""
counter = 0
found_value = False
for point in response.get("point"):
value = point.get("value")[0].get("fpVal")
if value is not None:
found_value = True
counter += value
if found_value:
return round(counter, 2)
# If no value is found, return None to keep sensor value as "Unknown"
LOGGER.debug("No float data points found for %s", response.get("dataSourceId"))
return None

if not found_value:
LOGGER.debug("No float data points found for %s", response.get("dataSourceId"))

return round(counter, 2)

def _get_latest_data_float(
self, response: FitnessDataPoint, index: int = 0
) -> float | None:
"""Get the most recent floating point value.
If no data exists in the account return None.
"""
value = None
data_points = response.get("insertedDataPoint")
latest_time = 0
Expand All @@ -175,6 +187,10 @@ def _get_latest_data_float(
def _get_latest_data_int(
self, response: FitnessDataPoint, index: int = 0
) -> int | None:
"""Get the most recent integer point value.
If no data exists in the account return None.
"""
value = None
data_points = response.get("insertedDataPoint")
latest_time = 0
Expand All @@ -193,11 +209,9 @@ def _get_latest_data_int(
return value

def _parse_sleep(self, response: FitnessObject) -> None:
found_point = False
data_points = response.get("point")

for point in data_points:
found_point = True
sleep_type = point.get("value")[0].get("intVal")
start_time_ns = point.get("startTimeNanos")
end_time_ns = point.get("endTimeNanos")
Expand All @@ -222,10 +236,6 @@ def _parse_sleep(self, response: FitnessObject) -> None:
"Home Assistant.", start_time_str, end_time_str
)
elif sleep_stage is not None:
# If field is still at None, initialise it to zero
if self.data[sleep_stage] is None:
self.data[sleep_stage] = 0

if end_time >= start_time:
self.data[sleep_stage] += end_time - start_time
else:
Expand All @@ -246,11 +256,6 @@ def _parse_sleep(self, response: FitnessObject) -> None:
"End Time (ns): {end_time}"
)

if found_point is False:
LOGGER.debug(
"No sleep type data points found. Values will be set to configured default."
)

def _parse_object(
self, entity: SumPointsSensorDescription, response: FitnessObject
) -> None:
Expand All @@ -269,30 +274,20 @@ def _parse_session(
) -> None:
"""Parse the given session data from the API according to the passed request_id."""
# Sum all the session times (in milliseconds) from within the response
summed_millis: int | None = None
summed_millis: int = 0
sessions = response.get("session")
if sessions is None:
raise UpdateFailed(
f"Google Fit returned invalid session data for source: {entity.source}.\r"
"Session data is None."
)
for session in sessions:
# Initialise data if it is None
if summed_millis is None:
summed_millis = 0

summed_millis += int(session.get("endTimeMillis")) - int(
session.get("startTimeMillis")
)

if summed_millis is not None:
# Time is in milliseconds, need to convert to seconds
self.data[entity.data_key] = summed_millis / 1000
else:
LOGGER.debug(
"No sessions from source %s found for time period in Google Fit account.",
entity.source,
)
# Time is in milliseconds, need to convert to seconds
self.data[entity.data_key] = summed_millis / 1000

def _parse_point(
self, entity: LastPointSensorDescription, response: FitnessDataPoint
Expand Down
8 changes: 0 additions & 8 deletions custom_components/google_fit/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
from .const import (
DEFAULT_ACCESS,
DOMAIN,
CONF_NO_DATA_USE_ZERO,
DEFAULT_SCAN_INTERVAL,
DEFAULT_NO_DATA_USE_ZERO,
)


Expand Down Expand Up @@ -144,12 +142,6 @@ async def async_step_init(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): config_validation.positive_int,
vol.Required(
CONF_NO_DATA_USE_ZERO,
default=self.config_entry.options.get(
CONF_NO_DATA_USE_ZERO, DEFAULT_NO_DATA_USE_ZERO
),
): config_validation.boolean,
}
),
)
4 changes: 0 additions & 4 deletions custom_components/google_fit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,8 @@
DOMAIN: Final = "google_fit"
MANUFACTURER: Final = "Google, Inc."

# Configuration schema
CONF_NO_DATA_USE_ZERO: Final = "use_zero"

# Default Configuration Values
DEFAULT_SCAN_INTERVAL = 5
DEFAULT_NO_DATA_USE_ZERO = True

# Useful constants
NANOSECONDS_SECONDS_CONVERSION: Final = 1000000000
Expand Down
15 changes: 1 addition & 14 deletions custom_components/google_fit/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
LOGGER,
ENTITY_DESCRIPTIONS,
DEFAULT_SCAN_INTERVAL,
DEFAULT_NO_DATA_USE_ZERO,
CONF_NO_DATA_USE_ZERO,
NANOSECONDS_SECONDS_CONVERSION,
)

Expand All @@ -43,7 +41,6 @@ class Coordinator(DataUpdateCoordinator):
_auth: AsyncConfigEntryAuth
_config: ConfigEntry
fitness_data: FitnessData | None = None
_use_zero: bool

def __init__(
self,
Expand All @@ -54,13 +51,9 @@ def __init__(
"""Initialise."""
self._auth = auth
self._config = config
self._use_zero = config.options.get(
CONF_NO_DATA_USE_ZERO, DEFAULT_NO_DATA_USE_ZERO
)
update_time = config.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
LOGGER.debug(
"Setting up Google Fit Coordinator. Use zero=%s and updating every %u minutes",
str(self._use_zero),
"Setting up Google Fit Coordinator. Updating every %u minutes",
update_time,
)
super().__init__(
Expand All @@ -82,11 +75,6 @@ def current_data(self) -> FitnessData | None:
"""Return the current data, or None is data is not available."""
return self.fitness_data

@property
def use_zero(self) -> bool:
"""Return the config option on whether to use zero for when there is no sensor data."""
return self._use_zero

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 @@ -182,7 +170,6 @@ def _get_session(activity_id: int) -> FitnessSessionResponse:
_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)}"
Expand Down
6 changes: 2 additions & 4 deletions custom_components/google_fit/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class GoogleFitBlueprintSensor(GoogleFitEntity, SensorEntity):
"""Google Fit Template Sensor class."""

entity_description: GoogleFitSensorDescription
coordinator: Coordinator

def __init__(
self,
Expand All @@ -41,6 +42,7 @@ def __init__(
"""Initialise the sensor class."""
super().__init__(coordinator)
self.entity_description = entity_description
self.coordinator = coordinator
# Follow method in core Google Mail component and use oauth session to create unique ID
if coordinator.oauth_session:
self._attr_unique_id = (
Expand All @@ -63,10 +65,6 @@ def _read_value(self) -> None:
if value is not None:
self._attr_native_value = value
self.async_write_ha_state()
# If value is None but config says to use zero value to prevent unknown
elif self.coordinator.use_zero:
self._attr_native_value = 0
self.async_write_ha_state()

@callback
def _handle_coordinator_update(self) -> None:
Expand Down
3 changes: 1 addition & 2 deletions custom_components/google_fit/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
"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.",
"use_zero": "When there is no data available on the account for a sensor set to zero. If unchecked, set to unknown."
"scan_interval": "Minutes between REST API queries."
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions custom_components/google_fit/translations/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@
"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.",
"use_zero": "Keď na účte nie sú k dispozícii žiadne údaje pre snímač nastavený na nulu. Ak nie je začiarknuté, nastavte na neznáme."
"scan_interval": "Minúty medzi dopytmi REST API."
}
}
}
}
}
}

0 comments on commit 6fbe42b

Please sign in to comment.