Compare commits

...

11 Commits

Author SHA1 Message Date
Ron
1029e21e23 Merge pull request #116 from alexives/add_body_composition_service
Add service for body composition
2024-01-02 12:33:56 +01:00
Ron
ac923b7517 Update services.yaml 2024-01-02 12:31:50 +01:00
Ron
cf5d39fb56 Update sensor.py 2024-01-02 12:31:16 +01:00
Ron
77dbbc9f47 Update manifest.json 2024-01-02 12:30:43 +01:00
Ron
2326be7455 Update const.py 2024-01-02 12:30:21 +01:00
Ron
bfb720ac5d Update __init__.py 2024-01-02 12:29:46 +01:00
Alex Ives
d20c9bedb2 Add service for body composition
Relates to https://github.com/cyberjunky/home-assistant-garmin_connect/issues/74
2023-12-30 15:42:19 -06:00
Ron
5e6f7ff6e1 Create FUNDING.yml 2023-12-26 20:26:50 +01:00
Ron
9d90c366d9 Merge pull request #113 from misa1515/patch-3
Update sk.json
2023-12-23 17:54:49 +01:00
Ron
891ddbce27 Bumped python-garminconnect version 2023-12-22 13:28:59 +01:00
misa1515
dc345c4d53 Update sk.json 2023-11-21 13:55:51 +01:00
7 changed files with 213 additions and 59 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: [cyberjunky] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -101,41 +101,46 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> dict:
"""Fetch data from Garmin Connect."""
summary = {}
body = {}
activites = {}
alarms = {}
gear = {}
gear_stats = {}
gear_defaults = {}
activity_types = {}
sleep_data = {}
sleep_score = None
try:
summary = await self.hass.async_add_executor_job(
self._api.get_user_summary, date.today().isoformat()
)
_LOGGER.debug(summary)
_LOGGER.debug(f"Summary data: {summary}")
body = await self.hass.async_add_executor_job(
self._api.get_body_composition, date.today().isoformat()
)
_LOGGER.debug(f"Body data: {body}")
activities = await self.hass.async_add_executor_job(
self._api.get_activities_by_date, (date.today()-timedelta(days=7)).isoformat(), (date.today()+timedelta(days=1)).isoformat()
)
_LOGGER.debug(f"Activities data: {activities}")
summary['lastActivities'] = activities
_LOGGER.debug(body)
alarms = await self.hass.async_add_executor_job(self._api.get_device_alarms)
_LOGGER.debug(alarms)
gear = await self.hass.async_add_executor_job(
self._api.get_gear, summary[GEAR.USERPROFILE_ID]
)
tasks: list[Awaitable] = [
self.hass.async_add_executor_job(
self._api.get_gear_stats, gear_item[GEAR.UUID]
)
for gear_item in gear
]
gear_stats = await asyncio.gather(*tasks)
_LOGGER.debug(f"Alarms data: {alarms}")
activity_types = await self.hass.async_add_executor_job(
self._api.get_activity_types
)
gear_defaults = await self.hass.async_add_executor_job(
self._api.get_gear_defaults, summary[GEAR.USERPROFILE_ID]
)
_LOGGER.debug(f"Activity types data: {activity_types}")
sleep_data = await self.hass.async_add_executor_job(
self._api.get_sleep_data, date.today().isoformat())
_LOGGER.debug(f"Sleep data: {sleep_data}")
except (
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
@@ -146,12 +151,33 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
raise UpdateFailed(error) from error
return {}
sleep_score = None
try:
gear = await self.hass.async_add_executor_job(
self._api.get_gear, summary[GEAR.USERPROFILE_ID]
)
_LOGGER.debug(f"Gear data: {gear}")
tasks: list[Awaitable] = [
self.hass.async_add_executor_job(
self._api.get_gear_stats, gear_item[GEAR.UUID]
)
for gear_item in gear
]
gear_stats = await asyncio.gather(*tasks)
_LOGGER.debug(f"Gear stats data: {gear_stats}")
gear_defaults = await self.hass.async_add_executor_job(
self._api.get_gear_defaults, summary[GEAR.USERPROFILE_ID]
)
_LOGGER.debug(f"Gear defaults data: {gear_defaults}")
except:
_LOGGER.debug("Gear data is not available")
try:
sleep_score = sleep_data["dailySleepDTO"]["sleepScores"]["overall"]["value"]
_LOGGER.debug(f"Sleep score data: {sleep_score}")
except KeyError:
_LOGGER.debug("sleepScore was absent")
summary['lastActivities'] = activities
_LOGGER.debug("Sleep score data is not available")
return {
**summary,
@@ -207,3 +233,27 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
await self.hass.async_add_executor_job(
self._api.set_gear_default, activity_type_id, entity.uuid, True
)
async def add_body_composition(self, entity, service_data):
"""Record a weigh in/body composition"""
if not await self.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
await self.hass.async_add_executor_job(
self._api.add_body_composition,
service_data.data.get("timestamp", None),
service_data.data.get("weight"),
service_data.data.get("percent_fat", None),
service_data.data.get("percent_hydration", None),
service_data.data.get("visceral_fat_mass", None),
service_data.data.get("bone_mass", None),
service_data.data.get("muscle_mass", None),
service_data.data.get("basal_met", None),
service_data.data.get("active_met", None),
service_data.data.get("physique_rating", None),
service_data.data.get("metabolic_age", None),
service_data.data.get("visceral_fat_rating", None),
service_data.data.get("bmi", None)
)

View File

@@ -4,11 +4,10 @@ from enum import Enum
from typing import NamedTuple
from homeassistant.const import (
LENGTH_METERS,
MASS_KILOGRAMS,
UnitOfMass,
UnitOfTime,
UnitOfLength,
PERCENTAGE,
TIME_MINUTES,
TIME_YEARS,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -46,7 +45,7 @@ GARMIN_ENTITY_LIST = {
"netCalorieGoal": ["Net Calorie Goal", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"totalDistanceMeters": [
"Total Distance Mtr",
LENGTH_METERS,
UnitOfLength.METERS,
"mdi:walk",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
@@ -71,7 +70,7 @@ GARMIN_ENTITY_LIST = {
"wellnessDescription": ["Wellness Description", "", "mdi:clock", None, SensorStateClass.TOTAL, False],
"wellnessDistanceMeters": [
"Wellness Distance Mtr",
LENGTH_METERS,
UnitOfLength.METERS,
"mdi:walk",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
@@ -88,18 +87,18 @@ GARMIN_ENTITY_LIST = {
"wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"highlyActiveSeconds": [
"Highly Active Time",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:fire",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
False,
],
"activeSeconds": ["Active Time", TIME_MINUTES, "mdi:fire", None, SensorStateClass.TOTAL, True],
"sedentarySeconds": ["Sedentary Time", TIME_MINUTES, "mdi:seat", None, SensorStateClass.TOTAL, True],
"sleepingSeconds": ["Sleeping Time", TIME_MINUTES, "mdi:sleep", None, SensorStateClass.TOTAL, True],
"activeSeconds": ["Active Time", UnitOfTime.MINUTES, "mdi:fire", None, SensorStateClass.TOTAL, True],
"sedentarySeconds": ["Sedentary Time", UnitOfTime.MINUTES, "mdi:seat", None, SensorStateClass.TOTAL, True],
"sleepingSeconds": ["Sleeping Time", UnitOfTime.MINUTES, "mdi:sleep", None, SensorStateClass.TOTAL, True],
"measurableAwakeDuration": [
"Awake Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -107,7 +106,7 @@ GARMIN_ENTITY_LIST = {
],
"measurableAsleepDuration": [
"Sleep Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -115,7 +114,7 @@ GARMIN_ENTITY_LIST = {
],
"floorsAscendedInMeters": [
"Floors Ascended Mtr",
LENGTH_METERS,
UnitOfLength.METERS,
"mdi:stairs",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
@@ -123,7 +122,7 @@ GARMIN_ENTITY_LIST = {
],
"floorsDescendedInMeters": [
"Floors Descended Mtr",
LENGTH_METERS,
UnitOfLength.METERS,
"mdi:stairs",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
@@ -163,10 +162,10 @@ GARMIN_ENTITY_LIST = {
"averageStressLevel": ["Avg Stress Level", "lvl", "mdi:flash-alert", None, SensorStateClass.TOTAL, True],
"maxStressLevel": ["Max Stress Level", "lvl", "mdi:flash-alert", None, SensorStateClass.TOTAL, True],
"stressQualifier": ["Stress Qualifier", None, "mdi:flash-alert", None, None, False],
"stressDuration": ["Stress Duration", TIME_MINUTES, "mdi:flash-alert", None, SensorStateClass.TOTAL, False],
"stressDuration": ["Stress Duration", UnitOfTime.MINUTES, "mdi:flash-alert", None, SensorStateClass.TOTAL, False],
"restStressDuration": [
"Rest Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -174,7 +173,7 @@ GARMIN_ENTITY_LIST = {
],
"activityStressDuration": [
"Activity Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -182,7 +181,7 @@ GARMIN_ENTITY_LIST = {
],
"uncategorizedStressDuration": [
"Uncat. Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -190,7 +189,7 @@ GARMIN_ENTITY_LIST = {
],
"totalStressDuration": [
"Total Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -198,7 +197,7 @@ GARMIN_ENTITY_LIST = {
],
"lowStressDuration": [
"Low Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -206,7 +205,7 @@ GARMIN_ENTITY_LIST = {
],
"mediumStressDuration": [
"Medium Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -214,7 +213,7 @@ GARMIN_ENTITY_LIST = {
],
"highStressDuration": [
"High Stress Duration",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
@@ -278,7 +277,7 @@ GARMIN_ENTITY_LIST = {
],
"moderateIntensityMinutes": [
"Moderate Intensity",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -286,7 +285,7 @@ GARMIN_ENTITY_LIST = {
],
"vigorousIntensityMinutes": [
"Vigorous Intensity",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:run-fast",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
@@ -294,7 +293,7 @@ GARMIN_ENTITY_LIST = {
],
"intensityMinutesGoal": [
"Intensity Goal",
TIME_MINUTES,
UnitOfTime.MINUTES,
"mdi:run-fast",
None,
SensorStateClass.TOTAL,
@@ -391,15 +390,15 @@ GARMIN_ENTITY_LIST = {
None,
False,
],
"weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"weight": ["Weight", UnitOfMass.KILOGRAMS, "mdi:weight-kilogram", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"bmi": ["BMI", "bmi", "mdi:food", None, SensorStateClass.TOTAL, False],
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
"bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, SensorStateClass.TOTAL, False],
"boneMass": ["Bone Mass", MASS_KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"boneMass": ["Bone Mass", UnitOfMass.KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"muscleMass": ["Muscle Mass", UnitOfMass.KILOGRAMS, "mdi:dumbbell", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"physiqueRating": ["Physique Rating", None, "mdi:numeric", None, SensorStateClass.TOTAL, False],
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
"metabolicAge": ["Metabolic Age", TIME_YEARS, "mdi:calendar-heart", None, SensorStateClass.TOTAL, False],
"metabolicAge": ["Metabolic Age", UnitOfTime.YEARS, "mdi:calendar-heart", None, SensorStateClass.TOTAL, False],
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", SensorDeviceClass.TIMESTAMP, None, True],
"lastActivities": ["Last Activities", None, "mdi:numeric", SensorStateClass.TOTAL, None, False],
"sleepScore": [

View File

@@ -6,6 +6,6 @@
"documentation": "https://github.com/cyberjunky/home-assistant-garmin_connect",
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"iot_class": "cloud_polling",
"requirements": ["garminconnect==0.2.3", "tzlocal"],
"version": "0.2.17"
"requirements": ["garminconnect==0.2.12", "tzlocal"],
"version": "0.2.19"
}

View File

@@ -15,10 +15,9 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
UnitOfLength,
ATTR_ENTITY_ID,
CONF_ID,
DEVICE_CLASS_TIMESTAMP,
LENGTH_KILOMETERS,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID
from homeassistant.core import HomeAssistant
@@ -101,6 +100,9 @@ async def async_setup_entry(
"set_active_gear", ENTITY_SERVICE_SCHEMA, coordinator.set_active_gear
)
platform.async_register_entity_service(
"add_body_composition", BODY_COMPOSITION_SERVICE_SCHEMA, coordinator.add_body_composition
)
ENTITY_SERVICE_SCHEMA = vol.Schema(
{
@@ -110,6 +112,24 @@ ENTITY_SERVICE_SCHEMA = vol.Schema(
}
)
BODY_COMPOSITION_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Optional("timestamp"): str,
vol.Required("weight"): float,
vol.Optional("percent_fat"): float,
vol.Optional("percent_hydration"): float,
vol.Optional("visceral_fat_mass"): float,
vol.Optional("bone_mass"): float,
vol.Optional("muscle_mass"): float,
vol.Optional("basal_met"): float,
vol.Optional("active_met"): float,
vol.Optional("physique_rating"): float,
vol.Optional("metabolic_age"): float,
vol.Optional("visceral_fat_rating"): float,
vol.Optional("bmi"): float
}
)
class GarminConnectSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Garmin Connect Sensor."""
@@ -248,7 +268,7 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
self._attr_name = name
self._attr_device_class = self._device_class
self._attr_icon = GEAR_ICONS[sensor_type]
self._attr_native_unit_of_measurement = LENGTH_KILOMETERS
self._attr_native_unit_of_measurement = UnitOfLength.KILOMETERS
self._attr_unique_id = f"{self._unique_id}_{self._uuid}"
self._attr_state_class = SensorStateClass.TOTAL
self._attr_device_class = "garmin_gear"

View File

@@ -1,8 +1,5 @@
set_active_gear:
name: Set active gear for activity
# target:
# entity:
# integration: "garmin_connect"
fields:
activity_type:
required: true
@@ -37,3 +34,79 @@ set_active_gear:
entity:
integration: garmin_connect
device_class: garmin_gear
add_body_composition:
name: Adds updated body composition metrics
fields:
weight:
required: true
name: Weight
description: Weight in KG
example: 82.3
timestamp:
required: false
name: Timestamp
description: Datetime string of when the measurements were recorded. Defaults to now.
example: 2023-12-30T07:34:00
bmi:
required: false
name: BMI (Body Mass Index)
description: Body mass index is based on weight and height.
example: 24.7
percent_fat:
required: false
name: Percent Fat
description: Percent body fat
example: 23.6
percent_hydration:
required: false
name: Percent Hydration
description: Percent body hydration
example: 51.2
visceral_fat_mass:
required: false
name: Visceral Fat Mass
description: Estimated mass of visceral fat in KG
example: 45.3
bone_mass:
required: false
name: Bone Mass
description: Estimated mass of bones in KG
example: 10.1
muscle_mass:
required: false
name: Muscle Mass
description: Estimated mass of muscle in KG
example: 15.2
basal_met:
required: false
name: Basel Metabolism
description: Basel metabolism
example: 1900
active_met:
required: false
name: Active Metabolism
description: Active metabolism
example: 840
physique_rating:
required: false
name: Physique Rating
description: Physique Rating
example: 28
metabolic_age:
required: false
name: Metabolic Age
description: Metabolic Age
example: 37
visceral_fat_rating:
required: false
name: Visceral Fat Rating
description: Visceral Fat Rating
example: 10
entity_id:
description: entity
required: true
selector:
entity:
integration: garmin_connect
device_class: weight

View File

@@ -16,7 +16,6 @@
"username": "Užívateľské meno"
},
"description": "Zadajte svoje poverenia.",
"title": "Garmin Connect"
}
}
}