From 8dfee4fa594ab6114b200ef11cf896061a33d4c3 Mon Sep 17 00:00:00 2001 From: sedy <65983953+sedy89@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:39:19 +0100 Subject: [PATCH] sensor updates and nightscout uploader (#61) --- README.md | 17 +- custom_components/carelink/__init__.py | 69 ++- custom_components/carelink/api.py | 38 +- custom_components/carelink/config_flow.py | 17 + custom_components/carelink/const.py | 23 +- .../carelink/nightscout_uploader.py | 479 ++++++++++++++++++ custom_components/carelink/strings.json | 4 +- .../carelink/translations/de.json | 4 +- .../carelink/translations/en.json | 4 +- .../carelink/translations/fr.json | 4 +- .../carelink/translations/nl.json | 4 +- 11 files changed, 617 insertions(+), 46 deletions(-) create mode 100644 custom_components/carelink/nightscout_uploader.py diff --git a/README.md b/README.md index 9d997d7..f63b269 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/custom_components/carelink/__init__.py b/custom_components/carelink/__init__.py index ab77fce..f5145c3 100644 --- a/custom_components/carelink/__init__.py +++ b/custom_components/carelink/__init__.py @@ -15,9 +15,11 @@ ) from .api import CarelinkClient +from .nightscout_uploader import NightscoutUploader from .const import ( CLIENT, + UPLOADER, DOMAIN, COORDINATOR, UNAVAILABLE, @@ -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, @@ -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() @@ -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 = {} @@ -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"] @@ -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 @@ -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""" diff --git a/custom_components/carelink/api.py b/custom_components/carelink/api.py index 447f275..b4297ab 100644 --- a/custom_components/carelink/api.py +++ b/custom_components/carelink/api.py @@ -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 @@ -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 @@ -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): @@ -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: @@ -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__": diff --git a/custom_components/carelink/config_flow.py b/custom_components/carelink/config_flow.py index 7227612..8b13dd7 100644 --- a/custom_components/carelink/config_flow.py +++ b/custom_components/carelink/config_flow.py @@ -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__) @@ -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, } ) @@ -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"} diff --git a/custom_components/carelink/const.py b/custom_components/carelink/const.py index 7f99d59..5fad921 100644 --- a/custom_components/carelink/const.py +++ b/custom_components/carelink/const.py @@ -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" @@ -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, @@ -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", diff --git a/custom_components/carelink/nightscout_uploader.py b/custom_components/carelink/nightscout_uploader.py new file mode 100644 index 0000000..bf9f748 --- /dev/null +++ b/custom_components/carelink/nightscout_uploader.py @@ -0,0 +1,479 @@ +import argparse +import asyncio +from datetime import datetime +import pytz +import hashlib +import json +import logging + +import httpx + +NS_USER_AGENT= "Home Assistant Carelink" +DEBUG = False + +_LOGGER = logging.getLogger(__name__) + + +def printdbg(msg): + """Debug logger/print function""" + _LOGGER.debug("Nightscout API: %s", msg) + + if DEBUG: + print(msg) + +class NightscoutUploader: + """Nightscout Uploader library""" + + def __init__( + self, + nightscout_url, + nightscout_secret + ): + + # Nightscout info + self.__nightscout_url = nightscout_url.lower().rstrip('/') + self.__hashedSecret = hashlib.sha1(nightscout_secret.encode('utf-8')).hexdigest() + self.__is_reachable=False + + self._async_client = None + self.__common_headers = { + # Common browser headers + 'API-SECRET' : self.__hashedSecret, + 'Content-Type': "application/json", + 'User-Agent': NS_USER_AGENT, + 'Accept': 'application/json', + } + + @property + def async_client(self): + """Return the httpx client.""" + if not self._async_client: + self._async_client = httpx.AsyncClient() + + return self._async_client + + async def fetch_async(self, url, headers, params=None): + """Perform an async get request.""" + response = await self.async_client.get( + url, + headers=headers, + params=params, + follow_redirects=True, + timeout=30, + ) + return response + + async def post_async(self, url, headers, data=None, params=None): + """Perform an async post request.""" + response = await self.async_client.post( + url, + headers=headers, + params=params, + data=data, + follow_redirects=True, + timeout=30, + ) + return response + + def __get_carbs(self, input_insulin, input_meal): + result = dict() + for marker in input_insulin: + for entry in marker.items(): + for meal in input_meal: + if entry[0] in meal: + result[entry[0]]={"insulin" : entry[1] , "carb" : meal[entry[0]]} + return result + + def __get_dict_values(self, input, key, value): + result = list() + for marker in input: + markerDict=dict() + if key in marker and value in marker: + markerDict[marker[key]]=marker[value] + result.append(markerDict) + return result + + def __get_treatments(self, input, key, value): + result = list() + for marker in input: + markerDict=dict() + isType=False + for entry in marker.items(): + if key == entry[0] and entry[1] == value: + isType=True + break + if isType: + for entry in marker.items(): + markerDict[entry[0]]=entry[1] + result.append(markerDict) + return result + + def __getDataStringFromIso(self, time, tz): + dt = tz.localize(datetime.fromisoformat(time.replace(".000-00:00", ""))) + timestamp = dt.timestamp() + date = int(timestamp * 1000) + date_string = dt.isoformat() + return date, date_string + + def __getDataString(self, time, tz): + dt = datetime.strptime(time,"%Y-%m-%dT%H:%M:%S.%fZ") + dt = tz.localize(dt) + timestamp = dt.timestamp() + date = int(timestamp * 1000) + date_string = dt.isoformat() + return date, date_string + + async def __setDeviceStatus(self, rawdata): + printdbg("__setDeviceStatus()") + try: + data = self.__getDeviceStatus(rawdata) + except Exception as error: + printdbg(f"__setDeviceStatus() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "devicestatus" + ) + + async def __setSGS(self, rawdata, tz): + printdbg("__setSGS()") + try: + data = self.__getSGS(rawdata, tz) + except Exception as error: + printdbg(f"__setSGS() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "entries" + ) + + async def __setBasal(self, rawdata, tz): + printdbg("__setBasal()") + try: + data = self.__getBasal(rawdata, tz) + except Exception as error: + printdbg(f"__setBasal() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __setBolus(self, rawdata, tz): + printdbg("__setBolus()") + try: + data = self.__getBolus(rawdata, tz) + except Exception as error: + printdbg(f"__setBolus() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __setAutoBolus(self, rawdata, tz): + printdbg("__setAutoBolus()") + try: + data = self.__getAutoBolus(rawdata, tz) + except Exception as error: + printdbg(f"__setAutoBolus() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __setAlarms(self, rawdata, tz): + printdbg("__setAlarms()") + try: + data = self.__getAlarms(rawdata, tz) + except Exception as error: + printdbg(f"__setAlarms() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __setMsgs(self, rawdata, tz): + printdbg("__setMsgs()") + try: + data = self.__getMsgs(rawdata, tz) + except Exception as error: + printdbg(f"__setMsgs() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __setAlerts(self, rawdata, tz): + printdbg("__setAlerts()") + try: + data = self.__getAlerts(rawdata, tz) + except Exception as error: + printdbg(f"__setAlerts() exeption: {error}") + data = [] + return await self.__set_data( + self.__nightscout_url, data, "treatments" + ) + + async def __set_data(self, host, data, data_type): + printdbg("__set_data()") + if len(data) == 0: + return False + success = True + url = f"{host}/api/v1/{data_type}" + try: + for entry in data: + response = await self.post_async(url, headers=self.__common_headers, data=json.dumps(entry)) + if not response.status_code == 200: + raise ValueError("__set_data() session response is not OK " + str(response.status_code)) + except Exception as error: + printdbg(f"__set_data() failed: exception {error}") + success = False + return success + + def __getMsgs(self, rawdata, tz): + msgs=self.__get_treatments(rawdata["clearedNotifications"], "type", "MESSAGE") + return self.__getMsgEntries(msgs, tz) + + def __getAlarms(self, rawdata, tz): + alarms=self.__get_treatments(rawdata["clearedNotifications"], "type", "ALARM") + return self.__getMsgEntries(alarms, tz) + + def __getAlerts(self, rawdata, tz): + alerts=self.__get_treatments(rawdata["clearedNotifications"], "type", "ALERT") + return self.__getMsgEntries(alerts, tz) + + def __getMsgEntries(self, raw, tz): + result = list() + for msg in raw: + date, date_string=self.__getDataStringFromIso(msg["dateTime"], tz) + if "sg" in msg.keys() and int(msg["sg"]) < 400: + result.append(dict( + timestamp=date, + enteredBy=NS_USER_AGENT, + created_at=date_string, + eventType="Note", + glucoseType="sensor", + glucose=float(msg["sg"]), + notes=self.__getNote(msg["messageId"]) + )) + else: + result.append(dict( + timestamp=date, + enteredBy=NS_USER_AGENT, + created_at=date_string, + eventType="Note", + notes=self.__getNote(msg["messageId"]) + )) + return result + + def __getNote(self, msg): + return msg.replace("BC_SID_", "").replace("BC_MESSAGE_", "") + + def __getBolus(self, raw, tz): + meal=self.__get_treatments(raw, "type", "MEAL") + meal_carbs = self.__get_dict_values(meal, "dateTime", "amount") + insulin=self.__get_treatments(raw, "type", "INSULIN") + recomm=self.__get_treatments(insulin, "activationType", "RECOMMENDED") + recomm_insulin=self.__get_dict_values(recomm, "dateTime", "deliveredFastAmount") + bolus_carbs=self.__get_carbs(recomm_insulin, meal_carbs) + return self.__getMealEntries(bolus_carbs, tz) + + def __getAutoBolus(self, raw, tz): + insulin=self.__get_treatments(raw, "type", "INSULIN") + autocorr=self.__get_treatments(insulin, "activationType", "AUTOCORRECTION") + return self.__getAutoBolusEntries(autocorr, tz) + + def __getBasal(self, raw, tz): + basal=self.__get_treatments(raw, "type", "AUTO_BASAL_DELIVERY") + return self.__getBasalEntries(basal, tz) + + def __getSGS(self, raw, tz): + sgs=self.__get_treatments(raw, "sensorState", "NO_ERROR_MESSAGE") + return self.__getSGSEntries(sgs, tz) + + def __getBasalEntries(self, raw, tz): + result = list() + for basal in raw: + _,date_string=self.__getDataStringFromIso(basal["dateTime"], tz) + result.append(dict( + enteredBy=NS_USER_AGENT, + eventType="Temp Basal", + duration=5, + absolute=basal["bolusAmount"], + created_at=date_string, + )) + return result + + def __getAutoBolusEntries(self, raw, tz): + result = list() + for corr in raw: + date, date_string=self.__getDataStringFromIso(corr["dateTime"], tz) + result.append(dict( + device=NS_USER_AGENT, + timestamp=date, + enteredBy=NS_USER_AGENT, + created_at=date_string, + eventType="Correction Bolus", + insulin=corr["deliveredFastAmount"], + )) + return result + + def __getMealEntries(self, meals, tz): + result = list() + for time, info in meals.items(): + date, date_string=self.__getDataStringFromIso(time, tz) + result.append(dict( + timestamp=date, + enteredBy=NS_USER_AGENT, + created_at=date_string, + eventType="Meal", + glucoseType="sensor", + carbs=info["carb"], + insulin=info["insulin"], + )) + return result + + def __ns_trend(self, present, past): + if present["sg"] == 0 or past["sg"] == 0: + return "null", "null" + delta = present["sg"] - past["sg"] + if delta == 0: + trend = "Flat" + elif delta < -30: + trend = "TripleDown" + elif delta < -15: + trend = "DoubleDown" + elif delta < -5: + trend = "SingleDown" + elif delta < 0: + trend = "FortyFiveDown" + elif delta > 30: + trend = "TripleUp" + elif delta > 15: + trend = "DoubleUp" + elif delta > 5: + trend = "SingleUp" + elif delta > 0: + trend = "FortyFiveUp" + else: + trend = "NOT COMPUTABLE" + return trend, delta + + def __getDeviceStatus(self, rawdata): + return [dict( + device=rawdata["pumpModelNumber"], + pump=dict( + battery=dict( + status=rawdata["conduitBatteryStatus"], + voltage=rawdata["conduitBatteryLevel"]), + reservoir=rawdata["activeInsulin"]["amount"], + status=dict( + status=rawdata["systemStatusMessage"], + suspended=rawdata["medicalDeviceSuspended"])))] + + def __getSGSEntries(self, sgs, tz): + result = list() + trend, delta="null", "null" + for count, sg in enumerate(sgs): + try: + trend, delta = self.__ns_trend(sgs[count], sgs[count-1]) + except Exception: + pass + date, date_string=self.__getDataString(sg["datetime"], tz) + result.append(dict( + device=NS_USER_AGENT, + direction=trend, + delta=delta, + type='sgv', + sgv=float(sg["sg"]), + date=date, + dateString=date_string, + noise=1)) + return result + + async def __slice_recent_data_for_transmission(self, recent_data): + # get timezone + local_tz = datetime.now().astimezone().tzname() + tz= pytz.timezone(local_tz) + # Sending device status + response = await self.__setDeviceStatus(recent_data) + if response: + printdbg("sending device status was ok") + # Sending all SGS + response = await self.__setSGS(recent_data["sgs"], tz) + if response: + printdbg("sending SGS entries was ok") + # Sending Basal + response = await self.__setBasal(recent_data["markers"], tz) + if response: + printdbg("sending basal was ok") + # Sending all Bolus + response = await self.__setBolus(recent_data["markers"], tz) + if response: + printdbg("sending meal bolus was ok") + # Sending all auto Bolus + response = await self.__setAutoBolus(recent_data["markers"], tz) + if response: + printdbg("sending auto bolus was ok") + response = await self.__setAlarms(recent_data["notificationHistory"], tz) + if response: + printdbg("sending alarm notifications was ok") + response = await self.__setMsgs(recent_data["notificationHistory"], tz) + if response: + printdbg("sending message notifications was ok") + response = await self.__setAlerts(recent_data["notificationHistory"], tz) + if response: + printdbg("sending alert notifications was ok") + + + # Periodic upload to Nightscout + async def send_recent_data( + self, recent_data + ): + printdbg("__send_recent_data()") + await self.__slice_recent_data_for_transmission(recent_data) + + async def __test_server_connection(self): + url = f"{self.__nightscout_url}/api/v1/devicestatus.json" + response = await self.fetch_async( + url, headers=self.__common_headers, params={} + ) + if response.status_code == 200: + self.__is_reachable = True + + # verify connection + async def reachServer(self): + """perform reach server check""" + if not self.__is_reachable: + await self.__test_server_connection() + return self.__is_reachable + + def run_in_console(self, data): + """If running this module directly""" + print("Sending...") + asyncio.run(self.reachServer()) + if self.__is_reachable: + asyncio.run(self.send_recent_data(data)) + +if __name__ == "__main__": + test_data={ + #fill me + } + parser = argparse.ArgumentParser( + description="Simulate upload process to Nightscout with testdata" + ) + parser.add_argument("-u", "--url", dest="url", help="Nightscout URL") + parser.add_argument("-s", "--secret", dest="secret", help="Nightscout API Secret" + ) + + args = parser.parse_args() + + if args.url is None: + raise ValueError("URL is required") + + if args.secret is None: + raise ValueError("Secret is required") + + TESTAPI = NightscoutUploader( + nightscout_url=args.url, + nightscout_secret=args.secret + ) + + TESTAPI.run_in_console(test_data) \ No newline at end of file diff --git a/custom_components/carelink/strings.json b/custom_components/carelink/strings.json index 1a198a9..e1e5a63 100644 --- a/custom_components/carelink/strings.json +++ b/custom_components/carelink/strings.json @@ -8,7 +8,9 @@ "data": { "country": "[%key:common::config_flow::data::country%]", "token": "[%key:common::config_flow::data::token%]", - "patientId": "[%key:common::config_flow::data::patientId%]" + "patientId": "[%key:common::config_flow::data::patientId%]", + "nightscout_url": "[%key:common::config_flow::data::nightscout_url%]", + "nightscout_api": "[%key:common::config_flow::data::nightscout_api%]" } } }, diff --git a/custom_components/carelink/translations/de.json b/custom_components/carelink/translations/de.json index 5a3826c..e2e59b3 100644 --- a/custom_components/carelink/translations/de.json +++ b/custom_components/carelink/translations/de.json @@ -15,7 +15,9 @@ "data": { "token": "Session Token", "country": "Land", - "patientId": "Patienten-ID (optional)" + "patientId": "Patienten-ID (optional)", + "nightscout_url": "Nightscout URL (optional)", + "nightscout_api": "Nightscout API Secret (optional)" } } } diff --git a/custom_components/carelink/translations/en.json b/custom_components/carelink/translations/en.json index 2640cd1..6489465 100644 --- a/custom_components/carelink/translations/en.json +++ b/custom_components/carelink/translations/en.json @@ -15,7 +15,9 @@ "data": { "country": "Country", "token": "Session Token", - "patientId": "Patient ID (Optional)" + "patientId": "Patient ID (Optional)", + "nightscout_url": "Nightscout URL (Optional)", + "nightscout_api": "Nightscout API Secret (Optional)" } } } diff --git a/custom_components/carelink/translations/fr.json b/custom_components/carelink/translations/fr.json index f4f83d6..4613bf9 100644 --- a/custom_components/carelink/translations/fr.json +++ b/custom_components/carelink/translations/fr.json @@ -15,7 +15,9 @@ "data": { "country": "Pays", "token": "Session Token", - "patientId": "ID patient (facultatif)" + "patientId": "ID patient (facultatif)", + "nightscout_url": "Nightscout URL (facultatif)", + "nightscout_api": "Nightscout API Secret (facultatif)" } } } diff --git a/custom_components/carelink/translations/nl.json b/custom_components/carelink/translations/nl.json index 4bd231b..baa5c8a 100644 --- a/custom_components/carelink/translations/nl.json +++ b/custom_components/carelink/translations/nl.json @@ -15,7 +15,9 @@ "data": { "country": "Land", "token": "Session Token", - "patientId": "Patient ID (Optioneel)" + "patientId": "Patient ID (Optioneel)", + "nightscout_url": "Nightscout URL (Optioneel)", + "nightscout_api": "Nightscout API Secret (Optioneel)" } } }