Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1c8dda2
Added code files
cyberjunky Jan 15, 2020
54a86b8
Correctly name init file
cyberjunky Jan 15, 2020
88c8039
Update codeowners
cyberjunky Jan 15, 2020
f8d6f14
Update requirements
cyberjunky Jan 15, 2020
0a6d74a
Added code files
cyberjunky Jan 15, 2020
51b1d5a
Correctly name init file
cyberjunky Jan 15, 2020
d688f80
Update codeowners
cyberjunky Jan 15, 2020
74059c9
Update requirements
cyberjunky Jan 15, 2020
774fe24
Black changes, added to coveragerc
cyberjunky Jan 15, 2020
c2793f9
Merge branch 'garmin-connect' of https://github.com/cyberjunky/home-a…
cyberjunky Jan 15, 2020
72350dc
Removed documentation location for now
cyberjunky Jan 15, 2020
03a7b75
Added documentation url
cyberjunky Jan 15, 2020
3c166ec
Fixed merge
cyberjunky Jan 15, 2020
48783bf
Fixed flake8 syntax
cyberjunky Jan 15, 2020
2a90f7c
Merge branch 'garmin-connect' of https://github.com/cyberjunky/home-a…
cyberjunky Jan 15, 2020
30ada90
Fixed isort
cyberjunky Jan 15, 2020
4aeaa0d
Removed false check and double throttle, applied time format change
cyberjunky Jan 16, 2020
9043a5e
Renamed email to username, used dict, deleted unused type, changed at…
cyberjunky Jan 16, 2020
2880a96
Async and ConfigFlow code
cyberjunky Jan 18, 2020
cd9b34f
Fixes
cyberjunky Jan 18, 2020
42857f6
Added device_class and misc fixes
cyberjunky Jan 19, 2020
ebfa841
isort and pylint fixes
cyberjunky Jan 19, 2020
3dea786
Removed from test requirements
cyberjunky Jan 19, 2020
cad6f49
Merge pull request #1 from cyberjunky/garmin-connect-configflow
cyberjunky Jan 19, 2020
92b46eb
Fixed isort checkblack
cyberjunky Jan 19, 2020
b4357b4
Removed host field
cyberjunky Jan 19, 2020
f7735f4
Fixed coveragerc
cyberjunky Jan 19, 2020
db3f856
Start working test file
cyberjunky Jan 20, 2020
d021103
Added more config_flow tests
cyberjunky Jan 22, 2020
60f5c11
Enable only most used sensors by default
cyberjunky Jan 25, 2020
a2f11ab
Added more default enabled sensors, fixed tests
cyberjunky Jan 25, 2020
cae9406
Fixed isort
cyberjunky Jan 25, 2020
08d0a6d
Test config_flow improvements
cyberjunky Jan 25, 2020
9a6ce6f
Remove unused import
cyberjunky Jan 25, 2020
53b53e2
Removed redundant patch calls
cyberjunky Jan 25, 2020
9a17cc1
Fixed mock return value
cyberjunky Jan 25, 2020
39df719
Updated to garmin_connect 0.1.8 fixed exceptions
cyberjunky Jan 26, 2020
37ee95a
Quick fix test patch to see if rest is error free
cyberjunky Jan 26, 2020
fb90fc0
Fixed mock routine
cyberjunky Jan 26, 2020
282fa4c
Code improvements from PR feedback
cyberjunky Jan 27, 2020
4325416
Fix entity indentifier
cyberjunky Jan 27, 2020
a4bf720
Reverted device identifier
cyberjunky Jan 27, 2020
d8c0839
Fixed abort message
cyberjunky Jan 27, 2020
d91f2de
Test fix
cyberjunky Jan 27, 2020
14453e9
Fixed unique_id MockConfigEntry
cyberjunky Jan 27, 2020
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
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ omit =
homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py
homeassistant/components/garadget/cover.py
homeassistant/components/garmin_connect/sensor.py
homeassistant/components/gc100/*
homeassistant/components/geniushub/*
homeassistant/components/gearbest/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garmin_connect/* @cyberjunky
homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_rss_events/* @exxamalte
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/garmin_connect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The Garmin Connect integration."""
8 changes: 8 additions & 0 deletions homeassistant/components/garmin_connect/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"domain": "garmin_connect",
"name": "Garmin Connect",
"documentation": "https://www.home-assistant.io/integrations/garmin_connect",
"dependencies": [],
"requirements": ["garminconnect==0.1.4"],
"codeowners": ["@cyberjunky"]
}
255 changes: 255 additions & 0 deletions homeassistant/components/garmin_connect/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
"""Platform for Garmin Connect integration."""
from datetime import date, timedelta
import logging

import garminconnect
import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_EMAIL,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_PASSWORD,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle

GARMIN_CONDITIONS_LIST = {
"totalSteps": ["Total Steps", "steps", "mdi:walk"],
"dailyStepGoal": ["Daily Step Goal", "steps", "mdi:walk"],
"totalKilocalories": ["Total KiloCalories", "kcal", "mdi:food"],
"activeKilocalories": ["Active KiloCalories", "kcal", "mdi:food"],
"bmrKilocalories": ["BMR KiloCalories", "kcal", "mdi:food"],
"consumedKilocalories": ["Consumed KiloCalories", "kcal", "mdi:food"],
"burnedKilocalories": ["Burned KiloCalories", "kcal", "mdi:food"],
"remainingKilocalories": ["Remaining KiloCalories", "kcal", "mdi:food"],
"netRemainingKilocalories": ["Net Remaining KiloCalories", "kcal", "mdi:food"],
"netCalorieGoal": ["Net Calorie Goal", "cal", "mdi:food"],
"totalDistanceMeters": ["Total Distance Mtr", "mtr", "mdi:walk"],
"wellnessStartTimeLocal": ["Wellness Start Time", "", "mdi:clock"],
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
"wellnessEndTimeLocal": ["Wellness End Time", "", "mdi:clock"],
"wellnessDescription": ["Wellness Description", "", "mdi:clock"],
"wellnessDistanceMeters": ["Wellness Distance Mtr", "mtr", "mdi:walk"],
"wellnessActiveKilocalories": ["Wellness Active KiloCalories", "kcal", "mdi:food"],
"wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food"],
"highlyActiveSeconds": ["Highly Active Time", "minutes", "mdi:fire"],
"activeSeconds": ["Active Time", "minutes", "mdi:fire"],
"sedentarySeconds": ["Sedentary Time", "minutes", "mdi:seat"],
"sleepingSeconds": ["Sleeping Time", "minutes", "mdi:sleep"],
"measurableAwakeDuration": ["Awake Duration", "minutes", "mdi:sleep"],
"measurableAsleepDuration": ["Sleep Duration", "minutes", "mdi:sleep"],
"floorsAscendedInMeters": ["Floors Ascended Mtr", "mtr", "mdi:stairs"],
"floorsDescendedInMeters": ["Floors Descended Mtr", "mtr", "mdi:stairs"],
"floorsAscended": ["Floors Ascended", "floors", "mdi:stairs"],
"floorsDescended": ["Floors Descended", "floors", "mdi:stairs"],
"userFloorsAscendedGoal": ["Floors Ascended Goal", "", "mdi:stairs"],
"minHeartRate": ["Min Heart Rate", "bpm", "mdi:heart-pulse"],
"maxHeartRate": ["Max Heart Rate", "bpm", "mdi:heart-pulse"],
"restingHeartRate": ["Resting Heart Rate", "bpm", "mdi:heart-pulse"],
"minAvgHeartRate": ["Min Avg Heart Rate", "bpm", "mdi:heart-pulse"],
"maxAvgHeartRate": ["Max Avg Heart Rate", "bpm", "mdi:heart-pulse"],
"abnormalHeartRateAlertsCount": ["Abnormal HR Counts", "", "mdi:heart-pulse"],
"lastSevenDaysAvgRestingHeartRate": [
"Last 7 Days Avg Heart Rate",
"bpm",
"mdi:heart-pulse",
],
"averageStressLevel": ["Avg Stress Level", "", "mdi:flash-alert"],
"maxStressLevel": ["Max Stress Level", "", "mdi:flash-alert"],
"stressQualifier": ["Stress Qualifier", "", "mdi:flash-alert"],
"stressDuration": ["Stress Duration", "minutes", "mdi:flash-alert"],
"restStressDuration": ["Rest Stress Duration", "minutes", "mdi:flash-alert"],
"activityStressDuration": [
"Activity Stress Duration",
"minutes",
"mdi:flash-alert",
],
"uncategorizedStressDuration": [
"Uncat. Stress Duration",
"minutes",
"mdi:flash-alert",
],
"totalStressDuration": ["Total Stress Duration", "minutes", "mdi:flash-alert"],
"lowStressDuration": ["Low Stress Duration", "minutes", "mdi:flash-alert"],
"mediumStressDuration": ["Medium Stress Duration", "minutes", "mdi:flash-alert"],
"highStressDuration": ["High Stress Duration", "minutes", "mdi:flash-alert"],
"stressPercentage": ["Stress Percentage", "%", "mdi:flash-alert"],
"restStressPercentage": ["Rest Stress Percentage", "%", "mdi:flash-alert"],
"activityStressPercentage": ["Activity Stress Percentage", "%", "mdi:flash-alert"],
"uncategorizedStressPercentage": [
"Uncat. Stress Percentage",
"%",
"mdi:flash-alert",
],
"lowStressPercentage": ["Low Stress Percentage", "%", "mdi:flash-alert"],
"mediumStressPercentage": ["Medium Stress Percentage", "%", "mdi:flash-alert"],
"highStressPercentage": ["High Stress Percentage", "%", "mdi:flash-alert"],
"moderateIntensityMinutes": ["Moderate Intensity", "minutes", "mdi:flash-alert"],
"vigorousIntensityMinutes": ["Vigorous Intensity", "minutes", "mdi:run-fast"],
"intensityMinutesGoal": ["Intensity Goal", "minutes", "mdi:run-fast"],
"bodyBatteryChargedValue": [
"Body Battery Charged",
"%",
"mdi:battery-charging-100",
],
"bodyBatteryDrainedValue": [
"Body Battery Drained",
"%",
"mdi:battery-alert-variant-outline",
],
"bodyBatteryHighestValue": ["Body Battery Highest", "%", "mdi:battery-heart"],
"bodyBatteryLowestValue": ["Body Battery Lowest", "%", "mdi:battery-heart-outline"],
"bodyBatteryMostRecentValue": [
"Body Battery Most Recent",
"%",
"mdi:battery-positive",
],
"averageSpo2": ["Average SPO2", "%", "mdi:diabetes"],
"lowestSpo2": ["Lowest SPO2", "%", "mdi:diabetes"],
"latestSpo2": ["Latest SPO2", "%", "mdi:diabetes"],
"latestSpo2ReadingTimeLocal": ["Latest SPO2 Time", "", "mdi:diabetes"],
"averageMonitoringEnvironmentAltitude": [
"Average Altitude",
"%",
"mdi:image-filter-hdr",
],
"durationInMilliseconds": ["Duration", "ms", "mdi:progress-clock"],
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
"highestRespirationValue": ["Highest Respiration", "brpm", "mdi:progress-clock"],
"lowestRespirationValue": ["Lowest Respiration", "brpm", "mdi:progress-clock"],
"latestRespirationValue": ["Latest Respiration", "brpm", "mdi:progress-clock"],
"latestRespirationTimeGMT": ["Latest Respiration Update", "", "mdi:progress-clock"],
}

_LOGGER = logging.getLogger(__name__)

GARMIN_DEFAULT_CONDITIONS = ["totalSteps"]
ATTRIBUTION = "Data provided by garmin.com"
DEFAULT_NAME = "Garmin"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_EMAIL): cv.string,
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(
CONF_MONITORED_CONDITIONS, default=GARMIN_DEFAULT_CONDITIONS
): vol.All(cv.ensure_list, [vol.In(GARMIN_CONDITIONS_LIST)]),
}
)


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Garmin Connect component."""

email = config.get(CONF_EMAIL)
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
password = config.get(CONF_PASSWORD)
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
prefix_name = config.get(CONF_NAME)
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated

try:
garmin_client = garminconnect.Garmin(email, password)
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
except ValueError as err:
_LOGGER.error("Error occured during Garmin Connect Client init: %s", err)
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
return

garmin_data = GarminConnectClient(garmin_client)

entities = []
for resource in config[CONF_MONITORED_CONDITIONS]:
sensor_type = resource
name = prefix_name + " " + GARMIN_CONDITIONS_LIST[resource][0]
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
unit = GARMIN_CONDITIONS_LIST[resource][1]
icon = GARMIN_CONDITIONS_LIST[resource][2]

_LOGGER.debug(
"Registered new sensor: %s, %s, %s, %s", sensor_type, name, unit, icon
)
entities.append(GarminConnectSensor(garmin_data, sensor_type, name, unit, icon))

add_entities(entities, True)


class GarminConnectClient:
"""Set up the Garmin Connect client."""

def __init__(self, client):
"""Initialize the client."""
self.client = client
self.data = None

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Fetch the latest data."""
today = date.today()
try:
self.data = self.client.fetch_stats(today.strftime("%Y-%m-%d"))
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
except ValueError as err:
_LOGGER.error("Error occured while fetching Garmin Connect data: %s", err)
return


class GarminConnectSensor(Entity):
"""Representation of a Garmin Connect Sensor."""

def __init__(self, data, sensor_type, name, unit, icon):
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
"""Initialize the sensor."""
self._data = data
self._type = sensor_type
self._name = name
self._icon = icon
self._unit = unit
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._icon

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit

@property
def device_state_attributes(self):
"""Return attributes for sensor."""

attributes = {}
if self._data.data:
attributes = {
"source": self._data.data["source"],
"last synced (GMT)": self._data.data["lastSyncTimestampGMT"],
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
ATTR_ATTRIBUTION: ATTRIBUTION,
}
return attributes

@Throttle(MIN_TIME_BETWEEN_UPDATES)
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
def update(self):
"""Update data and set sensor states."""
self._data.update()
data = self._data.data

if GARMIN_CONDITIONS_LIST[self._type] and self._type in data:
Comment thread
cyberjunky marked this conversation as resolved.
Outdated
if "Duration" in self._type:
self._state = data[self._type] // 60
elif "Seconds" in self._type:
self._state = data[self._type] // 60
else:
self._state = data[self._type]

_LOGGER.debug(
"Device %s set to state %s %s", self._type, self._state, self._unit
)
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ fritzhome==1.0.4
# homeassistant.components.google_translate
gTTS-token==1.1.3

# homeassistant.components.garmin_connect
garminconnect==0.1.4

# homeassistant.components.gearbest
gearbest_parser==1.0.7

Expand Down