Skip to content

Commit

Permalink
sensor updates and nightscout uploader (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
sedy89 authored Feb 7, 2024
1 parent eae2c9e commit 8dfee4f
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 46 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Carelink Integration - Home Assistant

Custom component for Home Assistant to interact the [Carelink platform by Medtronic](https://carelink.minimed.eu). The api is mostly the works of [@ondrej1024](https://github.com/ondrej1024) who made
Custom component for Home Assistant to interact the [Carelink platform by Medtronic](https://carelink.minimed.eu) with integrated Nightscout uploader. The api is mostly the works of [@ondrej1024](https://github.com/ondrej1024) who made
the [Python port](https://github.com/ondrej1024/carelink-python-client) from another JAVA api.

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration)
Expand Down Expand Up @@ -35,6 +35,21 @@ In order to authenticate to the Carelink server, the Carelink client needs a val
- Select option "Search Cookies: carelink.minimed.eu"
- Copy value of auth temp token and use it as Session token for initial setup of the Homeassistant Carelink integration

### Nightscout
To use the Nightscout uploader, it is mandatory to provide the Nightscout URL and the Nightscout API secret.
The Nightscout uploader can upload all SG data and add Treatments with the amount of carbs and insulin.
In order to be able to show the active insulin reported by the pump, the remaining reservoir amount parameter of the nightscout pump plugin has been reused.
![grafik](https://github.com/sedy89/Home-Assistant-Carelink/assets/65983953/2b0297b9-f33f-40ab-89e1-6cef69bf0445)

#### Uploaded data
- DeviceStatus
- Glucose entries
- Basal
- Bolus
- AutoBolus
- Alarms
- Alerts
- Messages

## Enable debug logging

Expand Down
69 changes: 54 additions & 15 deletions custom_components/carelink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
)

from .api import CarelinkClient
from .nightscout_uploader import NightscoutUploader

from .const import (
CLIENT,
UPLOADER,
DOMAIN,
COORDINATOR,
UNAVAILABLE,
Expand All @@ -31,8 +33,10 @@
SENSOR_KEY_SENSOR_DURATION_MINUTES,
SENSOR_KEY_LASTSG_MGDL,
SENSOR_KEY_LASTSG_MMOL,
SENSOR_KEY_UPDATE_TIMESTAMP,
SENSOR_KEY_LASTSG_TIMESTAMP,
SENSOR_KEY_LASTSG_TREND,
SENSOR_KEY_SG_DELTA,
SENSOR_KEY_RESERVOIR_LEVEL,
SENSOR_KEY_RESERVOIR_AMOUNT,
SENSOR_KEY_RESERVOIR_REMAINING_UNITS,
Expand Down Expand Up @@ -106,6 +110,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {CLIENT: carelink_client}

if "nightscout_url" in config and "nightscout_api" in config:
nightscout_uploader = NightscoutUploader(
config["nightscout_url"],
config["nightscout_api"]
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id].update({UPLOADER: nightscout_uploader})

coordinator = CarelinkCoordinator(hass, entry, update_interval=SCAN_INTERVAL)

await coordinator.async_config_entry_first_refresh()
Expand Down Expand Up @@ -135,9 +146,13 @@ def __init__(self, hass: HomeAssistant, entry, update_interval: timedelta):

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)

self.uploader = None
self.client = hass.data[DOMAIN][entry.entry_id][CLIENT]
self.timezone = hass.config.time_zone

if UPLOADER in hass.data[DOMAIN][entry.entry_id]:
self.uploader = hass.data[DOMAIN][entry.entry_id][UPLOADER]

async def _async_update_data(self):

data = {}
Expand All @@ -148,6 +163,9 @@ async def _async_update_data(self):
recent_data = await self.client.get_recent_data()
if recent_data is None:
recent_data = dict()
else:
if self.uploader:
await self.uploader.send_recent_data(recent_data)
try:
if recent_data is not None and "clientTimeZoneName" in recent_data:
client_timezone = recent_data["clientTimeZoneName"]
Expand All @@ -170,31 +188,31 @@ async def _async_update_data(self):

_LOGGER.debug("Using timezone %s", DEFAULT_TIME_ZONE)

recent_data["lastSG"] = recent_data.setdefault("lastSG", {})

recent_data["sLastSensorTime"] = recent_data.setdefault("sLastSensorTime", "")
recent_data["activeInsulin"] = recent_data.setdefault("activeInsulin", {})
recent_data["basal"] = recent_data.setdefault("basal", {})
recent_data["lastAlarm"] = recent_data.setdefault("lastAlarm", {})
recent_data["markers"] = recent_data.setdefault("markers", [])
recent_data["sgs"] = recent_data.setdefault("sgs", [])

if "datetime" in recent_data["lastSG"]:
# Last Glucose level sensors
# Last Update fetch

last_sg = recent_data["lastSG"]
if recent_data["sLastSensorTime"]:
date_time_local = convert_date_to_isodate(recent_data["sLastSensorTime"])
data[SENSOR_KEY_UPDATE_TIMESTAMP] = date_time_local.replace(tzinfo=timezone)

date_time_local = convert_date_to_isodate(last_sg["datetime"])
# Last Glucose level sensors

# Update glucose data only if data was logged. Otherwise, keep the old data and
# update the latest sensor state because it probably changed to an error state
if last_sg["sg"] > 0:
data[SENSOR_KEY_LASTSG_MMOL] = float(round(last_sg["sg"] * 0.0555, 2))
data[SENSOR_KEY_LASTSG_MGDL] = last_sg["sg"]
current_sg = get_sg(recent_data["sgs"], 0)
prev_sg = get_sg(recent_data["sgs"], 1)

if current_sg:
date_time_local = convert_date_to_isodate(current_sg["datetime"])
data[SENSOR_KEY_LASTSG_TIMESTAMP] = date_time_local.replace(tzinfo=timezone)
else:
data[SENSOR_KEY_LASTSG_MMOL] = UNAVAILABLE
data[SENSOR_KEY_LASTSG_MGDL] = UNAVAILABLE
data[SENSOR_KEY_LASTSG_TIMESTAMP] = UNAVAILABLE
data[SENSOR_KEY_LASTSG_MMOL] = float(round(current_sg["sg"] * 0.555, 2))
data[SENSOR_KEY_LASTSG_MGDL] = current_sg["sg"]
if prev_sg:
data[SENSOR_KEY_SG_DELTA] = (float(current_sg["sg"]) - float(prev_sg["sg"]))

# Sensors

Expand Down Expand Up @@ -420,6 +438,27 @@ async def _async_update_data(self):

return data

def get_sg(sgs: list, pos: int) -> dict:
"""Retrieve previous sg from list"""

try:
array = [sg for sg in sgs if "sensorState" in sg.keys() and sg["sensorState"] == "NO_ERROR_MESSAGE"]
sorted_array = sorted(
array,
key=lambda x: convert_date_to_isodate(x["datetime"]),
reverse=True,
)

if len(sorted_array) > pos:
return sorted_array[pos]
else:
return None
except Exception as error:
_LOGGER.error(
"the sg data could not be tracked correctly. A unknown error happened while parsing the data.",
error,
)
return None

def get_last_marker(marker_type: str, markers: list) -> dict:
"""Retrieve last marker from type in 24h marker list"""
Expand Down
38 changes: 14 additions & 24 deletions custom_components/carelink/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@

import argparse
import asyncio
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import json
import logging
import time
import os
import base64
from urllib.parse import parse_qsl, urlparse, urlunparse

import httpx

Expand Down Expand Up @@ -340,7 +339,7 @@ async def __execute_login_procedure(self):

async def __checkAuthorizationToken(self):
if self.__carelink_auth_token == None:
printdbg("No initial token found")
printdbg("No token found")
return False
try:
# Decode json web token payload
Expand All @@ -358,18 +357,18 @@ async def __checkAuthorizationToken(self):
token_validto = payload_json["exp"]
token_validto -= 600
except:
printdbg("Malformed initial token")
printdbg("Malformed token")
return False

# Save expiration time
self.__auth_token_validto = datetime.utcfromtimestamp(token_validto).strftime('%a %b %d %H:%M:%S UTC %Y')
self.__auth_token_validto = datetime.fromtimestamp(token_validto, tz=timezone.utc).strftime('%a %b %d %H:%M:%S UTC %Y')
# Check expiration time stamp
tdiff = token_validto - time.time()
if tdiff < 0:
printdbg("Initial token has expired %ds ago" % abs(tdiff))
printdbg("Token has expired %ds ago" % abs(tdiff))
return False

printdbg("Initial token expires in %ds (%s)" % (tdiff,self.__auth_token_validto))
printdbg("Token expires in %ds (%s)" % (tdiff,self.__auth_token_validto))
return True

async def __refreshToken(self, token):
Expand Down Expand Up @@ -407,20 +406,20 @@ async def __get_authorization_token(self):
printdbg("No valid token")
return None

if (datetime.strptime(auth_token_validto, '%a %b %d %H:%M:%S UTC %Y') - datetime.utcnow()) < timedelta(seconds=AUTH_EXPIRE_DEADLINE_MINUTES*60):
if (datetime.strptime(auth_token_validto, '%a %b %d %H:%M:%S UTC %Y').replace(tzinfo=timezone.utc) - datetime.now(tz=timezone.utc)) < timedelta(seconds=AUTH_EXPIRE_DEADLINE_MINUTES*60):
printdbg("Token is valid until " + self.__auth_token_validto)
if await self.__refreshToken(auth_token):
self.__carelink_auth_token = self.async_client.cookies[CARELINK_AUTH_TOKEN_COOKIE_NAME]
self.__auth_token_validto = self.async_client.cookies[CARELINK_TOKEN_VALIDTO_COOKIE_NAME]
printdbg("New Token created")
printdbg("New token is valid until " + self.__auth_token_validto)
try:
cookie=os.path.join(os.getcwd(), CON_CONTEXT_COOKIE)
printdbg(f"Cookiefile: {cookie}")
with open(cookie, "w") as file:
file.write(self.__carelink_auth_token)
printdbg("Writing new token to cookies.txt")
printdbg("Writing token to cookies.txt")
except:
printdbg("Failed to store refreshed token")
printdbg("New token is valid until " + self.__auth_token_validto)
printdbg("Failed to store token")
else:
# inital token is old, but updated token in file exists
try:
Expand Down Expand Up @@ -477,19 +476,10 @@ async def login(self):
def run_in_console(self):
"""If running this module directly, print all the values in the console."""
print("Reading...")
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(self.login(), return_exceptions=False))
asyncio.run(self.login())
if self.__logged_in:
loop = asyncio.get_event_loop()
results = loop.run_until_complete(
asyncio.gather(
self.get_recent_data(),
return_exceptions=False,
)
)

print(f"data: {results[0]}")

result = asyncio.run(self.get_recent_data())
print(f"data: {result}")

if __name__ == "__main__":

Expand Down
17 changes: 17 additions & 0 deletions custom_components/carelink/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from homeassistant.exceptions import HomeAssistantError

from .api import CarelinkClient
from .nightscout_uploader import NightscoutUploader
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
Expand All @@ -21,6 +22,8 @@
vol.Required("country"): str,
vol.Required("token"): str,
vol.Optional("patientId"): str,
vol.Optional("nightscout_url"): str,
vol.Optional("nightscout_api"): str,
}
)

Expand All @@ -42,6 +45,20 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
if not await client.login():
raise InvalidAuth

nightscout_url = None
nightscout_api = None
if "nightscout_url" in data:
nightscout_url = data["nightscout_url"]
if "nightscout_api" in data:
nightscout_api = data["nightscout_api"]

if nightscout_api and nightscout_url:
uploader = NightscoutUploader(
data["nightscout_url"], data["nightscout_api"]
)
if not await uploader.reachServer():
raise ConnectionError

return {"title": "Carelink"}


Expand Down
23 changes: 22 additions & 1 deletion custom_components/carelink/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
DOMAIN = "carelink"
CLIENT = "carelink_client"
COORDINATOR = "coordinator"
UPLOADER = "nightscout_uploader"

SENSOR_KEY_LASTSG_MMOL = "last_sg_mmol"
SENSOR_KEY_LASTSG_MGDL = "last_sg_mgdl"
SENSOR_KEY_UPDATE_TIMESTAMP = "last_update_timestamp"
SENSOR_KEY_LASTSG_TIMESTAMP = "last_sg_timestamp"
SENSOR_KEY_LASTSG_TREND = "last_sg_trend"
SENSOR_KEY_SG_DELTA = "last_sg_delta"
SENSOR_KEY_PUMP_BATTERY_LEVEL = "pump_battery_level"
SENSOR_KEY_SENSOR_BATTERY_LEVEL = "sensor_battery_level"
SENSOR_KEY_CONDUIT_BATTERY_LEVEL = "conduit_battery_status"
Expand Down Expand Up @@ -109,7 +112,16 @@
),
SensorEntityDescription(
key=SENSOR_KEY_LASTSG_TIMESTAMP,
name="Last sensor update",
name="Last glucose update",
native_unit_of_measurement=None,
state_class=None,
device_class=SensorDeviceClass.TIMESTAMP,
icon="mdi:clock",
entity_category=None,
),
SensorEntityDescription(
key=SENSOR_KEY_UPDATE_TIMESTAMP,
name="Last update",
native_unit_of_measurement=None,
state_class=None,
device_class=SensorDeviceClass.TIMESTAMP,
Expand All @@ -125,6 +137,15 @@
icon="mdi:chart-line",
entity_category=None,
),
SensorEntityDescription(
key=SENSOR_KEY_SG_DELTA,
name="Last glucose delta",
native_unit_of_measurement=None,
state_class=SensorStateClass.MEASUREMENT,
device_class=None,
icon="mdi:plus-minus-variant",
entity_category=None,
),
SensorEntityDescription(
key=SENSOR_KEY_PUMP_BATTERY_LEVEL,
name="Pump battery level",
Expand Down
Loading

0 comments on commit 8dfee4f

Please sign in to comment.