From af048715f0a474090482c078dd7d2d363ba05a4e Mon Sep 17 00:00:00 2001 From: Schachar Levin Date: Sat, 24 Oct 2020 00:01:29 +0300 Subject: [PATCH] Add next alarm time sensor to Garmin (#40420) * Add sensor for garmin next alarm(s) * code-review changes * type fix * linter fix * review fixes --- .coveragerc | 1 + .../components/garmin_connect/__init__.py | 12 +++++ .../components/garmin_connect/alarm_util.py | 50 +++++++++++++++++++ .../components/garmin_connect/const.py | 1 + .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 14 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/garmin_connect/alarm_util.py diff --git a/.coveragerc b/.coveragerc index 34cf79ad958..b67fb376a9f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -304,6 +304,7 @@ omit = homeassistant/components/garmin_connect/__init__.py homeassistant/components/garmin_connect/const.py homeassistant/components/garmin_connect/sensor.py + homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geizhals/sensor.py diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index c0a1012f051..896c243f386 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -90,6 +90,17 @@ class GarminConnectData: self.client = client self.data = None + async def _get_combined_alarms_of_all_devices(self): + """Combine the list of active alarms from all garmin devices.""" + alarms = [] + devices = await self.hass.async_add_executor_job(self.client.get_devices) + for device in devices: + device_settings = await self.hass.async_add_executor_job( + self.client.get_device_settings, device["deviceId"] + ) + alarms += device_settings["alarms"] + return alarms + @Throttle(MIN_SCAN_INTERVAL) async def async_update(self): """Update data via library.""" @@ -99,6 +110,7 @@ class GarminConnectData: self.data = await self.hass.async_add_executor_job( self.client.get_stats_and_body, today.isoformat() ) + self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, diff --git a/homeassistant/components/garmin_connect/alarm_util.py b/homeassistant/components/garmin_connect/alarm_util.py new file mode 100644 index 00000000000..4964d70e886 --- /dev/null +++ b/homeassistant/components/garmin_connect/alarm_util.py @@ -0,0 +1,50 @@ +"""Utility method for converting Garmin Connect alarms to python datetime.""" +from datetime import date, datetime, timedelta +import logging + +_LOGGER = logging.getLogger(__name__) + +DAY_TO_NUMBER = { + "Mo": 1, + "M": 1, + "Tu": 2, + "We": 3, + "W": 3, + "Th": 4, + "Fr": 5, + "F": 5, + "Sa": 6, + "Su": 7, +} + + +def calculate_next_active_alarms(alarms): + """ + Calculate garmin next active alarms from settings. + + Alarms are sorted by time + """ + active_alarms = [] + _LOGGER.debug(alarms) + + for alarm_setting in alarms: + if alarm_setting["alarmMode"] != "ON": + continue + for day in alarm_setting["alarmDays"]: + alarm_time = alarm_setting["alarmTime"] + if day == "ONCE": + midnight = datetime.combine(date.today(), datetime.min.time()) + alarm = midnight + timedelta(minutes=alarm_time) + if alarm < datetime.now(): + alarm += timedelta(days=1) + else: + start_of_week = datetime.combine( + date.today() - timedelta(days=datetime.today().isoweekday() % 7), + datetime.min.time(), + ) + days_to_add = DAY_TO_NUMBER[day] % 7 + alarm = start_of_week + timedelta(minutes=alarm_time, days=days_to_add) + if alarm < datetime.now(): + alarm += timedelta(days=7) + active_alarms.append(alarm.isoformat()) + return sorted(active_alarms) if active_alarms else None diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 77db3359a71..7a143e2e63a 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -348,4 +348,5 @@ GARMIN_ENTITY_LIST = { "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], "visceralFat": ["Visceral Fat", "", "mdi:food", None, False], "metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False], + "nextAlarm": ["Next Alarm Time", "", "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], } diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 7a57b1bf102..c7880f9b416 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.13"], + "requirements": ["garminconnect==0.1.16"], "codeowners": ["@cyberjunky"], "config_flow": true } diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 9b678011053..5d18f0a0dd0 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from .alarm_util import calculate_next_active_alarms from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST _LOGGER = logging.getLogger(__name__) @@ -123,11 +124,16 @@ class GarminConnectSensor(Entity): """Return attributes for sensor.""" if not self._data.data: return {} - return { + attributes = { "source": self._data.data["source"], "last_synced": self._data.data["lastSyncTimestampGMT"], ATTR_ATTRIBUTION: ATTRIBUTION, } + if self._type == "nextAlarm": + attributes["next_alarms"] = calculate_next_active_alarms( + self._data.data[self._type] + ) + return attributes @property def device_info(self) -> Dict[str, Any]: @@ -177,6 +183,12 @@ class GarminConnectSensor(Entity): self._type == "bodyFat" or self._type == "bodyWater" or self._type == "bmi" ): self._state = round(data[self._type], 2) + elif self._type == "nextAlarm": + active_alarms = calculate_next_active_alarms(data[self._type]) + if active_alarms: + self._state = active_alarms[0] + else: + self._available = False else: self._state = data[self._type] diff --git a/requirements_all.txt b/requirements_all.txt index c80ec2dd9ca..652605e7e71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ fritzconnection==1.2.0 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.13 +garminconnect==0.1.16 # homeassistant.components.geizhals geizhals==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15a35929ac..dccc1c0cdc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -305,7 +305,7 @@ foobot_async==0.3.2 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.13 +garminconnect==0.1.16 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed