pax_global_header00006660000000000000000000000064141532404540014514gustar00rootroot0000000000000052 comment=a4a87768173095d7d22745a06a015cf2202345a0 marciogranzotto-py-nightscout-a4a8776/000077500000000000000000000000001415324045400201145ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/.devcontainer/000077500000000000000000000000001415324045400226535ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/.devcontainer/Dockerfile000066400000000000000000000023441415324045400246500ustar00rootroot00000000000000#------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- # Update the VARIANT arg in devcontainer.json to pick a Python version: 3, 3.8, 3.7, 3.6 # To fully customize the contents of this image, use the following Dockerfile instead: # https://github.com/microsoft/vscode-dev-containers/tree/v0.128.0/containers/python-3/.devcontainer/base.Dockerfile ARG VARIANT="3" FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} # [Optional] If your requirements rarely change, uncomment this section to add them to the image. # # COPY requirements.txt /tmp/pip-tmp/ # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ # && rm -rf /tmp/pip-tmp # [Optional] Uncomment this section to install additional packages. # # RUN apt-get update \ # && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends marciogranzotto-py-nightscout-a4a8776/.devcontainer/devcontainer.json000066400000000000000000000040541415324045400262320ustar00rootroot00000000000000// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.128.0/containers/python-3 { "name": "Python 3.9", "build": { "dockerfile": "Dockerfile", "context": "..", // Update 'VARIANT' to pick a Python version. Rebuild the container // if it already exists to update. Available variants: 3, 3.6, 3.7, 3.8 "args": { "VARIANT": "3.9" } }, // Set *default* container specific settings.json values on container create. "settings": { "python.pythonPath": "/usr/local/bin/python", "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", "terminal.integrated.profiles.linux": { "zsh": { "path": "/usr/bin/zsh" } }, "terminal.integrated.defaultProfile.linux": "zsh" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", "davidanson.vscode-markdownlint", "yzhang.markdown-all-in-one", "spmeesseman.vscode-taskexplorer" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "pip install -r requirements.txt -U && pip install -r requirements_test.txt -U && pip install ." // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. // "remoteUser": "vscode" } marciogranzotto-py-nightscout-a4a8776/.github/000077500000000000000000000000001415324045400214545ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/.github/workflows/000077500000000000000000000000001415324045400235115ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/.github/workflows/python-package.yml000066400000000000000000000026761415324045400271610ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Install module run: | python -m pip install . - name: Test with pytest run: | pytest marciogranzotto-py-nightscout-a4a8776/.github/workflows/python-publish.yml000066400000000000000000000020441415324045400272210ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} marciogranzotto-py-nightscout-a4a8776/.gitignore000066400000000000000000000024741415324045400221130ustar00rootroot00000000000000config/* config2/* tests/testing_config/deps # Hide sublime text stuff *.sublime-project *.sublime-workspace # Hide some OS X stuff .DS_Store .AppleDouble .LSOverride Icon # Thumbnails ._* # IntelliJ IDEA .idea *.iml # pytest .pytest_cache .cache # GITHUB Proposed Python stuff: *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 pip-wheel-metadata .pypirc # Logs *.log pip-log.txt # Unit test / coverage reports .coverage .tox coverage.xml nosetests.xml htmlcov/ test-reports/ test-results.xml test-output.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .python-version # emacs auto backups *~ *# *.orig # venv stuff pyvenv.cfg pip-selfcheck.json venv .venv Pipfile* share/* Scripts/ # vimmy stuff *.swp *.swo tags ctags.tmp # vagrant stuff virtualization/vagrant/setup_done virtualization/vagrant/.vagrant virtualization/vagrant/config # Visual Studio Code .vscode/* !.vscode/cSpell.json !.vscode/extensions.json !.vscode/tasks.json .env # Built docs docs/build # Windows Explorer desktop.ini /.vs/* # mypy /.mypy_cache/* /.dmypy.json # Secrets .lokalise_token # monkeytype monkeytype.sqlite3 # This is left behind by Azure Restore Cache tmp_cache # python-language-server / Rope .ropeproject marciogranzotto-py-nightscout-a4a8776/.travis.yml000066400000000000000000000002531415324045400222250ustar00rootroot00000000000000language: python python: - '3.6' # command to install dependencies install: - pip install . - pip install -r requirements.txt # command to run tests script: py.test marciogranzotto-py-nightscout-a4a8776/.vscode/000077500000000000000000000000001415324045400214555ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/.vscode/extensions.json000066400000000000000000000001101415324045400245370ustar00rootroot00000000000000{ "recommendations": ["esbenp.prettier-vscode", "ms-python.python"] } marciogranzotto-py-nightscout-a4a8776/README.md000066400000000000000000000007621415324045400214000ustar00rootroot00000000000000# Python Nightscout client [![PyPi](https://img.shields.io/pypi/v/py_nightscout)](https://pypi.org/project/py-nightscout/) A simple async python client for accessing data stored in [Nightscout](https://github.com/nightscout/cgm-remote-monitor) Based on [ps2/python-nightscout](https://github.com/ps2/python-nightscout) ## Example Usage Checkout the [example.py](example.py) file in the repository. You can add your Nigthscoult URL and `API_TOKEN` and run this script to start using the library.marciogranzotto-py-nightscout-a4a8776/example.py000066400000000000000000000047131415324045400221260ustar00rootroot00000000000000import asyncio import datetime from aiohttp import ClientError, ClientConnectorError, ClientResponseError import py_nightscout as nightscout import pytz NIGHTSCOUT_URL = 'https://your_nightscout_site.herokuapp.com' API_SECRET = '' async def main(): """Example of library usage.""" try: if API_SECRET: # To use authentication, use yout api secret: api = nightscout.Api(NIGHTSCOUT_URL, api_secret=API_SECRET) else: # You can use the api without authentication: api = nightscout.Api(NIGHTSCOUT_URL) status = await api.get_server_status() except ClientResponseError as error: raise RuntimeError("Received ClientResponseError") from error except (ClientError, ClientConnectorError, TimeoutError, OSError) as error: raise RuntimeError("Received client error or timeout") from error #### Glucose Values (SGVs) #### # Get last 10 entries: entries = await api.get_sgvs() print("SGV values in mg/dL:") print([entry.sgv for entry in entries]) print("SGV values in mmol/L:") print([entry.sgv_mmol for entry in entries]) # Specify time ranges: entries = await api.get_sgvs({'count':0, 'find[dateString][$gte]': '2017-03-07T01:10:26.000Z'}) print("\nSGV values on timerange:") print([entry.sgv for entry in entries]) ### Treatments #### # To fetch recent treatments (boluses, temp basals): treatments = await api.get_treatments() print("\nTreatments:") print([treatment.eventType for treatment in treatments]) ### Profiles profile_definition_set = await api.get_profiles() profile_definition = profile_definition_set.get_profile_definition_active_at(datetime.datetime.now(tz=pytz.UTC)) profile = profile_definition.get_default_profile() print("\nDuration of insulin action = %d" % profile.dia) five_thirty_pm = datetime.datetime(2017, 3, 24, 17, 30) five_thirty_pm = profile.timezone.localize(five_thirty_pm) print("\nScheduled basal rate at 5:30pm is = %f" % profile.basal.value_at_date(five_thirty_pm)) ### Server Status server_status = await api.get_server_status() print("\nserver status: %s" % server_status.status) ### Device Status print("\nDevices:") devices_status = await api.get_latest_devices_status() for device_key in devices_status: print("\t%s battery: %d%%" % (device_key, devices_status[device_key].uploader.battery)) asyncio.run(main())marciogranzotto-py-nightscout-a4a8776/py_nightscout/000077500000000000000000000000001415324045400230135ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/py_nightscout/.gitignore000066400000000000000000000025001415324045400250000ustar00rootroot00000000000000config/* config2/* tests/testing_config/deps # Hide sublime text stuff *.sublime-project *.sublime-workspace # Hide some OS X stuff .DS_Store .AppleDouble .LSOverride Icon # Thumbnails ._* # IntelliJ IDEA .idea *.iml # pytest .pytest_cache .cache # GITHUB Proposed Python stuff: *.py[cod] # C extensions *.so # Packages *.egg *.egg-info *.egg-info/ dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 pip-wheel-metadata # Logs *.log pip-log.txt # Unit test / coverage reports .coverage .tox coverage.xml nosetests.xml htmlcov/ test-reports/ test-results.xml test-output.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .python-version # emacs auto backups *~ *# *.orig # venv stuff pyvenv.cfg pip-selfcheck.json venv .venv Pipfile* share/* Scripts/ # vimmy stuff *.swp *.swo tags ctags.tmp # vagrant stuff virtualization/vagrant/setup_done virtualization/vagrant/.vagrant virtualization/vagrant/config # Visual Studio Code .vscode/* !.vscode/cSpell.json !.vscode/extensions.json !.vscode/tasks.json .env # Built docs docs/build # Windows Explorer desktop.ini /.vs/* # mypy /.mypy_cache/* /.dmypy.json # Secrets .lokalise_token # monkeytype monkeytype.sqlite3 # This is left behind by Azure Restore Cache tmp_cache # python-language-server / Rope .ropeproject marciogranzotto-py-nightscout-a4a8776/py_nightscout/__init__.py000066400000000000000000000002441415324045400251240ustar00rootroot00000000000000from .api import Api # noqa: F401 from .models import ( # noqa: F401 SGV, ProfileDefinition, ProfileDefinitionSet, ServerStatus, Treatment, ) marciogranzotto-py-nightscout-a4a8776/py_nightscout/api.py000066400000000000000000000123421415324045400241400ustar00rootroot00000000000000"""A library that provides a Python interface to Nightscout""" import hashlib from typing import Any, Callable, Optional from aiohttp import ClientSession, ClientTimeout from .models import SGV, ProfileDefinitionSet, ServerStatus, Treatment, DeviceStatus class Api(object): """A python interface into Nightscout Example usage: To create an instance of the nightscout.Api class, with no authentication: >>> import nightscout >>> api = nightscout.Api('https://yournightscoutsite.herokuapp.com') To use authentication, instantiate the nightscout.Api class with your api secret: >>> api = nightscout.Api('https://yournightscoutsite.herokuapp.com', api_secret='your api secret') To fetch recent sensor glucose values (SGVs): >>> entries = api.get_sgvs() >>> print([entry.sgv for entry in entries]) """ def __init__( self, server_url: str, api_secret: Optional[str] = None, session: Optional[ClientSession] = None, timeout: Optional[ClientTimeout] = None, ): """Instantiate a new Api object.""" self.server_url = server_url.strip("/") self._api_kwargs = {"headers": self.request_headers(api_secret)} if timeout: self._api_kwargs["timeout"] = timeout self._session = session def request_headers(self, api_secret: Optional[str] = None): headers = {"Content-Type": "application/json", "Accept": "application/json"} if api_secret: if api_secret.startswith("token="): headers["api-secret"] = api_secret else: headers["api-secret"] = hashlib.sha1(api_secret.encode("utf-8")).hexdigest() return headers async def get_sgvs(self, params={}) -> [SGV]: """Fetch sensor glucose values Args: params: Mongodb style query params. For example, you can do things like: get_sgvs({'count':0, 'find[dateString][$gte]': '2017-03-07T01:10:26.000Z'}) Returns: A list of SGV objects """ json = await self.__get("/api/v1/entries/sgv.json") return [SGV.new_from_json_dict(x) for x in json] async def get_server_status(self, params={}) -> ServerStatus: """Fetch server status Returns: The current server status """ json = await self.__get("/api/v1/status.json") return ServerStatus.new_from_json_dict(json) async def get_treatments(self, params={}) -> [Treatment]: """Fetch treatments Args: params: Mongodb style query params. For example, you can do things like: get_treatments({'count':0, 'find[timestamp][$gte]': '2017-03-07T01:10:26.000Z'}) Returns: A list of Treatments """ json = await self.__get("/api/v1/treatments.json") return [Treatment.new_from_json_dict(x) for x in json] async def get_profiles(self, params={}) -> [ProfileDefinitionSet]: """Fetch profiles Args: params: Mongodb style query params. For example, you can do things like: get_profiles({'count':0, 'find[startDate][$gte]': '2017-03-07T01:10:26.000Z'}) Returns: ProfileDefinitionSet """ json = await self.__get("/api/v1/profile.json") return ProfileDefinitionSet.new_from_json_array(json) async def get_devices_status(self, params={}) -> [DeviceStatus]: """Fetch devices status Args: params: Mongodb style query params. For example, you can do things like: get_profiles({'count':0, 'find[startDate][$gte]': '2017-03-07T01:10:26.000Z'}) Returns: ProfileDefinitionSet """ json = await self.__get("/api/v1/devicestatus.json") return [DeviceStatus.new_from_json_dict(x) for x in json] async def get_latest_devices_status(self, params={}) -> [DeviceStatus]: """Fetch devices status Args: params: Mongodb style query params. For example, you can do things like: get_profiles({'count':0, 'find[startDate][$gte]': '2017-03-07T01:10:26.000Z'}) Returns: ProfileDefinitionSet """ results = await self.get_devices_status(params) grouped = dict() for entry in results: grouped.setdefault(entry.device, []).append(entry) output = dict() for device_name in grouped: entries = grouped[device_name] entries.sort(key=lambda x: x.created_at, reverse=True) output[device_name] = entries[0] return output async def __get(self, path): async def get(session: ClientSession): async with session.get( f"{self.server_url}{path}", **self._api_kwargs ) as response: response.raise_for_status() return await response.json() return await self.__call(get) async def __call(self, handler: Callable[[ClientSession], Any]): if not self._session: async with ClientSession() as request_session: return await handler(request_session) else: return await handler(self._session) marciogranzotto-py-nightscout-a4a8776/py_nightscout/models.py000066400000000000000000000500041415324045400246470ustar00rootroot00000000000000from datetime import datetime, timedelta import dateutil.parser import pytz class BaseModel(object): def __init__(self, **kwargs): self.param_defaults = {} @classmethod def json_transforms(cls, json_data): pass @classmethod def new_from_json_dict(cls, data, **kwargs): json_data = data.copy() if kwargs: for key, val in kwargs.items(): json_data[key] = val cls.json_transforms(json_data) c = cls(**json_data) c._json = data return c class ServerStatus(BaseModel): """Server Info and Status Server side status, default settings and capabilities Attributes: status (string): Server status version (string): Server version name (string): Server name apiEnabled (boolean): If the API is enabled """ def __init__(self, **kwargs): self.param_defaults = { "status": None, "version": None, "name": None, "apiEnabled": None, "settings": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) class SGV(BaseModel): """Sensor Glucose Value Represents a single glucose measurement and direction at a specific time. Attributes: sgv (int): Glucose measurement value in mg/dl. sgv_mmol (int): Glucose measurement value in mmol/L. delta (float): Delta between current and previous value. date (datetime): The time of the measurementpa direction (string): One of ['DoubleUp', 'SingleUp', 'FortyFiveUp', 'Flat', 'FortyFiveDown', 'SingleDown', 'DoubleDown'] device (string): the source of the measurement. For example, 'share2', if pulled from Dexcom Share servers """ def __init__(self, **kwargs): self.param_defaults = { "sgv": None, "sgv_mmol": None, "delta": None, "delta_mmol": None, "date": None, "direction": None, "device": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) self.sgv_mmol = self.mgdlTommolL(self.sgv) self.delta_mmol = self.mgdlTommolL(self.delta) @classmethod def json_transforms(cls, json_data): if json_data.get("dateString"): json_data["date"] = dateutil.parser.parse(json_data["dateString"]) def mgdlTommolL(self, mgdl): return round(mgdl / 18, 1) class Treatment(BaseModel): """Nightscout Treatment Represents an entry in the Nightscout treatments store, such as boluses, carb entries, temp basals, etc. Many of the following attributes will be set to None, depending on the type of entry. Attributes: eventType (string): The event type. Examples: ['Temp Basal', 'Correction Bolus', 'Meal Bolus', 'BG Check'] timestamp (datetime): The time of the treatment insulin (float): The amount of insulin delivered programmed (float): The amount of insulin programmed. May differ from insulin if the pump was suspended before delivery was finished. carbs (int): Amount of carbohydrates in grams consumed rate (float): Rate of insulin delivery for a temp basal, in U/hr. duration (int): Duration in minutes for a temp basal. enteredBy (string): The person who gave the treatment if entered in Care Portal, or the device that fetched the treatment from the pump. glucose (int): Glucose value for a BG check, in mg/dl. """ def __init__(self, **kwargs): self.param_defaults = { "temp": None, "enteredBy": None, "eventType": None, "glucose": None, "glucoseType": None, "units": None, "device": None, "created_at": None, "timestamp": None, "absolute": None, "rate": None, "duration": None, "carbs": None, "insulin": None, "unabsorbed": None, "suspended": None, "type": None, "programmed": None, "foodType": None, "absorptionTime": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) def __repr__(self): return "%s %s" % (self.timestamp, self.eventType) @classmethod def json_transforms(cls, json_data): timestamp = json_data.get("timestamp") if timestamp: if type(timestamp) == int: json_data["timestamp"] = datetime.fromtimestamp( timestamp / 1000.0, pytz.utc ) else: json_data["timestamp"] = dateutil.parser.parse(timestamp) if json_data.get("created_at"): json_data["created_at"] = dateutil.parser.parse(json_data["created_at"]) class ScheduleEntry(BaseModel): """ScheduleEntry Represents a change point in one of the schedules on a Nightscout profile. Attributes: offset (timedelta): The start offset of the entry value (float): The value of the entry. """ def __init__(self, offset, value): self.offset = offset self.value = value @classmethod def new_from_json_dict(cls, data): seconds_offset = data.get("timeAsSeconds") if seconds_offset is None: hours, minutes = data.get("time").split(":") seconds_offset = int(hours) * 60 * 60 + int(minutes) * 60 offset_in_seconds = int(seconds_offset) return cls(timedelta(seconds=offset_in_seconds), float(data["value"])) class AbsoluteScheduleEntry(BaseModel): def __init__(self, start_date, value): self.start_date = start_date self.value = value def __repr__(self): return "%s = %s" % (self.start_date, self.value) class Schedule(object): """Schedule Represents a schedule on a Nightscout profile. """ def __init__(self, entries, timezone): self.entries = entries self.entries.sort(key=lambda e: e.offset) self.timezone = timezone # Expects a localized timestamp here def value_at_date(self, local_date): """Get scheduled value at given date Args: local_date: The datetime of interest. Returns: The value of the schedule at the given time. """ offset = local_date - local_date.replace( hour=0, minute=0, second=0, microsecond=0 ) return [e.value for e in self.entries if e.offset <= offset][-1] def between(self, start_date, end_date): """Returns entries between given dates as AbsoluteScheduleEntry objects Times passed in should be timezone aware. Times returned will have a tzinfo matching the schedule timezone. Args: start_date: The start datetime of the period to retrieve entries for. end_date: The end datetime of the period to retrieve entries for. Returns: An array of AbsoluteScheduleEntry objects. """ if start_date > end_date: return [] start_date = start_date.astimezone(self.timezone) end_date = end_date.astimezone(self.timezone) reference_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) start_offset = start_date - reference_date end_offset = start_offset + (end_date - start_date) if end_offset > timedelta(days=1): boundary_date = start_date + (timedelta(days=1) - start_offset) return self.between(start_date, boundary_date) + self.between( boundary_date, end_date ) start_index = 0 end_index = len(self.entries) for index, item in enumerate(self.entries): if start_offset >= item.offset: start_index = index if end_offset < item.offset: end_index = index break return [ AbsoluteScheduleEntry(reference_date + entry.offset, entry.value) for entry in self.entries[start_index:end_index] ] @classmethod def new_from_json_array(cls, data, timezone): entries = [ScheduleEntry.new_from_json_dict(d) for d in data] return cls(entries, timezone) class Profile(BaseModel): """Profile Represents a Nightscout profile. Attributes: dia (float): The duration of insulin action, in hours. carb_ratio (Schedule): A schedule of carb ratios, which are in grams/U. sens (Schedule): A schedule of insulin sensitivity values, which are in mg/dl/U. timezone (timezone): The timezone of the schedule. basal (Schedule): A schedule of basal rates, which are in U/hr. target_low (Schedule): A schedule the low end of the target range, in mg/dl. target_high (Schedule): A schedule the high end of the target range, in mg/dl. """ def __init__(self, **kwargs): self.param_defaults = { "dia": None, "carb_ratio": None, "carbs_hr": None, "delay": None, "sens": None, "timezone": None, "basal": None, "target_low": None, "target_high": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) @classmethod def json_transforms(cls, json_data): timezone = None if json_data.get("timezone"): timezone = pytz.timezone(json_data.get("timezone")) json_data["timezone"] = timezone if json_data.get("carbratio"): json_data["carbratio"] = Schedule.new_from_json_array( json_data.get("carbratio"), timezone ) if json_data.get("sens"): json_data["sens"] = Schedule.new_from_json_array( json_data.get("sens"), timezone ) if json_data.get("target_low"): json_data["target_low"] = Schedule.new_from_json_array( json_data.get("target_low"), timezone ) if json_data.get("target_high"): json_data["target_high"] = Schedule.new_from_json_array( json_data.get("target_high"), timezone ) if json_data.get("basal"): json_data["basal"] = Schedule.new_from_json_array( json_data.get("basal"), timezone ) if json_data.get("dia"): json_data["dia"] = float(json_data["dia"]) class ProfileDefinition(BaseModel): """ProfileDefinition Represents a Nightscout profile definition, which can have multiple named profiles. Attributes: startDate (datetime): The time these profiles start at. """ def __init__(self, **kwargs): self.param_defaults = { "defaultProfile": None, "store": None, "startDate": None, "created_at": None, "units": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) def get_default_profile(self): return self.store[self.defaultProfile] @classmethod def json_transforms(cls, json_data): if json_data.get("startDate"): json_data["startDate"] = dateutil.parser.parse(json_data["startDate"]) if json_data.get("created_at"): json_data["created_at"] = dateutil.parser.parse(json_data["created_at"]) if json_data.get("store"): store = {} for profile_name in json_data["store"]: store[profile_name] = Profile.new_from_json_dict( json_data["store"][profile_name] ) json_data["store"] = store class ProfileDefinitionSet(object): """ProfileDefinitionSet Represents a set of Nightscout profile definitions, each covering a range of time from its start time, to the start time of the next profile definition, or until now if there are no newer profile defitions. """ def __init__(self, profile_definitions): self.profile_definitions = profile_definitions self.profile_definitions.sort(key=lambda d: d.startDate) def get_profile_definition_active_at(self, date): """Get the profile definition active at a given datetime Args: date: The profile definition containing this time will be returned. Returns: A ProfileDefinition object valid for the specified time. """ return [d for d in self.profile_definitions if d.startDate <= date][-1] @classmethod def new_from_json_array(cls, data): defs = [ProfileDefinition.new_from_json_dict(d) for d in data] return cls(defs) class DeviceStatus(BaseModel): """DeviceStatus Represents a Device on Nightscout. For example a MiaoMiao reader. Attributes: device (string): Device type and hostname for example openaps://hostname. created_at (datetime): Created date. openaps (string): OpenAPS devicestatus record. loop (string): Loop devicestatus record. pump (PumpDevice): Pump device. uploader (UploaderBattery): Uploader device's battery. xdripjs (XDripJs): xDripJS device. """ def __init__(self, **kwargs): self.param_defaults = { "device": None, "created_at": None, "openaps": None, "loop": None, "pump": None, "uploader": None, "xdripjs": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) @classmethod def json_transforms(cls, json_data): if json_data.get("created_at"): json_data["created_at"] = dateutil.parser.parse(json_data["created_at"]) if json_data.get("pump"): json_data["pump"] = PumpDevice.new_from_json_dict(json_data["pump"]) if json_data.get("uploader"): json_data["uploader"] = UploaderBattery.new_from_json_dict( json_data["uploader"] ) if json_data.get("xdripjs"): json_data["xdripjs"] = XDripJs.new_from_json_dict(json_data["xdripjs"]) class XDripJs(BaseModel): """XDripJs Represents a xDrip-js source. Attributes: state (int): CGM Sensor Session State Code stateString (string): CGM Sensor Session State String stateStringShort (string): CGM Sensor Session State Short String txId (string): CGM Transmitter ID txStatus (float): CGM Transmitter Status txStatusString (string): CGM Transmitter Status String txStatusStringShort (string): CGM Transmitter Status Short String txActivation (int): CGM Transmitter Activation Milliseconds After Epoch mode (string): Mode xdrip-js Application Operationg: expired, not expired, etc. timestamp (int): Last Update Milliseconds After Epoch rssi (int): Receive Signal Strength of Transmitter unfiltered (int): Most Recent Raw Unfiltered Glucose filtered (int): Most Recent Raw Filtered Glucose noise (int): Calculated Noise Value - 1=Clean, 2=Light, 3=Medium, 4=Heavy noiseString (float): Noise Value String slope (float): Calibration Slope Value intercept (int): Calibration Intercept Value calType (string): Algorithm Used to Calculate Calibration Values lastCalibrationDate (int): Most Recent Calibration Milliseconds After Epoch sessionStart (int): Sensor Session Start Milliseconds After Epoch batteryTimestamp (int): Most Recent Batter Status Read Milliseconds After Epoch voltagea (float): Voltage of Battery A voltageb (float): Voltage of Battery B temperature (float): Transmitter Temperature resistance (float): Sensor Resistance """ def __init__(self, **kwargs): self.param_defaults = { "state": None, "stateString": None, "stateStringShort": None, "txId": None, "txStatus": None, "txStatusString": None, "txStatusStringShort": None, "txActivation": None, "mode": None, "timestamp": None, "rssi": None, "unfiltered": None, "filtered": None, "noise": None, "noiseString": None, "slope": None, "intercept": None, "calType": None, "lastCalibrationDate": None, "sessionStart": None, "batteryTimestamp": None, "voltagea": None, "voltageb": None, "temperature": None, "resistance": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) class UploaderBattery(BaseModel): """UploaderBattery Represents a Uploader device's battery on Nightscout. Attributes: batteryVoltage (float): Battery Voltage. battery (int): Battery percentage. type (string): Uploader type. """ def __init__(self, **kwargs): self.param_defaults = { "batteryVoltage": None, "battery": None, "type": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) class PumpDevice(BaseModel): """PumpDevice Represents a Pump device on Nightscout. Attributes: clock (datetime): Clock datetime. battery (PumpBattery): Pump battery details. reservoir (float): Amount of insulin remaining in pump reservoir. status (PumpStatus): Pump status details. """ def __init__(self, **kwargs): self.param_defaults = { "clock": None, "battery": None, "reservoir": None, "status": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) @classmethod def json_transforms(cls, json_data): if json_data.get("clock"): json_data["clock"] = dateutil.parser.parse(json_data["clock"]) if json_data.get("battery"): json_data["battery"] = PumpBattery.new_from_json_dict(json_data["battery"]) if json_data.get("status"): json_data["status"] = PumpStatus.new_from_json_dict(json_data["status"]) class PumpBattery(BaseModel): """PumpBattery Represents the Pump's battery on Nightscout. Attributes: status (string): Pump Battery Status String. For example "normal". voltage (float): Pump Battery Voltage Level. """ def __init__(self, **kwargs): self.param_defaults = { "clock": None, "battery": None, "reservoir": None, "status": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) class PumpStatus(BaseModel): """PumpStatus Represents a Pump device status on Nightscout. Attributes: status (string): Pump Status String. bolusing (boolean): Is Pump Bolusing. suspended (boolean): Is Pump Suspended. timestamp (datetime): Date time of entry. """ def __init__(self, **kwargs): self.param_defaults = { "clock": None, "battery": None, "reservoir": None, "status": None, } for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) @classmethod def json_transforms(cls, json_data): timestamp = json_data.get("timestamp") if timestamp: if type(timestamp) == int: json_data["timestamp"] = datetime.fromtimestamp( timestamp / 1000.0, pytz.utc ) else: json_data["timestamp"] = dateutil.parser.parse(timestamp) marciogranzotto-py-nightscout-a4a8776/pylintrc000066400000000000000000000032061415324045400217040ustar00rootroot00000000000000[MASTER] ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 load-plugins=pylint_strict_informational persistent=no extension-pkg-whitelist=ciso8601 [BASIC] good-names=id,i,j,k,ex,Run,_,fp,T,ev [MESSAGES CONTROL] # Reasons disabled: # format - handled by black # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this disable= format, abstract-class-little-used, abstract-method, cyclic-import, duplicate-code, inconsistent-return-statements, locally-disabled, not-context-manager, too-few-public-methods, too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, too-many-boolean-expressions, unused-argument, wrong-import-order enable= use-symbolic-message-instead [REPORTS] score=no [TYPECHECK] # For attrs ignored-classes=_CountingAttr [FORMAT] expected-line-ending-format=LF [EXCEPTIONS] overgeneral-exceptions=BaseException,Exception,HomeAssistantError marciogranzotto-py-nightscout-a4a8776/requirements.txt000066400000000000000000000000541415324045400233770ustar00rootroot00000000000000python-dateutil coverage pytz aiohttp>=3.6.1marciogranzotto-py-nightscout-a4a8776/requirements_test.txt000066400000000000000000000001561415324045400244410ustar00rootroot00000000000000python-dateutil coverage pytz flake8 pytest pytest-asyncio pytest-cov aioresponses pylint_strict_informationalmarciogranzotto-py-nightscout-a4a8776/setup.cfg000066400000000000000000000021111415324045400217300ustar00rootroot00000000000000[tool:pytest] testpaths = tests norecursedirs = .git testing_config [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build doctests = True # To work with Black max-line-length = 88 # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # W504 line break after binary operator ignore = E501, W503, E203, D202, W504 [isort] # https://github.com/timothycrosley/isort # https://github.com/timothycrosley/isort/wiki/isort-Settings # splits long import on multiple lines indented by 4 spaces multi_line_output = 3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 indent = " " # by default isort don't check module indexes not_skip = __init__.py # will group `import x` and `from x import` of the same module. force_sort_within_sections = true sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section = THIRDPARTY known_first_party = homeassistant,tests forced_separate = tests combine_as_imports = truemarciogranzotto-py-nightscout-a4a8776/setup.py000066400000000000000000000021451415324045400216300ustar00rootroot00000000000000"""Python Nightscout api client See: https://github.com/marciogranzotto/py-nightscout https://github.com/nightscout/cgm-remote-monitor """ import pathlib from setuptools import setup, find_packages # The directory containing this file HERE = pathlib.Path(__file__).parent # The text of the README file README = (HERE / "README.md").read_text() setup( name="py_nightscout", version="1.3.3", description="A library that provides a Python async interface to Nightscout", long_description=README, long_description_content_type="text/markdown", url="https://github.com/marciogranzotto/py-nightscout", author="Marcio Granzotto", author_email="marciogranzotto@gmail.com", license="MIT", classifiers=[ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ], keywords="nightscout api client development", packages=find_packages(exclude=["tests"]), install_requires=["python-dateutil", "pytz", "aiohttp>=3.6.1"], ) marciogranzotto-py-nightscout-a4a8776/tests/000077500000000000000000000000001415324045400212565ustar00rootroot00000000000000marciogranzotto-py-nightscout-a4a8776/tests/schedule_test.py000066400000000000000000000024221415324045400244630ustar00rootroot00000000000000from py_nightscout.models import ( ScheduleEntry, AbsoluteScheduleEntry, Schedule, ) from datetime import datetime, timedelta import pytz from dateutil import tz def test_schedule_conversion_to_absolute_time(): # Schedule should be fixed offset, to match the pump's date math schedule_tz = tz.tzoffset(None, -(5 * 60 * 60)) # UTC-5 schedule = Schedule( [ ScheduleEntry(timedelta(hours=0), 1), ScheduleEntry(timedelta(hours=6), 0.7), ScheduleEntry(timedelta(hours=12), 0.8), ScheduleEntry(timedelta(hours=22), 0.9), ], schedule_tz, ) # Queries against the schedule are typically in utc items = schedule.between( datetime(2017, 7, 7, 20, tzinfo=pytz.utc), datetime(2017, 7, 8, 6, tzinfo=pytz.utc), ) expected = [ AbsoluteScheduleEntry(datetime(2017, 7, 7, 12, tzinfo=schedule_tz), 0.8), AbsoluteScheduleEntry(datetime(2017, 7, 7, 22, tzinfo=schedule_tz), 0.9), AbsoluteScheduleEntry(datetime(2017, 7, 8, 0, tzinfo=schedule_tz), 1), ] assert len(items) == len(expected) for item, expected_item in zip(items, expected): assert item.start_date == expected_item.start_date assert item.value == expected_item.value marciogranzotto-py-nightscout-a4a8776/tests/test_api.py000066400000000000000000000422401415324045400234420ustar00rootroot00000000000000import pytest import py_nightscout as nightscout import json from datetime import datetime from dateutil.tz import tzutc import dateutil.parser from aioresponses import aioresponses import pytz @pytest.fixture(name="api") def api_fixture() -> nightscout.Api: """Creates Api fixture.""" return nightscout.Api("http://testns.example.com") sgv_response = json.loads( '[{"_id":"5f2b01f5c3d0ac7c4090e223","device":"xDrip-LimiTTer","date":1596654066533,"dateString":"2020-08-05T19:01:06.533Z","sgv":169,"delta":-5.257,"direction":"FortyFiveDown","type":"sgv","filtered":182823.5157,"unfiltered":182823.5157,"rssi":100,"noise":1,"sysTime":"2020-08-05T19:01:06.533Z","utcOffset":-180},{"_id":"5f2b00c8c3d0ac7c4090e222","device":"xDrip-LimiTTer","date":1596653766048,"dateString":"2020-08-05T18:56:06.048Z","sgv":174,"delta":-2.028,"direction":"Flat","type":"sgv","filtered":187411.75065,"unfiltered":187411.75065,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:56:06.048Z","utcOffset":-180},{"_id":"5f2aff9dc3d0ac7c4090e221","device":"xDrip-LimiTTer","date":1596653466421,"dateString":"2020-08-05T18:51:06.421Z","sgv":176,"delta":-5.66,"direction":"FortyFiveDown","type":"sgv","filtered":189176.4564,"unfiltered":189176.4564,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:51:06.421Z","utcOffset":-180},{"_id":"5f2afe70c3d0ac7c4090e220","device":"xDrip-LimiTTer","date":1596653165840,"dateString":"2020-08-05T18:46:05.840Z","sgv":182,"delta":-2.97,"direction":"Flat","type":"sgv","filtered":194117.63249999998,"unfiltered":194117.63249999998,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:46:05.840Z","utcOffset":-180},{"_id":"5f2afd44c3d0ac7c4090e21f","device":"xDrip-LimiTTer","date":1596652865845,"dateString":"2020-08-05T18:41:05.845Z","sgv":185,"delta":-5.946,"direction":"FortyFiveDown","type":"sgv","filtered":196705.8676,"unfiltered":196705.8676,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:41:05.845Z","utcOffset":-180},{"_id":"5f2afc18c3d0ac7c4090e21e","device":"xDrip-LimiTTer","date":1596652566127,"dateString":"2020-08-05T18:36:06.127Z","sgv":191,"delta":-5.772,"direction":"FortyFiveDown","type":"sgv","filtered":201882.33779999998,"unfiltered":201882.33779999998,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:36:06.127Z","utcOffset":-180},{"_id":"5f2afa42c3d0ac7c4090e21d","device":"xDrip-LimiTTer","date":1596652095925,"dateString":"2020-08-05T18:28:15.925Z","sgv":200,"delta":-3.51,"direction":"Flat","type":"sgv","filtered":209764.69014999998,"unfiltered":209764.69014999998,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:28:15.925Z","utcOffset":-180},{"_id":"5f2af916c3d0ac7c4090e21c","device":"xDrip-LimiTTer","date":1596651795916,"dateString":"2020-08-05T18:23:15.916Z","sgv":203,"delta":-5.4,"direction":"FortyFiveDown","type":"sgv","filtered":212823.51345,"unfiltered":212823.51345,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:23:15.916Z","utcOffset":-180},{"_id":"5f2af7eac3d0ac7c4090e21b","device":"xDrip-LimiTTer","date":1596651495909,"dateString":"2020-08-05T18:18:15.909Z","sgv":209,"delta":2.702,"direction":"Flat","type":"sgv","filtered":217529.39544999998,"unfiltered":217529.39544999998,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:18:15.909Z","utcOffset":-180},{"_id":"5f2af6bec3d0ac7c4090e21a","device":"xDrip-LimiTTer","date":1596651196104,"dateString":"2020-08-05T18:13:16.104Z","sgv":206,"delta":-4.454,"direction":"Flat","type":"sgv","filtered":215176.45445,"unfiltered":215176.45445,"rssi":100,"noise":1,"sysTime":"2020-08-05T18:13:16.104Z","utcOffset":-180}]' ) treatments_response = json.loads( '[{"_id":"58be816483ab6d6632419686","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:38:35Z","timestamp":"2017-03-07T09:38:35Z","absolute":0.7,"rate":0.7,"duration":30,"carbs":null,"insulin":null},{"_id":"58be803d83ab6d6632419683","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:33:30Z","timestamp":"2017-03-07T09:33:30Z","absolute":1.675,"rate":1.675,"duration":30,"carbs":null,"insulin":null},{"_id":"58be7f0d83ab6d6632419680","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:28:30Z","timestamp":"2017-03-07T09:28:30Z","absolute":1.775,"rate":1.775,"duration":30,"carbs":null,"insulin":null}]' ) profile_response = json.loads( '[{"_id":"58c0e02447d5af0c00e37593","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.4","timeAsSeconds":"61200"},{"time":"20:30","value":"0.4","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"},"Test2":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.4","timeAsSeconds":"61200"},{"time":"20:30","value":"0.4","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2017-03-24T03:54:00.000Z","mills":"1489035240000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"},{"_id":"58b7777cdfb94b0c00366c7e","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.6","timeAsSeconds":"61200"},{"time":"20:30","value":"0.6","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2017-03-02T01:37:00.000Z","mills":"1488418620000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"},{"_id":"5719b2aa5c3e080b000dbfb1","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"18","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.6","timeAsSeconds":"61200"},{"time":"20:30","value":"0.6","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2016-04-22T05:06:00.000Z","mills":"1461301560000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"}]' ) server_status_response = json.loads( '{"status":"ok","name":"nightscout","version":"13.0.1","serverTime":"2020-08-05T18:14:02.032Z","serverTimeEpoch":1596651242032,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{"units":"mg/dl","timeFormat":12,"nightMode":false,"editMode":true,"showRawbg":"never","customTitle":"Nightscout","theme":"default","alarmUrgentHigh":true,"alarmUrgentHighMins":[30,60,90,120],"alarmHigh":true,"alarmHighMins":[30,60,90,120],"alarmLow":true,"alarmLowMins":[15,30,45,60],"alarmUrgentLow":true,"alarmUrgentLowMins":[15,30,45],"alarmUrgentMins":[30,60,90,120],"alarmWarnMins":[30,60,90,120],"alarmTimeagoWarn":true,"alarmTimeagoWarnMins":15,"alarmTimeagoUrgent":true,"alarmTimeagoUrgentMins":30,"alarmPumpBatteryLow":false,"language":"en","scaleY":"log","showPlugins":" delta direction upbat","showForecast":"ar2","focusHours":3,"heartbeat":60,"baseURL":"","authDefaultRoles":"readable","thresholds":{"bgHigh":260,"bgTargetTop":180,"bgTargetBottom":80,"bgLow":55},"insecureUseHttp":false,"secureHstsHeader":true,"secureHstsHeaderIncludeSubdomains":false,"secureHstsHeaderPreload":false,"secureCsp":false,"deNormalizeDates":false,"showClockDelta":false,"showClockLastTime":false,"DEFAULT_FEATURES":["bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile"],"alarmTypes":["predict"],"enable":["careportal","boluscalc","food","bwp","cage","sage","iage","iob","cob","basal","ar2","rawbg","pushover","bgi","pump","openaps","treatmentnotify","bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","ar2"]},"extendedSettings":{"devicestatus":{"advanced":true}},"authorized":null}' ) device_status_response = json.loads( '[{"_id":"617dc8ef0c76ce0182e37978","device":"Tomato","uploader":{"battery":20,"type":"BRIDGE"},"created_at":"2021-10-30T22:36:31.901Z","utcOffset":-180,"mills":1635633391901},{"_id":"617dc8ef0c76ce0182e37977","device":"samsung SM-N986B","uploader":{"battery":69,"type":"PHONE"},"created_at":"2021-10-30T22:36:31.844Z","utcOffset":-180,"mills":1635633391844},{"_id":"617dc7c40c76ce0182e37976","device":"Tomato","uploader":{"battery":20,"type":"BRIDGE"},"created_at":"2021-10-30T22:31:32.252Z","utcOffset":-180,"mills":1635633092252},{"_id":"617dc7c40c76ce0182e37975","device":"samsung SM-N986B","uploader":{"battery":70,"type":"PHONE"},"created_at":"2021-10-30T22:31:32.198Z","utcOffset":-180,"mills":1635633092198},{"_id":"617dc6980c76ce0182e37974","device":"Tomato","uploader":{"battery":20,"type":"BRIDGE"},"created_at":"2021-10-30T22:26:32.128Z","utcOffset":-180,"mills":1635632792128},{"_id":"617dc6980c76ce0182e37973","device":"samsung SM-N986B","uploader":{"battery":70,"type":"PHONE"},"created_at":"2021-10-30T22:26:32.043Z","utcOffset":-180,"mills":1635632792043},{"_id":"617dc56c0c76ce0182e37972","device":"Tomato","uploader":{"battery":20,"type":"BRIDGE"},"created_at":"2021-10-30T22:21:32.955Z","utcOffset":-180,"mills":1635632492955},{"_id":"617dc56c0c76ce0182e37971","device":"samsung SM-N986B","uploader":{"battery":70,"type":"PHONE"},"created_at":"2021-10-30T22:21:32.902Z","utcOffset":-180,"mills":1635632492902},{"_id":"617dc43f0c76ce0182e37970","device":"Tomato","uploader":{"battery":20,"type":"BRIDGE"},"created_at":"2021-10-30T22:16:31.839Z","utcOffset":-180,"mills":1635632191839},{"_id":"617dc43f0c76ce0182e3796f","device":"samsung SM-N986B","uploader":{"battery":70,"type":"PHONE"},"created_at":"2021-10-30T22:16:31.784Z","utcOffset":-180,"mills":1635632191784}]' ) @pytest.mark.asyncio async def test_get_sgv(api: nightscout.Api): """Tests using external session.""" with aioresponses() as response: response.get( "http://testns.example.com/api/v1/entries/sgv.json", payload=sgv_response, ) entries = await api.get_sgvs() assert 10 == len(entries) assert 169 == entries[0].sgv assert 9.4 == entries[0].sgv_mmol assert -5.257 == entries[0].delta assert -0.3 == entries[0].delta_mmol assert "FortyFiveDown" == entries[0].direction assert -5.257 == entries[0].delta assert datetime(2020, 8, 5, 19, 1, 6, 533000, tzinfo=tzutc()) == entries[0].date @pytest.mark.asyncio async def test_get_treatments(api: nightscout.Api): with aioresponses() as response: response.get( "http://testns.example.com/api/v1/treatments.json", payload=treatments_response, ) treatments = await api.get_treatments() assert 3 == len(treatments) assert "absolute" == treatments[0].temp assert "Temp Basal" == treatments[0].eventType timestamp = datetime(2017, 3, 7, 9, 38, 35, tzinfo=tzutc()) assert timestamp == treatments[0].timestamp assert timestamp == treatments[0].created_at @pytest.mark.asyncio async def test_get_profile(api: nightscout.Api): with aioresponses() as response: response.get( "http://testns.example.com/api/v1/profile.json", payload=profile_response, ) profile_definition_set = await api.get_profiles() profile_definition = profile_definition_set.get_profile_definition_active_at( datetime(2017, 3, 5, 0, 0, tzinfo=tzutc()) ) assert ( datetime(2017, 3, 2, 1, 37, tzinfo=tzutc()) == profile_definition.startDate ) assert "mg/dl" == profile_definition.units profile = profile_definition.get_default_profile() assert pytz.timezone("US/Central") == profile.timezone assert 4 == profile.dia five_thirty_pm = datetime(2017, 3, 24, 17, 30) five_thirty_pm = profile.timezone.localize(five_thirty_pm) assert 0.6 == profile.basal.value_at_date(five_thirty_pm) @pytest.mark.asyncio async def test_server_status(api: nightscout.Api): with aioresponses() as response: response.get( "http://testns.example.com/api/v1/status.json", payload=server_status_response, ) server_status = await api.get_server_status() assert "ok" == server_status.status assert "nightscout" == server_status.name assert server_status.apiEnabled assert "readable" == server_status.settings["authDefaultRoles"] @pytest.mark.asyncio async def test_devices_status(api: nightscout.Api): with aioresponses() as response: response.get( "http://testns.example.com/api/v1/devicestatus.json", payload=device_status_response, ) devices_status = await api.get_devices_status() assert 10 == len(devices_status) assert "Tomato" == devices_status[0].device assert 20 == devices_status[0].uploader.battery @pytest.mark.asyncio async def test_latest_devices_status(api: nightscout.Api): with aioresponses() as response: response.get( "http://testns.example.com/api/v1/devicestatus.json", payload=device_status_response, ) devices_status = await api.get_latest_devices_status() assert 2 == len(devices_status) assert devices_status["Tomato"] assert devices_status["samsung SM-N986B"] assert ( dateutil.parser.parse("2021-10-30T22:36:31.901Z") == devices_status["Tomato"].created_at ) assert 20 == devices_status["Tomato"].uploader.battery assert "BRIDGE" == devices_status["Tomato"].uploader.type assert ( dateutil.parser.parse("2021-10-30T22:36:31.844Z") == devices_status["samsung SM-N986B"].created_at ) assert 69 == devices_status["samsung SM-N986B"].uploader.battery assert "PHONE" == devices_status["samsung SM-N986B"].uploader.type