diff --git a/.gitignore b/.gitignore index b8b6cad51..4fcbc57e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ bower_components/ node_modules/ + +bundle/bundle.out.js + .idea/ *.iml my.env @@ -15,3 +18,5 @@ static/bower_components/ # istanbul output coverage/ + +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 562c8d9ed..c7b9ed5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,23 @@ language: node_js sudo: false node_js: - - "0.10" - - "0.12" + - 0.10 + - 0.12 + - 4 + - 5 +matrix: + fast_finish: true + allow_failures: + - node_js: 4 + - node_js: 5 services: mongodb script: make travis after_script: make report +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c63417fa9..5be3a99d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,19 @@ + + +**Table of Contents** + +- [Contributing to cgm-remote-monitor](#contributing-to-cgm-remote-monitor) + - [Design](#design) + - [Develop on `dev`](#develop-on-dev) + - [Style Guide](#style-guide) + - [Create a prototype](#create-a-prototype) + - [Submit a pull request](#submit-a-pull-request) + - [Comments and issues](#comments-and-issues) + - [Co-ordination](#co-ordination) + - [Other Dev Tips](#other-dev-tips) + + + # Contributing to cgm-remote-monitor @@ -30,6 +46,25 @@ design. We develop on the `dev` branch. You can get the dev branch checked out using `git checkout dev`. +## Style Guide + +Some simple rules, that will make it easier to maintain our codebase: + +* All indenting should use 2 space where possible (js, css, html, etc) +* A space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for calls easier +* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }` +* Don't include author names in the header of your files, if you need to give credit to someone else do it in the commit comment. +* Use single quotes. +* Use the comma first style, for example: + + ```javascript + var data = { + value: 'the value' + , detail: 'the details...' + , time: Date.now() + }; + ``` + ## Create a prototype Fork cgm-remote-monitor and create a branch. @@ -61,10 +96,8 @@ appropriate. ## Co-ordination -There is a google groups nightscout-core developers list where lots of -people discuss Nightscout. Most cgm-remote-monitor hackers use -github's ticketing system, along with Facebook cgm-in-the-cloud, and -gitter system. +Most cgm-remote-monitor hackers use github's ticketing system, along with Facebook cgm-in-the-cloud, and +gitter. We use git-flow, with `master` as our production, stable branch, and `dev` is used to queue up for upcoming releases. Everything else is @@ -77,3 +110,13 @@ the version correctly. See sem-ver for versioning strategy. Every commit is tested by travis. We encourage adding tests to validate your design. We encourage discussing your use cases to help everyone get a better understanding of your design. + +## Other Dev Tips + +* Join the [Gitter chat][gitter-url] +* Get a local dev environment setup if you haven't already +* Try breaking up big features/improvements into small parts. It's much easier to accept small PR's +* Create tests for your new code, and for the old code too. We are aiming for a full test coverage. +* If your going to be working in old code that needs lots of reformatting consider doing the clean as a separate PR. +* If you can find others to help test your PR is will help get them merged in sooner. + diff --git a/Makefile b/Makefile index 43db4909d..44e16a9d0 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,7 @@ MONGO_CONNECTION?=mongodb://localhost/test_db CUSTOMCONNSTR_mongo_settings_collection?=test_settings CUSTOMCONNSTR_mongo_collection?=test_sgvs MONGO_SETTINGS=MONGO_CONNECTION=${MONGO_CONNECTION} \ - CUSTOMCONNSTR_mongo_collection=${CUSTOMCONNSTR_mongo_collection} \ - CUSTOMCONNSTR_mongo_settings_collection=${CUSTOMCONNSTR_mongo_settings_collection} + CUSTOMCONNSTR_mongo_collection=${CUSTOMCONNSTR_mongo_collection} # XXX.bewest: Mocha is an odd process, and since things are being # wrapped and transformed, this odd path needs to be used, not the @@ -23,6 +22,7 @@ MOCHA=./node_modules/mocha/bin/_mocha # Pinned from dependency list. ISTANBUL=./node_modules/.bin/istanbul ANALYZED=./coverage/lcov.info +export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c all: test @@ -34,6 +34,9 @@ report: test -f ${ANALYZED} && \ (npm install coveralls && cat ${ANALYZED} | \ ./node_modules/.bin/coveralls) || echo "NO COVERAGE" + test -f ${ANALYZED} && \ + (npm install codacy-coverage && cat ${ANALYZED} | \ + YOURPACKAGE_COVERAGE=1 ./node_modules/codacy-coverage/bin/codacy-coverage.js) || echo "NO COVERAGE" test: ${MONGO_SETTINGS} ${MOCHA} -R tap ${TESTS} diff --git a/Procfile b/Procfile index 489b2700a..32dd1c83a 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node server.js +web: ./node_modules/.bin/forever --minUptime 100 -c node server.js diff --git a/README.md b/README.md index 69b1e5240..eca170715 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -cgm-remote-monitor (a.k.a. Nightscout) +Nightscout Web Monitor (a.k.a. cgm-remote-monitor) ====================================== +![nightscout horizontal](https://cloud.githubusercontent.com/assets/751143/8425633/93c94dc0-1ebc-11e5-99e7-71a8f464caac.png) + [![Build Status][build-img]][build-url] [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] +[![Codacy Badge][codacy-img]][codacy-url] [![Gitter chat][gitter-img]][gitter-url] -[![Stories in Ready][ready-img]][waffle] -[![Stories in Progress][progress-img]][waffle] -[![Deploy to Heroku][heroku-img]][heroku-url] +[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) [![Deploy to Heroku][heroku-img]][heroku-url] This acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patient's glucose data in @@ -19,26 +20,88 @@ and blood glucose values are predicted 0.5 hours ahead using an autoregressive second order model. Alarms are generated for high and low values, which can be cleared by any watcher of the data. +# [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. + Community maintained fork of the [original cgm-remote-monitor][original]. +[![Coverage Status](https://coveralls.io/repos/github/nightscout/cgm-remote-monitor/badge.svg?branch=dev)](https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=dev) + [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor [dependency-img]: https://img.shields.io/david/nightscout/cgm-remote-monitor.svg [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor -[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg -[coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master +[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/dev.svg +[coverage-url]: https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=dev +[codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b +[codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor [gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg [gitter-url]: https://gitter.im/nightscout/public -[ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready -[waffle]: https://waffle.io/nightscout/cgm-remote-monitor -[progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress [heroku-img]: https://www.herokucdn.com/deploy/button.png [heroku-url]: https://heroku.com/deploy [original]: https://github.com/rnpenguin/cgm-remote-monitor -Install ---------------- + + +**Table of Contents** + +- [Install](#install) +- [Usage](#usage) + - [Updating my version?](#updating-my-version) + - [What is my mongo string?](#what-is-my-mongo-string) + - [Configure my uploader to match](#configure-my-uploader-to-match) + - [Nightscout API](#nightscout-api) + - [Example Queries](#example-queries) + - [Environment](#environment) + - [Required](#required) + - [Features/Labs](#featureslabs) + - [Alarms](#alarms) + - [Core](#core) + - [Predefined values for your browser settings (optional)](#predefined-values-for-your-browser-settings-optional) + - [Plugins](#plugins) + - [Default Plugins](#default-plugins) + - [`delta` (BG Delta)](#delta-bg-delta) + - [`direction` (BG Direction)](#direction-bg-direction) + - [`upbat` (Uploader Battery)](#upbat-uploader-battery) + - [`timeago` (Time Ago)](#timeago-time-ago) + - [`devicestatus` (Device Status)](#devicestatus-device-status) + - [`errorcodes` (CGM Error Codes)](#errorcodes-cgm-error-codes) + - [`ar2` (AR2 Forecasting)](#ar2-ar2-forecasting) + - [`simplealarms` (Simple BG Alarms)](#simplealarms-simple-bg-alarms) + - [`profile` (Treatment Profile)](#profile-treatment-profile) + - [Advanced Plugins](#advanced-plugins) + - [`careportal` (Careportal)](#careportal-careportal) + - [`boluscalc` (Bolus Wizard)](#boluscalc-bolus-wizard) + - [`food` (Custom Foods)](#food-custom-foods) + - [`rawbg` (Raw BG)](#rawbg-raw-bg) + - [`iob` (Insulin-on-Board)](#iob-insulin-on-board) + - [`cob` (Carbs-on-Board)](#cob-carbs-on-board) + - [`bwp` (Bolus Wizard Preview)](#bwp-bolus-wizard-preview) + - [`cage` (Cannula Age)](#cage-cannula-age) + - [`sage` (Sensor Age)](#sage-sensor-age) + - [`iage` (Insulin Age)](#iage-insulin-age) + - [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications) + - [`basal` (Basal Profile)](#basal-basal-profile) + - [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge) + - [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge) + - [`pump` (Pump Monitoring)](#pump-pump-monitoring) + - [`openaps` (OpenAPS)](#openaps-openaps) + - [`loop` (Loop)](#loop-loop) + - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`cors` (CORS)](#cors-cors) + - [Extended Settings](#extended-settings) + - [Pushover](#pushover) + - [IFTTT Maker](#ifttt-maker) + - [Treatment Profile](#treatment-profile) + - [Setting environment variables](#setting-environment-variables) + - [Vagrant install](#vagrant-install) + - [Installation on Windows](#installation-on-windows) + - [More questions?](#more-questions) + - [License](#license) + + + +# Install Requirements: @@ -50,69 +113,374 @@ Clone this repo then install dependencies into the root of the project: $ npm install ``` -Usage ---------------- +#Usage The data being uploaded from the server to the client is from a -MongoDB server such as [mongolab][mongodb]. In order to access the -database, the appropriate credentials need to be filled into the -[JSON][json] file in the root directory. SGV data from the database -is assumed to have the following fields: date, sgv. Once all that is -ready, just host your web app on your service of choice. +MongoDB server such as [mongolab][mongodb]. [mongodb]: https://mongolab.com -[json]: https://github.com/rnpenguin/cgm-remote-monitor/blob/master/database_configuration.json [autoconfigure]: http://nightscout.github.io/pages/configure/ [mongostring]: http://nightscout.github.io/pages/mongostring/ [update-fork]: http://nightscout.github.io/pages/update-fork/ -### Updating my version? +## Updating my version? The easiest way to update your version of cgm-remote-monitor to our latest recommended version is to use the [update my fork tool][update-fork]. It even gives out stars if you are up to date. -### What is my mongo string? +## What is my mongo string? Try the [what is my mongo string tool][mongostring] to get a good idea of your mongo string. You can copy and paste the text in the gray box into your `MONGO_CONNECTION` environment variable. -### Configure my uploader to match +## Configure my uploader to match Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. -### Environment +## Nightscout API + +The Nightscout API enables direct access to your DData without the need for direct Mongo access. +You can find CGM data in `/api/v1/entries`, Care Portal Treatments in `/api/v1/treatments`, and Treatment Profiles in `/api/v1/profile`. +The server status and settings are available from `/api/v1/status.json`. + +By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. +You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. + +#### Example Queries + +(replace `http://localhost:1337` with your base url, YOUR-SITE) + + * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` + * Count of 100's in a month: `http://localhost:1337/api/v1/count/entries/where?find[dateString][$gte]=2016-09&find[dateString][$lte]=2016-10&find[sgv]=100` + * BGs between 2 days: `http://localhost:1337/api/v1/entries/sgv.json?find[dateString][$gte]=2015-08-28&find[dateString][$lte]=2015-08-30` + * Juice Box corrections in a year: `http://localhost:1337/api/v1/treatments.json?count=1000&find[carbs]=15&find[eventType]=Carb+Correction&find[created_at][$gte]=2015` + * Boluses over 2U: `http://localhost:1337/api/v1/treatments.json?find[insulin][$gte]=2` + +The API is Swagger enabled, so you can generate client code to make working with the API easy. +To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.html or review [swagger.yaml](swagger.yaml). + + +## Environment `VARIABLE` (default) - description -#### Required +### Required * `MONGO_CONNECTION` - Your mongo uri, for example: `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout` + * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. + * `BASE_URL` - Used for building links to your sites api, ie pushover callbacks, usually the URL of your Nightscout site you may want https instead of http -#### Features/Labs +### Features/Labs - * `ENABLE` - Used to enable optional features, expects a space delimited list such as: `careportal rawbg` (also `rawbg-on` to show raw data by default) + * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below + * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal + * `AUTH_DEFAULT_ROLES` (`readable`) - possible values `readable`, `denied`, or any valid role + name. When `readable`, anyone can view Nightscout without a token. + Setting it to `denied` will require a token from every visit, using `status-only` will enable api-secret based login. + * `IMPORT_CONFIG` - Used to import settings and extended settings from a url such as a gist. Structure of file should be something like: `{"settings": {"theme": "colors"}, "extendedSettings": {"upbat": {"enableAlerts": true}}}` + * `TREATMENTS_AUTH` (`on`) - possible values `on` or `off`. Deprecated, if set to `off` the `careportal` role will be added to `AUTH_DEFAULT_ROLES` + + +### Alarms + + These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) + + * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart * `BG_TARGET_BOTTOM` (`80`) - must be set using mg/dl units; the bottom of the target range, also used to draw the line on the chart * `BG_LOW` (`55`) - must be set using mg/dl units; the low BG outside the target range that is considered urgent - * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's - * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications for Care Portal treatments, this token is specific to the application you create from in [Pushover](https://pushover.net/) - * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site + * `ALARM_URGENT_HIGH` (`on`) - possible values `on` or `off` + * `ALARM_URGENT_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent high alarms, space separated for options in browser, first used for pushover + * `ALARM_HIGH` (`on`) - possible values `on` or `off` + * `ALARM_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze high alarms, space separated for options in browser, first used for pushover + * `ALARM_LOW` (`on`) - possible values `on` or `off` + * `ALARM_LOW_MINS` (`15 30 45 60`) - Number of minutes to snooze low alarms, space separated for options in browser, first used for pushover + * `ALARM_URGENT_LOW` (`on`) - possible values `on` or `off` + * `ALARM_URGENT_LOW_MINS` (`15 30 45`) - Number of minutes to snooze urgent low alarms, space separated for options in browser, first used for pushover + * `ALARM_URGENT_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover + * `ALARM_WARN_MINS` (`30 60 90 120`) - Number of minutes to snooze warning alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover -#### Core +### Core - * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. * `MONGO_COLLECTION` (`entries`) - The collection used to store SGV, MBG, and CAL records from your CGM device * `MONGO_TREATMENTS_COLLECTION` (`treatments`) -The collection used to store treatments entered in the Care Portal, see the `ENABLE` env var above * `MONGO_DEVICESTATUS_COLLECTION`(`devicestatus`) - The collection used to store device status information such as uploader battery + * `MONGO_PROFILE_COLLECTION`(`profile`) - The collection used to store your profiles + * `MONGO_FOOD_COLLECTION`(`food`) - The collection used to store your food database * `PORT` (`1337`) - The port that the node.js application will listen on. + * `HOSTNAME` - The hostname that the node.js application will listen on, null by default for any hostname for IPv6 you may need to use `::`. * `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js * `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js * `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js + * `HEARTBEAT` (`60`) - Number of seconds to wait in between database checks + + +### Predefined values for your browser settings (optional) + * `TIME_FORMAT` (`12`)- possible values `12` or `24` + * `NIGHT_MODE` (`off`) - possible values `on` or `off` + * `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise` + * `CUSTOM_TITLE` (`Nightscout`) - Usually name of T1 + * `THEME` (`default`) - possible values `default` or `colors` + * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning + * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm + * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled + * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` + * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used + * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. + * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. + * The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted. + * The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale. + * `EDIT_MODE` (`on`) - possible values `on` or `off`. Enable or disable icon allowing enter treatments edit mode + +### Plugins + + Plugins are used extend the way information is displayed, how notifications are sent, alarms are triggered, and more. + + The built-in/example plugins that are available by default are listed below. The plugins may still need to be enabled by adding to the `ENABLE` environment variable. + +#### Default Plugins + + These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` + +##### `delta` (BG Delta) + Calculates and displays the change between the last 2 BG values. + +##### `direction` (BG Direction) + Displays the trend direction. + +##### `upbat` (Uploader Battery) + Displays the most recent battery status from the uploader phone. . Use these [extended setting](#extended-settings) to adjust behavior: + * `UPBAT_ENABLE_ALERTS` (`false`) - Set to `true` to enable uploader battery alarms via Pushover and IFTTT. + * `UPBAT_WARN` (`30`) - Minimum battery percent to trigger warning. + * `UPBAT_URGENT` (`20`) - Minimum battery percent to trigger urgent alarm. + +##### `timeago` (Time Ago) + Displays the time since last CGM entry. Use these [extended setting](#extended-settings) to adjust behavior: + * `TIMEAGO_ENABLE_ALERTS` (`false`) - Set to `true` to enable stale data alarms via Pushover and IFTTT. + * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning + * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm + + +##### `devicestatus` (Device Status) + Used by `upbat` and other plugins to display device status info. Supports the `DEVICESTATUS_ADVANCED="true"` [extended setting](#extended-settings) to send all device statuses to the client for retrospective use and to support other plugins. + +##### `errorcodes` (CGM Error Codes) + Generates alarms for CGM codes `9` (hourglass) and `10` (???). + * Use [extended settings](#extended-settings) to adjust what errorcodes trigger notifications and alarms: + * `ERRORCODES_INFO` (`1 2 3 4 5 6 7 8`) - By default the needs calibration (blood drop) and other codes below 9 generate an info level notification, set to a space separate list of number or `off` to disable + * `ERRORCODES_WARN` (`off`) - By default there are no warning configured, set to a space separate list of numbers or `off` to disable + * `ERRORCODES_URGENT` (`9 10`) - By default the hourglass and ??? generate an urgent alarm, set to a space separate list of numbers or `off` to disable + +##### `ar2` (AR2 Forecasting) + Generates alarms based on forecasted values. See [Forecasting using AR2 algorithm](https://github.com/nightscout/nightscout.github.io/wiki/Forecasting) + * Enabled by default if no thresholds are set **OR** `ALARM_TYPES` includes `predict`. + * Use [extended settings](#extended-settings) to adjust AR2 behavior: + * `AR2_CONE_FACTOR` (`2`) - to adjust size of cone, use `0` for a single line. + +##### `simplealarms` (Simple BG Alarms) + Uses `BG_HIGH`, `BG_TARGET_TOP`, `BG_TARGET_BOTTOM`, `BG_LOW` thresholds to generate alarms. + * Enabled by default if 1 of these thresholds is set **OR** `ALARM_TYPES` includes `simple`. + +##### `profile` (Treatment Profile) + Add link to Profile Editor and allow to enter treatment profile settings. Also uses the extended setting: + * `PROFILE_HISTORY` (`off`) - possible values `on` or `off`. Enable/disable NS ability to keep history of your profiles (still experimental) + * `PROFILE_MULTIPLE` (`off`) - possible values `on` or `off`. Enable/disable NS ability to handle and switch between multiple treatment profiles + +#### Advanced Plugins: + +##### `careportal` (Careportal) + An optional form to enter treatments. + +##### `boluscalc` (Bolus Wizard) + +##### `food` (Custom Foods) + An option plugin to enable adding foods from database in Bolus Wizard and enable . + +##### `rawbg` (Raw BG) + Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels. + +##### `iob` (Insulin-on-Board) + Adds the IOB pill visualization in the client and calculates values that used by other plugins. Uses treatments with insulin doses and the `dia` and `sens` fields from the [treatment profile](#treatment-profile). + +##### `cob` (Carbs-on-Board) + Adds the COB pill visualization in the client and calculates values that used by other plugins. Uses treatments with carb doses and the `carbs_hr`, `carbratio`, and `sens` fields from the [treatment profile](#treatment-profile). + +##### `bwp` (Bolus Wizard Preview) + This plugin in intended for the purpose of automatically snoozing alarms when the CGM indicates high blood sugar but there is also insulin on board (IOB) and secondly, alerting to user that it might be beneficial to measure the blood sugar using a glucometer and dosing insulin as calculated by the pump or instructed by trained medicare professionals. ***The values provided by the plugin are provided as a reference based on CGM data and insulin sensitivity you have configured, and are not intended to be used as a reference for bolus calculation.*** The plugin calculates the bolus amount when above your target, generates alarms when you should consider checking and bolusing, and snoozes alarms when there is enough IOB to cover a high BG. Uses the results of the `iob` plugin and `sens`, `target_high`, and `target_low` fields from the [treatment profile](#treatment-profile). Defaults that can be adjusted with [extended setting](#extended-settings) + * `BWP_WARN` (`0.50`) - If `BWP` is > `BWP_WARN` a warning alarm will be triggered. + * `BWP_URGENT` (`1.00`) - If `BWP` is > `BWP_URGENT` an urgent alarm will be triggered. + * `BWP_SNOOZE_MINS` (`10`) - minutes to snooze when there is enough IOB to cover a high BG. + * `BWP_SNOOZE` - (`0.10`) If BG is higher then the `target_high` and `BWP` < `BWP_SNOOZE` alarms will be snoozed for `BWP_SNOOZE_MINS`. + +##### `cage` (Cannula Age) + Calculates the number of hours since the last `Site Change` treatment that was recorded. + * `CAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming cannula change. + * `CAGE_INFO` (`44`) - If time since last `Site Change` matches `CAGE_INFO`, user will be warned of upcoming cannula change + * `CAGE_WARN` (`48`) - If time since last `Site Change` matches `CAGE_WARN`, user will be alarmed to to change the cannula + * `CAGE_URGENT` (`72`) - If time since last `Site Change` matches `CAGE_URGENT`, user will be issued a persistent warning of overdue change. + * `CAGE_DISPLAY` (`hours`) - Possible values are 'hours' or 'days'. If 'days' is selected and age of canula is greater than 24h number is displayed in days and hours + +##### `sage` (Sensor Age) + Calculates the number of days and hours since the last `Sensor Start` and `Sensor Change` treatment that was recorded. + * `SAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming sensor change. + * `SAGE_INFO` (`144`) - If time since last sensor event matches `SAGE_INFO`, user will be warned of upcoming sensor change + * `SAGE_WARN` (`164`) - If time since last sensor event matches `SAGE_WARN`, user will be alarmed to to change/restart the sensor + * `SAGE_URGENT` (`166`) - If time since last sensor event matches `SAGE_URGENT`, user will be issued a persistent warning of overdue change. + +##### `iage` (Insulin Age) + Calculates the number of days and hours since the last `Insulin Change` treatment that was recorded. + * `IAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming insulin reservoir change. + * `IAGE_INFO` (`44`) - If time since last `Insulin Change` matches `IAGE_INFO`, user will be warned of upcoming insulin reservoir change + * `IAGE_WARN` (`48`) - If time since last `Insulin Change` matches `IAGE_WARN`, user will be alarmed to to change the insulin reservoir + * `IAGE_URGENT` (`72`) - If time since last `Insulin Change` matches `IAGE_URGENT`, user will be issued a persistent warning of overdue change. + +##### `treatmentnotify` (Treatment Notifications) + Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings). + +##### `basal` (Basal Profile) + Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). Also uses the extended setting: + * `BASAL_RENDER` (`none`) - Possible values are `none`, `default`, or `icicle` (inverted) + +##### `bridge` (Share2Nightscout bridge) + Glucose reading directly from the Share service, uses these extended settings: + * `BRIDGE_USER_NAME` - Your user name for the Share service. + * `BRIDGE_PASSWORD` - Your password for the Share service. + * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time to wait between each update. + * `BRIDGE_MAX_COUNT` (`1`) - The maximum number of records to fetch per update. + * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. + * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. + * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). + +##### `mmconnect` (MiniMed Connect bridge) + Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) + * `MMCONNECT_USER_NAME` - Your user name for CareLink Connect. + * `MMCONNECT_PASSWORD` - Your password for CareLink Connect. + * `MMCONNECT_INTERVAL` (`60000` *1 minute*) - Number of milliseconds to wait between requests to the CareLink server. + * `MMCONNECT_MAX_RETRY_DURATION` (`32`) - Maximum number of total seconds to spend retrying failed requests before giving up. + * `MMCONNECT_SGV_LIMIT` (`24`) - Maximum number of recent sensor glucose values to send to Nightscout on each request. + * `MMCONNECT_VERBOSE` - Set this to "true" to log CareLink request information to the console. + * `MMCONNECT_STORE_RAW_DATA` - Set this to "true" to store raw data returned from CareLink as `type: "carelink_raw"` database entries (useful for development). + +##### `pump` (Pump Monitoring) + Generic Pump Monitoring for OpenAPS, MiniMed Connect, RileyLink, t:slim, with more on the way + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `PUMP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications for Pump battery and reservoir. + * `PUMP_FIELDS` (`reservoir battery`) - The fields to display by default. Any of the following fields: `reservoir`, `battery`, `clock`, `status`, and `device` + * `PUMP_RETRO_FIELDS` (`reservoir battery clock`) - The fields to display in retro mode. Any of the above fields. + * `PUMP_WARN_CLOCK` (`30`) - The number of minutes ago that needs to be exceed before an alert is triggered. + * `PUMP_URGENT_CLOCK` (`60`) - The number of minutes ago that needs to be exceed before an urgent alarm is triggered. + * `PUMP_WARN_RES` (`10`) - The number of units remaining, a warning will be triggered when dropping below this threshold. + * `PUMP_URGENT_RES` (`5`) - The number of units remaining, an urgent alarm will be triggered when dropping below this threshold. + * `PUMP_WARN_BATT_P` (`30`) - The % of the pump battery remaining, a warning will be triggered when dropping below this threshold. + * `PUMP_URGENT_BATT_P` (`20`) - The % of the pump battery remaining, an urgent alarm will be triggered when dropping below this threshold. + * `PUMP_WARN_BATT_V` (`1.35`) - The voltage (if percent isn't available) of the pump battery, a warning will be triggered when dropping below this threshold. + * `PUMP_URGENT_BATT_V` (`1.30`) - The voltage (if percent isn't available) of the pump battery, an urgent alarm will be triggered when dropping below this threshold. + +##### `openaps` (OpenAPS) + Integrated OpenAPS loop monitoring, uses these extended settings: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts. + * `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered + * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered + * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` + * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. + + Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). + +##### `loop` (Loop) + iOS Loop app monitoring, uses these extended settings: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `LOOP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when Loop isn't looping. + * `LOOP_WARN` (`30`) - The number of minutes since the last loop that needs to be exceeded before an alert is triggered + * `LOOP_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceeded before an urgent alarm is triggered + * Add `loop` to `SHOW_FORECAST` to show forecasted BG. + +##### `alexa` (Amazon Alexa) + Integration with Amazon Alexa, [detailed setup instructions](lib/plugins/alexa-plugin.md) + +##### `cors` (CORS) + Enabled [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) so other websites can make request to your Nightscout site, uses these extended settings: + * `CORS_ALLOW_ORIGIN` (`*`) - The list of sites that are allow to make requests + +#### Extended Settings + Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin. + + Plugins only have access to their own extended settings, all the extended settings of client plugins will be sent to the browser. + +#### Pushover + In addition to the normal web based alarms, there is also support for [Pushover](https://pushover.net/) based alarms and notifications. + + To get started install the Pushover application on your iOS or Android device and create an account. + + Using that account login to [Pushover](https://pushover.net/), in the top left you’ll see your User Key, you’ll need this plus an application API Token/Key to complete this setup. + + You’ll need to [Create a Pushover Application](https://pushover.net/apps/build). You only need to set the Application name, you can ignore all the other settings, but setting an Icon is a nice touch. Maybe you'd like to use [this one](https://raw.githubusercontent.com/nightscout/cgm-remote-monitor/master/static/images/large.png)? + + Pushover is configured using the following Environment Variables: + + * `ENABLE` - `pushover` should be added to the list of plugin, for example: `ENABLE="pushover"`. + * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications, this token is specific to the application you create from in [Pushover](https://pushover.net/), ***[additional pushover information](#pushover)*** below. + * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site, this can also be a pushover delivery group key to send to a group rather than just a single user. This also supports a space delimited list of keys. To disable `INFO` level pushes set this to `off`. + * `PUSHOVER_ALARM_KEY` - An optional Pushover user/group key, will be used for system wide alarms (level > `WARN`). If not defined this will fallback to `PUSHOVER_USER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. To disable Alarm pushes set this to `off`. + * `PUSHOVER_ANNOUNCEMENT_KEY` - An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY` or `PUSHOVER_ALARM_KEY`. This also support a space delimited list of keys. To disable Announcement pushes set this to `off`. + * `BASE_URL` - Used for pushover callbacks, usually the URL of your Nightscout site, use https when possible. + * `API_SECRET` - Used for signing the pushover callback request for acknowledgments. + + If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"` + If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"` + If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"` + + If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements + + For testing/development try [localtunnel](http://localtunnel.me/). + +#### IFTTT Maker + In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker). + + With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. + + 1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join) + 2. Find your secret key on the [maker page](https://ifttt.com/maker) + 3. Configure Nightscout by setting these environment variables: + * `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`. + * `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys. + * `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. + 4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe) + + Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are: + * `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging. + * `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green. + * `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`. + * `ns-warning` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event`. + * `ns-urgent` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event`. + * see the [full list of events](lib/plugins/maker-setup.md#events) + + +### Treatment Profile + Some of the [plugins](#plugins) make use of a treatment profile that can be edited using the Profile Editor, see the link in the Settings drawer on your site. + + Treatment Profile Fields: + + * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* + * `units` (Profile Units) - blood glucose units used in the profile, either "mgdl" or "mmol" + * `dia` (Insulin duration) - value should be the duration of insulin action to use in calculating how much insulin is left active. Defaults to 3 hours. + * `carbs_hr` (Carbs per Hour) - The number of carbs that are processed per hour, for more information see [#DIYPS](http://diyps.org/2014/05/29/determining-your-carbohydrate-absorption-rate-diyps-lessons-learned/). + * `carbratio` (Carb Ratio) - grams per unit of insulin. + * `sens` (Insulin sensitivity) How much one unit of insulin will normally lower blood glucose. + * `basal` The basal rate set on the pump. + * `target_high` - Upper target for correction boluses. + * `target_low` - Lower target for correction boluses. + + Some example profiles are [here](example-profiles.md). ## Setting environment variables Easy to emulate on the commandline: @@ -146,6 +514,10 @@ The setup script will install OS packages then run `npm install`. The Vagrant VM serves to your host machine only on 192.168.33.10, you can access the web interface on [http://192.168.33.10:1337](http://192.168.33.10:1337) +## Installation on Windows + +If you have access to local computing resources and want to maintain more control over your data, you can host Nightscout and its database outside of the cloud. Windows Server supports MongoDB, Node.js, and Nightscout [installed on a single system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. + More questions? --------------- diff --git a/Release.md b/Release.md deleted file mode 100644 index 8e9ba512d..000000000 --- a/Release.md +++ /dev/null @@ -1,15 +0,0 @@ - -v0.4.1 / 2014-09-12 -================== - - * quick hack to prevent mbg records from crashing pebble - * add script to prep release branch - * tweak toolbar and button size/placement - * Merge pull request #128 from nightscout/wip/mbg - * Merge pull request #166 from nightscout/wip/id-rev - * Merge pull request #165 from nightscout/hotfix/pebble-sgv-string - * convert sgv to string - * add ability to easily id git rev-parse HEAD - * Merge branch 'release/0.4.0' into dev - * hack: only consider 'grey' sgv records - * Added searching for MBG data from mongo query. diff --git a/app.js b/app.js new file mode 100644 index 000000000..45c78b2fc --- /dev/null +++ b/app.js @@ -0,0 +1,86 @@ +'use strict'; + +var _ = require('lodash'); +var express = require('express'); +var compression = require('compression'); +var bodyParser = require('body-parser'); + +function create (env, ctx) { + var app = express(); + var appInfo = env.name + ' ' + env.version; + app.set('title', appInfo); + app.enable('trust proxy'); // Allows req.secure test on heroku https connections. + + if (ctx.bootErrors && ctx.bootErrors.length > 0) { + app.get('*', require('./lib/booterror')(ctx)); + return app; + } + + if (env.settings.isEnabled('cors')) { + var allowOrigin = _.get(env, 'extendedSettings.cors.allowOrigin') || '*'; + console.info('Enabled CORS, allow-origin:', allowOrigin); + app.use(function allowCrossDomain (req, res, next) { + res.header('Access-Control-Allow-Origin', allowOrigin); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); + + // intercept OPTIONS method + if ('OPTIONS' === req.method) { + res.send(200); + } else { + next(); + } + }); + } + + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + var api = require('./lib/api/')(env, ctx); + var ddata = require('./lib/data/endpoints')(env, ctx); + + app.use(compression({filter: function shouldCompress(req, res) { + //TODO: return false here if we find a condition where we don't want to compress + // fallback to standard filter function + return compression.filter(req, res); + }})); + // app.use(bodyParser({limit: 1048576 * 50, extended: true })); + + //if (env.api_secret) { + // console.log("API_SECRET", env.api_secret); + //} + app.use('/api/v1', bodyParser({limit: 1048576 * 50 }), api); + + app.use('/api/v2/properties', ctx.properties); + app.use('/api/v2/authorization', ctx.authorization.endpoints); + app.use('/api/v2/ddata', ddata); + + // pebble data + app.get('/pebble', ctx.pebble); + + // expose swagger.yaml + app.get('/swagger.yaml', function (req, res) { + res.sendFile(__dirname + '/swagger.yaml'); + }); + + //app.get('/package.json', software); + + // define static server + //TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution + var staticFiles = express.static(env.static_files, {maxAge: 60 * 60 * 1000}); + + // serve the static content + app.use(staticFiles); + + var bundle = require('./bundle')(); + app.use(bundle); + + // Handle errors with express's errorhandler, to display more readable error messages. + var errorhandler = require('errorhandler'); + //if (process.env.NODE_ENV === 'development') { + app.use(errorhandler()); + //} + return app; +} +module.exports = create; + diff --git a/app.json b/app.json index 77a1a308c..63a5aa37c 100644 --- a/app.json +++ b/app.json @@ -8,17 +8,27 @@ "required": true }, "API_SECRET": { - "description": "REQUIRED: User generated password used for REST API and optional features (12 character minimum).", + "description": "REQUIRED: A secret passphrase that must be at least 12 characters long, required to enable POST and PUT; also required for the Care Portal", "value": "", "required": true }, + "DISPLAY_UNITS": { + "description": "Choices: mg/dl and mmol. Setting to mmol puts the entire server into mmol mode by default, no further settings needed.", + "value": "", + "required": false + }, "ENABLE": { - "description": "Space delimited list of optional features to enable, such as 'careportal'.", + "description": "Used to enable optional features, expects a space delimited list, such as: careportal rawbg iob, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", + "value": "", + "required": false + }, + "DISABLE": { + "description": "Used to disable default features, expects a space delimited list, such as: direction upbat, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", "value": "", "required": false }, "ALARM_TYPES": { - "description": "Nightscout alarm behavior control. Default null value implies 'predict'. For adjustable alarm thresholds (set below), set to 'simple'.", + "description": "Alarm behavior currently 2 alarm types are supported simple and predict, and can be used independently or combined. The simple alarm type only compares the current BG to BG_ thresholds above, the predict alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. predict DOES NOT currently use any of the BG_* ENV's", "value": "", "required": false }, @@ -51,9 +61,110 @@ "description": "Pushover user key, required for Pushover notifications. Leave blank if not using Pushover.", "value": "", "required": false + }, + "PUSHOVER_ANNOUNCEMENT_KEY": { + "description": "An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. Leave blank if not using Pushover", + "value": "", + "required": false + }, + "CUSTOM_TITLE": { + "description": "Customize the name of the website, usually the name of T1.", + "value": "", + "required": false + }, + "THEME": { + "description": "Possible values default, colors or colorblindfriendly", + "value": "", + "required": false + }, + "SHOW_RAWBG": { + "description": "Possible values always, never or noise", + "value": "", + "required": false + }, + "BRIDGE_USER_NAME": { + "description": "Share bridge - Your user name for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "value": "", + "required": false + }, + "BRIDGE_PASSWORD": { + "description": "Share bridge - Your password for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "value": "", + "required": false + }, + "TIME_FORMAT": { + "description": "Browser default time mode valid settings are 12 or 24", + "value": "12", + "required": false + }, + "NIGHT_MODE": { + "description": "Browser defaults to night mode valid settings are on or off", + "value": "off", + "required": false + }, + "SHOW_RAWBG": { + "description": "Browser default raw display mode vaild settings are always never or noise", + "value": "never", + "required": false + }, + "THEME": { + "description": "Browser default theme setting vaild settings are default or colors", + "value": "default", + "required": false + }, + "ALARM_URGENT_HIGH": { + "description": "Browser default urgent high alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_HIGH": { + "description": "Browser default high alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_LOW": { + "description": "Browser default low alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_URGENT_LOW": { + "description": "Browser default urgent low alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_WARN": { + "description": "Browser default warn after time of last data exceeds ALARM_TIMEAGO_WARN_MINS alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_WARN_MINS": { + "description": "Browser default minutes since the last reading to trigger a warning", + "value": "15", + "required": false + }, + "ALARM_TIMEAGO_URGENT": { + "description": "Browser default urgent warning after time of last data exceeds ALARM_TIMEAGO_URGENT_MINS alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_URGENT_MINS": { + "description": "Browser default minutes since last reading to trigger an urgent alarm", + "value": "30", + "required": false + }, + "MAKER_KEY": { + "description": "Maker Key - Set this to your secret key Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker, Leave blank if not using maker", + "value": "", + "required": false + }, + "MAKER_ANNOUNCEMENT_KEY": { + "description": "Maker Announcement Key - Set this to your secret key for announcements Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker Leave blank if not using maker", + "value": "", + "required": false } }, "addons": [ - "mongolab:sandbox" + "mongolab:sandbox", + "papertrail" ] } diff --git a/azuredeploy.json b/azuredeploy.json new file mode 100644 index 000000000..4e43137b8 --- /dev/null +++ b/azuredeploy.json @@ -0,0 +1,325 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "siteLocation": { + "type": "string" + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + }, + "repoUrl": { + "type": "string" + + }, + "branch": { + "type": "string" + }, + "mongoConnection": { + "type": "string" + }, + "mongoCollection": { + "type": "string", + "defaultValue": "entries" + }, + "displayUnits": { + "type": "string", + "allowedValues": [ + "mg/dl", + "mmol" + ], + "defaultValue": "mg/dl" + }, + "apiSecret": { + "type": "string", + "minLength": "12", + "defaultValue": "Enter an API secret. Must be at least 12 characters" + }, + "theme": { + "type": "string", + "allowedValues": [ + "default", + "colors", + "colorblindfriendly" + ], + "defaultValue": "colors", + }, + "time_format": { + "type": "string", + "allowedValues": [ + "12", + "24" + ], + "defaultValue": "24", + }, + "language": { + "type": "string", + "allowedValues": [ + "bg", + "cs", + "dk", + "de", + "el", + "en", + "es", + "fr", + "he", + "hr", + "it", + "nl", + "nb", + "pl", + "pt", + "ro", + "ru", + "sk", + "sv", + "fi", + "ko" + ], + "defaultValue": "en", + }, + "custom_title": { + "type": "string", + "defaultValue": "Nightscout", + }, + "alarm_high": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "alarm_low": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "alarm_timeago_urgent": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "alarm_timeago_warn": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "alarm_urgent_high": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "alarm_urgent_low": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "basal_render": { + "type": "string", + "allowedValues": [ + "none", + "default", + "icicle" + ], + "defaultValue": "none", + }, + "scale_y": { + "type": "string", + "allowedValues": [ + "log", + "linear", + "log-dynamic" + ], + "defaultValue": "log", + }, + "enable": { + "type": "string", + "defaultValue": "basal bwp cage careportal cob rawbg sage iage treatmentnotify boluscalc profile", + }, + "night_mode": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + }, + "show_plugins": { + "type": "string", + "defaultValue": "careportal", + }, + "show_rawbg": { + "type": "string", + "allowedValues": [ + "always", + "never", + "noise" + ], + "defaultValue": "never", + }, + "devicestatus_advanced": { + "type": "string", + "allowedValues": [ + "true", + "false" + ], + "defaultValue": "false", + }, + "profile_multiple": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off", + } + }, + "resources": [{ + "apiVersion": "2015-04-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverFarms", + "location": "[parameters('siteLocation')]", + "properties": { + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, { + "apiVersion": "2015-08-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/Sites", + "location": "[parameters('siteLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" + }, + "properties": { + "serverFarmId": "[parameters('hostingPlanName')]", + "siteConfig": { + "appSettings": [{ + "name": "MONGO_CONNECTION", + "value": "[parameters('mongoConnection')]" + }, + { + "name": "MONGO_COLLECTION", + "value": "[parameters('mongoCollection')]" + }, { + "name": "DISPLAY_UNITS", + "value": "[parameters('displayUnits')]" + }, { + "name": "API_SECRET", + "value": "[parameters('apiSecret')]" + }, { + "name": "THEME", + "value": "[parameters('theme')]" + }, { + "name": "TIME_FORMAT", + "value": "[parameters('time_format')]" + }, { + "name": "LANGUAGE", + "value": "[parameters('language')]" + }, { + "name": "CUSTOM_TITLE", + "value": "[parameters('custom_title')]" + }, { + "name": "ALARM_HIGH", + "value": "[parameters('alarm_high')]" + }, { + "name": "ALARM_LOW", + "value": "[parameters('alarm_low')]" + }, { + "name": "ALARM_TIMEAGO_URGENT", + "value": "[parameters('alarm_timeago_urgent')]" + }, { + "name": "ALARM_TIMEAGO_WARN", + "value": "[parameters('alarm_timeago_warn')]" + }, { + "name": "ALARM_URGENT_HIGH", + "value": "[parameters('alarm_urgent_high')]" + }, { + "name": "ALARM_URGENT_LOW", + "value": "[parameters('alarm_urgent_low')]" + }, { + "name": "BASAL_RENDER", + "value": "[parameters('basal_render')]" + }, { + "name": "SCALE_Y", + "value": "[parameters('scale_y')]" + }, { + "name": "ENABLE", + "value": "[parameters('enable')]" + }, { + "name": "NIGHT_MODE", + "value": "[parameters('night_mode')]" + }, { + "name": "SHOW_PLUGINS", + "value": "[parameters('show_plugins')]" + }, { + "name": "SHOW_RAWBG", + "value": "[parameters('show_rawbg')]" + }, { + "name": "DEVICESTATUS_ADVANCED", + "value": "[parameters('devicestatus_advanced')]" + }, { + "name": "PROFILE_MULTIPLE", + "value": "[parameters('profile_multiple')]" + } + ] + } + }, + "resources": [{ + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": true + } + }] + }] +} diff --git a/bin/post-sgv.sh b/bin/post-sgv.sh new file mode 100755 index 000000000..354637525 --- /dev/null +++ b/bin/post-sgv.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Date is epoch in nanosecods...ie linux echo $(($(date +%s%N)/1000000)) +# $API_SECRET needs to be a hashed value of your secret key...ie linux echo -n "" | sha1sum + +curl -H "Content-Type: application/json" -H "api-secret: $API_SECRET" -XPOST 'http://localhost:1337/api/v1/entries/' -d '{ + "sgv": 100, + "type": "sgv", + "direction": "Flat", + "date": 1449872210706 +}' diff --git a/bower.json b/bower.json index 9de3a9bc4..79e5006b1 100644 --- a/bower.json +++ b/bower.json @@ -1,14 +1,15 @@ { "name": "nightscout", - "version": "0.6.4", + "version": "0.9.2", "dependencies": { - "angularjs": "1.3.0-beta.19", - "bootstrap": "~3.2.0", - "d3": "3.4.3", - "jquery": "2.1.0", + "colorbrewer": "~1.0.0", "jQuery-Storage-API": "~1.7.2", + "jquery": "2.1.0", + "jquery-flot": "0.8.3", + "jquery-ui": "~1.11.3", + "swagger-ui": "~2.1.2", "tipsy-jmalonzo": "~1.0.1", - "jsSHA": "~1.5.0" + "tone": "*" }, "resolutions": { "jquery": "2.1.0" diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js new file mode 100644 index 000000000..aa9d19e6f --- /dev/null +++ b/bundle/bundle.source.js @@ -0,0 +1,21 @@ +(function () { + + window._ = require('lodash'); + window.d3 = require('d3'); + window.$ = window.jQuery = require('jquery'); + window.moment = require('moment-timezone'); + window.sugar = require('sugar'); + window.crossfilter = require('crossfilter'); + window.Nightscout = window.Nightscout || {}; + + window.Nightscout = { + client: require('../lib/client') + , units: require('../lib/units')() + , report_plugins: require('../lib/report_plugins/')() + , admin_plugins: require('../lib/admin_plugins/')() + }; + + console.info('Nightscout bundle ready'); + +})(); + diff --git a/bundle/index.js b/bundle/index.js new file mode 100644 index 000000000..cb22a5ebc --- /dev/null +++ b/bundle/index.js @@ -0,0 +1,17 @@ +'use strict'; + +var browserify_express = require('browserify-express'); + +function bundle() { + return browserify_express({ + entry: __dirname + '/bundle.source.js', + watch: __dirname + '/../lib/', + mount: '/public/js/bundle.js', + verbose: true, + minify: true, + bundle_opts: { debug: true }, // enable inline sourcemap on js files + write_file: __dirname + '/bundle.out.js' + }); +} + +module.exports = bundle; diff --git a/deploy.sh b/deploy.sh index 4bc630de4..37b307e8a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -78,7 +78,7 @@ selectNodeVersion () { exitWithMessageOnError "getting node version failed" fi - if [[ -e "$DEPLOYMENT_TEMP/.tmp" ]]; then + if [[ -e "$DEPLOYMENT_TEMP/__npmVersion.tmp" ]]; then NPM_JS_PATH=`cat "$DEPLOYMENT_TEMP/__npmVersion.tmp"` exitWithMessageOnError "getting npm version failed" fi @@ -99,6 +99,7 @@ selectNodeVersion () { # ---------- echo Handling node.js deployment. +echo "\"$SCM_COMMIT_ID\"" > $DEPLOYMENT_SOURCE/scm-commit-id.json # 1. KuduSync if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then diff --git a/env.js b/env.js index 0218d6963..73d3ce58a 100644 --- a/env.js +++ b/env.js @@ -1,151 +1,196 @@ 'use strict'; -var env = { }; +var _ = require('lodash'); +var fs = require('fs'); var crypto = require('crypto'); var consts = require('./lib/constants'); -var fs = require('fs'); + +var env = { + settings: require('./lib/settings')() +}; + // Module to constrain all config and environment parsing to one spot. +// See the function config ( ) { - /* - * First inspect a bunch of environment variables: - * * PORT - serve http on this port - * * MONGO_CONNECTION, CUSTOMCONNSTR_mongo - mongodb://... uri - * * CUSTOMCONNSTR_mongo_collection - name of mongo collection with "sgv" documents - * * CUSTOMCONNSTR_mongo_settings_collection - name of mongo collection to store configurable settings - * * API_SECRET - if defined, this passphrase is fed to a sha1 hash digest, the hex output is used to create a single-use token for API authorization - * * NIGHTSCOUT_STATIC_FILES - the "base directory" to use for serving - * static files over http. Default value is the included `static` - * directory. + * See README.md for info about all the supported ENV VARs */ - var software = require('./package.json'); - var git = require('git-rev'); + env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); + env.PORT = readENV('PORT', 1337); + env.HOSTNAME = readENV('HOSTNAME', null); + env.IMPORT_CONFIG = readENV('IMPORT_CONFIG', null); + env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); - if (readENV('SCM_GIT_EMAIL') == 'windowsazure' && readENV('ScmType') == 'GitHub') { - git.cwd('/home/site/repository'); - } - if (readENV('SCM_COMMIT_ID')) { - env.head = readENV('SCM_COMMIT_ID'); - } else { - git.short(function record_git_head (head) { - console.log("GIT HEAD", head); - env.head = head; - }); + if (env.err) { + delete env.err; } - env.version = software.version; - env.name = software.name; - env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); - env.PORT = readENV('PORT', 1337); - env.mongo = readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI'); - env.mongo_collection = readENV('MONGO_COLLECTION', 'entries'); - env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); - env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); - env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); + setSSL(); + setAPISecret(); + setVersion(); + setStorage(); + updateSettings(); + + return env; +} - env.enable = readENV('ENABLE'); +function setSSL() { env.SSL_KEY = readENV('SSL_KEY'); env.SSL_CERT = readENV('SSL_CERT'); env.SSL_CA = readENV('SSL_CA'); env.ssl = false; if (env.SSL_KEY && env.SSL_CERT) { env.ssl = { - key: fs.readFileSync(env.SSL_KEY) - , cert: fs.readFileSync(env.SSL_CERT) + key: fs.readFileSync(env.SSL_KEY), cert: fs.readFileSync(env.SSL_CERT) }; if (env.SSL_CA) { env.ca = fs.readFileSync(env.SSL_CA); } } +} - var shasum = crypto.createHash('sha1'); - - ///////////////////////////////////////////////////////////////// - // A little ugly, but we don't want to read the secret into a var - ///////////////////////////////////////////////////////////////// +// A little ugly, but we don't want to read the secret into a var +function setAPISecret() { var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0); + //TODO: should we clear API_SECRET from process env? env.api_secret = null; // if a passphrase was provided, get the hex digest to mint a single token if (useSecret) { if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) { - var msg = ["API_SECRET should be at least", consts.MIN_PASSPHRASE_LENGTH, "characters"]; - var err = new Error(msg.join(' ')); - // console.error(err); - throw err; - process.exit(1); - } - shasum.update(readENV('API_SECRET')); - env.api_secret = shasum.digest('hex'); - } + var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters'].join(' '); + console.error(msg); + env.err = {desc: msg}; + } else { + var shasum = crypto.createHash('sha1'); + shasum.update(readENV('API_SECRET')); + env.api_secret = shasum.digest('hex'); - env.thresholds = { - bg_high: readIntENV('BG_HIGH', 260) - , bg_target_top: readIntENV('BG_TARGET_TOP', 180) - , bg_target_bottom: readIntENV('BG_TARGET_BOTTOM', 80) - , bg_low: readIntENV('BG_LOW', 55) - }; + if (!readENV('TREATMENTS_AUTH', true)) { - //NOTE: using +/- 1 here to make the thresholds look visibly wrong in the UI - // if all thresholds were set to the same value you should see 4 lines stacked right on top of each other - if (env.thresholds.bg_target_bottom >= env.thresholds.bg_target_top) { - console.warn('BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ') was >= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')'); - env.thresholds.bg_target_bottom = env.thresholds.bg_target_top - 1; - console.warn('BG_TARGET_BOTTOM is now ' + env.thresholds.bg_target_bottom); - } + } - if (env.thresholds.bg_target_top <= env.thresholds.bg_target_bottom) { - console.warn('BG_TARGET_TOP(' + env.thresholds.bg_target_top + ') was <= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')'); - env.thresholds.bg_target_top = env.thresholds.bg_target_bottom + 1; - console.warn('BG_TARGET_TOP is now ' + env.thresholds.bg_target_top); - } - if (env.thresholds.bg_low >= env.thresholds.bg_target_bottom) { - console.warn('BG_LOW(' + env.thresholds.bg_low + ') was >= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')'); - env.thresholds.bg_low = env.thresholds.bg_target_bottom - 1; - console.warn('BG_LOW is now ' + env.thresholds.bg_low); + } } +} - if (env.thresholds.bg_high <= env.thresholds.bg_target_top) { - console.warn('BG_HIGH(' + env.thresholds.bg_high + ') was <= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')'); - env.thresholds.bg_high = env.thresholds.bg_target_top + 1; - console.warn('BG_HIGH is now ' + env.thresholds.bg_high); - } +function setVersion() { + var software = require('./package.json'); + var git = require('git-rev'); - //if any of the BG_* thresholds are set, default to "simple" otherwise default to "predict" to preserve current behavior - var thresholdsSet = readIntENV('BG_HIGH') || readIntENV('BG_TARGET_TOP') || readIntENV('BG_TARGET_BOTTOM') || readIntENV('BG_LOW'); - env.alarm_types = readENV('ALARM_TYPES') || (thresholdsSet ? "simple" : "predict"); + if (readENV('APPSETTING_ScmType') === readENV('ScmType') && readENV('ScmType') === 'GitHub') { + env.head = require('./scm-commit-id.json'); + console.log('SCM COMMIT ID', env.head); + } else { + git.short(function record_git_head(head) { + console.log('GIT HEAD', head); + env.head = head || readENV('SCM_COMMIT_ID') || readENV('COMMIT_HASH', ''); + }); + } + env.version = software.version; + env.name = software.name; +} - // For pushing notifications to Pushover. - env.pushover_api_token = readENV('PUSHOVER_API_TOKEN'); - env.pushover_user_key = readENV('PUSHOVER_USER_KEY') || readENV('PUSHOVER_GROUP_KEY'); +function setStorage() { + env.storageURI = readENV('STORAGE_URI') || readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI') || readENV('MONGODB_URI'); + env.entries_collection = readENV('ENTRIES_COLLECTION') || readENV('MONGO_COLLECTION', 'entries'); + env.MQTT_MONITOR = readENV('MQTT_MONITOR', null); + if (env.MQTT_MONITOR) { + var hostDbCollection = [env.storageURI.split('mongodb://').pop().split('@').pop(), env.entries_collection].join('/'); + var mongoHash = crypto.createHash('sha1'); + mongoHash.update(hostDbCollection); + //some MQTT servers only allow the client id to be 23 chars + env.mqtt_client_id = mongoHash.digest('base64').substring(0, 23); + console.info('Using Mongo host/db/collection to create the default MQTT client_id', hostDbCollection); + if (env.MQTT_MONITOR.indexOf('?clientId=') === -1) { + console.info('Set MQTT client_id to: ', env.mqtt_client_id); + } else { + console.info('MQTT configured to use a custom client id, it will override the default: ', env.mqtt_client_id); + } + } + env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_'); + env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); + env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); + env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); + env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food'); // TODO: clean up a bit // Some people prefer to use a json configuration file instead. // This allows a provided json config to override environment variables var DB = require('./database_configuration.json'), - DB_URL = DB.url ? DB.url : env.mongo, - DB_COLLECTION = DB.collection ? DB.collection : env.mongo_collection, - DB_SETTINGS_COLLECTION = DB.settings_collection ? DB.settings_collection : env.settings_collection; - env.mongo = DB_URL; - env.mongo_collection = DB_COLLECTION; - env.settings_collection = DB_SETTINGS_COLLECTION; - env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); - - return env; + DB_URL = DB.url ? DB.url : env.storageURI, + DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection; + env.storageURI = DB_URL; + env.entries_collection = DB_COLLECTION; } -function readIntENV(varName, defaultValue) { - return parseInt(readENV(varName)) || defaultValue; +function updateSettings() { + + var envNameOverrides = { + UNITS: 'DISPLAY_UNITS' + }; + + env.settings.eachSettingAsEnv(function settingFromEnv (name) { + var envName = envNameOverrides[name] || name; + return readENV(envName); + }); + + //should always find extended settings last + env.extendedSettings = findExtendedSettings(process.env); + + if (!readENVTruthy('TREATMENTS_AUTH', true)) { + env.settings.authDefaultRoles = env.settings.authDefaultRoles || ""; + env.settings.authDefaultRoles += ' careportal'; + } + + } function readENV(varName, defaultValue) { - //for some reason Azure uses this prefix, maybe there is a good reason - var value = process.env['CUSTOMCONNSTR_' + varName] - || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] - || process.env[varName] - || process.env[varName.toLowerCase()]; + //for some reason Azure uses this prefix, maybe there is a good reason + var value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; + - return value || defaultValue; + return value != null ? value : defaultValue; +} + +function readENVTruthy(varName, defaultValue) { + var value = readENV(varName, defaultValue); + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + return value; +} + +function findExtendedSettings (envs) { + var extended = {}; + + function normalizeEnv (key) { + return key.toUpperCase().replace('CUSTOMCONNSTR_', ''); + } + + _.each(env.settings.enable, function eachEnable(enable) { + if (_.trim(enable)) { + _.forIn(envs, function eachEnvPair (value, key) { + var env = normalizeEnv(key); + if (_.startsWith(env, enable.toUpperCase() + '_')) { + var split = env.indexOf('_'); + if (split > -1 && split <= env.length) { + var exts = extended[enable] || {}; + extended[enable] = exts; + var ext = _.camelCase(env.substring(split + 1).toLowerCase()); + if (!isNaN(value)) { value = Number(value); } + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + exts[ext] = value; + } + } + }); + } + }); + return extended; } module.exports = config; diff --git a/example-profiles.md b/example-profiles.md new file mode 100644 index 000000000..fe9bc0f64 --- /dev/null +++ b/example-profiles.md @@ -0,0 +1,127 @@ + + +**Table of Contents** + +- [Example Profiles](#example-profiles) + - [Simple profile](#simple-profile) + - [Profile can also use time periods for any field, for example:](#profile-can-also-use-time-periods-for-any-field-for-example) + + + +#Example Profiles + +These are only examples, make sure you update all fields to fit your needs + +##Simple profile + ```json + { + "dia": 3, + "carbs_hr": 20, + "carbratio": 30, + "sens": 100, + "basal": 0.125, + "target_low": 100, + "target_high": 120 + } + ``` + +##Profile can also use time periods for any field, for example: + + ```json + { + "carbratio": [ + { + "time": "00:00", + "value": 30 + }, + { + "time": "06:00", + "value": 25 + }, + { + "time": "14:00", + "value": 28 + } + ], + "basal": [ + { + "time": "00:00", + "value": 0.175 + }, + { + "time": "02:30", + "value": 0.125 + }, + { + "time": "05:00", + "value": 0.075 + }, + { + "time": "08:00", + "value": 0.100 + }, + { + "time": "14:00", + "value": 0.125 + }, + { + "time": "20:00", + "value": 0.175 + }, + { + "time": "22:00", + "value": 0.200 + } + ] + } + ``` + +##Starting 0.9.0 profile data will be converted following way + + source (result from api call) - only [0] used now + ```[ { XXX, startDate: xxx }, { YYY, startDate: yyy } ]``` + + converted data + ``` + [ + { + defaultProfile: "Default" + , store: { + "Default" : { XXX } + } + , startDate: xxx + } + , { + defaultProfile: "Default" + , store: { + "Default" : { YYY } + } + , startDate: yyy + } + ] + ``` + + example of one new profile + + ``` + { + defaultProfile: "2-Weekend" + , store: { + "1-Weekday" : { AAA } + "2-Weekend" : { BBB } + "3-Exercise" : { CCC } + } + , startDate: xxx + } + ``` + + for basals currently used profile will be determined by last treatment record of format + ``` + { + eventType: "Profile Change" + , profile: "2-Weekend" + } + ``` + + for boluscalc profile used for calculation will be specified by key `profile` + diff --git a/lib/admin_plugins/cleanstatusdb.js b/lib/admin_plugins/cleanstatusdb.js new file mode 100644 index 000000000..4769a7247 --- /dev/null +++ b/lib/admin_plugins/cleanstatusdb.js @@ -0,0 +1,69 @@ +'use strict'; + +var cleanstatusdb = { + name: 'cleanstatusdb' + , label: 'Clean Mongo status database' + , pluginType: 'admin' +}; + +function init() { + return cleanstatusdb; +} + +module.exports = init; + +cleanstatusdb.actions = [ + { + name: 'Delete all documents from devicestatus collection' + , description: 'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete all documents' + , confirmText: 'Delete all documents from devicestatus collection?' + } + ]; + +cleanstatusdb.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + $.ajax('/api/v1/devicestatus.json?count=500', { + headers: client.headers() + , success: function (records) { + var recs = (records.length === 500 ? '500+' : records.length); + $status.hide().text(translate('Database contains %1 records',{ params: [recs] })).fadeIn('slow'); + } + , error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + } + }).done(function () { if (callback) { callback(); } }); +}; + +cleanstatusdb.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax({ + method: 'DELETE' + , url: '/api/v1/devicestatus/*' + , headers: client.headers() + }).done(function success () { + $status.hide().text(translate('All records removed ...')).fadeIn('slow'); + if (callback) { + callback(); + } + }).fail(function fail() { + $status.hide().text(translate('Error')).fadeIn('slow'); + if (callback) { + callback(); + } + }); +}; diff --git a/lib/admin_plugins/futureitems.js b/lib/admin_plugins/futureitems.js new file mode 100644 index 000000000..3d5acc230 --- /dev/null +++ b/lib/admin_plugins/futureitems.js @@ -0,0 +1,170 @@ +'use strict'; + +var futureitems = { + name: 'futureitems' + , label: 'Remove future items from mongo database' + , pluginType: 'admin' +}; + +function init() { + return futureitems; +} + +module.exports = init; + +futureitems.actions = [ + { + name: 'Find and remove treatments in the future' + , description: 'This task find and remove treatments in the future.' + , buttonLabel: 'Remove treatments in the future' + } + , { + name: 'Find and remove entries in the future' + , description: 'This task find and remove CGM data in the future created by uploader with wrong date/time.' + , buttonLabel: 'Remove entries in the future' + } + ]; + +futureitems.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + function valueOrEmpty (value) { + return value ? value : ''; + } + + function showOneTreatment (tr, table) { + table.append($('').css('background-color','#0f0f0f') + .append($('').attr('width','20%').append(new Date(tr.created_at).toLocaleString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) + .append($('').attr('width','20%').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').attr('width','10%').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.insulin))) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.carbs))) + .append($('').attr('width','10%').append(valueOrEmpty(tr.enteredBy))) + .append($('').attr('width','20%').append(valueOrEmpty(tr.notes))) + ); + } + + function showTreatments(treatments, table) { + table.append($('').css('background','#040404') + .append($('').css('width','80px').attr('align','left').append(translate('Time'))) + .append($('').css('width','150px').attr('align','left').append(translate('Event Type'))) + .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) + .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) + .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) + .append($('').css('width','150px').attr('align','left').append(translate('Entered By'))) + .append($('').css('width','300px').attr('align','left').append(translate('Notes'))) + ); + for (var t=0; t').css('margin-top','10px'); + $('#admin_' + futureitems.name + '_0_html').append(table); + showTreatments(records, table); + futureitems.actions[0].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + } + , error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.treatmentrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/treatments/' + _id + , headers: client.headers() + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.treatmentrecords.length; i++) { + deleteRecordById(futureitems.treatmentrecords[i]._id); + } + $('#admin_' + futureitems.name + '_0_html').html(''); + + if (callback) { + callback(); + } +}; + +futureitems.actions[1].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + var now = new Date().getTime(); + $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now + '&count=288', { + headers: client.headers() + , success: function (records) { + futureitems.entriesrecords = records; + $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); + futureitems.actions[1].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + } + , error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.entriesrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[1].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deteleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/entries/' + _id + , headers: client.headers() + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.entriesrecords.length; i++) { + deteleteRecordById(futureitems.entriesrecords[i]._id); + } + + if (callback) { + callback(); + } +}; diff --git a/lib/admin_plugins/index.js b/lib/admin_plugins/index.js new file mode 100644 index 000000000..c5e86cb54 --- /dev/null +++ b/lib/admin_plugins/index.js @@ -0,0 +1,86 @@ +'use strict'; + +var _ = require('lodash'); + +function init() { + var allPlugins = [ + require('./subjects')() + , require('./roles')() + , require('./cleanstatusdb')() + , require('./futureitems')() + ]; + + function plugins(name) { + if (name) { + return _.find(allPlugins, {name: name}); + } else { + return plugins; + } + } + + plugins.eachPlugin = function eachPlugin(f) { + _.each(allPlugins, f); + }; + + plugins.createHTML = function createHTML(client) { + var translate = client.translate; + plugins.eachPlugin(function addHtml(p) { + var fs = $('
'); + $('#admin_placeholder').append(fs); + fs.append($('').append(translate(p.label))); + for (var i = 0; i < p.actions.length; i++) { + if (i !== 0) { + fs.append('
'); + } + var a = p.actions[i]; + // add main plugin html + if (a.name) { + fs.append($('').css('text-decoration','underline').append(translate(a.name))); + fs.append('
'); + } + fs.append($('').append(translate(a.description))); + fs.append($('
').attr('id','admin_' + p.name + '_' + i + '_html')); + fs.append($(' + +
+ + Authentication status: + + +

+ Status: Not loaded +

+ + + + + + + + + + + + diff --git a/static/profile/js/profileeditor.js b/static/profile/js/profileeditor.js new file mode 100644 index 000000000..b5db65be0 --- /dev/null +++ b/static/profile/js/profileeditor.js @@ -0,0 +1,689 @@ +(function () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var moment = window.moment; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + + var c_profile = null; + + //some commonly used selectors + var peStatus = $('.pe_status'); + var timezoneInput = $('#pe_timezone'); + var databaseRecords = $('#pe_databaserecords'); + var timeInput = $('#pe_time'); + var dateInput = $('#pe_date'); + + client.init(function loaded () { + + var translate = client.translate; + + var defaultprofile = { + //General values + 'dia':3, + + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }] + , 'carbs_hr': 20 + , 'delay': 20 + , 'sens': [ + { + 'time': '00:00', + 'value': 100 + }] + , 'timezone': 'UTC' + + //perGIvalues style values + , 'perGIvalues': false + , 'carbs_hr_high': 30 + , 'carbs_hr_medium': 30 + , 'carbs_hr_low': 30 + , 'delay_high': 15 + , 'delay_medium': 20 + , 'delay_low': 20 + + , 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }] + , 'target_low':[ + { + 'time': '00:00', + 'value': 0 + }] + , 'target_high':[ + { + 'time': '00:00', + 'value': 0 + }] + ,startDate: new Date(0).toISOString() + }; + +// , 'startDate': new Date() +// defaultprofile.startDate.setSeconds(0); +// defaultprofile.startDate.setMilliseconds(0); + + var icon_add = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABa0lEQVQ4T6WTzysEYRjHP+/Mrv2hHOTuqJRNOfgPSORHokg4OClHcnVzVygHF6WIcuHMnyCHVRyI3ZYxa23vzOzs7LzamaI0e5i89fTWt/f5vPV5n1cQsXLbHepvfLv5JaLORoZNwMbyFo5vYfsWB0c7xAasLa5T/vCg45Oj48P4gJWFVYxCA63L5PzkND5gfm4Jo+Chd5W5OrtsDYgS1pQ1OTuNUfTQO8tcX9xE+QugYnS/X81MzGP7MpTWkEFVZY1KxcVPV3h27zAtA+oCagIcDfWUCgEje31qfHwK06gHjaF5iXQcHCV5lHmqqgQCNEAI0IsavCVDwNBurxoeGwmaAkDDwvYsqtIh//6AJUoklP97s62BbJYeAqIcpJNZsoM+r2aVbKKOekiBL8An3BuAEiGg1SSKAYnttpFxPdR9Jv4zipxFTUuQKqsfYbFGWfTYuO06yRfxIyweoLuG+iMsFuBfvzFy7FqE33vs2BFqlfN5AAAAAElFTkSuQmCC'; + var icon_remove = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrElEQVQ4T42Ty2sTQRzHv5tmk2yyjRNtpfZhL8V6s2KoUNC2XqwgaCsVQcGiFqpHi0c9iRdR/ANE9KR40FIQX4cueKoPaKFoLdSYNtE0abKT1+5s9iW7aUMiHtzTzO7v85md+c6PA4DrHbsPCKIgOWO1pA7dT6YXnXH949SE/F63pqwZtRrO+SCKgjQ5NUV+azpmHj2krMwaJC4c8Erj+/eRyloMMwWFKgbn1nC3ervlK1evkXBLGBZT8SOewotnTylTNLdgeg/pDgZDC2cPHSR8bB22DVC9hFe0SG/H0xFXcHlykjRHRDBWgJcZSCY38Xx2lhqMnRYE34Px/sN9vlQWeoHBAx2yXsRruVAVuFsIBaSJ8+eJGPaBqQV4NROJjTzez89jLBoFn6FgybQL54wS3uTyVDFQ3cL2IYpBv3RhdJSIIQ80tQyv7gEqJvS8AmUlBs7UXPhtjtZgh3UFNYngk86NHCfNAg9dMwHVBPu+CpsVkTXKeJeVG+AGgTOZ3tt6MSKKjy+NjEBjFrR4ElZmA4pdxstMFsyyJu6tZZ7Ux9vwB6EAL50ZGiRECEPPUOixVTRxHlicgSVWxEdZpuZWfNuS2hk48NjwMIkIYZglBnV5Cbqtws/5IaAJmsfCglrEl2y2QeKmEBJ80tixKmxrFpSVr0gV0viQoxho2YUuPohmeFD22PiklLC4ma5JuBvdrfLJI0dJd0s7bM0ES8aR/BXDXGaTskqlL+D3Lwy0tZEePoAd4EA5YF4tYymdonfjmQh3s6dTPjU4SHYGwjAKecSXFyGlM1TdytntE56T+ts7SC/vhw3gm6njc2Kd3vm5Ub1IwQAvnYhGiZpYw1wiWYPrIw7wnBTt7CLOOwdmut14kQQvqt24tfK/utGR6LaF+iRqMf4N/O/8D28HiiCRYqzAAAAAAElFTkSuQmCC'; + //var icon_clone = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACSklEQVQ4T5VTYU9SYRR+zn0vXhDEFyRBXcpmbZnZsOXsI2xpVnNcv/ilufET8h/oL8h/gN/aWhs2yjKt26rPSvUh5yQodQlqgKQCd5fbLg2jxlycT+/OOc85z/OcvYSa8MoRLqimUEuzOSgwBl0nFFUtW1K1J/Ho6Hxtr/H2yQqnarJ3fFHucNnC57tdXIUJhydl6CAwIhwfFXCwf5hUC6XpjYXAQgUcUngRzkxlgHc0Kl+57I443S7s5VQwxqCVCYVSGfkTFSQQmsQmHGVzKJ78nJMs2izBohRg8ZFB293iSPQPdPOiqiOdyiGdysdKmp4t64LXxlu9otUKTVdhkSSUjgooHOez7R1uvr2bA10cfx66cb0n3GS14tPHrWz6R34i+fT2m6q0rruv7tvs9gdOtxNk0mA2SRB0gqVZwGb8AOSbXIkMDV+Qt7cy2IinJ+LRWxWNf5ur+Kx2vub0OABRg2QSIYqEr/EMaHjqrdJ/tce/upqIxR76B/8FVzySFc7MzWvtnW6vyjSAAFFgyBkSrt17n7jU1+ldjcVn1x+PzNTZzkWLTXF5zvmYmaEslCEIxnUE7H07AA1NvVPaO9r8X5Kpic+Pfp+oNvpCH/R6rKo5GphUIq1Oh7y++T2wv/LHvLNAtTXqHX8Zkqyt4XQ6M73/emzuf4GnDLg/wu0tbYliqTifWhqZbniAAfCMLs/oRMHU0s26VzjTA6PI/QqXJC1BxAZ3XwSSjbA4/UyesWU/Y2Jw51mgIRmnA4ytXXcU385iINYIg1+OJdcoyf/hkgAAAABJRU5ErkJggg=='; + //var icon_apply = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACh0lEQVQ4T5XSXUhTYRgH8P85O+fMOZ3H2WButc7MD3AQWyaYIRZEqANpt9HHCiwvIhWim8gPiqArJailVmdhJGEI3USk4rAQc1MnTdMitZhtfm86ncvcYhMls2i9l+/Hj//zvA+B/1kmcKSArGIohmNIZoIMkRVE1O95sOQaOXCCK+KUYgWcy99gne2zRA1Q9VR1/u7DVcVcIdyrLshFCtQPNyIqgG6gtWImduB69lVM+r9EQqvEajQM8dEBokcxnSWas0fSElMw5B1EdlIuLM43GJxz1P0zgfhhjDFDmsFf1pVixOeASqTGgt+Llk+tEwCt2wDuQgsSWghgxwXYNxvL8iwbIoLjN3KvsQQVRCAUgIyWg3c0wRdYPmoz2C0E7qE2NUFdnpa4D73TfZgLLNThIirCCGuW8MWpRcYC9TF8DYwhPU6DtvEO2Kb666zF9sidMBC6lVsJES1EiCDwZPQZHHMfzBJB7ONdcbLOm3mVcP9wQi5UwO2bQdP7pxOi9aDOYrB7NgATQnfyb2Mq4AJN0eDiU9A82oL+6UFcyS7DXqkSfixDKpDB1P8Ai98Xdd1661aZBO6jNif5YPm5zFOYXZsCQzHYE8dhZP4jspK1mAm6oYpR49Xn1+iZtNW81fdU/zp8kSaSDSSfpzxkPK85jfn1WawSK9BI9mMJXkiFSXAvzYAfMNu79O90v0/u1jdKzPER5IzmJLyhefiJFchFyWApKUzWRnj8Hp1F370VfRPaNgfKZgWfIlEZL2WVQsAASUIZ2sY60Ou01bQXdm2L/kcgvJn5PJ3nElTG0qwSuHwutAy32tsLunZE/ysQPsh5caCaoYRlQgHjoUnC8PK4ZUf0TeAnGJ/iJEGClrwAAAAASUVORK5CYII='; + + var mongorecords = []; + var currentrecord = 0; + var currentprofile = null; + var dirty = false; + + // Fetch data from mongo + peStatus.hide().text(translate('Loading profile records ...')).fadeIn('slow'); + $.ajax('/api/v1/profile.json', { + headers: client.headers() + , success: function (records) { + if (!records.length) { + records.push(defaultprofile); + } + client.profilefunctions.loadData(records); // do a conversion if needed + mongorecords = client.profilefunctions.data; + // create new profile to be edited from last record + if (mongorecords.length) { + _.each(mongorecords, function eachMongoProfile (mongoprofile) { + _.each(mongoprofile.store, function eachStoredProfile (p) { + // allign with default profile + for (var key in defaultprofile) { + if (defaultprofile.hasOwnProperty(key) && !p.hasOwnProperty(key)) { + p[key] = defaultprofile[key]; + } + } + for (key in p) { + if (p.hasOwnProperty(key) && !defaultprofile.hasOwnProperty(key)) { + delete p[key]; + } + } + convertToRanges(p); + }); + }); + + peStatus.hide().text(translate('Values loaded.')).fadeIn('slow'); + } else { + mongorecords.push({ + defaultProfile: 'Default' + , store : { + 'Default': _.cloneDeep(defaultprofile) + } + , startDate: new Date().toISOString() + }); + peStatus.hide().text(translate('Default values used.')).fadeIn('slow'); + } + } + , error: function () { + mongorecords.push({ + defaultProfile: 'Default' + , store : { + 'Default': _.cloneDeep(defaultprofile) + } + , startDate: new Date().toISOString() + }); + peStatus.hide().text(translate('Error. Default values used.')).fadeIn('slow'); + } + }).done(initeditor); + + // convert simple values to ranges if needed + function convertToRanges(profile) { + if (typeof profile.carbratio !== 'object') { profile.carbratio = [{ 'time': '00:00', 'value': profile.carbratio }]; } + if (typeof profile.sens !== 'object') { profile.sens = [{ 'time': '00:00', 'value': profile.sens }]; } + if (typeof profile.target_low !== 'object') { profile.target_low = [{ 'time': '00:00', 'value': profile.target_low }]; } + if (typeof profile.target_high !== 'object') { profile.target_high = [{ 'time': '00:00', 'value': profile.target_high }]; } + if (typeof profile.basal !== 'object') { profile.basal = [{ 'time': '00:00', 'value': profile.basal }]; } + if (profile.target_high.length !== profile.target_low.length) { + window.alert(translate('Time ranges of target_low and target_high don\'t match. Values are restored to defaults.')); + profile.target_low = _.cloneDeep(defaultprofile.target_low); + profile.target_high = _.cloneDeep(defaultprofile.target_high); + } + } + + function initeditor() { + $('#pe_history').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); + $('#pe_multiple').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.multiple); + + // Load timezones + timezoneInput.empty(); + moment.tz.names().forEach(function addTz(tz) { + timezoneInput.append(''); + }); + + $('#pe_form').find('button').click(profileSubmit); + + $('#pe_profiles').unbind().bind('change', profileChange); + $('#pe_profile_add').unbind().bind('click', profileAdd); + $('#pe_profile_remove').unbind().bind('click', profileRemove); + $('#pe_profile_clone').unbind().bind('click', profileClone); + + $('#pe_databaserecords').unbind().bind('change',recordChange); + $('#pe_records_add').unbind().bind('click', recordAdd); + $('#pe_records_remove').unbind().bind('click', recordRemove); + $('#pe_records_clone').unbind().bind('click', recordClone); + + // Add handler for style switching + $('#pe_perGIvalues').unbind().on('change', switchStyle); + + // display status + $('#pe_units').text(client.settings.units); + $('#pe_timeformat').text(client.settings.timeFormat+'h'); + $('#pe_title').text(client.settings.customTitle); + + currentprofile = mongorecords[currentrecord].defaultProfile; + + // prepare basal profiles + initRecord(); + // hide unused style of ratios + switchStyle(); + + console.log('Done initeditor()'); + } + + function initRecord() { + databaseRecords.empty(); + for (var r = 0; r < mongorecords.length; r++ ) { + databaseRecords.append(''); + } + databaseRecords.val(currentrecord); + + timeInput.val(moment(mongorecords[currentrecord].startDate).format('HH:mm')); + dateInput.val(moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD')); + + initProfile(); + } + + function initProfile() { + var record = mongorecords[currentrecord]; + // fill profilenames + $('#pe_profiles').empty(); + + for (var key in record.store) { + if (record.store.hasOwnProperty(key)) { + $('#pe_profiles').append(''); + } + } + + $('#pe_profiles').val(currentprofile); + $('#pe_profile_name').val(currentprofile); + + c_profile = mongorecords[currentrecord].store[currentprofile]; + mongorecords[currentrecord].defaultProfile = currentprofile; + // Set values from profile to html + fillTimeRanges(); + } + + // Handling of record list box change + function recordChange (event) { + if (dirty && window.confirm(translate('Save current record before switching to new?'))) { + profileSubmit(); + } + currentrecord = databaseRecords.val(); + currentprofile = mongorecords[currentrecord].defaultProfile; + initRecord(); + dirty = false; + maybePreventDefault(event); + } + + function recordAdd (event) { + if (dirty && window.confirm(translate('Save current record before switching to new?'))) { + profileSubmit(); + } + mongorecords.push({ + startDate: new Date().toISOString() + , defaultProfile: 'Default' + , store: { + 'Default' : _.cloneDeep(defaultprofile) + } + }); + currentrecord = mongorecords.length - 1; + currentprofile = 'Default'; + initRecord(); + dirty = true; + maybePreventDefault(event); + } + + function recordRemove (event) { + if (mongorecords.length > 1 && window.confirm(translate('Delete record')+'?')) { + if (mongorecords[currentrecord]._id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/profile/'+mongorecords[currentrecord]._id + , headers: client.headers() + }).done(function postSuccess () { + console.info('profile deleted'); + peStatus.hide().text(status).fadeIn('slow'); + mongorecords.splice(currentrecord,1); + currentrecord = 0; + currentprofile = mongorecords[currentrecord].defaultProfile; + initRecord(); + dirty = false; + }).fail(function(xhr, status, errorThrown) { + console.error('Profile not removed', status, errorThrown); + peStatus.hide().text(status).fadeIn('slow'); + }); + } else { + mongorecords.splice(currentrecord,1); + currentrecord = 0; + currentprofile = mongorecords[currentrecord].defaultProfile; + initRecord(); + dirty = false; + } + } + maybePreventDefault(event); + return false; + } + + function recordClone (event) { + if (dirty && window.confirm(translate('Save current record before switching to new?'))) { + profileSubmit(); + } + GUIToObject(); + mongorecords.push(_.cloneDeep(mongorecords[currentrecord])); + currentrecord = mongorecords.length - 1; + mongorecords[currentrecord].startDate = new Date().toISOString(); + currentprofile = mongorecords[currentrecord].defaultProfile; + delete mongorecords[currentrecord]._id; + initRecord(); + dirty = true; + + maybePreventDefault(event); + } + + // Handling of profile list box change + function profileChange (event) { + var record = mongorecords[currentrecord]; + var newpr = $('#pe_profiles').val(); + // copy values from html to c_profile + GUIToObject(); + + var newname = $('#pe_profile_name').val(); + if (currentprofile !== newname) { + // rename if already exists + while (record.store[newname]) { + newname += '1'; + } + record.store[newname] = record.store[currentprofile]; + delete record.store[currentprofile]; + dirty = true; + } + if (newpr === currentprofile) { // fake call to update values + newpr = newname; + } + currentprofile = newpr; + initProfile(); + + maybePreventDefault(event); + return false; + } + + function profileAdd (event) { + var record = mongorecords[currentrecord]; + var newname = 'New profile'; + while (record.store[newname]) { + newname += '1'; + } + record.store[newname] = _.cloneDeep(defaultprofile); + currentprofile = newname; + dirty = true; + + initProfile(); + maybePreventDefault(event); + return false; + } + + function profileRemove (event) { + var record = mongorecords[currentrecord]; + var availableProfile = getFirstAvailableProfile(record); + if (availableProfile) { + delete record.store[currentprofile]; + currentprofile = availableProfile; + initProfile(); + dirty = true; + } + + maybePreventDefault(event); + return false; + } + + function profileClone (event) { + GUIToObject(); + var record = mongorecords[currentrecord]; + var newname = $('#pe_profile_name').val() + ' (copy)'; + while (record.store[newname]) { + newname += '1'; + } + record.store[newname] = _.cloneDeep(record.store[currentprofile]); + currentprofile = newname; + dirty = true; + + initProfile(); + maybePreventDefault(event); + return false; + } + + // Handling html events and setting/getting values + function switchStyle(event) { + if (!$('#pe_perGIvalues').is(':checked')) { + $('#pe_simple').show('slow'); + $('#pe_advanced').hide('slow'); + } else { + $('#pe_simple').hide('slow'); + $('#pe_advanced').show('slow'); + } + maybePreventDefault(event); + } + + function fillTimeRanges(event) { + if (event) { + GUIToObject(); + } + + function shouldAddTime(i, time, array) { + if (i === 0 && time === 0) { + return true; + } else if (i === 0) { + return false; + } else { + var minutesFromMidnight = toMinutesFromMidnight(c_profile[array][i - 1].time); + return !isNaN(minutesFromMidnight) && minutesFromMidnight < time * 30; + } + } + + function addSingleLine(e,i) { + var tr = $(''); + var select = $('').attr('id',e.prefix+'_val_'+i).attr('value',c_profile[e.array][i].value))); + var icons_td = $('').append($('').attr('class','addsingle').attr('style','cursor:pointer').attr('title',translate('Add new interval before')).attr('src',icon_add).attr('array',e.array).attr('pos',i)); + if (c_profile[e.array].length>1) { + icons_td.append($('').attr('class','delsingle').attr('style','cursor:pointer').attr('title',translate('Delete interval')).attr('src',icon_remove).attr('array',e.array).attr('pos',i)); + } + tr.append(icons_td); + + if (lowest>toMinutesFromMidnight(c_profile[e.array][i].time)) { + c_profile[e.array][i].time = toTimeString(lowest); + } + return tr[0].outerHTML; + } + + // Fill dropdown boxes + _.each([{prefix:'pe_basal', array:'basal', label: translate('Basal rate') + ' : '}, + {prefix:'pe_ic', array:'carbratio', label: translate('I:C') + ' : '}, + {prefix:'pe_isf', array:'sens', label: translate('ISF') + ' : '} + ], function (e) { + var html = ''; + for (var i=0; i'; + html += '
'; + $('#'+e.prefix+'_placeholder').html(html); + }); + + $('.addsingle').click(function addsingle_click() { + var array = $(this).attr('array'); + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile[array].splice(pos,0,{time:'00:00',value:0}); + return fillTimeRanges(); + }); + + $('.delsingle').click(function delsingle_click() { + var array = $(this).attr('array'); + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile[array].splice(pos,1); + c_profile[array][0].time = '00:00'; + return fillTimeRanges(); + }); + + function addBGLine(i) { + var tr = $(''); + var select = $('').attr('id','pe_targetbg_low_'+i).attr('value',c_profile.target_low[i].value))); + tr.append($('').append(translate('High') + ' : ').append($('').attr('id','pe_targetbg_high_'+i).attr('value',c_profile.target_high[i].value))); + var icons_td = $('').append($('').attr('class','addtargetbg').attr('style','cursor:pointer').attr('title',translate('Add new interval before')).attr('src',icon_add).attr('pos',i)); + if (c_profile.target_low.length>1) { + icons_td.append($('').attr('class','deltargetbg').attr('style','cursor:pointer').attr('title',translate('Delete interval')).attr('src',icon_remove).attr('pos',i)); + } + tr.append(icons_td); + + // Fix time to correct value after add or change + if (lowesttime>toMinutesFromMidnight(c_profile.target_low[i].time)) { + c_profile.target_low[i].time = toTimeString(lowesttime); + } + return tr[0].outerHTML; + } + + + // target BG + var html = ''; + for (var i=0; i'; + html += '
'; + $('#pe_targetbg_placeholder').html(html); + + $('.addtargetbg').click(function addtargetbg_click() { + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile.target_low.splice(pos,0,{time:'00:00',value:0}); + c_profile.target_high.splice(pos,0,{time:'00:00',value:0}); + dirty = true; + return fillTimeRanges(); + }); + + $('.deltargetbg').click(function deltargetbg_click() { + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile.target_low.splice(pos,1); + c_profile.target_high.splice(pos,1); + c_profile.target_low[0].time = '00:00'; + c_profile.target_high[0].time = '00:00'; + dirty = true; + return fillTimeRanges(); + }); + + $('.pe_selectabletime').unbind().on('change', fillTimeRanges); + + objectToGUI(); + maybePreventDefault(event); + return false; + } + + // fill GUI with values from c_profile object + function objectToGUI() { + + $('#pe_dia').val(c_profile.dia); + $('#pe_hr').val(c_profile.carbs_hr); + $('#pe_perGIvalues').prop('checked', c_profile.perGIvalues); + $('#pe_hr_high').val(c_profile.carbs_hr_high); + $('#pe_hr_medium').val(c_profile.carbs_hr_medium); + $('#pe_hr_low').val(c_profile.carbs_hr_low); + $('#pe_delay_high').val(c_profile.delay_high); + $('#pe_delay_medium').val(c_profile.delay_medium); + $('#pe_delay_low').val(c_profile.delay_low); + timezoneInput.val(c_profile.timezone); + + var index; + [ { prefix:'pe_basal', array:'basal' }, + { prefix:'pe_ic', array:'carbratio' }, + { prefix:'pe_isf', array:'sens' } + ].forEach(function (e) { + for (index=0; index + + + + + Nightscout radio + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Nightscout

+
+ +
+

Audio

+

+ Convert glucose to frequency. + +

+
+ +
+ +
+ Authentication status: + + + + + + + + + + + diff --git a/static/radio/js/radio.js b/static/radio/js/radio.js new file mode 100644 index 000000000..b5bdf48ab --- /dev/null +++ b/static/radio/js/radio.js @@ -0,0 +1,110 @@ +window.Nightscout.client.init(function loaded() { + /* + Many thanks to @ps2, Pete Schwamb + * https://gist.github.com/ps2/314145bb91fa720bba59cf58f7e9cad2 + 2**((numOctaves/(maxBG-minBG))*(bg-minBG) + Math.log2(minFreq)) + */ + function convert (opts) { + opts = opts || { }; + var octaves = opts.octaves || 9; + var maxBG = opts.maxBG || 400; + var minBG = opts.minBG || 40; + var minFreq = opts.minFreq || 55; + var x = minBG + , y = minFreq + , z = octaves/(maxBG - minBG) + ; + + function freq (bg) { + return Math.pow(2, (z* (bg - x ) ) + Math.log2(y)) ; + // return Math.pow(2, (z* (bg + x ) ) + Math.log2(y)) ; + } + + function invert (freq) { + + return ((Math.log2(freq) - Math.log2(y)) / z ) + x; + } + + function api (glucose) { + return freq(glucose); + } + + api.invert = invert; + api.freq = freq; + return api; + } + + + + function createLoop (synth, sgvs) { + function callback (time, note) { + console.log(time, note); + synth.triggerAttackRelease(note, '16n', time); + } + var seq = new Tone.Sequence(callback, sgvs, '16n'); + seq.loop = false; + return seq; + } + + function glucose (sgv) { + if (sgv) { + return parseInt(sgv.mgdl || sgv.sgv || sgv.glucose || 35); + } + + return 20; + } + + $(document).ready(function ( ) { + console.log('OK'); + var converter = convert( ); + var synth = new Tone.PolySynth(16, Tone.MonoSynth); + // default volume always makes my ears bleed + synth.chain(new Tone.Volume(-26), Tone.Master); + // synth.toMaster(); + Tone.Transport.timeSignature = [ 3, 2 ]; + Tone.Transport.bpm.value = 320; + + // function play_next (time) { + // var sgv = sgvs.shift( ); + // console.log(sgv); + // if (!sgv) { + // loop.stop( ); + // } + // if (sgv) { + // var freq = converter.freq(sgv.mgdl || 30); + // synth.triggerAttackRelease(parseInt(sgv.mgdl || sgv.sgv || sgv.glucose || 39) * 4, '8n', time); + // } + // } + + // var loop = new Tone.Loop(play_next, '4n'); + var loop; + function play_data ( ) { + var sgvs = Nightscout.client.sbx.data.sgvs.slice( ).map(glucose).map(converter.freq); + console.log('last two hours', sgvs.length); + var new_loop = createLoop(synth, sgvs); + if (loop) { + loop.stop( ); + loop.dispose( ); + loop = null; + } + loop = new_loop; + Nightscout.client.radio.loop = loop; + loop.start( ); + } + + Nightscout.client.radio = { + converter: converter + , synth: synth + , loop: loop + }; + + Nightscout.client.socket.on('dataUpdate', function ( ) { + play_data( ); + }); + $('#again').on('click', function ( ) { + play_data( ); + }); + Tone.Transport.start( ); + + }); +}); \ No newline at end of file diff --git a/static/report/compare.html b/static/report/compare.html new file mode 100644 index 000000000..532c8c3f2 --- /dev/null +++ b/static/report/compare.html @@ -0,0 +1,159 @@ + + + + Nightscout performance comparison + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nightscout performance comparison

+ +
+ + + + + + + + + + + + +
+
+
+
+
+
+ +
+
+
+
+ + from + +
+
+
+
+
+
+
+ + +
+ Authentication status: + + + + + + + + + + + + + + + + + + + + diff --git a/static/report/css/compare.css b/static/report/css/compare.css new file mode 100644 index 000000000..1be8cac1e --- /dev/null +++ b/static/report/css/compare.css @@ -0,0 +1,97 @@ + +@import url("../../mfb/mfb.min.css"); +#chartContainer, #toolbar, #view, #templates { + margin: 0 5%; + width: 80%; + position: relative; + min-height: 1em; +} + +#chartContainer svg { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + + +.reticle .controls>span, +.reticle .controls .begin, +.reticle .controls .end { + display: block; + float: left; + margin: 0; + +} +.reticle .controls .interstice { + width: 100%; + text-align: center; + line-height: 120%; + font-size: 200%; +} +.reticle .controls .begin { + position: absolute; + left: 0; +} +.reticle .controls .end { + position: absolute; + right: 0; +} + +.reticle .controls INPUT { + line-height: 120%; + font-size: 200%; +} +.reticle .controls INPUT.end-input { + text-align: right; +} + +#menu_holder { +} + +#menu { +} + +.reticle .timeline { + min-height: 8em; + border: 1px solid pink; +} + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.pool { + min-height: 500px; + border: 1px solid blue; +} + +#templates { + display: none; +} + +.debug #templates { + display: block; +} + +#view.collapse .pool { + position: absolute; + left: 0; + top: 0; + right: 0; + border: 1px solid silver; +} + +#view .observations { + position: relative; + padding-bottom: 500px; +} + +#view .gridlines line { + stroke: silver; + stroke-opacity: 60%; +} + diff --git a/static/report/css/mfb.min.css b/static/report/css/mfb.min.css new file mode 100644 index 000000000..0c4d5e49d --- /dev/null +++ b/static/report/css/mfb.min.css @@ -0,0 +1 @@ +.mfb-component,.mfb-component--bl,.mfb-component--br,.mfb-component--tl,.mfb-component--tr{box-sizing:border-box;margin:25px;position:fixed;white-space:nowrap;z-index:30;padding-left:0;list-style:none}.mfb-component *,.mfb-component :after,.mfb-component :before,.mfb-component--bl *,.mfb-component--bl :after,.mfb-component--bl :before,.mfb-component--br *,.mfb-component--br :after,.mfb-component--br :before,.mfb-component--tl *,.mfb-component--tl :after,.mfb-component--tl :before,.mfb-component--tr *,.mfb-component--tr :after,.mfb-component--tr :before{box-sizing:inherit}.mfb-component--tl{left:0;top:0}.mfb-component--tr{right:0;top:0}.mfb-component--bl{left:0;bottom:0}.mfb-component--br{right:0;bottom:0}.mfb-component__button,.mfb-component__button--child,.mfb-component__button--main{background-color:#E40A5D;display:inline-block;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);cursor:pointer;outline:0;padding:0;position:relative;-webkit-user-drag:none;font-weight:700;color:#f1f1f1}.mfb-component__list{list-style:none;margin:0;padding:0}.mfb-component__list>li{display:block;position:absolute;top:0;right:1px;padding:10px 0;margin:-10px 0}.mfb-component__child-icon,.mfb-component__icon,.mfb-component__main-icon--active,.mfb-component__main-icon--resting{position:absolute;font-size:18px;text-align:center;line-height:56px;width:100%}.mfb-component__wrap{padding:25px;margin:-25px}[data-mfb-state=open] .mfb-component__child-icon,[data-mfb-state=open] .mfb-component__icon,[data-mfb-state=open] .mfb-component__main-icon--active,[data-mfb-state=open] .mfb-component__main-icon--resting,[data-mfb-toggle=hover]:hover .mfb-component__child-icon,[data-mfb-toggle=hover]:hover .mfb-component__icon,[data-mfb-toggle=hover]:hover .mfb-component__main-icon--active,[data-mfb-toggle=hover]:hover .mfb-component__main-icon--resting{-webkit-transform:scale(1) rotate(0deg);transform:scale(1) rotate(0deg)}.mfb-component__button--main{height:56px;width:56px;z-index:20}.mfb-component__button--child{height:56px;width:56px}.mfb-component__main-icon--active,.mfb-component__main-icon--resting{-webkit-transform:scale(1) rotate(360deg);transform:scale(1) rotate(360deg);-webkit-transition:-webkit-transform 150ms cubic-bezier(.4,0,1,1);transition:transform 150ms cubic-bezier(.4,0,1,1)}.mfb-component__child-icon{line-height:56px;font-size:18px}.mfb-component__main-icon--active{opacity:0}[data-mfb-state=open] .mfb-component__main-icon,[data-mfb-toggle=hover]:hover .mfb-component__main-icon{-webkit-transform:scale(1) rotate(0deg);transform:scale(1) rotate(0deg)}[data-mfb-state=open] .mfb-component__main-icon--resting,[data-mfb-toggle=hover]:hover .mfb-component__main-icon--resting{opacity:0}[data-mfb-state=open] .mfb-component__main-icon--active,[data-mfb-toggle=hover]:hover .mfb-component__main-icon--active{opacity:1}.mfb-component--tl.mfb-slidein .mfb-component__list li,.mfb-component--tr.mfb-slidein .mfb-component__list li{opacity:0;transition:all .5s}.mfb-component--tl.mfb-slidein[data-mfb-state=open] .mfb-component__list li,.mfb-component--tl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li,.mfb-component--tr.mfb-slidein[data-mfb-state=open] .mfb-component__list li,.mfb-component--tr.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li{opacity:1}.mfb-component--tl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px);transform:translateY(70px)}.mfb-component--tl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px);transform:translateY(140px)}.mfb-component--tl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px);transform:translateY(210px)}.mfb-component--tl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px);transform:translateY(280px)}.mfb-component--bl.mfb-slidein .mfb-component__list li,.mfb-component--br.mfb-slidein .mfb-component__list li{opacity:0;transition:all .5s}.mfb-component--bl.mfb-slidein[data-mfb-state=open] .mfb-component__list li,.mfb-component--bl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li,.mfb-component--br.mfb-slidein[data-mfb-state=open] .mfb-component__list li,.mfb-component--br.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li{opacity:1}.mfb-component--bl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--bl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px);transform:translateY(-70px)}.mfb-component--bl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--bl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px);transform:translateY(-140px)}.mfb-component--bl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--bl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px);transform:translateY(-210px)}.mfb-component--bl.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--bl.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-slidein[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-slidein[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px);transform:translateY(-280px)}.mfb-component--tl.mfb-slidein-spring .mfb-component__list li,.mfb-component--tr.mfb-slidein-spring .mfb-component__list li{opacity:0;transition:all .5s cubic-bezier(.68,-.55,.265,1.55)}.mfb-component--tl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li,.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li,.mfb-component--tr.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li,.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li{opacity:1}.mfb-component--tl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px);transform:translateY(70px)}.mfb-component--tl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px);transform:translateY(140px)}.mfb-component--tl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px);transform:translateY(210px)}.mfb-component--tl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px);transform:translateY(280px)}.mfb-component--bl.mfb-slidein-spring .mfb-component__list li,.mfb-component--br.mfb-slidein-spring .mfb-component__list li{opacity:0;transition:all .5s cubic-bezier(.68,-.55,.265,1.55)}.mfb-component--bl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li,.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li,.mfb-component--br.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li,.mfb-component--br.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li{opacity:1}.mfb-component--bl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px);transform:translateY(-70px)}.mfb-component--bl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px);transform:translateY(-140px)}.mfb-component--bl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px);transform:translateY(-210px)}.mfb-component--bl.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-slidein-spring[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-slidein-spring[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px);transform:translateY(-280px)}.mfb-component--tl.mfb-zoomin .mfb-component__list li,.mfb-component--tr.mfb-zoomin .mfb-component__list li{-webkit-transform:scale(0);transform:scale(0)}.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px) scale(0);transform:translateY(70px) scale(0);transition:all .5s;transition-delay:.15s}.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px) scale(0);transform:translateY(140px) scale(0);transition:all .5s;transition-delay:.1s}.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px) scale(0);transform:translateY(210px) scale(0);transition:all .5s;transition-delay:.05s}.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px) scale(0);transform:translateY(280px) scale(0);transition:all .5s;transition-delay:0s}.mfb-component--tl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px) scale(1);transform:translateY(70px) scale(1);transition-delay:.05s}.mfb-component--tl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px) scale(1);transform:translateY(140px) scale(1);transition-delay:.1s}.mfb-component--tl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px) scale(1);transform:translateY(210px) scale(1);transition-delay:.15s}.mfb-component--tl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px) scale(1);transform:translateY(280px) scale(1);transition-delay:.2s}.mfb-component--bl.mfb-zoomin .mfb-component__list li,.mfb-component--br.mfb-zoomin .mfb-component__list li{-webkit-transform:scale(0);transform:scale(0)}.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px) scale(0);transform:translateY(-70px) scale(0);transition:all .5s;transition-delay:.15s}.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px) scale(0);transform:translateY(-140px) scale(0);transition:all .5s;transition-delay:.1s}.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px) scale(0);transform:translateY(-210px) scale(0);transition:all .5s;transition-delay:.05s}.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px) scale(0);transform:translateY(-280px) scale(0);transition:all .5s;transition-delay:0s}.mfb-component--bl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--bl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px) scale(1);transform:translateY(-70px) scale(1);transition-delay:.05s}.mfb-component--bl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--bl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px) scale(1);transform:translateY(-140px) scale(1);transition-delay:.1s}.mfb-component--bl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--bl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px) scale(1);transform:translateY(-210px) scale(1);transition-delay:.15s}.mfb-component--bl.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--bl.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-zoomin[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-zoomin[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px) scale(1);transform:translateY(-280px) scale(1);transition-delay:.2s}.mfb-component--tl.mfb-fountain .mfb-component__list li,.mfb-component--tr.mfb-fountain .mfb-component__list li{-webkit-transform:scale(0);transform:scale(0)}.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px) scale(0);transform:translateY(-70px) scale(0);transition:all .5s;transition-delay:.15s}.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px) scale(0);transform:translateY(-140px) scale(0);transition:all .5s;transition-delay:.1s}.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px) scale(0);transform:translateY(-210px) scale(0);transition:all .5s;transition-delay:.05s}.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px) scale(0);transform:translateY(-280px) scale(0);transition:all .5s;transition-delay:0s}.mfb-component--tl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--tr.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px) scale(1);transform:translateY(70px) scale(1);transition-delay:.05s}.mfb-component--tl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--tr.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px) scale(1);transform:translateY(140px) scale(1);transition-delay:.1s}.mfb-component--tl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--tr.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px) scale(1);transform:translateY(210px) scale(1);transition-delay:.15s}.mfb-component--tl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--tr.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px) scale(1);transform:translateY(280px) scale(1);transition-delay:.2s}.mfb-component--bl.mfb-fountain .mfb-component__list li,.mfb-component--br.mfb-fountain .mfb-component__list li{-webkit-transform:scale(0);transform:scale(0)}.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(1){-webkit-transform:translateY(70px) scale(0);transform:translateY(70px) scale(0);transition:all .5s;transition-delay:.15s}.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(2){-webkit-transform:translateY(140px) scale(0);transform:translateY(140px) scale(0);transition:all .5s;transition-delay:.1s}.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(3){-webkit-transform:translateY(210px) scale(0);transform:translateY(210px) scale(0);transition:all .5s;transition-delay:.05s}.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(4){-webkit-transform:translateY(280px) scale(0);transform:translateY(280px) scale(0);transition:all .5s;transition-delay:0s}.mfb-component--bl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--bl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(1),.mfb-component--br.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(1){-webkit-transform:translateY(-70px) scale(1);transform:translateY(-70px) scale(1);transition-delay:.05s}.mfb-component--bl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--bl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(2),.mfb-component--br.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(2){-webkit-transform:translateY(-140px) scale(1);transform:translateY(-140px) scale(1);transition-delay:.1s}.mfb-component--bl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--bl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(3),.mfb-component--br.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(3){-webkit-transform:translateY(-210px) scale(1);transform:translateY(-210px) scale(1);transition-delay:.15s}.mfb-component--bl.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--bl.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-fountain[data-mfb-state=open] .mfb-component__list li:nth-child(4),.mfb-component--br.mfb-fountain[data-mfb-toggle=hover]:hover .mfb-component__list li:nth-child(4){-webkit-transform:translateY(-280px) scale(1);transform:translateY(-280px) scale(1);transition-delay:.2s}[data-mfb-label]:after{content:attr(data-mfb-label);opacity:0;background:rgba(0,0,0,.4);padding:4px 10px;border-radius:3px;color:rgba(255,255,255,.8);font-size:13px;pointer-events:none;position:absolute;top:50%;margin-top:-10.5px;transition:all .5s}[data-mfb-state=open] [data-mfb-label]:after,[data-mfb-toggle=hover] [data-mfb-label]:hover:after{content:attr(data-mfb-label);opacity:1;transition:all .3s}.mfb-component--br .mfb-component__list [data-mfb-label]:after,.mfb-component--br [data-mfb-label]:after,.mfb-component--tr .mfb-component__list [data-mfb-label]:after,.mfb-component--tr [data-mfb-label]:after{content:attr(data-mfb-label);right:70px}.mfb-component--bl .mfb-component__list [data-mfb-label]:after,.mfb-component--bl [data-mfb-label]:after,.mfb-component--tl .mfb-component__list [data-mfb-label]:after,.mfb-component--tl [data-mfb-label]:after{content:attr(data-mfb-label);left:70px} \ No newline at end of file diff --git a/static/report/index.html b/static/report/index.html new file mode 100644 index 000000000..bb67dd152 --- /dev/null +++ b/static/report/index.html @@ -0,0 +1,131 @@ + + + + Nightscout reporting + + + + + + + + + + + + + + + + + + + + + + + + + +

Nightscout reporting

+
    +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ From: + To: + Today + Last 2 days + Last 3 days + Last week + Last 2 weeks + Last month + Last 3 months +
+ Notes contain: +
+ Event Type: +
+ Mo + Tu + We + Th + Fr + Sa + Su +
+ Target bg range bottom: + + top: + +
+ Order: + + +   + + +
+ +
+
+
+ +
+ Authentication status: + + + + + + + + + + + + + + diff --git a/static/report/js/compare.js b/static/report/js/compare.js new file mode 100644 index 000000000..bfa608c03 --- /dev/null +++ b/static/report/js/compare.js @@ -0,0 +1,639 @@ + + + +function slippy (dom, opt) { + + var svg = { }; + var chart; + var container; + var scales = { x: { }, y: { } }; + var dom_width, dom_height, width, height; + var scaleExtent = [ 0, 200 ]; + var zoomer; + + var margin = {top: 20, right: 50, bottom: 20, left: 50}; + // get_dimensions( ); + + + function frame ( ) { + get_dimensions( ); + var range = frame.getRange( ); + // var width = dom.width( ); + // var height = dom.height( ); + svg.attr('width', dom_width) + // .attr('height', height - margin.right - margin.left) + ; + var begin = scales.x.invert(0); + var end = scales.x.invert(width); + range = frame.setRange(begin, end); + var delta = moment(begin).from(moment(end)); + // .replace(' ago', ''); + opt.controls.find('.begin-input').val(delta); + opt.controls.find('.end-input').val(end.format(Date.ISO8601_DATETIME)); + opt.controls.data('range', {begin: begin, end: end}); + // console.log(width, height, delta) + + // console.log(opt); + chart.select(".x.axis").call(frame.xAxis); + dom.trigger('refocus', [range, frame]); + + } + + frame.getRange = function _getRange ( ) { + return [frame.begin, frame.end]; + } + + frame.setRange = function _setRange (begin, end) { + frame.begin = Date.create(begin); + frame.end = Date.create(end); + return frame.getRange( ); + } + + function get_dimensions( ) { + dom_width = dom.width( ); + margin = {top: 20, right: 50, bottom: 20, left: 50}; + width = dom_width - margin.left - margin.right; + dom_height = dom.height( ); + height = dom_height - margin.top - margin.bottom; + } + + function init ( ) { + get_dimensions( ); + var begin = opt.begin || opt.controls.find('INPUT.begin-input').val( ); + var end = opt.end || opt.controls.find('INPUT.end-input').val( ); + var range = frame.setRange(begin, end); + // var width = dom.width( ); + // var height = dom.height( ); + svg = d3.select(dom.get(0)).append('svg') + .attr('class', 'slippy-chart') + ; + ; + chart = svg.append('g') + .attr('class', 'widget') + .attr("tranform", "translate(" + 0 + ", " + margin.top + ")") + ; + scales.x = d3.time.scale( ) + .domain(range) + .nice(d3.time.week) + .rangeRound([0, dom.width( )]) + ; + scales.y = d3.scale.log( ) + .domain([ 40, 400 ]) + .rangeRound([1, dom.height( )]) + ; + frame.xAxis = d3.svg.axis( ) + .scale(scales.x) + .ticks(7) + .tickSize(12, 1, 1) + ; + chart.append("g") + .attr("transform", "translate(" + 0 + ", " + (height / 2) + ")") + .attr("class", "x axis") + .call(frame.xAxis) + ; + + zoomer = d3.behavior.zoom( ) + .x(scales.x) + .scaleExtent(scaleExtent) + .on('zoom', frame); + ; + svg.call(zoomer); + + // frame.setRange(begin, end); + opt.controls.on('change', 'INPUT', refocus); + frame( ); + $(window).on('resize', resize); + return frame; + } + + function adjust_sizes (ev) { + get_dimensions( ); + scales.x + .rangeRound([0, dom.width( )]) + ; + scales.y + .rangeRound([1, dom.height( )]) + ; + chart.selectAll('.x.axis') + .call(frame.xAxis) + } + + function resize (ev) { + adjust_sizes(ev); + frame( ); + } + + function refocus (ev) { + var target = $(ev.target); + if (target.is('INPUT')) { + var begin = opt.controls.find('INPUT.begin-input').val( ); + var end = opt.controls.find('INPUT.end-input').val( ); + var range = frame.setRange(begin, end); + } + scales.x.domain(range) + .nice(d3.time.week) + ; + frame( ); + } + + return init( ); + // return frame; +} + +function cached (opts) { + var storage = opts.storage || crossfilter([ ]); + var dims = { + byDate: storage.dimension(dateStringDate) + }; + function api ( ) { + } + + function within (start, end) { + start = Date.create(start); + end = Date.create(end); + return dims.byDate.filterRange([start, end]).top(Infinity); + } + + function cx ( ) { + return storage; + } + + function get ( ) { + } + + function add (records) { + storage.add(records); + } + + function dateString (d) { return d.dateString; } + function dateStringDate (d) { return Date.create(d.dateString); } + function sgv (d) { return d.sgv; } + function is_clean (d) { return sgv(d) > 39; } + function is_in_range (d) { return sgv(d) >= range.low && sgv(d) <= range.high; } + function is_high (d) { return sgv(d) > range.high; } + function is_low (d) { return sgv(d) < range.low; } + + api.dims = dims; + api.get = get; + api.add = add; + api.within = within; + api.cx = cx; + return api; +} + +function manager (view, data, opts) { + + // var colorize = d3.scale.category20b( ); + var colorize = d3.scale.ordinal( ) + .domain([0, 10]) + .range(colorbrewer.Set3[10]); + var templates = opts.templates; + var item_opts = opts.item_opts || { }; + var pools = [ ]; + var master = { }; + var cache = opts.cache || cached({ }); + function manage ( ) { + } + + function init ( ) { + rows = view.find('.reticle'); + if (rows.length > 0) { + } else { + var item = make({ }); + pools.push(item); + } + view.on('refocus', '.reticle', on_refocus); + return manage; + } + + function make (data) { + var item = templates.find('.reticle').clone(true); + var pool = templates.find('.pool').clone(true); + var end = data.end || Date.create('now').format(Date.ISO8601_DATETIME); + var begin = data.begin || Date.create(end).rewind({weeks: 6}).format(Date.ISO8601_DATETIME); + view.find('.ranges').append(item); + view.find('.observations').append(pool); + var control = slippy(item.find('.timeline'), + { controls: item.find('.controls') + , begin: begin + , end: end + } ); + var reticle = {dom: item, control: control}; + var color = colorize(pools.length); + var lense = ranger(pool, {color: color, begin: begin, end: end, cache: cache}); + var display = { dom: pool, control: lense }; + reticle.dom.find('.timeline').css('border', '1px solid ' + color); + display.dom.css('border-color', color); + reticle.dom.find("span, :input").css('color', color); + // reticle.dom.find(".x.axis line").('styl', color); + var axis = d3.select(reticle.dom.get(0)).selectAll('.x.axis'); + axis.selectAll('line').style('stroke', color); + axis.selectAll('path').style('stroke', color); + axis.selectAll('text').style('color', color); + return { reticle: reticle, display: display, color: color }; + } + + function on_refocus (ev, range) { + var target = $(ev.target); + var pool = pools.filter(function (pool) { + var timeline = pool.reticle.dom.find('.timeline'); + return timeline.is(target); + }).pop( ); + if (pool) { + pool.display.control.setRange(range[0], range[1]); + } + } + + function add_new ( ) { + var item = make({ }); + pools.push(item); + } + + manage.add_new = add_new; + + return init( ); +} + +function pager (opts) { + var query = { }; + var url = opts.url || '/api/v1/entries.json?find[type]=sgv&find[sgv][$gt]=39&count=500000&'; + query.begin = opts.begin; + query.end = opts.end; + var payload = [ ]; + var days = { }; + var cache = opts.cache; + var DATE_FMT = "{yyyy}-{MM}-{dd}"; + + var bisect = d3.bisector(function (d) { return Date.create(d.dateString); }); + function page ( ) { + } + + function refresh (start, end) { + // console.log("PAYLOAD?", payload); + var q = { + start: Date.create(Date.create(start).format(DATE_FMT)) + , end: Date.create(Date.create(end).format(DATE_FMT)) + }; + query.begin = start; + query.end = end; + var range = d3.time.days(q.start, q.end); + days = { }; + console.log('QUERY FOR', range.length, 'days', q, query); + iter_query(range); + return; + if (payload && payload.length > 0) { + console.log("NUM DAYS", range.length); + // TODO: soft update, only get deltas against the edges of the + // cursor. + // start.isBefore + // start.isBetween + // start.isAfter + // end.isBefore + // end.isBetween + // end.isAfter + var holding = { + begin: Date.create(payload[0].dateString) + , last: Date.create(payload.slice(-1).pop( ).dateString) + } + console.log("BISECT LEFT start", bisect.left(payload, { dateString: start })); + console.log("BISECT LEFT end", end, bisect.right(payload, { dateString: end })); + do_query(start, end, first_page); + } else { + do_query(start, end, first_page); + } + query.begin = start; + query.end = end; + } + + function param_string (begin, end) { + return [ + "find[dateString][$gte]=" + Date.create(begin).format(DATE_FMT) + , "find[dateString][$lte]=" + Date.create(end).format(DATE_FMT) + ].join('&') + } + + function iter (prev, current, index, tail) { + } + + function iter_query (range) { + payload = [ ]; + range.forEach(function (day, i) { + var start = day; + var end = d3.time.day.offset(start, 1); + var data = cache.within(start, end); + if (data.length < 10) { + do_query(start, end, function _iter_ (resp) { + days[Date.create(day).format(DATE_FMT)] = true; + collate(resp); + }); + } else { + days[Date.create(day).format(DATE_FMT)] = true; + payload = payload.concat(data); + if (Object.keys(days).length >= range.length) { + do_payload( ); + } + } + }); + + } + + function accrue (begin, end, cb) { + var q = { + start: Date.create(Date.create(start).format(DATE_FMT)) + , end: Date.create(Date.create(end).format(DATE_FMT)) + }; + var range = d3.time.days(q.start, q.end); + if (range.length > 3) { + var half = range.length / 2; + var head = range.slice(0, half); + var tail = range.slice(half); + // head.reduce(iter, { }); + // tail.reduce(iter, { }); + } + } + + function collate (resp) { + console.log('payload', payload.length, 'resp', resp.length); + payload = payload.concat(resp); + cache.add(resp); + payload.sort(cmp_dateString); + var range = d3.time.days(query.start, query.end); + if (Object.keys(days).length >= range.length) { + do_payload( ); + } + } + + function cmp_dateString (a, b) { + return a.dateString > b.dateString; + } + + function do_query (begin, end, cb) { + var range = d3.time.days(Date.create(begin), Date.create(end)); + var fetch = url + param_string(begin, end); + console.log('QUERY FOR', range.length, 'days', query, fetch); + $.getJSON(fetch, cb); + } + + function first_page (resp) { + // console.log('resp', resp); + payload = resp; + do_payload( ); + } + + function do_payload ( ) { + if (opts.callback && opts.callback.call) { + opts.callback(payload); + } else { + // console.log(payload); + } + } + + function init ( ) { + if (query.begin && query.end) { + refresh(query.begin, query.end); + // do_query(query.begin, query.end, first_page); + } + page( ); + return page; + } + + page.refresh = refresh; + + return init( ); +} + +function time_in_range (data, opts) { + var range = { + high: 180 + , low: 80 + }; + + function sgv (d) { return d.sgv; } + function is_clean (d) { return sgv(d) > 39; } + function is_in_range (d) { return sgv(d) >= range.low && sgv(d) <= range.high; } + function is_high (d) { return sgv(d) > range.high; } + function is_low (d) { return sgv(d) < range.low; } + var results = [ ]; + var days = d3.nest( ) + .key(function (d) { + return Date.create(d.dateString).format('{yyyy}-{MM}-{dd}') + } ) + .rollup(function (leaves) { + var clean = leaves.filter(is_clean); + var nominal = clean.filter(is_in_range); + var highs = clean.filter(is_high); + var lows = clean.filter(is_low); + return { + length: clean.length + , color: opts.color || '#eee' + , target: nominal.length / clean.length * 100 + , highs: highs.length / clean.length * 100 + , lows: lows.length / clean.length * 100 + , mean: d3.mean(clean, sgv) + , median: d3.median(clean, sgv) + }; + } ) + .entries(data); + ; + return days; +} + +function ranger (dom, opts) { + var scales = { x: { }, y: { } }; + var dimensions = {height: null, width: null }; + var margin = {top: 20, right: 50, bottom: 20, left: 50}; + var xAxis, yAxis; + var dom_width, dom_height; + var width, height; + var root = { }; + var dots = { }; + var chart = { }; + + function my ( ) { + // dots = chart.selectAll('circle') + // dots.enter( ) + // .append("circle") + // console.log('data', my.data); + var selection = dots.selectAll('.dots').data(my.data || [ ]); + // console.log('dots', dots, selection); + selection.enter( ).append('circle').attr('class', 'dots'); + selection.exit( ).remove( ); + selection.call(render_circles) + // .exit( ).remove( ); + } + + function make_x_axis ( ) { + return d3.svg.axis( ) + .scale(scales.x) + ; + } + + function make_y_axis ( ) { + return d3.svg.axis() + .scale(scales.y) + ; + } + + function gridlines (selection) { + selection.selectAll('.x.gridlines') + // .attr("transform", "translate(0," + height + ")") + .attr("transform", "translate(" + margin.left + "," + height + ")") + // .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + .call(make_x_axis( ) + .tickSize(-height, 0, 0) + .tickFormat("") + ) + + selection.selectAll('.y.gridlines') + .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + .call(make_y_axis( ) + .tickSize(-width , 0, 0) + .tickValues([40, 80, 120, 180, 240, 300, 400]) + .tickFormat("") + .orient("left") + // .ticks(6) + ) + + } + function render_circles (dots) { + dots + .transition( ) + .attr("r", 5) + .attr("title", function (d) { return d.key; }) + .attr("fill", function (d) { return d.values.color; }) + .attr("cx", function (d) { return scales.x(d.values.target); }) + .attr("cy", function (d) { return scales.y(d.values.mean); }) + return dots; + } + + function render (selection) { + console.log('selection'); + } + + my.setRange = function _setRange (begin, end) { + adjuster(begin, end); + } + + function adjust_range (begin, end) { + my.page.refresh(begin, end); + } + + var adjuster = _.debounce(adjust_range, 1000); + + function on_data (payload) { + var days = time_in_range(payload, opts); + my.data = days; + if (dots && dots.selectAll) { + my( ); + } + // console.log('got days', days); + } + + function init ( ) { + my.page = pager({begin: opts.begin, end: opts.end, callback: on_data, cache: opts.cache}); + get_dimensions( ); + + scales.x = d3.scale.linear( ) + .domain([0, 100]) + .range([0, width]) + ; + + scales.y = d3.scale.log( ) + .domain( [40, 400] ) + .rangeRound( [height - (margin.top + margin.bottom), 1] ) + .base(12) + ; + + + // Axis + xAxis = make_x_axis( ) + .ticks(10) + .tickFormat(function(d) { return parseInt(d, 10) + "%"; }) + .orient('bottom') + ; + + + yAxis = make_y_axis( ) + .tickValues([40, 60, 70, 80, 120, 160, 180, 200, 220, 260, 300, 350, 400]) + .tickFormat(d3.format("d")) + .tickSize(6, 3, 1) + .orient('left') + ; + + root = d3.select(dom.get(0)).append('svg') + .attr('class', 'ranger') + .attr('height', dom_height) + .attr('width', dom_width) + ; + + chart = root.append('g') + .attr("transform", "translate(" + 0 + ", " + (margin.top) + ")") + .attr('class', 'ranger-chart') + ; + + chart.append('g').attr('class', 'x gridlines') + .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + ; + chart.append('g').attr('class', 'y gridlines') + .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + ; + dots = chart.append("g") + .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + .attr("class", "scatter") + ; + chart.append("g") + .attr("transform", "translate(" + margin.left + ", " + (height - margin.top) + ")") + .attr("class", "x axis") + .call(xAxis) + ; + chart.append("g") + .attr("transform", "translate(" + margin.left + ", " + (margin.top) + ")") + // .attr("transform", "translate(" + dom_width + ", " + (0) + ")") + .attr("class", "y axis") + .call(yAxis) + ; + // dots = chart.selectAll(".dot"); + $(window).on('resize', resize); + adjust_frames( ); + return my; + } + + function resize (ev) { + adjust_frames(ev); + my( ); + } + function adjust_frames (ev) { + get_dimensions( ); + root + .attr('class', 'ranger') + .attr('height', dom_height) + .attr('width', dom_width) + ; + chart + .attr("transform", "translate(" + 0 + ", " + (margin.top) + ")") + .attr('class', 'ranger-chart') + ; + scales.x + // .range([0, width]) + .range([0, width]) + ; + scales.y + .rangeRound( [height - (margin.top + margin.bottom), 1] ) + ; + + chart.selectAll('.x.axis').call(xAxis); + chart.selectAll('.y.axis').call(yAxis); + chart.call(gridlines) + + } + + function get_dimensions( ) { + dom_width = dom.width( ); + margin = {top: 20, right: 50, bottom: 20, left: 50}; + width = dom_width - margin.left - margin.right; + dom_height = dom.height( ) - margin.top; + height = dom_height - margin.top - margin.bottom; + } + + return init( ); +} diff --git a/static/report/js/flotcandle.js b/static/report/js/flotcandle.js new file mode 100644 index 000000000..3ac641d1c --- /dev/null +++ b/static/report/js/flotcandle.js @@ -0,0 +1,87 @@ +(function ($) { + var options = { + series: { candle: null } // or number/string + }; + var offset, x, y; + + function init(plot) { + plot.hooks.processOptions.push(processOptions); + function processOptions(plot,options){ + if(options.series.candle){ + //plot.hooks.processRawData.push(processRawData); + plot.hooks.drawSeries.push(drawSeries); + } + } + /*function processRawData(plot,s,data,datapoints){ + if(s.candle){ + } + }*/ + function drawSeries(plot, ctx, serie){ + if (serie.candle) { + offset = plot.getPlotOffset(); + offset.left = offset.left; + var x1 = serie.xaxis.p2c(serie.data[0][0]); + var x2 = serie.xaxis.p2c(serie.data[1][0]); + var width = (x2 - x1) * 4 / 5; + for (var j = 0; j < serie.data.length; j++) { getAndDrawCandle(ctx, serie, width, serie.data[j]);} + } + } + function getAndDrawCandle(ctx, serie, width, data){ + var dt = data[0]; + var open = data[1]; + var close = data[2]; + var low = data[3]; + var high = data[4]; + drawCandle(ctx, serie, width, dt, open, low, close, high); + } + function drawCandle(ctx, serie, width, dt, open, low, close, high){ + var height; + if (open < close){ //Rising + y = offset.top + serie.yaxis.p2c(open); + height = serie.yaxis.p2c(close) - serie.yaxis.p2c(open); + ctx.fillStyle = '#51FF21'; + } else { //Decending + y = offset.top + serie.yaxis.p2c(close); + height = serie.yaxis.p2c(open) - serie.yaxis.p2c(close); + ctx.fillStyle = '#FF0000'; + } + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 0; + x = offset.left + serie.xaxis.p2c(dt); + + //body + ctx.fillRect (x, y, width, height); + ctx.strokeRect(x, y, width, height); + + var highY = serie.yaxis.p2c(high); + var lowY = serie.yaxis.p2c(low); + + //top + var lineX; + if (highY < y + height){ + ctx.beginPath(); + lineX = x + (width /2); + ctx.moveTo(lineX,y + height); + ctx.lineTo(lineX,highY); + ctx.closePath(); + ctx.stroke(); + } + + //bottom + if (lowY > y){ + ctx.beginPath(); + ctx.moveTo(lineX,y); + ctx.lineTo(lineX,lowY); + ctx.closePath(); + ctx.stroke(); + } + } + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'candle', + version: '1.0' + }); +})(jQuery); \ No newline at end of file diff --git a/static/report/js/report.js b/static/report/js/report.js new file mode 100644 index 000000000..8c337ae70 --- /dev/null +++ b/static/report/js/report.js @@ -0,0 +1,798 @@ +// TODO: +// - bypass nightmode in reports +// - on save/delete treatment ctx.bus.emit('data-received'); is not enough. we must add something like 'data-updated' + +(function () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var moment = window.moment; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var report_plugins = Nightscout.report_plugins; + + client.init(function loaded () { + + // init HTML code + report_plugins.addHtmlFromPlugins( client ); + // make show() accessible outside for treatments.js + report_plugins.show = show; + + var translate = client.translate; + + var maxInsulinValue = 0 + ,maxCarbsValue = 0 + ,maxDailyCarbsValue = 0; + var maxdays = 6 * 31; + var datastorage = {}; + var daystoshow = {}; + var sorteddaystoshow = []; + + var targetBGdefault = { + 'mg/dl': { + low: client.settings.thresholds.bgTargetBottom + , high: client.settings.thresholds.bgTargetTop + } + , 'mmol': { + low: client.utils.scaleMgdl(client.settings.thresholds.bgTargetBottom) + , high: client.utils.scaleMgdl(client.settings.thresholds.bgTargetTop) + } + }; + + var ONE_MIN_IN_MS = 60000; + + prepareGUI(); + + // ****** FOOD CODE START ****** + var food_categories = []; + var food_list = []; + + var filter = { + category: '' + , subcategory: '' + , name: '' + }; + + function fillFoodForm(event) { + $('#rp_category').empty().append(''); + Object.keys(food_categories).forEach(function eachCategory(s) { + $('#rp_category').append(''); + }); + filter.category = ''; + fillFoodSubcategories(); + + $('#rp_category').change(fillFoodSubcategories); + $('#rp_subcategory').change(doFoodFilter); + $('#rp_name').on('input',doFoodFilter); + + return maybePrevent(event); + } + + function fillFoodSubcategories(event) { + filter.category = $('#rp_category').val(); + filter.subcategory = ''; + $('#rp_subcategory').empty().append(''); + if (filter.category !== '') { + Object.keys(food_categories[filter.category] || {}).forEach(function eachSubCategory(s) { + $('#rp_subcategory').append(''); + }); + } + doFoodFilter(); + return maybePrevent(event); + } + + function doFoodFilter(event) { + if (event) { + filter.category = $('#rp_category').val(); + filter.subcategory = $('#rp_subcategory').val(); + filter.name = $('#rp_name').val(); + } + $('#rp_food').empty(); + for (var i=0; i' + o + ''); + } + + return maybePrevent(event); + } + + $('#info').html(''+translate('Loading food database')+' ...'); + $.ajax('/api/v1/food/regular.json', { + headers: client.headers() + , success: function foodLoadSuccess(records) { + records.forEach(function (r) { + food_list.push(r); + if (r.category && !food_categories[r.category]) { food_categories[r.category] = {}; } + if (r.category && r.subcategory) { food_categories[r.category][r.subcategory] = true; } + }); + fillFoodForm(); + } + }).done(function() { + if (food_list.length) { + enableFoodGUI(); + } else { + disableFoodGUI(); + } + }).fail(function() { + disableFoodGUI(); + }); + + function enableFoodGUI( ) { + $('#info').html(''); + + $('.rp_foodgui').css('display',''); + $('#rp_food').change(function (event) { + $('#rp_enablefood').prop('checked',true); + return maybePrevent(event); + }); + } + + function disableFoodGUI(){ + $('#info').html(''); + $('.rp_foodgui').css('display','none'); + } + + // ****** FOOD CODE END ****** + + function prepareGUI() { + $('.presetdates').click(function(event) { + var days = $(this).attr('days'); + $('#rp_enabledate').prop('checked',true); + return setDataRange(event,days); + }); + $('#rp_show').click(show); + $('#rp_notes').bind('input', function (event) { + $('#rp_enablenotes').prop('checked',true); + return maybePrevent(event); + }); + $('#rp_eventtype').bind('input', function (event) { + $('#rp_enableeventtype').prop('checked',true); + return maybePrevent(event); + }); + + // fill careportal events + $('#rp_eventtype').empty(); + _.each(client.careportal.events, function eachEvent(event) { + $('#rp_eventtype').append(''); + }); + $('#rp_eventtype').append(''); + + $('#rp_targetlow').val(targetBGdefault[client.settings.units.toLowerCase()].low); + $('#rp_targethigh').val(targetBGdefault[client.settings.units.toLowerCase()].high); + + if (client.settings.scaleY === 'linear') { + $('#rp_linear').prop('checked', true); + } else { + $('#rp_log').prop('checked', true); + } + + $('.menutab').click(switchreport_handler); + + setDataRange(null,7); + } + + function sgvToColor(sgv,options) { + var color = 'darkgreen'; + + if (sgv > options.targetHigh) { + color = 'red'; + } else if (sgv < options.targetLow) { + color = 'red'; + } + + return color; + } + + function show(event) { + var options = { + width: 1000 + , height: 300 + , targetLow: 3.5 + , targetHigh: 10 + , raw: true + , notes: true + , food: true + , insulin: true + , carbs: true + , iob : true + , cob : true + , basal : true + , scale: report_plugins.consts.scaleYFromSettings(client) + , units: client.settings.units + }; + + // default time range if no time range specified in GUI + var zone = client.sbx.data.profile.getTimezone(); + var timerange = '&find[created_at][$gte]='+moment.tz('2000-01-01',zone).toISOString(); + //console.log(timerange,zone); + options.targetLow = parseFloat($('#rp_targetlow').val().replace(',','.')); + options.targetHigh = parseFloat($('#rp_targethigh').val().replace(',','.')); + options.raw = $('#rp_optionsraw').is(':checked'); + options.iob = $('#rp_optionsiob').is(':checked'); + options.cob = $('#rp_optionscob').is(':checked'); + options.openAps = $('#rp_optionsopenaps').is(':checked'); + options.basal = $('#rp_optionsbasal').is(':checked'); + options.notes = $('#rp_optionsnotes').is(':checked'); + options.food = $('#rp_optionsfood').is(':checked'); + options.insulin = $('#rp_optionsinsulin').is(':checked'); + options.carbs = $('#rp_optionscarbs').is(':checked'); + options.scale = ( $('#rp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG ); + options.order = ( $('#rp_oldestontop').is(':checked') ? report_plugins.consts.ORDER_OLDESTONTOP : report_plugins.consts.ORDER_NEWESTONTOP ); + options.width = parseInt($('#rp_size :selected').attr('x')); + options.height = parseInt($('#rp_size :selected').attr('y')); + + var matchesneeded = 0; + + // date range + function datefilter() { + if ($('#rp_enabledate').is(':checked')) { + matchesneeded++; + var from = moment.tz($('#rp_from').val().replace(/\//g,'-') + 'T00:00:00',zone); + var to = moment.tz($('#rp_to').val().replace(/\//g,'-') + 'T23:59:59',zone); + timerange = '&find[created_at][$gte]='+from.toISOString()+'&find[created_at][$lt]='+to.toISOString(); + //console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange); + while (from <= to) { + if (daystoshow[from.format('YYYY-MM-DD')]) { + daystoshow[from.format('YYYY-MM-DD')]++; + } else { + daystoshow[from.format('YYYY-MM-DD')] = 1; + } + from.add(1, 'days'); + } + } + //console.log('Dayfilter: ',daystoshow); + foodfilter(); + } + + //food filter + function foodfilter() { + if ($('#rp_enablefood').is(':checked')) { + matchesneeded++; + var _id = $('#rp_food').val(); + if (_id) { + var treatmentData; + var tquery = '?find[boluscalc.foods._id]=' + _id + timerange; + $.ajax('/api/v1/treatments.json'+tquery, { + headers: client.headers() + , success: function (xhr) { + treatmentData = xhr.map(function (treatment) { + return moment.tz(treatment.created_at,zone).format('YYYY-MM-DD'); + }); + // unique it + treatmentData = $.grep(treatmentData, function(v, k){ + return $.inArray(v ,treatmentData) === k; + }); + treatmentData.sort(function(a, b) { return a > b; }); + } + }).done(function () { + //console.log('Foodfilter: ',treatmentData); + for (var d=0; d b; }); + } + }).done(function () { + //console.log('Notesfilter: ',treatmentData); + for (var d=0; d b; }); + } + }).done(function () { + //console.log('Eventtypefilter: ',treatmentData); + for (var d=0; d'+translate('Loading')+' ...
'); + for (var d in daystoshow) { + if (count < maxdays) { + $('#info').append($('
')); + count++; + loadData(d, options, dataLoadedCallback); + } else { + $('#info').append($('
'+d+' '+translate('not displayed')+'.
')); + delete daystoshow[d]; + } + } + if (count===0) { + $('#info').html(''+translate('Result is empty')+''); + $('#rp_show').css('display',''); + } + } + + var dayscount = 0; + var loadeddays = 0; + + function countDays() { + for (var d in daystoshow) { + if (daystoshow.hasOwnProperty(d)) { + if (daystoshow[d]===matchesneeded) { + if (dayscount < maxdays) { + dayscount++; + } + } else { + delete daystoshow[d]; + } + } + } + //console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); + } + + function addPreviousDayTreatments() { + for (var d in daystoshow) { + if (daystoshow.hasOwnProperty(d)) { + var day = moment.tz(d,zone); + var previous = day.subtract(1,'days'); + var formated = previous.format('YYYY-MM-DD'); + if (!daystoshow[formated]) { + daystoshow[formated] = { treatmentsonly: true}; + console.log('Adding ' + formated + ' for loading treatments'); + dayscount++; + } + } + } + //console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); + } + + function dataLoadedCallback (day) { + loadeddays++; + if (!daystoshow[day].treatmentsonly) { + sorteddaystoshow.push(day); + } + if (loadeddays === dayscount) { + sorteddaystoshow.sort(); + var from = sorteddaystoshow[0]; + if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { + sorteddaystoshow.reverse(); + } + loadProfileSwitch(from, function loadProfileSwitchCallback() { + $('#info > b').html(''+translate('Rendering')+' ...'); + window.setTimeout(function () {showreports(options); }, 0); + }); + } + } + + $('#rp_show').css('display','none'); + daystoshow = {}; + + datefilter(); + return maybePrevent(event); + } + + function showreports(options) { + // prepare some data used in more reports + datastorage.allstatsrecords = []; + datastorage.alldays = 0; + sorteddaystoshow.forEach(function eachDay(day) { + if (!daystoshow[day].treatmentsonly) { + datastorage.allstatsrecords = datastorage.allstatsrecords.concat(datastorage[day].statsrecords); + datastorage.alldays++; + } + }); + options.maxInsulinValue = maxInsulinValue; + options.maxCarbsValue = maxCarbsValue; + options.maxDailyCarbsValue = maxDailyCarbsValue; + + datastorage.treatments = []; + datastorage.devicestatus = []; + datastorage.combobolusTreatments = []; + datastorage.tempbasalTreatments = []; + Object.keys(daystoshow).forEach( function eachDay(day) { + datastorage.treatments = datastorage.treatments.concat(datastorage[day].treatments); + datastorage.devicestatus = datastorage.devicestatus.concat(datastorage[day].devicestatus); + datastorage.combobolusTreatments = datastorage.combobolusTreatments.concat(datastorage[day].combobolusTreatments); + datastorage.tempbasalTreatments = datastorage.tempbasalTreatments.concat(datastorage[day].tempbasalTreatments); + }); + datastorage.tempbasalTreatments = Nightscout.client.ddata.processDurations(datastorage.tempbasalTreatments); + + for (var d in daystoshow) { + if (daystoshow.hasOwnProperty(d)) { + if (daystoshow[d].treatmentsonly) { + delete daystoshow[d]; + delete datastorage[d]; + } + } + } + + report_plugins.eachPlugin(function (plugin) { + // jquery plot doesn't draw to hidden div + $('#'+plugin.name+'-placeholder').css('display',''); + + console.log('Drawing ',plugin.name); + + var skipRender = false; + + if (plugin.name == 'daytoday' && ! $('#daytoday').hasClass('selected')) skipRender = true; + if (plugin.name == 'treatments' && ! $('#treatments').hasClass('selected')) skipRender = true; + + if (skipRender) { + console.log('Skipping ',plugin.name); + } else { + plugin.report(datastorage,sorteddaystoshow,options); + } + + if (!$('#'+plugin.name).hasClass('selected')) { + $('#'+plugin.name+'-placeholder').css('display','none'); + } + }); + + $('#info').html(''); + $('#rp_show').css('display',''); + } + + function setDataRange(event,days) { + $('#rp_to').val(moment().format('YYYY-MM-DD')); + $('#rp_from').val(moment().add(-days+1, 'days').format('YYYY-MM-DD')); + return maybePrevent(event); + } + + function switchreport_handler(event) { + var id = $(this).attr('id'); + + $('.menutab').removeClass('selected'); + $('#'+id).addClass('selected'); + + $('.tabplaceholder').css('display','none'); + $('#'+id+'-placeholder').css('display',''); + return maybePrevent(event); + } + + function loadData(day, options, callback) { + // check for loaded data + if (options.openAps && datastorage[day] && !datastorage[day].devicestatus) { + // OpenAPS requested but data not loaded. Load anyway ... + } else if (datastorage[day] && day !== moment().format('YYYY-MM-DD')) { + callback(day); + return; + } + // patientData = [actual, predicted, mbg, treatment, cal, devicestatusData]; + var data = {}; + var cgmData = [] + , mbgData = [] + , treatmentData = [] + , calData = [] + ; + var from; + if (client.sbx.data.profile.getTimezone()) { + from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').format('x'); + } else { + from = moment(day).startOf('day').format('x'); + } + from = parseInt(from); + var to = from + 1000 * 60 * 60 * 24; + + function loadCGMData() { + if (daystoshow[day].treatmentsonly) { + data.sgv = []; + data.mbg = []; + data.cal = []; + return $.Deferred().resolve(); + } + $('#info-' + day).html(''+translate('Loading CGM data of')+' '+day+' ...'); + var query = '?find[date][$gte]='+from+'&find[date][$lt]='+to+'&count=10000'; + return $.ajax('/api/v1/entries.json'+query, { + headers: client.headers() + , success: function (xhr) { + xhr.forEach(function (element) { + if (element) { + if (element.mbg) { + mbgData.push({ + y: element.mbg + , mills: element.date + , d: element.dateString + , device: element.device + }); + } else if (element.sgv) { + cgmData.push({ + y: element.sgv + , mills: element.date + , d: element.dateString + , device: element.device + , filtered: element.filtered + , unfiltered: element.unfiltered + , noise: element.noise + , rssi: element.rssi + , sgv: element.sgv + }); + } else if (element.type === 'cal') { + calData.push({ + mills: element.date + , d: element.dateString + , scale: element.scale + , intercept: element.intercept + , slope: element.slope + }); + } + } + }); + // sometimes cgm contains duplicates. uniq it. + data.sgv = cgmData.slice(); + data.sgv.sort(function(a, b) { return a.mills - b.mills; }); + var lastDate = 0; + data.sgv = data.sgv.filter(function(d) { + var ok = (lastDate + ONE_MIN_IN_MS) < d.mills; + lastDate = d.mills; + return ok; + }); + data.mbg = mbgData.slice(); + data.mbg.sort(function(a, b) { return a.mills - b.mills; }); + data.cal = calData.slice(); + data.cal.sort(function(a, b) { return a.mills - b.mills; }); + } + }); + } + + function loadTreatmentData() { + $('#info-' + day).html(''+translate('Loading treatments data of')+' '+day+' ...'); + var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString(); + return $.ajax('/api/v1/treatments.json'+tquery, { + headers: client.headers() + , success: function (xhr) { + treatmentData = xhr.map(function (treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.mills = timestamp.getTime(); + return treatment; + }); + data.treatments = treatmentData.slice(); + data.treatments.sort(function(a, b) { return a.mills - b.mills; }); + // filter 'Combo Bolus' events + data.combobolusTreatments = data.treatments.filter( function filterComboBoluses(t) { + return t.eventType === 'Combo Bolus'; + }); + // filter temp basal treatments + data.tempbasalTreatments = data.treatments.filter(function filterTempBasals(t) { + return t.eventType === 'Temp Basal'; + }); + } + }); + } + + function loadDevicestatusData() { + if (daystoshow[day].treatmentsonly) { + data.devicestatus = []; + return $.Deferred().resolve(); + } + if(options.iob || options.cob || options.openAps) { + $('#info-' + day).html(''+translate('Loading device status data of')+' '+day+' ...'); + var tquery = '?find[created_at][$gte]=' + new Date(from).toISOString() + '&find[created_at][$lt]=' + new Date(to).toISOString() + '&count=10000'; + return $.ajax('/api/v1/devicestatus.json'+tquery, { + headers: client.headers() + , success: function (xhr) { + data.devicestatus = xhr.map(function (devicestatus) { + devicestatus.mills = new Date(devicestatus.timestamp || devicestatus.created_at).getTime(); + return devicestatus; + }); + } + }); + } else { + data.devicestatus = []; + return $.Deferred().resolve(); + } + } + + $.when(loadCGMData(), loadTreatmentData(), loadDevicestatusData()).done(function () { + $('#info-' + day).html(''+translate('Processing data of')+' '+day+' ...'); + processData(data, day, options, callback); + }); + } + + function loadProfileSwitch(from, callback) { + $('#info > b').html(''+translate('Loading profile switch data') + ' ...'); + var tquery = '?find[eventType]=Profile Switch'; + $.ajax('/api/v1/treatments.json'+tquery, { + headers: client.headers() + , success: function (xhr) { + var treatmentData = xhr.map(function (treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.mills = timestamp.getTime(); + return treatment; + }); + datastorage.profileSwitchTreatments = treatmentData.slice(); + datastorage.profileSwitchTreatments.sort(function(a, b) { return a.mills - b.mills; }); + } + }).done(function () { + callback(); + }); + } + + function processData(data, day, options, callback) { + if (daystoshow[day].treatmentsonly) { + datastorage[day] = data; + $('#info-' + day).html(''); + callback(day); + return; + } + // treatments + data.dailyCarbs = 0; + data.treatments.forEach(function (d) { + if (parseFloat(d.insulin) > maxInsulinValue) { + maxInsulinValue = parseFloat(d.insulin); + } + if (parseFloat(d.carbs) > maxCarbsValue) { + maxCarbsValue = parseFloat(d.carbs); + } + if (d.carbs) { + data.dailyCarbs += d.carbs; + } + }); + if (data.dailyCarbs > maxDailyCarbsValue) { + maxDailyCarbsValue = data.dailyCarbs; + } + + var cal = data.cal[data.cal.length-1]; + var temp1 = [ ]; + var rawbg = client.rawbg; + if (cal) { + temp1 = data.sgv.map(function (entry) { + entry.mgdl = entry.y; // value names changed from enchilada + var rawBg = rawbg.calc(entry, cal); + return { mills: entry.mills, date: new Date(entry.mills - 2 * 1000), y: rawBg, sgv: client.utils.scaleMgdl(rawBg), color: 'gray', type: 'rawbg', filtered: entry.filtered, unfiltered: entry.unfiltered }; + }).filter(function(entry) { return entry.y > 0}); + } + var temp2 = data.sgv.map(function (obj) { + return { mills: obj.mills, date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: sgvToColor(client.utils.scaleMgdl(obj.y),options), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered}; + }); + data.sgv = [].concat(temp1, temp2); + + //Add MBG's also, pretend they are SGV's + data.sgv = data.sgv.concat(data.mbg.map(function (obj) { return { date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: 'red', type: 'mbg', device: obj.device } })); + + // make sure data range will be exactly 24h + var from; + if (client.sbx.data.profile.getTimezone()) { + from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').toDate(); + } else { + from = moment(day).startOf('day').toDate(); + } + var to = new Date(from.getTime() + 1000 * 60 * 60 * 24); + data.sgv.push({ date: from, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); + data.sgv.push({ date: to, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); + + // clear error data. we don't need it to display them + data.sgv = data.sgv.filter(function (d) { + if (d.y < 39) { + return false; + } + return true; + }); + + data.sgv = data.sgv.map(function eachSgv (sgv) { + var status = _.find(data.devicestatus, function (d) { + return d.mills >= sgv.mills && d.mills < sgv.mills + 5 * 60 * 1000; + }); + + if (status && status.openaps) { + sgv.openaps = status.openaps; + } + return sgv; + }); + + // for other reports + data.statsrecords = data.sgv.filter(function(r) { + if (r.type) { + return r.type === 'sgv'; + } else { + return true; + } + }).map(function (r) { + var ret = {}; + ret.sgv = parseFloat(r.sgv); + ret.bgValue = parseInt(r.y); + ret.displayTime = r.date; + return ret; + }); + + + datastorage[day] = data; + $('#info-' + day).html(''); + callback(day); + } + + function maybePrevent(event) { + if (event) { + event.preventDefault(); + } + return false; + } + }); +})(); diff --git a/static/translations/index.html b/static/translations/index.html new file mode 100644 index 000000000..ce0eb8dd8 --- /dev/null +++ b/static/translations/index.html @@ -0,0 +1,44 @@ + + + + Nightscout translations + + + + + + + + + + + + + + + + + + + + + + + + + +

Nightscout translations

+
+ +
+ Authentication status: + + + + + + + + + + diff --git a/static/translations/js/translations.js b/static/translations/js/translations.js new file mode 100644 index 000000000..286efa24f --- /dev/null +++ b/static/translations/js/translations.js @@ -0,0 +1,54 @@ +(function () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + + client.init(function loaded() { + + var language = client.language; + var result = {}; + + language.languages.forEach(function eachLanguage(l) { + result[l.code] = {total: 0, ok: 0, missing: 0, keys: []}; + _.forEach(language.translations, function (n, t) { + result[l.code].total++; + if (language.translations[t][l.code]) { + result[l.code].ok++; + } else { + result[l.code].missing++; + result[l.code].keys.push(t); + } + }); + }); + + var table = $('').append(''); + var table2 = $('
LanguageCodeTranslatedNot translatedPercent
').append(''); + language.languages.forEach(function eachLanguage(l) { + if (l.code === 'en') { + return; + } + var tr = $(''); + tr.append($(''); + tr2.append($('').should.be.greaterThan(-1); //dailystats + result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: ').should.be.greaterThan(-1); // distribution + result.indexOf('').should.be.greaterThan(-1); // hourlystats + result.indexOf('
').should.be.greaterThan(-1); //success + result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations + result.indexOf('
').should.be.greaterThan(-1); //treatments + + done(); + }); + }); + +}); diff --git a/tests/sandbox.test.js b/tests/sandbox.test.js new file mode 100644 index 000000000..d1be11b1e --- /dev/null +++ b/tests/sandbox.test.js @@ -0,0 +1,87 @@ +var should = require('should'); + +describe('sandbox', function ( ) { + var sandbox = require('../lib/sandbox')(); + + var now = Date.now(); + + it('init on client', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + , thresholds:{ + bgHigh: 260 + , bgTargetTop: 180 + , bgTargetBottom: 80 + , bgLow: 55 + } + } + , pluginBase: {} + }; + + var data = {sgvs: [{mgdl: 100, mills: now}]}; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + sbx.pluginBase.should.equal(ctx.pluginBase); + sbx.data.should.equal(data); + sbx.lastSGVMgdl().should.equal(100); + + done(); + }); + + function createServerSandbox() { + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + return sandbox.serverInit(env, ctx); + } + + it('init on server', function (done) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 100, mills: now}]; + + should.exist(sbx.notifications.requestNotify); + should.not.exist(sbx.notifications.process); + should.not.exist(sbx.notifications.ack); + sbx.lastSGVMgdl().should.equal(100); + + done(); + }); + + it('display 39 as LOW and 401 as HIGH', function () { + var sbx = createServerSandbox(); + + sbx.displayBg({mgdl: 39}).should.equal('LOW'); + sbx.displayBg({mgdl: '39'}).should.equal('LOW'); + sbx.displayBg({mgdl: 401}).should.equal('HIGH'); + sbx.displayBg({mgdl: '401'}).should.equal('HIGH'); + }); + + it('build BG Now line using properties', function ( ) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 99, mills: now}]; + sbx.properties = { delta: {display: '+5' }, direction: {value: 'FortyFiveUp', label: '↗', entity: '↗'} }; + + sbx.buildBGNowLine().should.equal('BG Now: 99 +5 ↗ mg/dl'); + + }); + + it('build default message using properties', function ( ) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 99, mills: now}]; + sbx.properties = { + delta: {display: '+5' } + , direction: {value: 'FortyFiveUp', label: '↗', entity: '↗'} + , rawbg: {displayLine: 'Raw BG: 100 mg/dl'} + , iob: {displayLine: 'IOB: 1.25U'} + , cob: {displayLine: 'COB: 15g'} + }; + + sbx.buildDefaultMessage().should.equal('BG Now: 99 +5 ↗ mg/dl\nRaw BG: 100 mg/dl\nIOB: 1.25U\nCOB: 15g'); + + }); + +}); diff --git a/tests/security.test.js b/tests/security.test.js index 42cfa4215..80959cfee 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -3,43 +3,28 @@ var request = require('supertest'); var should = require('should'); var load = require('./fixtures/load'); +var language = require('../lib/language')(); describe('API_SECRET', function ( ) { var api = require('../lib/api/'); - api.should.be.ok; var scope = this; function setup_app (env, fn) { - var ctx = { }; - ctx.wares = require('../lib/middleware/')(env); - ctx.store = require('../lib/storage')(env); - ctx.archive = require('../lib/entries').storage(env.mongo_collection, ctx.store); - ctx.settings = require('../lib/settings')(env.settings_collection, ctx.store); - - ctx.store(function ( ) { - ctx.app = api(env, ctx.wares, ctx.archive, ctx.settings); + require('../lib/bootevent')(env, language).boot(function booted (ctx) { + ctx.app = api(env, ctx); scope.app = ctx.app; - ctx.archive.create(load('json'), fn); - scope.archive = ctx.archive; + scope.entries = ctx.entries; + fn(ctx); }); - - return ctx; } - /* - before(function (done) { - - }); - */ - after(function (done) { - scope.archive( ).remove({ }, done); - }); it('should work fine absent', function (done) { delete process.env.API_SECRET; var env = require('../env')( ); should.not.exist(env.api_secret); - var ctx = setup_app(env, function ( ) { - ctx.app.enabled('api').should.be.false; + setup_app(env, function (ctx) { + + ctx.app.enabled('api').should.equal(false); ping_status(ctx.app, again); function again ( ) { ping_authorized_endpoint(ctx.app, 404, done); @@ -54,9 +39,9 @@ describe('API_SECRET', function ( ) { process.env.API_SECRET = 'this is my long pass phrase'; var env = require('../env')( ); env.api_secret.should.equal(known); - var ctx = setup_app(env, function ( ) { + setup_app(env, function (ctx) { // console.log(this.app.enabled('api')); - ctx.app.enabled('api').should.be.true; + ctx.app.enabled('api').should.equal(true); // ping_status(ctx.app, done); // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); @@ -75,9 +60,9 @@ describe('API_SECRET', function ( ) { process.env.API_SECRET = 'this is my long pass phrase'; var env = require('../env')( ); env.api_secret.should.equal(known); - var ctx = setup_app(env, function ( ) { + setup_app(env, function (ctx) { // console.log(this.app.enabled('api')); - ctx.app.enabled('api').should.be.true; + ctx.app.enabled('api').should.equal(true); // ping_status(ctx.app, done); // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); @@ -90,14 +75,11 @@ describe('API_SECRET', function ( ) { }); it('should not work short', function ( ) { - var known = 'c1d117818a97e847bdf286aa02d9dc8e8f7148f5'; delete process.env.API_SECRET; process.env.API_SECRET = 'tooshort'; - var env; - (function ( ) { - env = require('../env')( ); - }).should.throw( ); - should.not.exist(env); + var env = require('../env')( ); + should.not.exist(env.api_secret); + env.err.desc.should.startWith('API_SECRET should be at least'); }); function ping_status (app, fn) { @@ -109,7 +91,7 @@ describe('API_SECRET', function ( ) { res.body.status.should.equal('ok'); fn( ); // console.log('err', err, 'res', res); - }) + }); } function ping_authorized_endpoint (app, fails, fn) { @@ -123,7 +105,7 @@ describe('API_SECRET', function ( ) { } fn( ); // console.log('err', err, 'res', res); - }) + }); } }); diff --git a/tests/sensorage.test.js b/tests/sensorage.test.js new file mode 100644 index 000000000..6c0165ef7 --- /dev/null +++ b/tests/sensorage.test.js @@ -0,0 +1,169 @@ +'use strict'; + +var should = require('should'); +var times = require('../lib/times'); +var levels = require('../lib/levels'); + +describe('sage', function ( ) { + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + var sage = require('../lib/plugins/sensorage')(ctx); + var sandbox = require('../lib/sandbox')(); + + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + return sbx; + } + + it('set a pill to the current age since start with change', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(15).msecs} + , {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '15 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + options.info[3].label.should.equal('Sensor Start'); + options.info[4].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[5].should.match({ label: 'Notes', value: 'Bar' }); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since start without change', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info[0].label.should.equal('Sensor Start'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Bar' }); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since change without start', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since change after start', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(10).msecs} + , {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info.length.should.equal(3); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('trigger an alarm when sensor is 6 days and 22 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - times.days(6).msecs - times.hours(22).msecs; + + ctx.ddata.sensorTreatments = [{eventType: 'Sensor Start', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': true }; + sage.setProperties(sbx); + sage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('SAGE'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Sensor age 6 days 22 hours'); + done(); + }); + + it('not trigger an alarm when sensor is 6 days and 23 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - times.days(6).msecs - times.hours(23).msecs; + + ctx.ddata.sensorTreatments = [{eventType: 'Sensor Start', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': true }; + sage.setProperties(sbx); + sage.checkNotifications(sbx); + + should.not.exist(ctx.notifications.findHighestAlarm('SAGE')); + done(); + }); + +}); diff --git a/tests/settings.test.js b/tests/settings.test.js new file mode 100644 index 000000000..531e69448 --- /dev/null +++ b/tests/settings.test.js @@ -0,0 +1,238 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('settings', function ( ) { + var settings = require('../lib/settings')(); + + it('have defaults ready', function () { + settings.timeFormat.should.equal(12); + settings.nightMode.should.equal(false); + settings.showRawbg.should.equal('never'); + settings.customTitle.should.equal('Nightscout'); + settings.theme.should.equal('default'); + settings.alarmUrgentHigh.should.equal(true); + settings.alarmUrgentHighMins.should.eql([30, 60, 90, 120]); + settings.alarmHigh.should.equal(true); + settings.alarmHighMins.should.eql([30, 60, 90, 120]); + settings.alarmLow.should.equal(true); + settings.alarmLowMins.should.eql([15, 30, 45, 60]); + settings.alarmUrgentLow.should.equal(true); + settings.alarmUrgentLowMins.should.eql([15, 30, 45]); + settings.alarmUrgentMins.should.eql([30, 60, 90, 120]); + settings.alarmWarnMins.should.eql([30, 60, 90, 120]); + settings.alarmTimeagoWarn.should.equal(true); + settings.alarmTimeagoWarnMins.should.equal(15); + settings.alarmTimeagoUrgent.should.equal(true); + settings.alarmTimeagoUrgentMins.should.equal(30); + settings.language.should.equal('en'); + settings.showPlugins.should.equal(''); + }); + + it('support setting from env vars', function () { + var expected = [ + 'ENABLE' + , 'DISABLE' + , 'UNITS' + , 'TIME_FORMAT' + , 'NIGHT_MODE' + , 'SHOW_RAWBG' + , 'CUSTOM_TITLE' + , 'THEME' + , 'ALARM_TYPES' + , 'ALARM_URGENT_HIGH' + , 'ALARM_HIGH' + , 'ALARM_LOW' + , 'ALARM_URGENT_LOW' + , 'ALARM_TIMEAGO_WARN' + , 'ALARM_TIMEAGO_WARN_MINS' + , 'ALARM_TIMEAGO_URGENT' + , 'ALARM_TIMEAGO_URGENT_MINS' + , 'LANGUAGE' + , 'SHOW_PLUGINS' + , 'BG_HIGH' + , 'BG_TARGET_TOP' + , 'BG_TARGET_BOTTOM' + , 'BG_LOW' + , 'SCALE_Y' + ]; + + expected.length.should.equal(24); + + var seen = { }; + settings.eachSettingAsEnv(function markSeenNames(name) { + seen[name] = true; + }); + + + var expectedAndSeen = _.filter(expected, function (name) { + return seen[name]; + }); + + expectedAndSeen.length.should.equal(expected.length); + }); + + it('support setting each', function () { + var expected = [ + 'enable' + , 'disable' + , 'units' + , 'timeFormat' + , 'nightMode' + , 'showRawbg' + , 'customTitle' + , 'theme' + , 'alarmTypes' + , 'alarmUrgentHigh' + , 'alarmHigh' + , 'alarmLow' + , 'alarmUrgentLow' + , 'alarmTimeagoWarn' + , 'alarmTimeagoWarnMins' + , 'alarmTimeagoUrgent' + , 'alarmTimeagoUrgentMins' + , 'language' + , 'showPlugins' + ]; + + expected.length.should.equal(19); + + var seen = { }; + settings.eachSetting(function markSeenNames(name) { + seen[name] = true; + }); + + + var expectedAndSeen = _.filter(expected, function (name) { + return seen[name]; + }); + + expectedAndSeen.length.should.equal(expected.length); + + }); + + it('have default features', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function () { + return undefined; + }); + + _.each(fresh.DEFAULT_FEATURES, function eachDefault (feature) { + fresh.enable.should.containEql(feature); + }); + + }); + + it('support disabling default features', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'DISABLE' ? + fresh.DEFAULT_FEATURES.join(' ') + ' ar2' //need to add ar2 here since it will be auto enabled + : undefined; + }); + + fresh.enable.length.should.equal(0); + }); + + it('parse custom snooze mins', function () { + var userSetting = { + ALARM_URGENT_LOW_MINS: '5 10 15' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return userSetting[name]; + }); + + fresh.alarmUrgentLowMins.should.eql([5, 10, 15]); + + fresh.snoozeMinsForAlarmEvent({eventName: 'low', level: levels.URGENT}).should.eql([5, 10, 15]); + fresh.snoozeFirstMinsForAlarmEvent({eventName: 'low', level: levels.URGENT}).should.equal(5); + }); + + it('set thresholds', function () { + var userThresholds = { + BG_HIGH: '200' + , BG_TARGET_TOP: '170' + , BG_TARGET_BOTTOM: '70' + , BG_LOW: '60' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return userThresholds[name]; + }); + + fresh.thresholds.bgHigh.should.equal(200); + fresh.thresholds.bgTargetTop.should.equal(170); + fresh.thresholds.bgTargetBottom.should.equal(70); + fresh.thresholds.bgLow.should.equal(60); + + should.deepEqual(fresh.alarmTypes, ['simple']); + }); + + it('default to predict if no thresholds are set', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function ( ) { + return undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict']); + }); + + it('ignore junk alarm types', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'ALARM_TYPES' ? 'beep bop' : undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict']); + }); + + it('allow multiple alarm types to be set', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'ALARM_TYPES' ? 'predict simple' : undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict', 'simple']); + }); + + it('handle screwed up thresholds in a way that will display something that looks wrong', function () { + var screwedUp = { + BG_HIGH: '89' + , BG_TARGET_TOP: '90' + , BG_TARGET_BOTTOM: '95' + , BG_LOW: '96' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return screwedUp[name]; + }); + + fresh.thresholds.bgHigh.should.equal(91); + fresh.thresholds.bgTargetTop.should.equal(90); + fresh.thresholds.bgTargetBottom.should.equal(89); + fresh.thresholds.bgLow.should.equal(88); + + should.deepEqual(fresh.alarmTypes, ['simple']); + }); + + it('check if a feature isEnabled', function () { + var fresh = require('../lib/settings')(); + fresh.enable = ['feature1']; + fresh.isEnabled('feature1').should.equal(true); + fresh.isEnabled('feature2').should.equal(false); + }); + + it('check if any listed feature isEnabled', function () { + var fresh = require('../lib/settings')(); + fresh.enable = ['feature1']; + fresh.isEnabled(['unknown', 'feature1']).should.equal(true); + fresh.isEnabled(['unknown', 'feature2']).should.equal(false); + }); + +}); diff --git a/tests/simplealarms.test.js b/tests/simplealarms.test.js new file mode 100644 index 000000000..e12b36241 --- /dev/null +++ b/tests/simplealarms.test.js @@ -0,0 +1,79 @@ +var should = require('should'); +var levels = require('../lib/levels'); + +describe('simplealarms', function ( ) { + + var simplealarms = require('../lib/plugins/simplealarms')(); + + var env = require('../env')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('should trigger a warning when above target', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 171}, {mills: now, mgdl: 181}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + bgnow.setProperties(sbx); + simplealarms.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.message.should.equal('BG Now: 181 +10 mg/dl'); + done(); + }); + + it('should trigger a urgent alarm when really high', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 400}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + it('should trigger a warning when below target', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 70}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.WARN); + + done(); + }); + + it('should trigger a urgent alarm when really low', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 40}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + +}); \ No newline at end of file diff --git a/tests/timeago.test.js b/tests/timeago.test.js new file mode 100644 index 000000000..e47e9873d --- /dev/null +++ b/tests/timeago.test.js @@ -0,0 +1,124 @@ +var should = require('should'); +var levels = require('../lib/levels'); +var times = require('../lib/times'); + +describe('timeago', function ( ) { + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + + var timeago = require('../lib/plugins/timeago')(ctx); + + var env = require('../env')(); + + function freshSBX() { + //set extendedSettings right before calling withExtendedSettings, there's some strange test interference here + env.extendedSettings = {timeago: {enableAlerts: true}}; + var sbx = require('../lib/sandbox')().serverInit(env, ctx).withExtendedSettings(timeago); + return sbx; + } + + it('Not trigger an alarm when data is current', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: Date.now(), mgdl: 100, type: 'sgv'}]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('Time Ago')); + + done(); + }); + + it('Not trigger an alarm with future data', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: Date.now() + times.mins(15).msecs, mgdl: 100, type: 'sgv'}]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('Time Ago')); + + done(); + }); + + it('should trigger a warning when data older than 15m', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv'}]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm('Time Ago'); + highest.level.should.equal(levels.WARN); + highest.message.should.equal('Last received: 16 mins ago\nBG Now: 100 mg/dl'); + done(); + }); + + it('should trigger an urgent alarm when data older than 30m', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: Date.now() - times.mins(31).msecs, mgdl: 100, type: 'sgv'}]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm('Time Ago'); + highest.level.should.equal(levels.URGENT); + highest.message.should.equal('Last received: 31 mins ago\nBG Now: 100 mg/dl'); + done(); + }); + + it('calc timeago displays', function() { + var now = Date.now(); + + should.deepEqual( + timeago.calcDisplay({ mills: now + times.mins(15).msecs }, now) + , {label: 'in the future', shortLabel: 'future'} + ); + + //TODO: current behavior, we can do better + //just a little in the future, pretend it's ok + should.deepEqual( + timeago.calcDisplay({ mills: now + times.mins(4).msecs }, now) + , {value: 1, label: 'min ago', shortLabel: 'm'} + ); + + should.deepEqual( + timeago.calcDisplay(null, now) + , {label: 'time ago', shortLabel: 'ago'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now }, now) + , {value: 1, label: 'min ago', shortLabel: 'm'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - 1 }, now) + , {value: 1, label: 'min ago', shortLabel: 'm'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.sec(30).msecs }, now) + , {value: 1, label: 'min ago', shortLabel: 'm'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.mins(30).msecs }, now) + , {value: 30, label: 'mins ago', shortLabel: 'm'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.hours(5).msecs }, now) + , {value: 5, label: 'hours ago', shortLabel: 'h'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.days(5).msecs }, now) + , {value: 5, label: 'days ago', shortLabel: 'd'} + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.days(10).msecs }, now) + , {label: 'long ago', shortLabel: 'ago'} + ); + }); + +}); \ No newline at end of file diff --git a/tests/times.test.js b/tests/times.test.js new file mode 100644 index 000000000..5bb52bb39 --- /dev/null +++ b/tests/times.test.js @@ -0,0 +1,30 @@ +'use strict'; + +require('should'); + +describe('times', function ( ) { + var times = require('../lib/times'); + + it('hours to mins, secs, and msecs', function () { + times.hour().mins.should.equal(60); + times.hour().secs.should.equal(3600); + times.hour().msecs.should.equal(3600000); + times.hours(3).mins.should.equal(180); + times.hours(3).secs.should.equal(10800); + times.hours(3).msecs.should.equal(10800000); + }); + + it('mins to secs and msecs', function () { + times.min().secs.should.equal(60); + times.min().msecs.should.equal(60000); + times.mins(2).secs.should.equal(120); + times.mins(2).msecs.should.equal(120000); + }); + + it('secs as msecs', function () { + times.sec().msecs.should.equal(1000); + times.secs(15).msecs.should.equal(15000); + }); + + +}); diff --git a/tests/treatmentnotify.test.js b/tests/treatmentnotify.test.js new file mode 100644 index 000000000..d88722783 --- /dev/null +++ b/tests/treatmentnotify.test.js @@ -0,0 +1,126 @@ +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('treatmentnotify', function ( ) { + + var treatmentnotify = require('../lib/plugins/treatmentnotify')(); + + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var now = Date.now(); + + it('Request a snooze for a recent treatment and request an info notify', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{eventType: 'BG Check', glucose: '100', mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + _.first(ctx.notifications.findUnSnoozeable()).level.should.equal(levels.INFO); + + done(); + }); + + it('Not Request a snooze for an older treatment and not request an info notification', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now - (15 * 60 * 1000)}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + should.not.exist(_.first(ctx.notifications.findUnSnoozeable())); + + done(); + }); + + it('Request a snooze for a recent calibration and request an info notify', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.mbgs = [{mgdl: '100', mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + _.first(ctx.notifications.findUnSnoozeable()).level.should.equal(levels.INFO); + + done(); + }); + + it('Not Request a snooze for an older calibration treatment and not request an info notification', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.mbgs = [{mgdl: '100', mills: now - (15 * 60 * 1000)}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + should.not.exist(_.first(ctx.notifications.findUnSnoozeable())); + + done(); + }); + + it('Request a notification for an announcement even there is an active snooze', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.treatments = [{mills: now, mgdl: 40, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + + var fakeSnooze = { + level: levels.URGENT + , title: 'Snoozing alarms for the test' + , message: 'testing...' + , lengthMills: 60000 + }; + + sbx.notifications.requestSnooze(fakeSnooze); + + treatmentnotify.checkNotifications(sbx); + + var announcement = _.first(ctx.notifications.findUnSnoozeable()); + + should.exist(announcement); + announcement.title.should.equal('Urgent Announcement'); + announcement.level.should.equal(levels.URGENT); + announcement.pushoverSound.should.equal('persistent'); + should.deepEqual(ctx.notifications.findHighestAlarm('Announcement'), announcement); + ctx.notifications.snoozedBy(announcement).should.equal(false); + + + done(); + }); + + it('Request a notification for a non-error announcement', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.treatments = [{mills: now, mgdl: 100, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + + treatmentnotify.checkNotifications(sbx); + + var announcement = _.first(ctx.notifications.findUnSnoozeable()); + + should.exist(announcement); + announcement.title.should.equal('Announcement'); + announcement.level.should.equal(levels.INFO); + should.not.exist(announcement.pushoverSound); + should.not.exist(ctx.notifications.findHighestAlarm()); + ctx.notifications.snoozedBy(announcement).should.equal(false); + + done(); + }); + +}); \ No newline at end of file diff --git a/tests/units.test.js b/tests/units.test.js new file mode 100644 index 000000000..b6e8a9faa --- /dev/null +++ b/tests/units.test.js @@ -0,0 +1,16 @@ +'use strict'; + +require('should'); + +describe('units', function ( ) { + var units = require('../lib/units')(); + + it('should convert 99 to 5.5', function () { + units.mgdlToMMOL(99).should.equal('5.5'); + }); + + it('should convert 180 to 10.0', function () { + units.mgdlToMMOL(180).should.equal('10.0'); + }); + +}); diff --git a/tests/upbat.test.js b/tests/upbat.test.js new file mode 100644 index 000000000..0d70e5817 --- /dev/null +++ b/tests/upbat.test.js @@ -0,0 +1,109 @@ +'use strict'; + +require('should'); + +describe('Uploader Battery', function ( ) { + var data = {devicestatus: [{mills: Date.now(), uploader: {battery: 20}}]}; + + it('display uploader battery status', function (done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + }; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('upbat'); + var result = setter(); + result.display.should.equal('20%'); + result.status.should.equal('urgent'); + result.min.value.should.equal(20); + result.min.level.should.equal(25); + done(); + }; + + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + + }); + + it('set a pill to the uploader battery status', function (done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('20%'); + options.labelClass.should.equal('icon-battery-25'); + options.pillClass.should.equal('urgent'); + done(); + } + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + + }); + + it('hide the pill if there is no uploader battery status', function (done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {}); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + }); + + it('hide the pill if there is uploader battery status is -1', function (done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.hide.should.equal(true); + done(); + } + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {devicestatus: [{uploader: {battery: -1}}]}); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + }); + + it('should handle alexa requests', function (done) { + + var ctx = { + settings: {} + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + + upbat.alexa.intentHandlers.length.should.equal(1); + + upbat.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Uploader battery'); + response.should.equal('Your uploader battery is at 20%'); + + done(); + }, [], sbx); + + }); + +}); diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 000000000..86ea7ea12 --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,26 @@ +'use strict'; + +require('should'); + +describe('utils', function ( ) { + var settings = { + alarmTimeagoUrgentMins: 30 + , alarmTimeagoWarnMins: 15 + }; + + var utils = require('../lib/utils')(settings); + + it('format numbers', function () { + utils.toFixed(5.499999999).should.equal('5.50'); + }); + + it('merge date and time', function () { + var result = utils.mergeInputTime('22:35', '2015-07-14'); + result.hours().should.equal(22); + result.minutes().should.equal(35); + result.year().should.equal(2015); + result.format('MMM').should.equal('Jul'); + result.date().should.equal(14); + }); + +}); diff --git a/tests/verifyauth.test.js b/tests/verifyauth.test.js new file mode 100644 index 000000000..871dd56f1 --- /dev/null +++ b/tests/verifyauth.test.js @@ -0,0 +1,68 @@ +'use strict'; + +var request = require('supertest'); +var language = require('../lib/language')(); + +describe('verifyauth', function ( ) { + var api = require('../lib/api/'); + + var scope = this; + function setup_app (env, fn) { + require('../lib/bootevent')(env, language).boot(function booted (ctx) { + ctx.app = api(env, ctx); + scope.app = ctx.app; + fn(ctx); + }); + } + + after(function (done) { + done(); + }); + + it('should fail unauthorized', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = ''; + ping_authorized_endpoint(ctx.app, 401, done); + }); + + }); + + + it('should work fine authorized', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = env.api_secret; + ping_authorized_endpoint(ctx.app, 200, done); + }); + + }); + + + function ping_authorized_endpoint (app, fails, fn) { + request(app) + .get('/verifyauth') + .set('api-secret', app.api_secret || '') + .expect(fails) + .end(function (err, res) { + //console.log(res.body); + if (fails < 400) { + res.body.status.should.equal(200); + } + fn( ); + // console.log('err', err, 'res', res); + }); + } + +}); +
LanguageCodeMissing
').append(l.language)); + tr.append($('').append(l.code)); + tr.append($('').append(result[l.code].ok)); + tr.append($('').append(result[l.code].missing)); + tr.append($('').append((result[l.code].ok / result[l.code].total * 100).toFixed(1) + '%')); + + var tr2 = $('
').append(l.language)); + tr2.append($('').append(l.code)); + tr2.append($('').attr('width', '300px').append(result[l.code].keys.join('
'))); + + table.append(tr); + table2.append(tr2); + }); + + var placeholder = $('#translations'); + placeholder.html(table); + placeholder.append('
'); + placeholder.append(table2); + }); +})(); \ No newline at end of file diff --git a/static/treatments.html b/static/treatments.html deleted file mode 100644 index 24761c4d7..000000000 --- a/static/treatments.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Nightscout: Treatments - - - - - - - - -
-

Nightscout: Treatments

- - - - - - - - - - - - - - - - - - - - - - - -
TimeEvent TypeBGInsulinCarbsEntered ByNotes
{{treatment.created_at | date:'short'}}{{treatment.eventType}}{{glucoseDisplay(treatment)}}{{treatment.insulin | number: 2}}{{treatment.carbs}}{{treatment.enteredBy}}{{treatment.notes}}
-
- - - diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 000000000..ce9204cee --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,637 @@ +swagger: '2.0' +info: + title: Nightscout API + description: Own your DData with the Nightscout API + version: "0.8.0" + license: + name: AGPL 3 + url: https://www.gnu.org/licenses/agpl.txt +basePath: /api/v1 +produces: + - application/json +security: + - api_secret: [] + +paths: + + /entries/{spec}: + get: + summary: All Entries matching query + description: | + The Entries endpoint returns information about the + Nightscout entries. + + parameters: + - name: spec + in: path + type: string + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + + default: sgv + required: true + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Entries + schema: + $ref: '#/definitions/Entries' + + /slice/{storage}/{field}/{type}/{prefix}/{regex}: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: storage + in: path + type: string + description: Prefix to use in constructing a prefix-based regex, default is `entries`. + required: true + default: entries + - name: field + in: path + type: string + description: Name of the field to use Regex against in query object, default is `dateString`. + default: dateString + required: true + - name: type + in: path + type: string + description: The type field to search against, default is sgv. + required: true + default: sgv + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + default: '2015' + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + default: .* + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /echo/{storage}/{spec}: + get: + summary: View generated Mongo Query object + description: | + Information about the mongo query object created by the query. + + parameters: + - name: storage + in: path + type: string + description: | + `entries`, or `treatments` to select the storage layer. + + default: sgv + required: true + - name: spec + in: path + type: string + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + This parameter is optional. + + default: sgv + required: true + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + - Debug + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/MongoQuery' + + /times/echo/{prefix}/{regex}: + get: + summary: Echo the query object to be used. + description: Echo debug information about the query object constructed. + parameters: + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + - Debug + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/MongoQuery' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + + /times/{prefix}/{regex}: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + + /entries: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + post: + tags: + - Entries + summary: Add new entries. + description: "" + operationId: addEntries + consumes: + - application/json + - text/plain + produces: + - application/json + - text/plain + parameters: + - in: body + name: body + description: Entries to be uploaded. + required: true + schema: + $ref: "#/definitions/Entries" + responses: + "405": + description: Invalid input + "200": + description: Rejected list of entries. Empty list is success. + delete: + tags: + - Entries + summary: Delete entries matching query. + description: "Remove entries, same search syntax as GET." + operationId: remove + parameters: + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + responses: + "200": + description: Empty list is success. + + /treatments: + get: + summary: Treatments + description: The Treatments endpoint returns information about the Nightscout treatments. + tags: + - Treatments + parameters: + - name: find + in: query + description: + The query used to find entries, supports nested query syntax. Examples + `find[insulin][$gte]=3` + `find[carb][$gte]=100` + `find[eventType]=Correction+Bolus` + All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + responses: + "200": + description: An array of treatments + schema: + $ref: '#/definitions/Treatments' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + post: + tags: + - Treatments + summary: Add new treatments. + description: "" + operationId: addTreatments + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: Treatments to be uploaded. + required: true + schema: + $ref: "#/definitions/Treatments" + responses: + "405": + description: Invalid input + "200": + description: Rejected list of treatments. Empty list is success. + + /profile: + get: + summary: Profile + description: The Profile endpoint returns information about the Nightscout Treatment Profiles. + tags: + - Profile + responses: + "200": + description: An array of treatments + schema: + $ref: '#/definitions/Profile' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /status: + get: + summary: Status + description: Server side status, default settings and capabilities. + tags: + - Status + responses: + "200": + description: Server capabilities and status. + schema: + $ref: '#/definitions/Status' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + +securityDefinitions: + api_secret: + type: apiKey + name: api_secret + in: header + description: The hash of the API_SECRET env var + +definitions: + Entry: + properties: + type: + type: string + description: "sgv, mbg, cal, etc" + dateString: + type: string + description: dateString, prefer ISO `8601` + date: + type: number + description: Epoch + sgv: + type: number + description: The glucose reading. (only available for sgv types) + direction: + type: string + description: Direction of glucose trend reported by CGM. (only available for sgv types) + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + filtered: + type: number + description: The raw filtered value directly from CGM transmitter. (only available for sgv types) + unfiltered: + type: number + description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + rssi: + type: number + description: The signal strength from CGM transmitter. (only available for sgv types) + + Entries: + type: array + items: + $ref: '#/definitions/Entry' + + Treatment: + properties: + _id: + type: string + description: Internally assigned id. + eventType: + type: string + description: The type of treatment event. + created_at: + type: string + description: The date of the event, might be set retroactively . + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + description: Method used to obtain glucose, Finger or Sensor. + carbs: + type: number + description: Number of carbs. + insulin: + type: number + description: Amount of insulin, if any. + units: + type: string + description: The units for the glucose value, mg/dl or mmol. + notes: + type: string + description: Description/notes of treatment. + enteredBy: + type: string + description: Who entered the treatment. + + Treatments: + type: array + items: + $ref: '#/definitions/Treatment' + + Profile: + properties: + sens: + type: integer + description: 'Internally assigned id' + dia: + type: integer + description: 'Internally assigned id' + carbratio: + type: integer + description: 'Internally assigned id' + carbs_hr: + type: integer + description: 'Internally assigned id' + _id: + type: string + description: 'Internally assigned id' + + Status: + properties: + apiEnabled: + type: boolean + description: 'Whether or not the REST API is enabled.' + careportalEnabled: + type: boolean + description: 'Whether or not the careportal is enabled in the API.' + head: + type: string + description: 'The git identifier for the running instance of the app.' + name: + type: string + description: Nightscout (static) + version: + type: string + description: 'The version label of the app.' + settings: + $ref: '#/definitions/Settings' + extendedSettings: + $ref: '#/definitions/ExtendedSettings' + + Settings: + properties: + units: + type: string + description: Default units for glucose measurements across the server. + timeFormat: + type: string + description: Default time format + enum: + - 12 + - 24 + customTitle: + type: string + description: Default custom title to be displayed system wide. + nightMode: + type: boolean + description: Should Night mode be enabled by default? + theme: + type: string + description: Default theme to be displayed system wide, `default`, `colors`, `colorblindfriendly`. + language: + type: string + description: Default language code to be used system wide + showPlugins: + type: string + description: Plugins that should be shown by default + showRawbg: + type: string + description: If Raw BG is enabled when should it be shown? `never`, `always`, `noise` + alarmTypes: + type: array + items: + type: string + enum: + - simple + - predict + description: Enabled alarm types, can be multiple + alarmUrgentHigh: + type: boolean + description: Enable/Disable client-side Urgent High alarms by default, for use with `simple` alarms. + alarmHigh: + type: boolean + description: Enable/Disable client-side High alarms by default, for use with `simple` alarms. + alarmLow: + type: boolean + description: Enable/Disable client-side Low alarms by default, for use with `simple` alarms. + alarmUrgentLow: + type: boolean + description: Enable/Disable client-side Urgent Low alarms by default, for use with `simple` alarms. + alarmTimeagoWarn: + type: boolean + description: Enable/Disable client-side stale data alarms by default. + alarmTimeagoWarnMins: + type: number + description: Number of minutes before a stale data warning is generated. + alarmTimeagoUrgent: + type: boolean + description: Enable/Disable client-side urgent stale data alarms by default. + alarmTimeagoUrgentMins: + type: number + description: Number of minutes before a stale data warning is generated. + enable: + type: array + items: + type: string + description: Enabled features + thresholds: + $ref: '#/definitions/Threshold' + + Threshold: + properties: + bg_high: + type: integer + description: 'High BG range.' + bg_target_top: + type: integer + description: 'Top of target range.' + bg_target_bottom: + type: integer + description: 'Bottom of target range.' + bg_low: + type: integer + description: 'Low BG range.' + + ExtendedSettings: + description: Extended settings of client side plugins + + MongoQuery: + description: Mongo Query object. + + + Error: + properties: + code: + type: integer + format: int32 + message: + type: string + fields: + type: object + diff --git a/testing/convert-treatments.js b/testing/convert-treatments.js index 1ef65786b..3c3cbf17f 100644 --- a/testing/convert-treatments.js +++ b/testing/convert-treatments.js @@ -1,16 +1,16 @@ db.treatments.find().forEach( - function (elem) { - db.treatments.update( - { - _id: elem._id - }, - { - $set: { - glucose: elem.glucoseValue, - insulin: elem.insulinGiven, - carbs: elem.carbsGiven - } - } - ); - } + function (elem) { + db.treatments.update( + { + _id: elem._id + }, + { + $set: { + glucose: elem.glucoseValue, + insulin: elem.insulinGiven, + carbs: elem.carbsGiven + } + } + ); + } ); diff --git a/testing/make_high_data.js b/testing/make_high_data.js index 8cbf19178..bfdb21c34 100644 --- a/testing/make_high_data.js +++ b/testing/make_high_data.js @@ -1,26 +1,26 @@ var fs = require('fs'); +var times = require('../lib/times'); -var data = "" +var data = ''; var END_TIME = Date.now(); -var FIVE_MINS_IN_MS = 300000; var TIME_PERIOD_HRS = 24; var DATA_PER_HR = 12; var START_BG = 50; var currentBG = START_BG; -var currentTime = END_TIME - (TIME_PERIOD_HRS * DATA_PER_HR * FIVE_MINS_IN_MS); +var currentTime = END_TIME - (TIME_PERIOD_HRS * DATA_PER_HR * times.mins(5).msecs); for(var i = 0; i < TIME_PERIOD_HRS * DATA_PER_HR; i++) { - currentBG += Math.ceil(Math.cos(i)*5+.2); - currentTime += FIVE_MINS_IN_MS; - data += "1," + currentBG + ",,,,,,,,," + new Date(currentTime).toString() + ",,,,\n"; + currentBG += Math.ceil(Math.cos(i)*5+.2); + currentTime += times.mins(5).msecs; + data += '1,' + currentBG + ',,,,,,,,,' + new Date(currentTime).toString() + ',,,,\n'; } -fs.writeFile("../Dexcom.csv", data); +fs.writeFile('../Dexcom.csv', data); function makedata() { - currentBG -= 1; - currentTime += FIVE_MINS_IN_MS; - data += "1," + currentBG + ",,,,,,,,," + new Date(currentTime).toString() + ",,,,\n"; - fs.writeFile("../Dexcom.csv", data); + currentBG -= 1; + currentTime += times.mins(5).msecs; + data += '1,' + currentBG + ',,,,,,,,,' + new Date(currentTime).toString() + ',,,,\n'; + fs.writeFile('../Dexcom.csv', data); } -setInterval(makedata, 1000 * 10) \ No newline at end of file +setInterval(makedata, 1000 * 10); \ No newline at end of file diff --git a/testing/populate.js b/testing/populate.js index f4fff19ae..34c307b77 100644 --- a/testing/populate.js +++ b/testing/populate.js @@ -1,125 +1,33 @@ 'use strict'; -/////////////////////////////////////////////////// -// This script is intended to be run as a cron job -// every n-minutes or whatever the equiv is on windows -// -// Author: John A. [euclidjda](https://github.com/euclidjda) -// Source: https://gist.github.com/euclidjda/4ae207a89921f21382a9 -/////////////////////////////////////////////////// -/////////////////////////////////////////////////// -// DB Connection setup and utils -/////////////////////////////////////////////////// +var mongodb = require('mongodb'); +var env = require('./../env')(); -var mongodb = require('mongodb'); -var software = require('./package.json'); -var env = require('./env')( ); +var util = require('./helpers/util'); main(); -function main( ) { +function main() { + var MongoClient = mongodb.MongoClient; + MongoClient.connect(env.storageURI, function connected(err, db) { - var MongoClient = mongodb.MongoClient; - - MongoClient.connect(env.mongo, function connected (err, db) { - - console.log("Connected to mongo, ERROR: %j", err); - if (err) { throw err; } - populate_collection( db ); - - }); - -} - -function populate_collection( db ) { - - //console.log( 'mongo = ' + env.mongo ); - //console.log( 'collection = ' + env.mongo_collection ); - - var cgm_collection = db.collection( env.mongo_collection ); - - var new_cgm_record = get_cgm_record(); - - cgm_collection.insert( new_cgm_record, function(err,created) { - - // TODO: Error checking - process.exit( 0 ); - - } ); - - -} - -function get_cgm_record( ) { - - var dateobj = new Date(); - var datemil = dateobj.getTime(); - var datesec = datemil / 1000; - var datestr = getDateString( dateobj ); - - // We put the time in a range from -1 to +1 for every thiry minute period - var range = (datesec % 1800)/900 - 1.0; - - // The we push through a COS function and scale between 40 and 400 (so it is like a bg level) - var sgv = Math.floor(360*(Math.cos( 10.0 * range / 3.14 ) / 2 + 0.5)) + 40; - var dir = range > 0.0 ? "FortyFiveDown" : "FortyFiveUp"; - - console.log( 'Writing Record: '); - console.log( 'sgv = ' + sgv ); - console.log( 'date = ' + datemil ); - console.log( 'dir = ' + dir ); - console.log( 'str = ' + datestr ); - - var mondo_db = null; - var doc = { 'device' :'dexcom' , - 'date' : datemil , - 'sgv' : sgv , - 'direction' : dir , - 'dateString' : datestr }; - - - return doc; + console.log('Connecting to mongo...'); + if (err) { + console.log('Error occurred: ', err); + throw err; + } + populate_collection(db); + }); } -function getDateString( d ) { - - // How I wish js had strftime. This would be one line of code! - - var month = d.getMonth(); - var day = d.getDay(); - var year = d.getFullYear(); - - if (month < 10 ) month = '0'+month; - if (day < 10 ) day = '0'+day; +function populate_collection(db) { + var cgm_collection = db.collection(env.entries_collection); + var new_cgm_record = util.get_cgm_record(); - var hour = d.getHours(); - var min = d.getMinutes(); - var sec = d.getSeconds(); - - var ampm = 'PM'; - - if (hour < 12) - { - ampm = "AM"; - } - else - { - ampm = "PM"; - } - - if (hour == 0) - { - hour = 12; + cgm_collection.insert(new_cgm_record, function (err) { + if (err) { + throw err; } - if (hour > 12) - { - hour = hour - 12; - } - - if (hour < 10) hour = '0' + hour; - if (min < 10) min = '0' + min; - if (sec < 10) sec = '0' + sec; - - return month + '/' + day + '/' + year + ' ' + hour + ':' + min + ':' + sec + ' ' + ampm; - + process.exit(0); + }); } diff --git a/testing/populate_rest.js b/testing/populate_rest.js new file mode 100644 index 000000000..a3f75fadd --- /dev/null +++ b/testing/populate_rest.js @@ -0,0 +1,40 @@ +'use strict'; + +var env = require('./../env')(); +var http = require('http'); +var util = require('./util'); + +main(); + +function main() { + send_entry_rest(); +} + +function send_entry_rest() { + var new_cgm_record = util.get_cgm_record(); + var new_cgm_record_string = JSON.stringify(new_cgm_record); + + var options = { + host: 'localhost', + port: env.PORT, + path: '/api/v1/entries/', + method: 'POST', + headers: { + 'api-secret' : env.api_secret, + 'Content-Type': 'application/json', + 'Content-Length': new_cgm_record_string.length + } + }; + + var req = http.request(options, function(res) { + console.log('Ok: ', res.statusCode); + }); + + req.on('error', function(e) { + console.error('error'); + console.error(e); + }); + + req.write(new_cgm_record_string); + req.end(); +} \ No newline at end of file diff --git a/testing/util.js b/testing/util.js new file mode 100644 index 000000000..82a61fd47 --- /dev/null +++ b/testing/util.js @@ -0,0 +1,29 @@ +'use strict'; + +exports.get_cgm_record = function() { + var dateobj = new Date(); + var datemil = dateobj.getTime(); + var datesec = datemil / 1000; + var datestr = dateobj.toISOString(); + + // We put the time in a range from -1 to +1 for every thiry minute period + var range = (datesec % 1800) / 900 - 1.0; + + // The we push through a COS function and scale between 40 and 400 (so it is like a bg level) + var sgv = Math.floor(360 * (Math.cos(10.0 * range / 3.14) / 2 + 0.5)) + 40; + var dir = range > 0.0 ? 'FortyFiveDown' : 'FortyFiveUp'; + + console.log('Writing Record: '); + console.log('sgv = ' + sgv); + console.log('date = ' + datemil); + console.log('dir = ' + dir); + console.log('str = ' + datestr); + + return { + 'device': 'dexcom', + 'date': datemil, + 'sgv': sgv, + 'direction': dir, + 'dateString': datestr + }; +}; \ No newline at end of file diff --git a/tests/admintools.test.js b/tests/admintools.test.js new file mode 100644 index 000000000..59230ff71 --- /dev/null +++ b/tests/admintools.test.js @@ -0,0 +1,227 @@ +'use strict'; + +require('should'); +var _ = require('lodash'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] +}; + +var someData = { + '/api/v1/devicestatus.json?count=500': [ + { + '_id': { + '$oid': '56096da3c5d0fef41b212362' + }, + 'uploaderBattery': 37, + 'created_at': '2015-09-28T16:41:07.144Z' + }, + { + '_id': { + '$oid': '56096da3c5d0fef41b212363' + }, + 'uploaderBattery': 38, + 'created_at': '2025-09-28T16:41:07.144Z' + } + ], + '/api/v1/treatments.json?&find[created_at][$gte]=': [ + { + '_id': '5609a9203c8104a8195b1c1e', + 'enteredBy': '', + 'eventType': 'Carb Correction', + 'carbs': 3, + 'created_at': '2025-09-28T20:54:00.000Z' + } + ], + '/api/v1/entries.json?&find[date][$gte]=': [ + { + '_id': '560983f326c5a592d9b9ae0c', + 'device': 'dexcom', + 'date': 1543464149000, + 'sgv': 83, + 'direction': 'Flat', + 'type': 'sgv', + 'filtered': 107632, + 'unfiltered': 106256, + 'rssi': 178, + 'noise': 1 + } + ] + }; + + +describe('admintools', function ( ) { + var self = this; + + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + + var indexHtml = read(__dirname + '/../static/admin/index.html', 'utf8'); + self.$('body').html(indexHtml); + + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + + self.$.ajax = function mockAjax (url, opts) { + if (url && url.url) { + url = url.url; + } + //logfile.write(url+'\n'); + //console.log(url,opts); + if (opts && opts.success && opts.success.call) { + if (url.indexOf('/api/v1/treatments.json?&find[created_at][$gte]=')===0) { + url = '/api/v1/treatments.json?&find[created_at][$gte]='; + } + if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) { + url = '/api/v1/entries.json?&find[date][$gte]='; + } + return { + done: function mockDone (fn) { + if (someData[url]) { + console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + return { + done: function mockDone (fn) { + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: 'OK'}); + } + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + }; + + self.$.plot = function mockPlot () { + }; + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn (event, callback) { + if ('connect' === event && callback) { + callback(); + } + } + , emit: function mockEmit (event, data, callback) { + if ('authorize' === event && callback) { + callback({ + read: true + }); + } + } + }; + } + } + }); + + benv.require(__dirname + '/../bundle/bundle.source.js'); + benv.require(__dirname + '/../static/admin/js/admin.js'); + + done(); + }); + }); + + after(function (done) { + benv.teardown(true); + done(); + }); + + it ('should produce some html', function (done) { + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm (text) { + console.log('Confirm:', text); + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + client.init(); + client.dataUpdate(nowData); + + //var result = $('body').html(); + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + //logfile.write($('body').html()); + + //console.log(result); + + $('#admin_cleanstatusdb_0_html + button').text().should.equal('Delete all documents'); // devicestatus button + $('#admin_cleanstatusdb_0_status').text().should.equal('Database contains 2 records'); // devicestatus init result + + $('#admin_cleanstatusdb_0_html + button').click(); + $('#admin_cleanstatusdb_0_status').text().should.equal('All records removed ...'); // devicestatus code result + + $('#admin_futureitems_0_html + button').text().should.equal('Remove treatments in the future'); // futureitems button 0 + $('#admin_futureitems_0_status').text().should.equal('Database contains 1 future records'); // futureitems init result 0 + + $('#admin_futureitems_0_html + button').click(); + $('#admin_futureitems_0_status').text().should.equal('Record 5609a9203c8104a8195b1c1e removed ...'); // futureitems code result 0 + + $('#admin_futureitems_1_html + button').text().should.equal('Remove entries in the future'); // futureitems button 1 + $('#admin_futureitems_1_status').text().should.equal('Database contains 1 future records'); // futureitems init result 1 + + $('#admin_futureitems_1_html + button').click(); + $('#admin_futureitems_1_status').text().should.equal('Record 560983f326c5a592d9b9ae0c removed ...'); // futureitems code result 1 + + done(); + }); + +}); diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index d8a60230b..dd3e1eb2c 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -1,49 +1,162 @@ +'use strict'; var request = require('supertest'); -var should = require('should'); var load = require('./fixtures/load'); +var bootevent = require('../lib/bootevent'); +var language = require('../lib/language')(); +require('should'); describe('Entries REST api', function ( ) { var entries = require('../lib/api/entries/'); + + this.timeout(10000); before(function (done) { var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; this.wares = require('../lib/middleware/')(env); - var store = require('../lib/storage')(env); - this.archive = require('../lib/entries').storage(env.mongo_collection, store); + this.archive = null; this.app = require('express')( ); this.app.enable('api'); var self = this; - store(function ( ) { - self.app.use('/', entries(self.app, self.wares, self.archive)); - self.archive.create(load('json'), done); + bootevent(env, language).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx)); + self.archive = require('../lib/entries')(env, ctx); + + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + self.archive.create(creating, done); }); }); - after(function (done) { + + beforeEach(function (done) { + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + this.archive.create(creating, done); + }); + + afterEach(function (done) { this.archive( ).remove({ }, done); }); - it('should be a module', function ( ) { - entries.should.be.ok; + after(function (done) { + this.archive( ).remove({ }, done); + }); + // keep this test pinned at or near the top in order to validate all + // entries successfully uploaded. if res.body.length is short of the + // expected value, it may indicate a regression in the create + // function callback logic in entries.js. + it('gets requested number of entries', function (done) { + var count = 30; + request(this.app) + .get('/entries.json?find[dateString][$gte]=2014-07-19&count=' + count) + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(count); + done(); + }); }); - it('/entries.json', function (done) { + + it('gets default number of entries', function (done) { + var defaultCount = 10; request(this.app) - .get('/entries.json') + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') .expect(200) - .end(function (err, res) { - // console.log('body', res.body); - res.body.length.should.equal(10); + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount); done( ); }); }); - it('/entries.json', function (done) { + it('/echo/ api shows query', function (done) { request(this.app) - .get('/entries.json?count=30') + .get('/echo/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Object); + res.body.query.should.be.instanceof(Object); + res.body.input.should.be.instanceof(Object); + res.body.input.find.should.be.instanceof(Object); + res.body.storage.should.equal('entries'); + done( ); + }); + }); + + it('/slice/ can slice time', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/2014-07.json?count=20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(20); + done( ); + }); + }); + + + it('/times/echo can describe query', function (done) { + var app = this.app; + request(app) + .get('/times/echo/2014-07/.*T{00..05}:.json?count=20&find[sgv][$gte]=160') .expect(200) - .end(function (err, res) { - // console.log('body', res.body); - res.body.length.should.equal(30); + .end(function (err, res) { + res.body.should.be.instanceof(Object); + res.body.req.should.have.property('query'); + res.body.should.have.property('pattern').with.lengthOf(6); + done( ); + }); + }); + + it('/slice/ can slice with multiple prefix', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/2014-07-{17..20}.json?count=20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(20); + done( ); + }); + }); + + it('/slice/ can slice time with prefix and no results', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/1999-07.json?count=20&find[sgv][$lte]=401') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(0); + done( ); + }); + }); + + it('/times/ can get modal times', function (done) { + var app = this.app; + request(app) + .get('/times/2014-07-/{0..30}T.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); + }); + + it('/times/ can get modal minutes and times', function (done) { + var app = this.app; + request(app) + .get('/times/20{14..15}-07/T{09..10}.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); + }); + it('/times/ can get multiple prefixen and modal minutes and times', function (done) { + var app = this.app; + request(app) + .get('/times/20{14..15}/T.*:{00..60}.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); done( ); }); }); @@ -52,29 +165,69 @@ describe('Entries REST api', function ( ) { request(this.app) .get('/entries/current.json') .expect(200) - .end(function (err, res) { - res.body.length.should.equal(1); - done( ); - // console.log('err', err, 'res', res); + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + res.body[0].sgv.should.equal(100); + done(); + }); + }); + + it('/entries/:id', function (done) { + var app = this.app; + this.archive.list({count: 1}, function(err, records) { + var currentId = records.pop()._id.toString(); + request(app) + .get('/entries/'+currentId+'.json') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + res.body[0]._id.should.equal(currentId); + done( ); + }); }); + }); + it('/entries/:model', function (done) { + var app = this.app; + request(app) + .get('/entries/sgv/.json?count=10&find[dateString][$gte]=2014') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); }); - it('/entries/preview', function (done) { + it('disallow POST by readable /entries/preview', function (done) { + request(this.app) + .post('/entries/preview.json') + .send(load('json')) + .expect(401) + .end(function (err, res) { + // res.body.should.be.instanceof(Array).and.have.lengthOf(30); + done(); + }); + }); - request(this.app) - .post('/entries/preview.json') - .send(load('json')) - .expect(201) - .end(function (err, res) { - // console.log(res.body); - res.body.length.should.equal(30); - done( ); - // console.log('err', err, 'res', res); - }) - ; + it('disallow deletes unauthorized', function (done) { + var app = this.app; + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(401) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done(); + }); + } + }); }); }); - diff --git a/tests/api.status.test.js b/tests/api.status.test.js index c668bba9f..1da9f793a 100644 --- a/tests/api.status.test.js +++ b/tests/api.status.test.js @@ -1,29 +1,27 @@ +'use strict'; var request = require('supertest'); -var should = require('should'); +var language = require('../lib/language')(); + +require('should'); describe('Status REST api', function ( ) { var api = require('../lib/api/'); before(function (done) { var env = require('../env')( ); - env.enable = "careportal rawbg"; + env.settings.enable = ['careportal', 'rawbg']; + env.settings.authDefaultRoles = 'readable'; env.api_secret = 'this is my long pass phrase'; this.wares = require('../lib/middleware/')(env); - var store = require('../lib/storage')(env); this.app = require('express')( ); this.app.enable('api'); var self = this; - store(function ( ) { - var entriesStorage = require('../lib/entries').storage(env.mongo_collection, store); - self.app.use('/api', api(env, entriesStorage)); + require('../lib/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/api', api(env, ctx)); done(); }); }); - it('should be a module', function ( ) { - api.should.be.ok; - }); - it('/status.json', function (done) { request(this.app) .get('/api/status.json') @@ -31,7 +29,9 @@ describe('Status REST api', function ( ) { .end(function (err, res) { res.body.apiEnabled.should.equal(true); res.body.careportalEnabled.should.equal(true); - res.body.enabledOptions.should.equal('careportal rawbg'); + res.body.settings.enable.length.should.equal(2); + res.body.settings.enable.should.containEql('careportal'); + res.body.settings.enable.should.containEql('rawbg'); done( ); }); }); @@ -42,10 +42,31 @@ describe('Status REST api', function ( ) { .end(function(err, res) { res.type.should.equal('text/html'); res.statusCode.should.equal(200); - done() + done(); + }); + }); + + it('/status.svg', function (done) { + request(this.app) + .get('/api/status.svg') + .end(function(err, res) { + res.statusCode.should.equal(302); + done(); + }); + }); + + it('/status.txt', function (done) { + request(this.app) + .get('/api/status.txt') + .expect(200, 'STATUS OK') + .end(function(err, res) { + res.type.should.equal('text/plain'); + res.statusCode.should.equal(200); + done(); }); }); + it('/status.js', function (done) { request(this.app) .get('/api/status.js') @@ -53,7 +74,7 @@ describe('Status REST api', function ( ) { res.type.should.equal('application/javascript'); res.statusCode.should.equal(200); res.text.should.startWith('this.serverSettings ='); - done() + done(); }); }); @@ -63,7 +84,7 @@ describe('Status REST api', function ( ) { .end(function(err, res) { res.headers.location.should.equal('http://img.shields.io/badge/Nightscout-OK-green.png'); res.statusCode.should.equal(302); - done() + done(); }); }); diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js new file mode 100644 index 000000000..6a1608158 --- /dev/null +++ b/tests/api.treatments.test.js @@ -0,0 +1,145 @@ +'use strict'; + +var request = require('supertest'); +var should = require('should'); +var language = require('../lib/language')(); + +describe('Treatment API', function ( ) { + this.timeout(2000); + var self = this; + + var api = require('../lib/api/'); + beforeEach(function (done) { + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../env')(); + self.env.settings.authDefaultRoles = 'readable'; + self.env.settings.enable = ['careportal', 'api']; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/bootevent')(self.env, language).boot(function booted(ctx) { + self.ctx = ctx; + self.ctx.ddata = require('../lib/data/ddata')(); + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + after(function () { + // delete process.env.API_SECRET; + }); + + it('post single treatments', function (done) { + var doneCalled = false; + + self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { + self.ctx.ddata.treatments.length.should.equal(3); + self.ctx.ddata.treatments[0].mgdl.should.equal(100); + should.not.exist(self.ctx.ddata.treatments[0].eventTime); + should.not.exist(self.ctx.ddata.treatments[0].notes); + + should.not.exist(self.ctx.ddata.treatments[1].eventTime); + self.ctx.ddata.treatments[1].insulin.should.equal(2); + self.ctx.ddata.treatments[2].carbs.should.equal(30); + + //if travis is slow the 2 posts take long enough that 2 data-loaded events are emitted + if (!doneCalled) { done(); } + + doneCalled = true; + }); + + self.ctx.treatments().remove({ }, function ( ) { + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send({eventType: 'BG Check', glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + + }); + }); + + it('post a treatment array', function (done) { + var doneCalled = false; + + self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { + self.ctx.ddata.treatments.length.should.equal(3); + should.not.exist(self.ctx.ddata.treatments[0].eventTime); + should.not.exist(self.ctx.ddata.treatments[1].eventTime); + + //if travis is slow the 2 posts take long enough that 2 data-loaded events are emitted + if (!doneCalled) { done(); } + + doneCalled = true; + }); + + self.ctx.treatments().remove({ }, function ( ) { + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send([ + {eventType: 'BG Check', glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''} + , {eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} + ]) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + }); + }); + + it('post a treatment array and dedupe', function (done) { + var doneCalled = false; + + self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { + self.ctx.ddata.treatments.length.should.equal(3); + self.ctx.ddata.treatments[0].mgdl.should.equal(100); + + //if travis is slow the 2 posts take long enough that 2 data-loaded events are emitted + if (!doneCalled) { done(); } + + doneCalled = true; + }); + + self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send([ + {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} + ]) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + }); + }); +}); diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js new file mode 100644 index 000000000..23ff9c165 --- /dev/null +++ b/tests/api.unauthorized.test.js @@ -0,0 +1,163 @@ +'use strict'; + +var request = require('supertest'); +var load = require('./fixtures/load'); +var should = require('should'); +var language = require('../lib/language')(); + +describe('authed REST api', function ( ) { + var entries = require('../lib/api/entries/'); + + before(function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; + this.wares = require('../lib/middleware/')(env); + this.archive = null; + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + self.known_key = known; + require('../lib/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx)); + self.archive = require('../lib/entries')(env, ctx); + + var creating = load('json'); + // creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + self.archive.create(creating, done); + }); + }); + + beforeEach(function (done) { + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + this.archive.create(creating, done); + }); + + afterEach(function (done) { + this.archive( ).remove({ }, done); + }); + + after(function (done) { + this.archive( ).remove({ }, done); + }); + + it('disallow unauthorized POST', function (done) { + var app = this.app; + + var new_entry = {type: 'sgv', sgv: 100, date: Date.now() }; + new_entry.dateString = new Date(new_entry.date).toISOString( ); + request(app) + .post('/entries.json?') + .send([new_entry]) + .expect(401) + .end(function (err, res) { + res.body.status.should.equal(401); + res.body.message.should.equal('Unauthorized'); + should.exist(res.body.description); + done(err); + }); + }); + + it('/entries/preview', function (done) { + var known_key = this.known_key; + request(this.app) + .post('/entries/preview.json') + .set('api-secret', known_key) + .send(load('json')) + .expect(201) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(30); + done(); + }); + }); + + it('allow authorized POST', function (done) { + var app = this.app; + var known_key = this.known_key; + + var new_entry = {type: 'sgv', sgv: 100, date: Date.now() }; + new_entry.dateString = new Date(new_entry.date).toISOString( ); + request(app) + .post('/entries.json?') + .set('api-secret', known_key) + .send([new_entry]) + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + request(app) + .get('/slice/entries/dateString/sgv/' + new_entry.dateString.split('T')[0] + '.json') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + + if (err) { + done(err); + } else { + request(app) + .delete('/entries/sgv?find[dateString]=' + new_entry.dateString) + .set('api-secret', known_key) + .expect(200) + .end(function (err) { + done(err); + }); + } + }); + }); + }); + + it('disallow deletes unauthorized', function (done) { + var app = this.app; + + request(app) + .get('/entries.json?find[dateString][$gte]=2014-07-18') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + // .set('api-secret', 'missing') + .expect(401) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done(); + }); + } + }); + }); + }); + + it('allow deletes when authorized', function (done) { + var app = this.app; + + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .set('api-secret', this.known_key) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(0); + done(); + }); + } + }); + }); + + + +}); diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js new file mode 100644 index 000000000..6a7514a83 --- /dev/null +++ b/tests/api.verifyauth.test.js @@ -0,0 +1,47 @@ +'use strict'; + +var request = require('supertest'); +var language = require('../lib/language')(); + +require('should'); + +describe('Verifyauth REST api', function ( ) { + var self = this; + + var api = require('../lib/api/'); + before(function (done) { + self.env = require('../env')( ); + self.env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')( ); + self.app.enable('api'); + require('../lib/bootevent')(self.env, language).boot(function booted (ctx) { + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + it('/verifyauth should return UNAUTHORIZED', function (done) { + request(self.app) + .get('/api/verifyauth') + .expect(200) + .end(function(err, res) { + res.body.message.should.equal('UNAUTHORIZED'); + done(); + }); + }); + + it('/verifyauth should return OK', function (done) { + request(self.app) + .get('/api/verifyauth') + .set('api-secret', self.env.api_secret || '') + .expect(200) + .end(function(err, res) { + res.body.message.should.equal('OK'); + done(); + }); + }); + + +}); + diff --git a/tests/ar2.test.js b/tests/ar2.test.js new file mode 100644 index 000000000..28ec4f557 --- /dev/null +++ b/tests/ar2.test.js @@ -0,0 +1,162 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +var FIVE_MINS = 300000; +var SIX_MINS = 360000; + +describe('ar2', function ( ) { + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var ar2 = require('../lib/plugins/ar2')(ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); + + var env = require('../env')(); + + var now = Date.now(); + var before = now - FIVE_MINS; + + function prepareSandbox(base) { + var sbx = base || require('../lib/sandbox')().serverInit(env, ctx); + bgnow.setProperties(sbx); + ar2.setProperties(sbx); + return sbx; + } + + it('should plot a cone', function () { + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + var sbx = prepareSandbox(); + var cone = ar2.forecastCone(sbx); + cone.length.should.equal(26); + }); + + it('should plot a line if coneFactor is 0', function () { + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + + var env0 = require('../env')(); + env0.extendedSettings = { ar2: { coneFactor: 0 } }; + var sbx = require('../lib/sandbox')().serverInit(env0, ctx).withExtendedSettings(ar2); + bgnow.setProperties(sbx); + var cone = ar2.forecastCone(sbx); + cone.length.should.equal(13); + }); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('should trigger a warning when going above target', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 150, mills: before}, {mgdl: 170, mills: now}]; + + var sbx = prepareSandbox(); + sbx.offerProperty('iob', function setFakeIOB() { + return {displayLine: 'IOB: 1.25U'}; + }); + sbx.offerProperty('direction', function setFakeDirection() { + return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; + }); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, HIGH predicted'); + highest.message.should.equal('BG Now: 170 +20 ↗ mg/dl\nBG 15m: 206 mg/dl\nIOB: 1.25U'); + + done(); + }); + + it('should trigger a urgent alarm when going high fast', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 140, mills: before}, {mgdl: 200, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Urgent, HIGH'); + + done(); + }); + + it('should trigger a warning when below target', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 90, mills: before}, {mgdl: 80, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW'); + + done(); + }); + + it('should trigger a warning when almost below target', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 90, mills: before}, {mgdl: 83, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW predicted'); + + done(); + }); + + it('should trigger a urgent alarm when falling fast', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 120, mills: before}, {mgdl: 85, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Urgent, LOW predicted'); + + done(); + }); + + it('should trigger a warning alarm by interpolating when more than 5mins apart', function (done) { + ctx.notifications.initRequests(); + + //same as previous test but prev is 10 mins ago, so delta isn't enough to trigger an urgent alarm + ctx.ddata.sgvs = [{mgdl: 120, mills: before - SIX_MINS}, {mgdl: 85, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW predicted'); + + done(); + }); + + it('should handle alexa requests', function (done) { + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + var sbx = prepareSandbox(); + + ar2.alexa.intentHandlers.length.should.equal(1); + + ar2.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('AR2 Forecast'); + response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); + done(); + }, [], sbx); + }); + +}); \ No newline at end of file diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js new file mode 100644 index 000000000..40ac9563b --- /dev/null +++ b/tests/basalprofileplugin.test.js @@ -0,0 +1,111 @@ +var should = require('should'); + +describe('basalprofile', function ( ) { + + var sandbox = require('../lib/sandbox')(); + var env = require('../env')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var basal = require('../lib/plugins/basalprofile')(ctx); + + var profileData = + { + 'timezone': 'UTC', + 'startDate': '2015-06-21', + 'basal': [ + { + 'time': '00:00', + 'value': 0.175 + }, + { + 'time': '02:30', + 'value': 0.125 + }, + { + 'time': '05:00', + 'value': 0.075 + }, + { + 'time': '08:00', + 'value': 0.1 + }, + { + 'time': '14:00', + 'value': 0.125 + }, + { + 'time': '20:00', + 'value': 0.3 + }, + { + 'time': '22:00', + 'value': 0.225 + } + ] + }; + + + var profile = require('../lib/profilefunctions')([profileData]); + + it('update basal profile pill', function (done) { + var data = {}; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0.175U'); + done(); + } + } + , language: require('../lib/language')() + }; + + var time = new Date('2015-06-21T00:00:00').getTime(); + + + var sbx = sandbox.clientInit(ctx, time, data); + sbx.data.profile = profile; + basal.setProperties(sbx); + basal.updateVisualisation(sbx); + + }); + + it('should handle alexa requests', function (done) { + var data = {}; + + var ctx = { + settings: {} + , pluginBase: { } + , language: require('../lib/language')() + }; + + var time = new Date('2015-06-21T00:00:00').getTime(); + + + var sbx = sandbox.clientInit(ctx, time, data); + sbx.data.profile = profile; + + basal.alexa.intentHandlers.length.should.equal(1); + basal.alexa.rollupHandlers.length.should.equal(1); + + basal.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current Basal'); + response.should.equal('Your current basal is 0.175 units per hour'); + + basal.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + should.not.exist(err); + response.results.should.equal('Your current basal is 0.175 units per hour'); + response.priority.should.equal(1); + done(); + }); + + }, [], sbx); + }); + +}); \ No newline at end of file diff --git a/tests/bgnow.test.js b/tests/bgnow.test.js new file mode 100644 index 000000000..7a21da64a --- /dev/null +++ b/tests/bgnow.test.js @@ -0,0 +1,224 @@ +'use strict'; + +require('should'); +var _ =require('lodash'); + +var FIVE_MINS = 300000; +var SIX_MINS = 360000; + +describe('BG Now', function ( ) { + var ctx = { + language: require('../lib/language')() + }; + var bgnow = require('../lib/plugins/bgnow')(ctx); + var sandbox = require('../lib/sandbox')(); + + var now = Date.now(); + var before = now - FIVE_MINS; + + it('should calculate BG Delta', function (done) { + var ctx = { + settings: { units: 'mg/dl' } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal(ctx.settings.units); + options.value.should.equal('+5'); + options.info.length.should.equal(0); + done(); + } + , language: { translate: function(text) { return text; } } + } + }; + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + bgnow.setProperties(sbx); + + var delta = sbx.properties.delta; + delta.mgdl.should.equal(5); + delta.interpolated.should.equal(false); + delta.scaled.should.equal(5); + delta.display.should.equal('+5'); + + bgnow.updateVisualisation(sbx); + }); + + it('should calculate BG Delta by interpolating when more than 5mins apart', function (done) { + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal(ctx.settings.units); + options.value.should.equal('+2 *'); + findInfoValue('Elapsed Time', options.info).should.equal('11 mins'); + findInfoValue('Absolute Delta', options.info).should.equal('5 mg/dl'); + findInfoValue('Interpolated', options.info).should.equal('103 mg/dl'); + done(); + } + } + , language: require('../lib/language')() + }; + + var sbx = sandbox.clientInit(ctx, now, data); + + bgnow.setProperties(sbx); + + var delta = sbx.properties.delta; + delta.mgdl.should.equal(2); + delta.interpolated.should.equal(true); + delta.scaled.should.equal(2); + delta.display.should.equal('+2'); + bgnow.updateVisualisation(sbx); + + }); + + it('should calculate BG Delta in mmol', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + }; + + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(105); + bgnowProp.last.should.equal(105); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(5); + result.interpolated.should.equal(false); + result.scaled.should.equal(0.2); + result.display.should.equal('+0.2'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(105); + buckets[1].mean.should.equal(100); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + }; + + bgnow.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol and not show a change because of rounding', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + }; + + var data = {sgvs: [{mills: before, mgdl: 85}, {mills: now, mgdl: 85}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(85); + bgnowProp.last.should.equal(85); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(0); + result.interpolated.should.equal(false); + result.scaled.should.equal(0); + result.display.should.equal('+0'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(85); + buckets[1].mean.should.equal(85); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + + }; + + bgnow.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol by interpolating when more than 5mins apart', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + }; + + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(105); + bgnowProp.last.should.equal(105); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(2); + result.interpolated.should.equal(true); + result.scaled.should.equal(0.1); + result.display.should.equal('+0.1'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(105); + buckets[1].isEmpty.should.equal(true); + buckets[2].mean.should.equal(100); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + }; + + bgnow.setProperties(sbx); + }); + +}); + +function findInfoValue (label, info) { + var found = _.find(info, function checkLine (line) { + return line.label === label; + }); + return found && found.value; +} diff --git a/tests/boluswizardpreview.test.js b/tests/boluswizardpreview.test.js new file mode 100644 index 000000000..c29098923 --- /dev/null +++ b/tests/boluswizardpreview.test.js @@ -0,0 +1,299 @@ +var should = require('should'); +var Stream = require('stream'); +var levels = require('../lib/levels'); + +describe('boluswizardpreview', function ( ) { + var env = require('../env')(); + env.testMode = true; + + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var boluswizardpreview = require('../lib/plugins/boluswizardpreview')(ctx); + var ar2 = require('../lib/plugins/ar2')(ctx); + var iob = require('../lib/plugins/iob')(ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); + + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + bgnow.setProperties(sbx); + ar2.setProperties(sbx); + iob.setProperties(sbx); + boluswizardpreview.setProperties(sbx); + sbx.offerProperty('direction', function setFakeDirection() { + return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; + }); + + return sbx; + } + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + var profile = { + dia: 3 + , sens: 90 + , target_high: 120 + , target_low: 100 + }; + + it('should calculate IOB results correctly with 0 IOB', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(0); + results.effectDisplay.should.equal(0); + results.outcome.should.equal(100); + results.outcomeDisplay.should.equal(100); + results.bolusEstimate.should.equal(0); + results.displayLine.should.equal('BWP: 0U'); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now, insulin: '1.0'}]; + + var profile = { + dia: 3 + , sens: 50 + , target_high: 100 + , target_low: 50 + }; + + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + Math.round(results.effect).should.equal(50); + results.effectDisplay.should.equal(50); + Math.round(results.outcome).should.equal(50); + results.outcomeDisplay.should.equal(50); + results.bolusEstimate.should.equal(0); + results.displayLine.should.equal('BWP: 0U'); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB resulting in going low', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now, insulin: '1.0'}]; + + var profile = { + dia: 3 + , sens: 50 + , target_high: 200 + , target_low: 100 + , basal: 1 + }; + + + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + Math.round(results.effect).should.equal(50); + results.effectDisplay.should.equal(50); + Math.round(results.outcome).should.equal(50); + results.outcomeDisplay.should.equal(50); + Math.round(results.bolusEstimate).should.equal(-1); + results.displayLine.should.equal('BWP: -1.00U'); + results.tempBasalAdjustment.thirtymin.should.equal(-100); + results.tempBasalAdjustment.onehour.should.equal(0); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB resulting in going low in MMOL', function (done) { + + // boilerplate for client sandbox running in mmol + + var profileData = { + dia: 3 + , units: 'mmol' + , sens: 10 + , target_high: 10 + , target_low: 5.6 + , basal: 1 + }; + + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + }; + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]}; + data.treatments = [{mills: now, insulin: '1.0'}]; + data.devicestatus = []; + data.profile = require('../lib/profilefunctions')([profileData]); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sbx.properties.iob = iob.calcTotal(data.treatments, data.devicestatus, data.profile, now); + + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(10); + results.outcome.should.equal(-4.4); + results.bolusEstimate.should.equal(-1); + results.displayLine.should.equal('BWP: -1.00U'); + results.tempBasalAdjustment.thirtymin.should.equal(-100); + results.tempBasalAdjustment.onehour.should.equal(0); + + done(); + }); + + + it('should calculate IOB results correctly with 0.45 U IOB resulting in going low in MMOL', function (done) { + + // boilerplate for client sandbox running in mmol + + var profileData = { + dia: 3 + , units: 'mmol' + , sens: 9 + , target_high: 6 + , target_low: 5 + , basal: 0.125 + }; + + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + }; + var data = {sgvs: [{mills: before, mgdl: 175}, {mills: now, mgdl: 153}]}; + data.treatments = [{mills: now, insulin: '0.45'}]; + data.devicestatus = []; + data.profile = require('../lib/profilefunctions')([profileData]); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sbx.properties.iob = iob.calcTotal(data.treatments, data.devicestatus, data.profile, now); + + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(4.05); + results.outcome.should.equal(4.45); + Math.round(results.bolusEstimate*100).should.equal(-6); + results.displayLine.should.equal('BWP: -0.07U'); + results.tempBasalAdjustment.thirtymin.should.equal(2); + results.tempBasalAdjustment.onehour.should.equal(51); + + done(); + }); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 95}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('trigger a warning when going out of range', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 175}, {mills: now, mgdl: 180}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, Check BG, time to bolus?'); + highest.message.should.equal('BG Now: 180 +5 ↗ mg/dl\nBG 15m: 187 mg/dl\nBWP: 0.66U'); + done(); + }); + + it('trigger an urgent alarms when going too high', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + it('request a snooze when there is enough IOB', function (done) { + + ctx.notifications.resetStateForTests(); + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.ddata.treatments = [{mills: before, insulin: '5.0'}]; + ctx.ddata.profiles = [profile]; + + var sbx = prepareSandbox(); + + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + notify.clear.should.equal(true); + if (notify.clear) { + done(); + } + }); + + ar2.checkNotifications(sbx); + boluswizardpreview.checkNotifications(sbx); + ctx.notifications.process(); + + }); + + it('set a pill to the BWP with infos', function (done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('BWP'); + options.value.should.equal('0.50U'); + done(); + } + } + }; + + var loadedProfile = require('../lib/profilefunctions')(); + loadedProfile.loadData([profile]); + + var data = { + sgvs: [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}] + , treatments: [{mills: before, insulin: '1.5'}] + , devicestatus: [] + , profile: loadedProfile + }; + + var sbx = require('../lib/sandbox')().clientInit(ctx, Date.now(), data); + + iob.setProperties(sbx); + boluswizardpreview.setProperties(sbx); + boluswizardpreview.updateVisualisation(sbx); + }); + +}); diff --git a/tests/bridge.test.js b/tests/bridge.test.js new file mode 100644 index 000000000..99c1587fa --- /dev/null +++ b/tests/bridge.test.js @@ -0,0 +1,42 @@ +'use strict'; + +var should = require('should'); + +describe('bridge', function ( ) { + var bridge = require('../lib/plugins/bridge'); + + var env = { + extendedSettings: { + bridge: { + userName: 'nightscout' + , password: 'wearenotwaiting' + } + } + }; + + it('be creatable', function () { + var configed = bridge(env); + should.exist(configed); + should.exist(configed.startEngine); + should.exist(configed.startEngine.call); + }); + + it('set options from env', function () { + var opts = bridge.options(env); + should.exist(opts); + + opts.login.accountName.should.equal('nightscout'); + opts.login.password.should.equal('wearenotwaiting'); + }); + + it('store entries from share', function (done) { + var mockEntries = { + create: function mockCreate (err, callback) { + callback(null); + done(); + } + }; + bridge.bridged(mockEntries)(null); + }); + +}); diff --git a/tests/cannulaage.test.js b/tests/cannulaage.test.js new file mode 100644 index 000000000..824952d06 --- /dev/null +++ b/tests/cannulaage.test.js @@ -0,0 +1,94 @@ +'use strict'; + +require('should'); +var levels = require('../lib/levels'); + +describe('cage', function ( ) { + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + + var cage = require('../lib/plugins/cannulaage')(ctx); + var sandbox = require('../lib/sandbox')(); + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + sbx.offerProperty('iob', function () { + return {iob: 0}; + }); + return sbx; + } + + it('set a pill to the current cannula age', function (done) { + + var data = { + sitechangeTreatments: [ + {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Site Change', notes: 'Bar', mills: Date.now() - 24 * 60 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('24h'); + options.info[1].value.should.equal('Bar'); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cage.setProperties(sbx); + cage.updateVisualisation(sbx); + + }); + + it('set a pill to the current cannula age', function (done) { + + var data = { + sitechangeTreatments: [ + {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Site Change', notes: '', mills: Date.now() - 59 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0h'); + options.info.length.should.equal(1); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cage.setProperties(sbx); + cage.updateVisualisation(sbx); + + }); + + + it('trigger a warning when cannula is 48 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - (48 * 60 * 60 * 1000); + + ctx.ddata.sitechangeTreatments = [{eventType: 'Site Change', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + cage.setProperties(sbx); + cage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('CAGE'); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Cannula age 48 hours'); + done(); + }); + +}); diff --git a/tests/careportal.test.js b/tests/careportal.test.js new file mode 100644 index 000000000..83a3ce2ee --- /dev/null +++ b/tests/careportal.test.js @@ -0,0 +1,90 @@ +'use strict'; + +require('should'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] + , treatments: [] +}; + +describe('client', function ( ) { + var self = this; + + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + headless.setup({mockAjax: true}, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + it ('open careportal, and enter a treatment', function (done) { + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + + client.init(); + client.dataUpdate(nowData); + + client.careportal.prepareEvents(); + client.careportal.toggleDrawer(); + + $('#eventType').val('Snack Bolus'); + $('#glucoseValue').val('100'); + $('#carbsGiven').val('10'); + $('#insulinGiven').val('0.60'); + $('#preBolus').val(15); + $('#notes').val('Testing'); + $('#enteredBy').val('Dad'); + + //simulate some events + client.careportal.eventTimeTypeChange(); + client.careportal.dateTimeFocus(); + client.careportal.dateTimeChange(); + + window.confirm = function mockConfirm (message) { + function containsLine (line) { + message.indexOf(line + '\n').should.be.greaterThan(0); + } + + containsLine('Event Type: Snack Bolus'); + containsLine('Blood Glucose: 100'); + containsLine('Carbs Given: 10'); + containsLine('Insulin Given: 0.60'); + containsLine('Carb Time: 15 mins'); + containsLine('Notes: Testing'); + containsLine('Entered By: Dad'); + + return true; + }; + + window.alert = function mockAlert() {}; + + client.careportal.save(); + + done(); + }); + +}); diff --git a/tests/client.test.js.temporary_removed b/tests/client.test.js.temporary_removed new file mode 100644 index 000000000..ed7e89841 --- /dev/null +++ b/tests/client.test.js.temporary_removed @@ -0,0 +1,138 @@ +'use strict'; + +require('should'); +var times = require('../lib/times'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var TEST_TITLE = 'Test Title'; + +var stored = { }; +var removed = { }; + +var now = Date.now(); +var next = Date.now() + times.mins(5).msecs; + +var nowData = { + sgvs: [ + { device: 'dexcom', mgdl: 100, mills: now, direction: 'Flat', type: 'sgv', filtered: 113984, unfiltered: 111920, rssi: 179, noise: 1 } + ], mbgs: [ + { mgdl: 100, mills: now } + ], cals: [ + { + device: 'dexcom' + , slope: 895.8571693029189 + , intercept: 34281.06876195567 + , scale: 1 + , type: 'cal' + } + ], devicestatus: { uploaderBattery: 100 } + , treatments: [ + { + eventType: 'Snack Bolus' + , insulin: '1.00' + , carbs: '18' + , mills: now + } + ] +}; + +var nextData = { + sgvs: [ + { device: 'dexcom', mgdl: 101, mills: next, direction: 'Flat', type: 'sgv', filtered: 113984, unfiltered: 111920, rssi: 179, noise: 1 } + ], mbgs: [ ] + , cals: [] + , devicestatus: { uploaderBattery: 100 } + , treatments: [] +}; + +describe('client', function ( ) { + var self = this; + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = { + get: function mockGet (name) { + return name === 'alarmTimeagoWarnMins' ? 99 : undefined; + } + , set: function mockSet (name, value) { + stored[name] = value; + } + , remove: function mockRemove (name) { + removed[name] = true; + } + }; + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + + it ('not blow up with mg/dl', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + }); + + it ('handle 2 updates', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + client.dataUpdate(nextData); + }); + + it ('not blow up with mmol', function () { + serverSettings.settings.units = 'mmol'; + serverSettings.settings.timeFormat = 24; + + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + }); + + it ('load, store, and clear settings', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + var browserSettings = require('../lib/client/browser-settings')(client, plugins, serverSettings, self.$); + browserSettings.alarmTimeagoWarnMins.should.equal(99); + browserSettings.customTitle.should.equal(TEST_TITLE); + + self.$('#save').click(); + stored.customTitle.should.equal(TEST_TITLE); + self.$('#useDefaults').click(); + removed.customTitle.should.equal(true); + }); + +}); diff --git a/tests/cob.test.js b/tests/cob.test.js new file mode 100644 index 000000000..9f917cb46 --- /dev/null +++ b/tests/cob.test.js @@ -0,0 +1,195 @@ +'use strict'; + +var _ = require('lodash'); + +require('should'); + +describe('COB', function ( ) { + var ctx = {}; + ctx.settings = {}; + ctx.language = require('../lib/language')(); + + var cob = require('../lib/plugins/cob')(ctx); + + var profileData = { + sens: 95 + , carbratio: 18 + , carbs_hr: 30 + }; + + var profile = require('../lib/profilefunctions')([profileData]); + + it('should calculate IOB, multiple treatments', function() { + + var treatments = [ + { + 'carbs': '100', + 'mills': new Date('2015-05-29T02:03:48.827Z').getTime() + }, + { + 'carbs': '10', + 'mills': new Date('2015-05-29T03:45:10.670Z').getTime() + } + ]; + + var devicestatus = []; + + var after100 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T02:03:49.827Z').getTime()); + var before10 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T03:45:10.670Z').getTime()); + var after10 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T03:45:11.670Z').getTime()); + + after100.cob.should.equal(100); + Math.round(before10.cob).should.equal(59); + Math.round(after10.cob).should.equal(69); //WTF == 128 + }); + + it('should calculate IOB, single treatment', function() { + + var treatments = [ + { + 'carbs': '8', + 'mills': new Date('2015-05-29T04:40:40.174Z').getTime() + } + ]; + + var devicestatus = []; + + var rightAfterCorrection = new Date('2015-05-29T04:41:40.174Z').getTime(); + var later1 = new Date('2015-05-29T05:04:40.174Z').getTime(); + var later2 = new Date('2015-05-29T05:20:00.174Z').getTime(); + var later3 = new Date('2015-05-29T05:50:00.174Z').getTime(); + var later4 = new Date('2015-05-29T06:50:00.174Z').getTime(); + + var result1 = cob.cobTotal(treatments, devicestatus, profile, rightAfterCorrection); + var result2 = cob.cobTotal(treatments, devicestatus, profile, later1); + var result3 = cob.cobTotal(treatments, devicestatus, profile, later2); + var result4 = cob.cobTotal(treatments, devicestatus, profile, later3); + var result5 = cob.cobTotal(treatments, devicestatus, profile, later4); + + result1.cob.should.equal(8); + result2.cob.should.equal(6); + result3.cob.should.equal(0); + result4.cob.should.equal(0); + result5.cob.should.equal(0); + }); + + it('set a pill to the current COB', function (done) { + var data = { + treatments: [{ + carbs: '8' + , 'mills': Date.now() - 60000 //1m ago + }] + , profile: profile + }; + + ctx.pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('8g'); + done(); + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cob.setProperties(sbx); + cob.updateVisualisation(sbx); + + }); + + it('should handle alexa requests', function (done) { + var data = { + treatments: [{ + carbs: '8' + , 'mills': Date.now() - 60000 //1m ago + }] + , profile: profile + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cob.setProperties(sbx); + + cob.alexa.intentHandlers.length.should.equal(1); + + cob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current COB'); + response.should.equal('You have 8 carbohydrates on board'); + done(); + }, [], sbx); + + }); + + describe('from devicestatus', function () { + var time = Date.now(); + var treatments = [{ + mills: time - 1, + carbs: '20' + }]; + + var OPENAPS_DEVICESTATUS = { + device: 'openaps://pi1', + openaps: { + enacted: { + COB: 30 + } + } + }; + + var treatmentCOB = cob.fromTreatments(treatments, OPENAPS_DEVICESTATUS, profile, time).cob; + + it('should fall back to treatment data if no devicestatus data', function() { + cob.cobTotal(treatments, [], profile, time).should.containEql({ + source: 'Care Portal', + cob: treatmentCOB + }); + }); + + it('should fall back to treatments if openaps devicestatus is present but empty', function() { + var devicestatus = [{ + device: 'openaps://pi1', + mills: time - 1, + openaps: {} + }]; + cob.cobTotal(treatments, devicestatus, profile, time).cob.should.equal(treatmentCOB); + }); + + it('should fall back to treatments if openaps devicestatus is present but too stale', function() { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - cob.RECENCY_THRESHOLD - 1, openaps: {enacted: {COB: 5, timestamp: time - cob.RECENCY_THRESHOLD - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + source: 'Care Portal', + cob: treatmentCOB + }); + }); + + it('should return COB data from OpenAPS', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {enacted: {COB: 5, timestamp: time - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + cob: 5, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return COB data from Loop', function () { + + var LOOP_DEVICESTATUS = { + device: 'loop://iPhone', + loop: { + cob: { + cob: 5 + } + } + }; + + var devicestatus = [_.merge(LOOP_DEVICESTATUS, { mills: time - 1, loop: {cob: {timestamp: time - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + cob: 5, + source: 'Loop', + device: 'loop://iPhone' + }); + }); + + }); + + +}); diff --git a/tests/data.calcdelta.test.js b/tests/data.calcdelta.test.js new file mode 100644 index 000000000..dedb24304 --- /dev/null +++ b/tests/data.calcdelta.test.js @@ -0,0 +1,68 @@ +'use strict'; + +require('should'); + +var calcDelta = require('../lib/data/calcdelta'); + +describe('Data', function ( ) { + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + it('should return original data if there are no changes', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,ddata); + delta.should.equal(ddata); + }); + + it('adding one sgv record should return delta with one sgv', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.sgvs.length.should.equal(1); + }); + + it('adding one treatment record should return delta with one treatment', function() { + var ddata = require('../lib/data/ddata')(); + ddata.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now},{_id: 'someid_3', mgdl: 100, mills:98}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + }); + + it('changes to treatments, mbgs and cals should be calculated even if sgvs is not changed', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + ddata.mbgs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.cals = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.treatments = [{_id: 'someid_3', mgdl: 100, mills:101},{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + newData.mbgs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.cals = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + delta.mbgs.length.should.equal(1); + delta.cals.length.should.equal(1); + }); + + it('delta should include profile', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.profiles = {foo:true}; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.profiles = {bar:true}; + var delta = calcDelta(ddata,newData); + delta.profiles.bar.should.equal(true); + }); + +}); \ No newline at end of file diff --git a/tests/data.treatmenttocurve.test.js b/tests/data.treatmenttocurve.test.js new file mode 100644 index 000000000..2cdc5b004 --- /dev/null +++ b/tests/data.treatmenttocurve.test.js @@ -0,0 +1,37 @@ +'use strict'; + +require('should'); + +var fitTreatmentsToBGCurve = require('../lib/data/treatmenttocurve'); + +describe('Data', function ( ) { + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + var settings = require('../lib/settings')(); + + it('update treatment display BGs', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 90, mills: before},{mgdl: 100, mills: now}]; + ddata.treatments = [ + {_id: 'someid_1', mills: before, glucose: 100, units: 'mgdl'} //with glucose and units + , {_id: 'someid_2', mills: before, glucose: 5.5, units: 'mmol'} //with glucose and units + , {_id: 'someid_3', mills: now - 120000, insulin: '1.00'} //without glucose, between sgvs + , {_id: 'someid_4', mills: now + 60000, insulin: '1.00'} //without glucose, after sgvs + , {_id: 'someid_5', mills: before - 120000, insulin: '1.00'} //without glucose, before sgvs + ]; + fitTreatmentsToBGCurve(ddata, { + settings: settings + } + , { + language: require('../lib/language')() + } + ); + ddata.treatments[0].mgdl.should.equal(100); + ddata.treatments[1].mmol.should.equal(5.5); + ddata.treatments[2].mgdl.should.equal(95); + ddata.treatments[3].mgdl.should.equal(100); + ddata.treatments[4].mgdl.should.equal(90); + }); + +}); \ No newline at end of file diff --git a/tests/ddata.test.js b/tests/ddata.test.js new file mode 100644 index 000000000..f3757348c --- /dev/null +++ b/tests/ddata.test.js @@ -0,0 +1,73 @@ + +'use strict'; + +var should = require('should'); + + +describe('ddata', function ( ) { + // var sandbox = require('../lib/sandbox')(); + // var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + + it('should be a module', function (done) { + var libddata = require('../lib/data/ddata'); + var ddata = libddata( ); + should.exist(ddata); + should.exist(libddata); + should.exist(libddata.call); + ddata = ctx.ddata.clone( ); + should.exist(ddata); + done( ); + }); + + it('has #clone( )', function (done) { + should.exist(ctx.ddata.treatments); + should.exist(ctx.ddata.sgvs); + should.exist(ctx.ddata.mbgs); + should.exist(ctx.ddata.cals); + should.exist(ctx.ddata.profiles); + should.exist(ctx.ddata.devicestatus); + should.exist(ctx.ddata.lastUpdated); + var ddata = ctx.ddata.clone( ); + should.exist(ddata); + should.exist(ddata.treatments); + should.exist(ddata.sgvs); + should.exist(ddata.mbgs); + should.exist(ddata.cals); + should.exist(ddata.profiles); + should.exist(ddata.devicestatus); + should.exist(ddata.lastUpdated); + done( ); + }); + + it('has #split( )', function (done) { + var date = new Date( ); + var time = date.getTime( ); + var cutoff = 1000 * 60 * 5; + var max = 1000 * 60 * 60 * 24 * 2; + var pieces = ctx.ddata.splitRecent(time, cutoff, max); + should.exist(pieces); + should.exist(pieces.first); + should.exist(pieces.rest); + + done( ); + }); + + // TODO: ensure partition function gets called via: + // Properties + // * ddata.devicestatus + // * ddata.mbgs + // * ddata.sgvs + // * ddata.treatments + // * ddata.profiles + // * ddata.lastUpdated + // Methods + // * ddata.processTreatments + // * ddata.processDurations + // * ddata.clone + // * ddata.split + + +}); + diff --git a/tests/direction.test.js b/tests/direction.test.js new file mode 100644 index 000000000..9a1192837 --- /dev/null +++ b/tests/direction.test.js @@ -0,0 +1,103 @@ +'use strict'; + +require('should'); + +describe('BG direction', function ( ) { + + var now = Date.now(); + + function setupSandbox(data, pluginBase) { + var ctx = { + settings: {} + , pluginBase: pluginBase || {} + }; + + + var sandbox = require('../lib/sandbox')(); + return sandbox.clientInit(ctx, Date.now(), data); + } + + it('set the direction property - Flat', function (done) { + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'Flat'}]}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('direction'); + var result = setter(); + result.value.should.equal('Flat'); + result.label.should.equal('→'); + result.entity.should.equal('→'); + done(); + }; + + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + + }); + + it('set the direction property Double Up', function (done) { + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'DoubleUp'}]}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('direction'); + var result = setter(); + result.value.should.equal('DoubleUp'); + result.label.should.equal('⇈'); + result.entity.should.equal('⇈'); + done(); + }; + + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + + }); + + it('set a pill to the direction', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('→︎'); + done(); + } + }; + + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'Flat'}]}, pluginBase); + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + direction.updateVisualisation(sbx); + }); + + it('get the info for a direction', function () { + var direction = require('../lib/plugins/direction')(); + + direction.info({mills: now, direction: 'NONE'}).label.should.equal('⇼'); + direction.info({mills: now, direction: 'NONE'}).entity.should.equal('⇼'); + + direction.info({mills: now, direction: 'DoubleUp'}).label.should.equal('⇈'); + direction.info({mills: now, direction: 'DoubleUp'}).entity.should.equal('⇈'); + + direction.info({mills: now, direction: 'SingleUp'}).label.should.equal('↑'); + direction.info({mills: now, direction: 'SingleUp'}).entity.should.equal('↑'); + + direction.info({mills: now, direction: 'FortyFiveUp'}).label.should.equal('↗'); + direction.info({mills: now, direction: 'FortyFiveUp'}).entity.should.equal('↗'); + + direction.info({mills: now, direction: 'Flat'}).label.should.equal('→'); + direction.info({mills: now, direction: 'Flat'}).entity.should.equal('→'); + + direction.info({mills: now, direction: 'FortyFiveDown'}).label.should.equal('↘'); + direction.info({mills: now, direction: 'FortyFiveDown'}).entity.should.equal('↘'); + + direction.info({mills: now, direction: 'SingleDown'}).label.should.equal('↓'); + direction.info({mills: now, direction: 'SingleDown'}).entity.should.equal('↓'); + + direction.info({mills: now, direction: 'DoubleDown'}).label.should.equal('⇊'); + direction.info({mills: now, direction: 'DoubleDown'}).entity.should.equal('⇊'); + + direction.info({mills: now, direction: 'NOT COMPUTABLE'}).label.should.equal('-'); + direction.info({mills: now, direction: 'NOT COMPUTABLE'}).entity.should.equal('-'); + + direction.info({mills: now, direction: 'RATE OUT OF RANGE'}).label.should.equal('⇕'); + direction.info({mills: now, direction: 'RATE OUT OF RANGE'}).entity.should.equal('⇕'); + }); + + +}); diff --git a/tests/env.test.js b/tests/env.test.js new file mode 100644 index 000000000..12d9f5793 --- /dev/null +++ b/tests/env.test.js @@ -0,0 +1,55 @@ +'use strict'; + +require('should'); + +describe('env', function ( ) { + it('show the right plugins', function () { + process.env.SHOW_PLUGINS = 'iob'; + process.env.ENABLE = 'iob cob'; + + var env = require('../env')(); + var showPlugins = env.settings.showPlugins; + showPlugins.should.containEql('iob'); + showPlugins.should.containEql('delta'); + showPlugins.should.containEql('direction'); + showPlugins.should.containEql('upbat'); + + delete process.env.SHOW_PLUGINS; + delete process.env.ENABLE; + }); + + it('get extended settings', function () { + process.env.ENABLE = 'scaryplugin'; + process.env.SCARYPLUGIN_DO_THING = 'yes'; + + var env = require('../env')(); + env.settings.isEnabled('scaryplugin').should.equal(true); + + //Note the camelCase + env.extendedSettings.scaryplugin.doThing.should.equal('yes'); + + delete process.env.ENABLE; + delete process.env.SCARYPLUGIN_DO_THING; + }); + + it('add pushover to enable if one of the env vars is set', function () { + process.env.PUSHOVER_API_TOKEN = 'abc12345'; + + var env = require('../env')(); + env.settings.enable.should.containEql('pushover'); + env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + + delete process.env.PUSHOVER_API_TOKEN; + }); + + it('add pushover to enable if one of the weird azure env vars is set', function () { + process.env.CUSTOMCONNSTR_PUSHOVER_API_TOKEN = 'abc12345'; + + var env = require('../env')(); + env.settings.enable.should.containEql('pushover'); + env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + + delete process.env.PUSHOVER_API_TOKEN; + }); + +}); diff --git a/tests/errorcodes.test.js b/tests/errorcodes.test.js new file mode 100644 index 000000000..bdb09de7e --- /dev/null +++ b/tests/errorcodes.test.js @@ -0,0 +1,111 @@ +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('errorcodes', function ( ) { + + var errorcodes = require('../lib/plugins/errorcodes')(); + + var now = Date.now(); + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 100, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); + + done(); + }); + + it('should trigger a urgent alarm when ???', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 10, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + ctx.notifications.findHighestAlarm('CGM Error Code').level.should.equal(levels.URGENT); + + done(); + }); + + it('should trigger a urgent alarm when hourglass', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 9, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + var findHighestAlarm = ctx.notifications.findHighestAlarm('CGM Error Code'); + findHighestAlarm.level.should.equal(levels.URGENT); + findHighestAlarm.pushoverSound.should.equal('alien'); + + done(); + }); + + it('should trigger a low notification when needing calibration', function (done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: 5, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); + var info = _.first(ctx.notifications.findUnSnoozeable()); + info.level.should.equal(levels.INFO); + info.pushoverSound.should.equal('intermission'); + + done(); + }); + + it('should trigger a low notification when code < 9', function (done) { + + for (var i = 1; i < 9; i++) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{mgdl: i, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); + _.first(ctx.notifications.findUnSnoozeable()).level.should.be.lessThan(levels.WARN); + } + done(); + }); + + it('convert a code to display', function () { + errorcodes.toDisplay(5).should.equal('?NC'); + errorcodes.toDisplay(9).should.equal('?AD'); + errorcodes.toDisplay(10).should.equal('???'); + }); + + it('have default code to level mappings', function () { + var mapping = errorcodes.buildMappingFromSettings({}); + mapping[1].should.equal(levels.INFO); + mapping[2].should.equal(levels.INFO); + mapping[3].should.equal(levels.INFO); + mapping[4].should.equal(levels.INFO); + mapping[5].should.equal(levels.INFO); + mapping[6].should.equal(levels.INFO); + mapping[7].should.equal(levels.INFO); + mapping[8].should.equal(levels.INFO); + mapping[9].should.equal(levels.URGENT); + mapping[10].should.equal(levels.URGENT); + _.keys(mapping).length.should.equal(10); + }); + + it('allow config of custom code to level mappings', function () { + var mapping = errorcodes.buildMappingFromSettings({ + info: 'off' + , warn: '9 10' + , urgent: 'off' + }); + mapping[9].should.equal(levels.WARN); + mapping[10].should.equal(levels.WARN); + _.keys(mapping).length.should.equal(2); + }); + +}); \ No newline at end of file diff --git a/tests/fixtures/default-server-settings.js b/tests/fixtures/default-server-settings.js new file mode 100644 index 000000000..113a5fa4a --- /dev/null +++ b/tests/fixtures/default-server-settings.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + name: 'Nightscout' + , version: '0.8.0' + , apiEnabled: true + , careportalEnabled: true + , head: 'ae71dca' + , settings: { + units: 'mg/dl' + , timeFormat: 12 + , nightMode: false + , showRawbg: 'noise' + , customTitle: 'Test Title' + , theme: 'colors' + , alarmUrgentHigh: true + , alarmHigh: true + , alarmLow: true + , alarmUrgentLow: true + , alarmTimeagoWarn: true + , alarmTimeagoWarnMins: 15 + , alarmTimeagoUrgent: true + , alarmTimeagoUrgentMins: 30 + , language: 'en' + , enable: 'iob rawbg careportal delta direction upbat errorcodes' + , showPlugins: 'iob' + , alarmTypes: 'predict' + , thresholds: { + bgHigh: 200 + , bgTargetTop: 170 + , bgTargetBottom: 80 + , bgLow: 55 + } + , extendedSettings: { } + } +}; \ No newline at end of file diff --git a/tests/fixtures/example.json b/tests/fixtures/example.json index 88da09c6f..f2c3e6de0 100644 --- a/tests/fixtures/example.json +++ b/tests/fixtures/example.json @@ -1 +1,242 @@ -[{"sgv":"5","dateString":"07/19/2014 10:49:15 AM","date":1405792155000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:44:15 AM","date":1405791855000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:39:15 AM","date":1405791555000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:34:15 AM","date":1405791255000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:29:15 AM","date":1405790955000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:24:15 AM","date":1405790655000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:19:15 AM","date":1405790355000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:14:15 AM","date":1405790055000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:09:15 AM","date":1405789755000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:04:15 AM","date":1405789455000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 09:59:15 AM","date":1405789155000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 09:54:15 AM","date":1405788855000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"178","dateString":"07/19/2014 03:59:15 AM","date":1405767555000,"device":"dexcom","direction":"Flat"},{"sgv":"179","dateString":"07/19/2014 03:54:15 AM","date":1405767255000,"device":"dexcom","direction":"Flat"},{"sgv":"178","dateString":"07/19/2014 03:49:15 AM","date":1405766955000,"device":"dexcom","direction":"Flat"},{"sgv":"177","dateString":"07/19/2014 03:44:15 AM","date":1405766655000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:39:15 AM","date":1405766355000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:34:15 AM","date":1405766055000,"device":"dexcom","direction":"Flat"},{"sgv":"175","dateString":"07/19/2014 03:29:16 AM","date":1405765756000,"device":"dexcom","direction":"Flat"},{"sgv":"174","dateString":"07/19/2014 03:24:15 AM","date":1405765455000,"device":"dexcom","direction":"Flat"},{"sgv":"174","dateString":"07/19/2014 03:19:15 AM","date":1405765155000,"device":"dexcom","direction":"Flat"},{"sgv":"175","dateString":"07/19/2014 03:14:15 AM","date":1405764855000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:09:15 AM","date":1405764555000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:04:15 AM","date":1405764255000,"device":"dexcom","direction":"Flat"},{"sgv":"173","dateString":"07/19/2014 02:59:15 AM","date":1405763955000,"device":"dexcom","direction":"Flat"},{"sgv":"171","dateString":"07/19/2014 02:54:15 AM","date":1405763655000,"device":"dexcom","direction":"Flat"},{"sgv":"170","dateString":"07/19/2014 02:49:15 AM","date":1405763355000,"device":"dexcom","direction":"Flat"},{"sgv":"171","dateString":"07/19/2014 02:44:15 AM","date":1405763055000,"device":"dexcom","direction":"Flat"},{"sgv":"169","dateString":"07/19/2014 02:39:15 AM","date":1405762755000,"device":"dexcom","direction":"Flat"},{"sgv":"169","dateString":"07/19/2014 02:34:15 AM","date":1405762455000,"device":"dexcom","direction":"Flat"}] \ No newline at end of file +[ + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:49:15.000-07:00", + "date": 1405792155000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:44:15.000-07:00", + "date": 1405791855000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:39:15.000-07:00", + "date": 1405791555000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:34:15.000-07:00", + "date": 1405791255000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:29:15.000-07:00", + "date": 1405790955000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:24:15.000-07:00", + "date": 1405790655000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:19:15.000-07:00", + "date": 1405790355000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:14:15.000-07:00", + "date": 1405790055000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:09:15.000-07:00", + "date": 1405789755000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:04:15.000-07:00", + "date": 1405789455000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T09:59:15.000-07:00", + "date": 1405789155000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T09:54:15.000-07:00", + "date": 1405788855000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "178", + "dateString": "2014-07-19T03:59:15.000-07:00", + "date": 1405767555000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "179", + "dateString": "2014-07-19T03:54:15.000-07:00", + "date": 1405767255000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "178", + "dateString": "2014-07-19T03:49:15.000-07:00", + "date": 1405766955000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "177", + "dateString": "2014-07-19T03:44:15.000-07:00", + "date": 1405766655000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:39:15.000-07:00", + "date": 1405766355000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:34:15.000-07:00", + "date": 1405766055000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "175", + "dateString": "2014-07-19T03:29:16.000-07:00", + "date": 1405765756000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "174", + "dateString": "2014-07-19T03:24:15.000-07:00", + "date": 1405765455000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "174", + "dateString": "2014-07-19T03:19:15.000-07:00", + "date": 1405765155000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "175", + "dateString": "2014-07-19T03:14:15.000-07:00", + "date": 1405764855000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:09:15.000-07:00", + "date": 1405764555000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:04:15.000-07:00", + "date": 1405764255000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "173", + "dateString": "2014-07-19T02:59:15.000-07:00", + "date": 1405763955000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "171", + "dateString": "2014-07-19T02:54:15.000-07:00", + "date": 1405763655000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "170", + "dateString": "2014-07-19T02:49:15.000-07:00", + "date": 1405763355000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "171", + "dateString": "2014-07-19T02:44:15.000-07:00", + "date": 1405763055000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "169", + "dateString": "2014-07-19T02:39:15.000-07:00", + "date": 1405762755000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "169", + "dateString": "2014-07-19T02:34:15.000-07:00", + "date": 1405762455000, + "device": "dexcom", + "direction": "Flat" + } +] diff --git a/tests/fixtures/headless.js b/tests/fixtures/headless.js new file mode 100644 index 000000000..6606fdb68 --- /dev/null +++ b/tests/fixtures/headless.js @@ -0,0 +1,165 @@ + +var read = require('fs').readFileSync; +var _ = require('lodash'); + +function headless (benv, binding) { + var self = binding; + function root ( ) { + return benv; + } + + function init (opts, callback) { + var localStorage = opts.localStorage || './localstorage'; + var htmlFile = opts.htmlFile || __dirname + '/../../static/index.html'; + var serverSettings = opts.serverSettings || require('./default-server-settings'); + var someData = opts.mockAjax || { }; + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require(localStorage); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(htmlFile, 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + if (opts.mockProfileEditor) { + self.$.plot = function mockPlot () { + }; + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + } + if (opts.mockSimpleAjax) { + someData = opts.mockSimpleAjax; + self.$.ajax = function mockAjax (url, opts) { + if (url && url.url) { + url = url.url; + } + + var returnVal = someData[url] || []; + if (opts && typeof opts.success === 'function') { + opts.success(returnVal); + return self.$.Deferred().resolveWith(returnVal); + } else { + return { + done: function mockDone (fn) { + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: 'OK'}); + } + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + }; + } + if (opts.mockAjax) { + self.$.ajax = function mockAjax (url, opts) { + + if (url && url.url) { + url = url.url; + } + + //logfile.write(url+'\n'); + //console.log(url,opts); + if (opts && opts.success && opts.success.call) { + return { + done: function mockDone (fn) { + if (someData[url]) { + console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + return { + done: function mockDone (fn) { + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: 'OK'}); + } + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + }; + } + + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn (event, callback) { + if ('connect' === event && callback) { + callback(); + } + } + , emit: function mockEmit (event, data, callback) { + if ('authorize' === event && callback) { + callback({ + read: true + }); + } + } + }; + } + } + }); + + var extraRequires = opts.benvRequires || [ ]; + extraRequires.forEach(function (req) { + benv.require(req); + }); + callback( ); + }); + + } + + function teardown ( ) { + benv.teardown(); + } + root.setup = init; + root.teardown = teardown; + + return root; +} + +module.exports = headless; + diff --git a/tests/fixtures/localstorage.js b/tests/fixtures/localstorage.js new file mode 100644 index 000000000..0942cc2ff --- /dev/null +++ b/tests/fixtures/localstorage.js @@ -0,0 +1,20 @@ +'use strict'; + +var browserStorage = []; + +var localstorage = { + get: function Get(item) { + return browserStorage[item] || null; + } + , set: function Set(item, value) { + browserStorage[item] = value; + } + , remove: function Remove(item) { + delete browserStorage[item]; + } + , removeAll: function RemoveAll() { + browserStorage = []; + } +}; + +module.exports = localstorage; \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json b/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json new file mode 100644 index 000000000..36ea14225 --- /dev/null +++ b/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json @@ -0,0 +1,66 @@ +[ + { + "slope": 841.6474113376482, + "system_time": "2016-10-23T17:47:16-07:00", + "scale": 1.0, + "date": 1477248820000.0, + "decay": 0.0, + "display_time": "2016-10-23T11:53:40-07:00", + "subrecords": [ + { + "applied": "2016-10-20T12:27:28", + "entered": "2016-10-20T12:20:38", + "sensor": 132192, + "meter": 148 + }, + { + "applied": "2016-10-21T12:02:23", + "entered": "2016-10-21T11:55:18", + "sensor": 98880, + "meter": 106 + }, + { + "applied": "2016-10-22T00:22:21", + "entered": "2016-10-22T00:15:33", + "sensor": 129344, + "meter": 144 + }, + { + "applied": "2016-10-22T08:52:19", + "entered": "2016-10-22T08:48:23", + "sensor": 73504, + "meter": 80 + }, + { + "applied": "2016-10-23T07:32:15", + "entered": "2016-10-23T07:29:48", + "sensor": 227520, + "meter": 259 + }, + { + "applied": "2016-10-23T13:47:13", + "entered": "2016-10-23T13:40:06", + "sensor": 92976, + "meter": 94 + }, + { + "applied": "2016-10-23T17:47:12", + "entered": "2016-10-23T17:42:48", + "sensor": 116704, + "meter": 136 + }, + { + "applied": "2016-10-23T17:47:12", + "entered": "2016-10-23T17:43:16", + "sensor": 116704, + "meter": 136 + } + ], + "dateString": "2016-10-23T11:53:40-07:00", + "numsub": 8, + "raw": "24f0b00e449db00e12d2fee52d4d8a4090532cf8dc0bbd40000000000000f03f03060300000000000000000816afac0e9400000060040200b0b0ac0e00a6faad0e6a000000408201004ffcad0e0025a8ae0e9000000040f90100bda9ae0e005720af0e50000000201f01004321af0e006c5fb00e03010000c0780300ff5fb00e0036b6b00e5e000000306b0100e1b7b00e0018efb00e88000000e0c7010020f0b00e0034efb00e88000000e0c7010020f0b00e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333333333333d33f555555555555e53f3cbe", + "intercept": 7435.863161821748, + "device": "openaps://cgm", + "type": "cal" + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json b/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json new file mode 100644 index 000000000..7734b30dd --- /dev/null +++ b/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json @@ -0,0 +1,60 @@ +[ + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:33:36-07:00", + "noise": 1, + "system_time": "2016-10-23T17:27:13-07:00", + "direction": "Flat", + "sgv": 102, + "dateString": "2016-10-23T11:33:36-07:00", + "date": 1477247616000, + "device": "openaps://indy-e1/cgm", + "type": "sgv", + "glucose": 102 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:28:36-07:00", + "noise": 1, + "system_time": "2016-10-23T17:22:13-07:00", + "direction": "Flat", + "sgv": 98, + "dateString": "2016-10-23T11:28:36-07:00", + "device": "openaps://indy-e1/cgm", + "date": 1477247316000, + "type": "sgv", + "glucose": 98 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:23:37-07:00", + "noise": 1, + "system_time": "2016-10-23T17:17:13-07:00", + "direction": "Flat", + "sgv": 91, + "dateString": "2016-10-23T11:23:37-07:00", + "date": 1477247017000, + "unfiltered": 90928, + "filtered": 86880, + "device": "openaps://indy-e1/cgm", + "rssi": 196, + "type": "sgv", + "glucose": 91 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:18:37-07:00", + "noise": 1, + "system_time": "2016-10-23T17:12:13-07:00", + "direction": "Flat", + "sgv": 87, + "dateString": "2016-10-23T11:18:37-07:00", + "device": "openaps://indy-e1/cgm", + "unfiltered": 87760, + "rssi": 193, + "date": 1477246717000, + "filtered": 85264, + "type": "sgv", + "glucose": 87 + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/config.json b/tests/fixtures/openaps-storage/config.json new file mode 100644 index 000000000..bec353ed5 --- /dev/null +++ b/tests/fixtures/openaps-storage/config.json @@ -0,0 +1,19 @@ +{ + "collections": { + "entries": { + "input": [ + "../../tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json" + , "../../tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json" + ] + } + , "treatments": { + "input": "../../tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json" + } + , "devicestatus": { + "input": "../../tests/fixtures/openaps-storage/loop/monitor/openaps-status.json" + } + , "profile": { + "input": "../../tests/fixtures/openaps-storage/ns-profile.json" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json b/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json new file mode 100644 index 000000000..54f56134a --- /dev/null +++ b/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json @@ -0,0 +1,219 @@ +[ + { + "device": "openaps://indy-e1", + "pump": { + "battery": { + "status": "normal", + "voltage": 1.42 + }, + "status": { + "status": "normal", + "timestamp": "2016-10-23T18:36:13.000Z", + "bolusing": false, + "suspended": false + }, + "reservoir": 124.425, + "clock": "2016-10-23T11:36:28-07:00" + }, + "uploader": { + "battery": 84, + "batteryVoltage": 4017 + }, + "openaps": { + "suggested": { + "bg": 102, + "temp": "absolute", + "snoozeBG": 125, + "timestamp": "2016-10-23T18:38:02.000Z", + "reason": "COB: 16, Dev: 27, BGI: -1.71, ISF: 83, Target: 80; Eventual BG 82 >= 80, 25m@1.050 = 0.437 > req 0.02+20%. Setting temp basal of 0.7U/hr", + "rate": 0.7, + "COB": 16, + "eventualBG": 82, + "duration": 30, + "tick": "+4", + "IOB": 0.569 + }, + "iob": { + "netbasalinsulin": 0.35, + "timestamp": "2016-10-23T18:36:28.000Z", + "activity": 0.0041, + "basaliob": 0.075, + "hightempinsulin": 0.45, + "bolussnooze": 0.341, + "iob": 0.569 + }, + "enacted": { + "bg": 102, + "temp": "absolute", + "snoozeBG": 125, + "recieved": true, + "predBGs": { + "COB": [ + 102, + 105, + 107, + 109, + 111, + 113, + 114, + 115, + 115, + 115, + 114, + 113, + 113, + 111, + 110, + 109, + 107, + 105, + 103, + 102, + 100, + 98, + 96, + 94, + 93, + 91, + 90, + 88, + 87, + 86, + 85, + 84, + 83, + 82, + 82, + 81, + 81, + 81, + 81, + 80 + ], + "aCOB": [ + 102, + 110, + 116, + 122, + 127, + 131, + 134, + 136, + 137, + 137, + 136, + 134, + 132, + 128, + 125, + 121, + 117, + 114, + 110, + 106, + 103, + 100, + 97, + 94, + 92, + 90, + 88, + 86, + 84, + 83, + 82, + 81, + 80, + 80, + 79, + 79, + 78 + ], + "IOB": [ + 102, + 105, + 107, + 108, + 109, + 109, + 109, + 108, + 107, + 104, + 102, + 98, + 95, + 91, + 88, + 84, + 80, + 76, + 73, + 69, + 66, + 63, + 60, + 57, + 55, + 53, + 51, + 49, + 47, + 46, + 45, + 44, + 43, + 42, + 42, + 41 + ] + }, + "rate": 0.675, + "reason": "COB: 16, Dev: 27, BGI: -1.71, ISF: 83, Target: 80; Eventual BG 82 >= 80, 25m@1.050 = 0.437 > req 0.02+20%. Setting temp basal of 0.7U/hr", + "COB": 16, + "eventualBG": 82, + "timestamp": "2016-10-23T18:38:23.000Z", + "duration": 30, + "tick": "+4", + "IOB": 0.569 + } + }, + "mmtune": { + "scanDetails": [ + [ + "916.564", + 5, + -94 + ], + [ + "916.588", + 5, + -92 + ], + [ + "916.612", + 5, + -90 + ], + [ + "916.636", + 5, + -90 + ], + [ + "916.660", + 5, + -90 + ], + [ + "916.684", + 5, + -91 + ] + ], + "setFreq": 916.66, + "timestamp": "2016-10-23T18:35:52.000Z", + "usedDefault": false + } + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json b/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json new file mode 100644 index 000000000..48c7f5435 --- /dev/null +++ b/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json @@ -0,0 +1,60 @@ +[ + { + "raw_rate": { + "_type": "TempBasal", + "temp": "absolute", + "_description": "TempBasal 2016-10-23T11:45:22 head[2], body[1] op[0x33]", + "timestamp": "2016-10-23T11:45:22-07:00", + "_body": "00", + "_head": "3339", + "rate": 1.425, + "_date": "96ad0b5710" + }, + "raw_duration": { + "_type": "TempBasalDuration", + "_description": "TempBasalDuration 2016-10-23T11:45:22 head[2], body[0] op[0x16]", + "timestamp": "2016-10-23T11:45:22-07:00", + "_body": "", + "_head": "1601", + "duration (min)": 30, + "_date": "96ad0b5710" + }, + "created_at": "2016-10-23T11:45:22-07:00", + "enteredBy": "openaps://medtronic/723", + "rate": 1.425, + "eventType": "Temp Basal", + "timestamp": "2016-10-23T11:45:22-07:00", + "duration": "30", + "medtronic": "mm://openaps/mm-format-ns-treatments/Temp Basal", + "absolute": "1.425" + }, + { + "raw_rate": { + "_type": "TempBasal", + "temp": "absolute", + "_description": "TempBasal 2016-10-23T11:38:50 head[2], body[1] op[0x33]", + "timestamp": "2016-10-23T11:38:50-07:00", + "_body": "00", + "_head": "331b", + "rate": 0.675, + "_date": "b2a60b5710" + }, + "raw_duration": { + "_type": "TempBasalDuration", + "_description": "TempBasalDuration 2016-10-23T11:38:50 head[2], body[0] op[0x16]", + "timestamp": "2016-10-23T11:38:50-07:00", + "_body": "", + "_head": "1601", + "duration (min)": 30, + "_date": "b2a60b5710" + }, + "created_at": "2016-10-23T11:38:50-07:00", + "enteredBy": "openaps://medtronic/723", + "rate": 0.675, + "eventType": "Temp Basal", + "timestamp": "2016-10-23T11:38:50-07:00", + "duration": "30", + "medtronic": "mm://openaps/mm-format-ns-treatments/Temp Basal", + "absolute": "0.675" + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/ns-profile.json b/tests/fixtures/openaps-storage/ns-profile.json new file mode 100644 index 000000000..3cfae033f --- /dev/null +++ b/tests/fixtures/openaps-storage/ns-profile.json @@ -0,0 +1,119 @@ +[ + { + "_id": "5428ec28b289cbc5f9b7898b", + "defaultProfile": "Default", + "store": { + "Default": { + "dia": "3", + "carbratio": [ + { + "time": "00:00", + "value": "18", + "timeAsSeconds": "0" + }, + { + "time": "06:00", + "value": "12", + "timeAsSeconds": "21600" + }, + { + "time": "10:30", + "value": "20", + "timeAsSeconds": "37800" + }, + { + "time": "12:00", + "value": "20", + "timeAsSeconds": "43200" + }, + { + "time": "17:00", + "value": "18", + "timeAsSeconds": "61200" + } + ], + "carbs_hr": "30", + "delay": "20", + "sens": [ + { + "time": "00:00", + "value": "90", + "timeAsSeconds": "0" + } + ], + "timezone": "America/Los_Angeles", + "basal": [ + { + "time": "00:00", + "value": "0.6", + "timeAsSeconds": "0" + }, + { + "time": "02:30", + "value": "0.6", + "timeAsSeconds": "9000" + }, + { + "time": "03:30", + "value": "0.525", + "timeAsSeconds": "12600" + }, + { + "time": "04:00", + "value": "0.45", + "timeAsSeconds": "14400" + }, + { + "time": "07:00", + "value": "0.55", + "timeAsSeconds": "25200" + }, + { + "time": "11:00", + "value": "0.6", + "timeAsSeconds": "39600" + }, + { + "time": "14:00", + "value": "0.7", + "timeAsSeconds": "50400" + }, + { + "time": "18:30", + "value": "0.6", + "timeAsSeconds": "66600" + }, + { + "time": "19:30", + "value": "0.75", + "timeAsSeconds": "70200" + }, + { + "time": "22:00", + "value": "0.825", + "timeAsSeconds": "79200" + } + ], + "target_low": [ + { + "time": "00:00", + "value": "115", + "timeAsSeconds": "0" + } + ], + "target_high": [ + { + "time": "00:00", + "value": "115", + "timeAsSeconds": "0" + } + ], + "startDate": "1970-01-01T00:00:00.000Z", + "units": "mg/dl" + } + }, + "startDate": "2015-08-17T04:20:00.000Z", + "created_at": "2015-11-08T16:59:03.920Z", + "mills": "1439785200000" + } +] \ No newline at end of file diff --git a/tests/hashauth.test.js b/tests/hashauth.test.js new file mode 100644 index 000000000..46073983a --- /dev/null +++ b/tests/hashauth.test.js @@ -0,0 +1,169 @@ +'use strict'; + +require('should'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +describe('hashauth', function ( ) { + var self = this; + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + headless.setup({mockAjax: true}, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + /* + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + */ + + it ('should make module unauthorized', function () { + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = false; + next(true); + }; + + client.init(); + + hashauth.inlineCode().indexOf('Not authorized').should.be.greaterThan(0); + hashauth.isAuthenticated().should.equal(false); + var testnull = (hashauth.hash()===null); + testnull.should.equal(true); + }); + + it ('should make module authorized', function () { + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + client.init(); + + hashauth.inlineCode().indexOf('Admin authorized').should.be.greaterThan(0); + hashauth.isAuthenticated().should.equal(true); + }); + + it ('should store hash and the remove authentication', function () { + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + hashauth.updateSocketAuth = function mockUpdateSocketAuth() {}; + + client.init(); + + hashauth.processSecret('this is my long pass phrase',true); + + hashauth.hash().should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + localStorage.get('apisecrethash').should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + hashauth.isAuthenticated().should.equal(true); + + hashauth.removeAuthentication(); + hashauth.isAuthenticated().should.equal(false); + }); + + it ('should not store hash', function () { + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + client.init(); + + hashauth.processSecret('this is my long pass phrase',false); + + hashauth.hash().should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + var testnull = (localStorage.get('apisecrethash')===null); + testnull.should.equal(true); + hashauth.isAuthenticated().should.equal(true); + }); + + it ('should report secret too short', function () { + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client, self.$); + + client.init(); + + window.alert = function mockConfirm (message) { + function containsLine (line) { + message.indexOf(line).should.be.greaterThan(-1); + } + containsLine('Too short API secret'); + return true; + }; + + hashauth.processSecret('short passp',false); + }); +}); diff --git a/tests/insulinage.test.js b/tests/insulinage.test.js new file mode 100644 index 000000000..f01573ec0 --- /dev/null +++ b/tests/insulinage.test.js @@ -0,0 +1,94 @@ +'use strict'; + +require('should'); +var levels = require('../lib/levels'); + +describe('cage', function ( ) { + var env = require('../env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + + var iage = require('../lib/plugins/insulinage')(ctx); + var sandbox = require('../lib/sandbox')(); + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + sbx.offerProperty('iob', function () { + return {iob: 0}; + }); + return sbx; + } + + it('set a pill to the current insulin age', function (done) { + + var data = { + insulinchangeTreatments: [ + {eventType: 'Insulin Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Insulin Change', notes: 'Bar', mills: Date.now() - 24 * 60 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('1d0h'); + options.info[1].value.should.equal('Bar'); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + iage.setProperties(sbx); + iage.updateVisualisation(sbx); + + }); + + it('set a pill to the current insulin age', function (done) { + + var data = { + insulinchangeTreatments: [ + {eventType: 'Insulin Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Insulin Change', notes: '', mills: Date.now() - 59 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0h'); + options.info.length.should.equal(1); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + iage.setProperties(sbx); + iage.updateVisualisation(sbx); + + }); + + + it('trigger a warning when insulin is 48 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - (48 * 60 * 60 * 1000); + + ctx.ddata.insulinchangeTreatments = [{eventType: 'Insulin Change', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + iage.setProperties(sbx); + iage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('IAGE'); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Insulin reservoir age 48 hours'); + done(); + }); + +}); diff --git a/tests/iob.test.js b/tests/iob.test.js new file mode 100644 index 000000000..39968f25f --- /dev/null +++ b/tests/iob.test.js @@ -0,0 +1,260 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); + +describe('IOB', function() { + var ctx = {}; + ctx.language = require('../lib/language')(); + + var iob = require('../lib/plugins/iob')(ctx); + + it('should handle alexa requests', function (done) { + + var sbx = { + properties: { + iob: { + iob: 1.5 + } + } + }; + + iob.alexa.intentHandlers.length.should.equal(1); + iob.alexa.rollupHandlers.length.should.equal(1); + + iob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current IOB'); + response.should.equal('You have 1.50 units of insulin on board'); + + iob.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + should.not.exist(err); + response.results.should.equal('and you have 1.50 units of insulin on board.'); + response.priority.should.equal(2); + done(); + }); + + }, [], sbx); + + }); + + describe('from treatments', function ( ) { + + it('should calculate IOB', function() { + + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } + ]; + + + var profileData = { + dia: 3, + sens: 0}; + + var profile = require('../lib/profilefunctions')([profileData]); + + var rightAfterBolus = iob.calcTotal(treatments, [], profile, time); + + rightAfterBolus.display.should.equal('1.00'); + + var afterSomeTime = iob.calcTotal(treatments, [], profile, time + (60 * 60 * 1000)); + + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); + + var afterDIA = iob.calcTotal(treatments, [], profile, time + (3 * 60 * 60 * 1000)); + + afterDIA.iob.should.equal(0); + + }); + + it('should calculate IOB using defaults', function() { + + var treatments = [{ + mills: Date.now() - 1, + insulin: '1.00' + }]; + + var rightAfterBolus = iob.calcTotal(treatments, []); + + rightAfterBolus.display.should.equal('1.00'); + + }); + + it('should not show a negative IOB when approaching 0', function() { + + var time = Date.now() - 1; + + var treatments = [{ + mills: time, + insulin: '5.00' + }]; + + var whenApproaching0 = iob.calcTotal(treatments, [], undefined, time + (3 * 60 * 60 * 1000) - (90 * 1000)); + + //before fix we got this: AssertionError: expected '-0.00' to be '0.00' + whenApproaching0.display.should.equal('0.00'); + + }); + + it('should calculate IOB using a 4 hour duration', function() { + + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } ]; + + var profileData = { + dia: 4, + sens: 0}; + + var profile = require('../lib/profilefunctions')([profileData]); + + + var rightAfterBolus = iob.calcTotal(treatments, [], profile, time); + + rightAfterBolus.display.should.equal('1.00'); + + var afterSomeTime = iob.calcTotal(treatments, [], profile, time + (60 * 60 * 1000)); + + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); + + var after3hDIA = iob.calcTotal(treatments, [], profile, time + (3 * 60 * 60 * 1000)); + + after3hDIA.iob.should.greaterThan(0); + + var after4hDIA = iob.calcTotal(treatments, [], profile, time + (4 * 60 * 60 * 1000)); + + after4hDIA.iob.should.equal(0); + + }); + + + }); + + describe('from devicestatus', function () { + var time = Date.now(); + var profile = require('../lib/profilefunctions')([{ dia: 3, sens: 0 }]); + var treatments = [{ + mills: time - 1, + insulin: '3.00' + }]; + var treatmentIOB = iob.fromTreatments(treatments, profile, time).iob; + + var OPENAPS_DEVICESTATUS = { + device: 'openaps://pi1', + openaps: { + iob: { + iob: 0.047, + basaliob: -0.298, + activity: 0.0147 + } + } + }; + + it('should fall back to treatment data if no devicestatus data', function() { + iob.calcTotal(treatments, [], profile, time).should.containEql({ + source: 'Care Portal', + iob: treatmentIOB + }); + }); + + it('should fall back to treatments if openaps devicestatus is present but empty', function() { + var devicestatus = [{ + device: 'openaps://pi1', + mills: time - 1, + openaps: {} + }]; + iob.calcTotal(treatments, devicestatus, profile, time).iob.should.equal(treatmentIOB); + }); + + it('should fall back to treatments if openaps devicestatus is present but too stale', function() { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - iob.RECENCY_THRESHOLD - 1, openaps: {iob: {timestamp: time - iob.RECENCY_THRESHOLD - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + source: 'Care Portal', + iob: treatmentIOB + }); + }); + + it('should return IOB data from openaps', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: {timestamp: time - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return IOB data from openaps post AMA (an array)', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: [{ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + time: time - 1 + }]}})]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return IOB data from Loop', function () { + + var LOOP_DEVICESTATUS = { + device: 'loop://iPhone', + loop: { + iob: { + iob: 0.75 + } + } + }; + + var devicestatus = [_.merge(LOOP_DEVICESTATUS, { mills: time - 1, loop: {iob: {timestamp: time - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.75, + source: 'Loop', + device: 'loop://iPhone' + }); + }); + + it('should return IOB data from openaps from multiple devices', function () { + var devicestatus = [ + _.merge(OPENAPS_DEVICESTATUS, { mills: time - 1000, openaps: {iob: {timestamp: time - 1000} } }) + , _.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: {timestamp: time - 1} } }) + , _.merge(OPENAPS_DEVICESTATUS, { mills: time - 20000, openaps: {iob: {timestamp: time - 20000} } }) + ]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return IOB data from MiniMed Connect', function () { + var devicestatus = [{ + device: 'connect://paradigm', + mills: time - 1, + pump: { iob: { bolusiob: 0.87 } }, + connect: { sensorState: 'copacetic' } + }]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.87, + source: 'MM Connect', + device: 'connect://paradigm' + }); + }); + + }); + +}); diff --git a/tests/language.test.js b/tests/language.test.js new file mode 100644 index 000000000..fb024b859 --- /dev/null +++ b/tests/language.test.js @@ -0,0 +1,30 @@ +'use strict'; + +require('should'); + +describe('language', function ( ) { + + it('use English by default', function () { + var language = require('../lib/language')(); + language.translate('Carbs').should.equal('Carbs'); + }); + + it('translate to French', function () { + var language = require('../lib/language')(); + language.set('fr'); + language.translate('Carbs').should.equal('Glucides'); + }); + + it('translate to Czech', function () { + var language = require('../lib/language')(); + language.set('cs'); + language.translate('Carbs').should.equal('Sacharidy'); + }); + + it('translate to Czech uppercase', function () { + var language = require('../lib/language')(); + language.set('cs'); + language.translate('carbs', { ci: true }).should.equal('Sacharidy'); + }); + +}); diff --git a/tests/levels.test.js b/tests/levels.test.js new file mode 100644 index 000000000..537b8be34 --- /dev/null +++ b/tests/levels.test.js @@ -0,0 +1,39 @@ +'use strict'; + +require('should'); + +describe('levels', function ( ) { + var levels = require('../lib/levels'); + + it('have levels', function () { + levels.URGENT.should.equal(2); + levels.WARN.should.equal(1); + levels.INFO.should.equal(0); + levels.LOW.should.equal(-1); + levels.LOWEST.should.equal(-2); + levels.NONE.should.equal(-3); + }); + + it('convert to display', function () { + levels.toDisplay(levels.URGENT).should.equal('Urgent'); + levels.toDisplay(levels.WARN).should.equal('Warning'); + levels.toDisplay(levels.INFO).should.equal('Info'); + levels.toDisplay(levels.LOW).should.equal('Low'); + levels.toDisplay(levels.LOWEST).should.equal('Lowest'); + levels.toDisplay(levels.NONE).should.equal('None'); + levels.toDisplay(42).should.equal('Unknown'); + levels.toDisplay(99).should.equal('Unknown'); + }); + + it('convert to lowercase', function () { + levels.toLowerCase(levels.URGENT).should.equal('urgent'); + levels.toLowerCase(levels.WARN).should.equal('warning'); + levels.toLowerCase(levels.INFO).should.equal('info'); + levels.toLowerCase(levels.LOW).should.equal('low'); + levels.toLowerCase(levels.LOWEST).should.equal('lowest'); + levels.toLowerCase(levels.NONE).should.equal('none'); + levels.toLowerCase(42).should.equal('unknown'); + levels.toLowerCase(99).should.equal('unknown'); + }); + +}); diff --git a/tests/loop.test.js b/tests/loop.test.js new file mode 100644 index 000000000..77795b3ae --- /dev/null +++ b/tests/loop.test.js @@ -0,0 +1,272 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var moment = require('moment'); + +var ctx = { + language: require('../lib/language')() +}; +var env = require('../env')(); +var loop = require('../lib/plugins/loop')(ctx); +var sandbox = require('../lib/sandbox')(); +var levels = require('../lib/levels'); + +var statuses = [ + { + 'created_at':'2016-08-13T20:09:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'enacted':{ + 'timestamp':'2016-08-13T20:09:15Z', + 'rate':0.875, + 'duration':30, + 'received':true + }, + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:09:15Z', + 'predicted':{ + 'startDate':'2016-08-13T20:03:47Z', + 'values':[ + 149, + 149, + 148, + 148, + 147, + 147 + ] + }, + 'iob':{ + 'timestamp':'2016-08-13T20:05:00Z', + 'iob':0.1733152537837709 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T20:04:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:04:15Z', + 'failureReason':'SomeError', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:18:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:15:00Z', + 'iob':-0.1205140849137931 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:13:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:10:00Z', + 'iob':-0.1205140849137931 + }, + 'failureReason':'StaleDataError(\"Glucose Date: 2016-08-12 23:23:49 +0000 or Pump status date: 2016-08-13 01:13:10 +0000 older than 15.0 min\")', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:15Z', + 'pump':{ + 'reservoir':90.5, + 'clock':'2016-08-13T01:13:10Z', + 'battery':{ + 'status':'normal', + 'voltage':1.5 + }, + 'pumpID':'543204' + }, + 'device':'loop://ExamplePhone', + 'uploader':{ + 'timestamp':'2016-08-13T01:13:15Z', + 'battery':43, + 'name':'ExamplePhone' + } + } +]; + +var now = moment(statuses[0].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +describe('loop', function ( ) { + + it('should set the property and update the pill and add forecast points', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop ⌁'); + options.value.should.equal('1m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Temp Basal Started 0.88U/hour for 30m, IOB: 0.17U'); + } + , addForecastPoints: function mockAddForecastPoints (points) { + points.length.should.equal(6); + done(); + } + } + , language: require('../lib/language')() + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('⌁'); + result.display.code.should.equal('enacted'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + loop.updateVisualisation(sbx); + }); + + it('should show errors', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop x'); + options.value.should.equal('1m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Error: SomeError'); + done(); + } + , language: require('../lib/language')() + } + }; + + var errorTime = moment(statuses[1].created_at); + + var sbx = sandbox.clientInit(ctx, errorTime.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + + loop.updateVisualisation(sbx); + + }); + + + it('should check the recieved flag to see if it was received', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + , language: require('../lib/language')() + }; + + ctx.notifications.initRequests(); + + var notStatuses = _.cloneDeep(statuses); + notStatuses[0].loop.enacted.received = false; + var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + done(); + }; + + loop.setProperties(sbx); + }); + + it('should generate an alert for a stuck loop', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + , language: require('../lib/language')() + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(2, 'hours').valueOf(), {devicestatus: statuses}); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + loop.setProperties(sbx); + loop.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Loop'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Loop isn\'t looping'); + done(); + }); + + it('should handle alexa requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + , language: require('../lib/language')() + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + loop.setProperties(sbx); + + loop.alexa.intentHandlers.length.should.equal(2); + + loop.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Loop Forecast'); + response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); + + loop.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last loop'); + response.should.equal('The last successful loop was a few seconds ago'); + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/maker.test.js b/tests/maker.test.js new file mode 100644 index 000000000..a1256421d --- /dev/null +++ b/tests/maker.test.js @@ -0,0 +1,87 @@ +var should = require('should'); +var levels = require('../lib/levels'); + +describe('maker', function ( ) { + var maker = require('../lib/plugins/maker')({extendedSettings: {maker: {key: '12345'}}}); + + //prevent any calls to iftt + function noOpMakeRequest (key, event, eventName, callback) { + if (callback) { callback(); } + } + + maker.makeKeyRequest = noOpMakeRequest; + + it('turn values to a query', function (done) { + maker.valuesToQuery({ + value1: 'This is a title' + , value2: 'This is the message' + }).should.equal('?value1=This%20is%20a%20title&value2=This%20is%20the%20message'); + done(); + }); + + it('send a request', function (done) { + maker.sendEvent({name: 'test', message: 'This is the message', level: levels.toLowerCase(levels.WARN)}, function sendCallback (err) { + should.not.exist(err); + done(); + }); + }); + + it('not send a request without a name', function (done) { + maker.sendEvent({level: levels.toLowerCase(levels.WARN)}, function sendCallback (err) { + should.exist(err); + done(); + }); + }); + + it('not send a request without a level', function (done) { + maker.sendEvent({name: 'test'}, function sendCallback (err) { + should.exist(err); + done(); + }); + }); + + it('send a allclear, but only once', function (done) { + function mockedToTestSingleDone (key, event, eventName, callback) { + callback(); done(); + } + + maker.makeKeyRequest = mockedToTestSingleDone; + maker.sendAllClear({}, function sendCallback (err, result) { + should.not.exist(err); + result.sent.should.equal(true); + }); + + //send again, if done is called again test will fail + maker.sendAllClear({}, function sendCallback (err, result) { + should.not.exist(err); + result.sent.should.equal(false); + }); + }); +}); + + +describe('multi announcement maker', function ( ) { + var maker = require('../lib/plugins/maker')({extendedSettings: {maker: {key: 'use announcementKey instead', announcementKey: '12345 6789'}}}); + + it('send 2 requests for the 2 keys', function (done) { + + var key1Found = false; + var key2Found = false; + + maker.makeKeyRequest = function expect2Keys (key, event, eventName, callback) { + if (callback) { callback(); } + + key1Found = key1Found || key === '12345'; + key2Found = key2Found || key === '6789'; + + if (eventName === 'ns-warning-test' && key1Found && key2Found) { + done(); + } + }; + + maker.sendEvent({name: 'test', level: levels.toLowerCase(levels.WARN), isAnnouncement: true}, function sendCallback (err) { + should.not.exist(err); + }); + }); + +}); diff --git a/tests/mmconnect.test.js b/tests/mmconnect.test.js new file mode 100644 index 000000000..674ae8cd8 --- /dev/null +++ b/tests/mmconnect.test.js @@ -0,0 +1,83 @@ +/* jshint node: true */ +/* globals describe, it */ +'use strict'; + +var _ = require('lodash'), + should = require('should'); + +describe('mmconnect', function () { + var mmconnect = require('../lib/plugins/mmconnect'); + + var env = { + extendedSettings: { + mmconnect: { + // 'userName' for consistency with the bridge plugin + userName: 'nightscout' + , password: 'wearenotwaiting' + , sgvLimit: '99' + , interval: '12000' + , maxRetryDuration: 1024 + , verbose: 'true' + } + } + }; + + describe('init()', function () { + it('should create a runner if env vars are present', function () { + var runner = mmconnect.init(env); + should.exist(runner); + should.exist(runner.run); + runner.run.should.be.instanceof(Function); + }); + + it('should not create a runner if any env vars are absent', function () { + [ + {} + , {mmconnect: {}} + , {mmconnect: {userName: 'nightscout'}} + , {mmconnect: {password: 'wearenotwaiting'}} + ].forEach(function (extendedSettings) { + should.not.exist(mmconnect.init({extendedSettings: extendedSettings})); + }); + }); + }); + + + describe('getOptions()', function () { + it('should set the carelink client config from env', function () { + mmconnect.getOptions(env).should.have.properties({ + username: 'nightscout' + , password: 'wearenotwaiting' + , sgvLimit: 99 + , interval: 12000 + , maxRetryDuration: 1024 + , verbose: true + }); + }); + + }); + + describe('rawDataEntry()', function () { + it('should generate a "carelink_raw" entry with sgs truncated and PII redacted', function () { + var data = { + 'lastMedicalDeviceDataUpdateServerTime': 1445471797479 + , 'sgs': _.range(10) + , 'firstName': 'sensitive' + , 'lastName': 'sensitive' + , 'medicalDeviceSerialNumber': 'sensitive' + }; + var entry = mmconnect.rawDataEntry(data); + entry.should.have.properties({ + 'date': 1445471797479 + , 'type': 'carelink_raw' + }); + entry.data.should.have.properties({ + 'firstName': '' + , 'lastName': '' + , 'medicalDeviceSerialNumber': '' + }); + entry.data.sgs.length.should.equal(6); + }); + }); + +}); diff --git a/tests/mongo-storage.test.js b/tests/mongo-storage.test.js new file mode 100644 index 000000000..d8e8cc88e --- /dev/null +++ b/tests/mongo-storage.test.js @@ -0,0 +1,55 @@ +'use strict'; + +var should = require('should'); +var assert = require('assert'); + +describe('mongo storage', function () { + var env = require('../env')(); + + before(function (done) { + delete env.api_secret; + done(); + }); + + it('The module should be OK.', function (done) { + should.exist(require('../lib/storage/mongo-storage')); + done(); + }); + + it('After initializing the storage class it should re-use the open connection', function (done) { + var store = require('../lib/storage/mongo-storage'); + store(env, function (err1, db1) { + should.not.exist(err1); + + store(env, function (err2, db2) { + should.not.exist(err2); + assert(db1.db, db2.db, 'Check if the handlers are the same.'); + + done(); + }); + }); + }); + + it('When no connection-string is given the storage-class should throw an error.', function (done) { + delete env.storageURI; + should.not.exist(env.storageURI); + + (function () { + return require('../lib/storage/mongo-storage')(env, false, true); + }).should.throw('MongoDB connection string is missing'); + + done(); + }); + + it('An invalid connection-string should throw an error.', function (done) { + env.storageURI = 'This is not a MongoDB connection-string'; + + (function () { + return require('../lib/storage/mongo-storage')(env, false, true); + }).should.throw(Error); + + done(); + }); + +}); + diff --git a/tests/mqtt.test.js b/tests/mqtt.test.js new file mode 100644 index 000000000..25d2a6061 --- /dev/null +++ b/tests/mqtt.test.js @@ -0,0 +1,182 @@ +'use strict'; + +var should = require('should'); + +var FIVE_MINS = 5 * 60 * 1000; + +describe('mqtt', function ( ) { + + var self = this; + + before(function () { + process.env.MQTT_MONITOR = 'mqtt://user:password@localhost:12345'; + process.env.STORAGE_URI='mongodb://localhost/test_db'; + process.env.ENTRIES_COLLECTION='test_sgvs'; + self.env = require('../env')(); + self.es = require('event-stream'); + self.results = self.es.through(function (ch) { this.push(ch); }); + function outputs (fn) { + return self.es.writeArray(function (err, results) { + fn(err, results); + self.results.write(err || results); + }); + } + function written (data, fn) { + self.results.write(data); + setTimeout(fn, 5); + } + self.mqtt = require('../lib/mqtt')(self.env, {entries: { persist: outputs, create: written }, devicestatus: { create: written } }); + }); + + after(function () { + delete process.env.MQTT_MONITOR; + }); + + var now = Date.now() + , prev1 = now - FIVE_MINS + , prev2 = prev1 - FIVE_MINS + ; + + it('setup env correctly', function (done) { + self.env.mqtt_client_id.should.equal('fSjoHx8buyCtAc474tg8Dt3'); + done(); + }); + + it('handle a download with only sgvs', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + done(); + + }); + + it('merge sgvs and sensor records that match up', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + merged.filter(function (sgv) { + return sgv.filtered && sgv.unfiltered && sgv.rssi; + }).length.should.equal(packet.sgv.length); + + done(); + + }); + + it('downloadProtobuf should dispatch', function (done) { + + var payload = new Buffer('0a1108b70110d6d1fa6318f08df963200428011a1d323031352d30382d32335432323a35333a35352e3634392d30373a303020d7d1fa6328004a1508e0920b10c0850b18b20120d5d1fa6328ef8df963620a534d34313837393135306a053638393250', 'hex'); + + // var payload = self.mqtt.downloads.format(packet); + console.log('yaploda', '/downloads/protobuf', payload); + var l = [ ]; + self.results.on('data', function (chunk) { + l.push(chunk); + console.log('test data', l.length, chunk.length, chunk); + switch (l.length) { + case 0: // devicestatus + break; + case 2: // sgv + break; + case 3: // sgv + chunk.length.should.equal(1); + var first = chunk[0]; + should.exist(first.sgv); + should.exist(first.noise); + should.exist(first.date); + should.exist(first.dateString); + first.type.should.equal('sgv'); + break; + case 4: // cal + break; + case 1: // meter + break; + default: + break; + } + if (l.length >= 5) { + self.results.end( ); + } + }); + self.results.on('end', function ( ) { + done( ); + }); + self.mqtt.client.emit('message', '/downloads/protobuf', payload); + }); + + it('merge sgvs and sensor records that match up, and get the sgvs that don\'t match', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + var withBoth = merged.filter(function (sgv) { + return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; + }); + + withBoth.length.should.equal(1); + + done(); + + }); + + it('merge sgvs and sensor records that match up, and get the sensors that don\'t match', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sensor.length); + + var withBoth = merged.filter(function (sgv) { + return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; + }); + + withBoth.length.should.equal(1); + + done(); + + }); + + +}); diff --git a/tests/notifications-api.test.js b/tests/notifications-api.test.js new file mode 100644 index 000000000..4597aa69b --- /dev/null +++ b/tests/notifications-api.test.js @@ -0,0 +1,85 @@ +'use strict'; + +var request = require('supertest'); +var should = require('should'); +var Stream = require('stream'); + +var levels = require('../lib/levels'); +var notificationsAPI = require('../lib/api/notifications-api'); + +function examplePlugin () {} + +describe('Notifications API', function ( ) { + + it('ack notifications', function (done) { + + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + env.testMode = true; + + var ctx = { + bus: new Stream + , ddata: { + lastUpdated: Date.now() + } + , store: { + collection: function ( ) { + return { }; + } + } + }; + + ctx.authorization = require('../lib/authorization')(env, ctx); + + var notifications = require('../lib/notifications')(env, ctx); + ctx.notifications = notifications; + + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + if (notify.clear) { + done(); + } + }); + + var exampleWarn = { + title: 'test' + , message: 'testing' + , level: levels.WARN + , plugin: examplePlugin + }; + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + + var app = require('express')(); + app.enable('api'); + var wares = require('../lib/middleware/')(env); + app.use('/', notificationsAPI(app, wares, ctx)); + + function makeRequest () { + request(app) + .get('/notifications/ack?level=1') + .set('api-secret', env.api_secret || '') + .expect(200) + .end(function (err) { + should.not.exist(err); + if (err) { + console.error(err); + } + }); + } + + makeRequest(); + + //2nd call should have no effect, done should NOT be called again + makeRequest(); + }); +}); \ No newline at end of file diff --git a/tests/notifications.test.js b/tests/notifications.test.js new file mode 100644 index 000000000..332f057bf --- /dev/null +++ b/tests/notifications.test.js @@ -0,0 +1,206 @@ +var should = require('should'); +var Stream = require('stream'); + +var levels = require('../lib/levels'); + +describe('notifications', function ( ) { + + var env = {testMode: true}; + + var ctx = { + bus: new Stream + , ddata: { + lastUpdated: Date.now() + } + }; + + var notifications = require('../lib/notifications')(env, ctx); + + function examplePlugin () {} + + var exampleInfo = { + title: 'test' + , message: 'testing' + , level: levels.INFO + , plugin: examplePlugin + }; + + var exampleWarn = { + title: 'test' + , message: 'testing' + , level: levels.WARN + , plugin: examplePlugin + }; + + var exampleUrgent = { + title: 'test' + , message: 'testing' + , level: levels.URGENT + , plugin: examplePlugin + }; + + var exampleSnooze = { + level: levels.WARN + , title: 'exampleSnooze' + , message: 'exampleSnooze message' + , lengthMills: 10000 + }; + + var exampleSnoozeNone = { + level: levels.WARN + , title: 'exampleSnoozeNone' + , message: 'exampleSnoozeNone message' + , lengthMills: 1 + }; + + var exampleSnoozeUrgent = { + level: levels.URGENT + , title: 'exampleSnoozeUrgent' + , message: 'exampleSnoozeUrgent message' + , lengthMills: 10000 + }; + + + function expectNotification (check, done) { + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + if (check(notify)) { + done(); + } + }); + } + + function clearToDone (done) { + expectNotification(function expectClear (notify) { + return notify.clear; + }, done); + } + + function notifyToDone (done) { + expectNotification(function expectNotClear (notify) { + return ! notify.clear; + }, done); + } + + it('initAndReInit', function (done) { + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.initRequests(); + should.not.exist(notifications.findHighestAlarm()); + done(); + }); + + + it('emitAWarning', function (done) { + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback ( ) { + done(); + }); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + }); + + it('emitAnInfo', function (done) { + notifyToDone(done); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleInfo); + should.not.exist(notifications.findHighestAlarm()); + + notifications.process(); + }); + + it('emitAllClear 1 time after alarm is auto acked', function (done) { + clearToDone(done); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + + notifications.initRequests(); + //don't request a notify this time, and an auto ack should be sent + should.not.exist(notifications.findHighestAlarm()); + notifications.process(); + + var alarm = notifications.getAlarmForTests(levels.WARN); + alarm.level.should.equal(levels.WARN); + alarm.silenceTime.should.equal(1); + alarm.lastAckTime.should.be.approximately(Date.now(), 2000); + should.not.exist(alarm.lastEmitTime); + + //clear last emit time, even with that all clear shouldn't be sent again since there was no alarm cleared + delete alarm.lastEmitTime; + + //process 1 more time to make sure all clear is only sent once + notifications.initRequests(); + //don't request a notify this time, and an auto ack should be sent + should.not.exist(notifications.findHighestAlarm()); + notifications.process(); + }); + + it('Can be snoozed', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnooze); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnooze); + notifications.process(); + + done(); + }); + + it('Can be snoozed by last snooze', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnoozeNone); + notifications.requestSnooze(exampleSnooze); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnooze); + notifications.process(); + + done(); + }); + + it('Urgent alarms can\'t be snoozed by warn', function (done) { + clearToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleUrgent); + notifications.requestSnooze(exampleSnooze); + should.not.exist(notifications.snoozedBy(exampleUrgent)); + notifications.process(); + + done(); + }); + + it('Warnings can be snoozed by urgent', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnoozeUrgent); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnoozeUrgent); + notifications.process(); + + done(); + }); + +}); diff --git a/tests/openaps-storage.test.js b/tests/openaps-storage.test.js new file mode 100644 index 000000000..d40a034b8 --- /dev/null +++ b/tests/openaps-storage.test.js @@ -0,0 +1,117 @@ +'use strict'; + +var should = require('should'); + +describe('openaps storage', function () { + + var env = require('../env')(); + + + before(function (done) { + delete env.api_secret; + env.storageURI = 'openaps://../../tests/fixtures/openaps-storage/config'; + done(); + }); + + it('The module class should be OK.', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + should.exist(storage.ensureIndexes); + done(); + }); + }); + + it('find sgv entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('entries').find({type: 'sgv'}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(4); + results[0].sgv.should.equal(102); + + done(); + }); + }); + }); + + it('find cal entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('entries').find({type: 'cal'}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(1); + results[0].slope.should.equal(841.6474113376482); + + done(); + }); + }); + }); + + it('find devicestatus entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('devicestatus').find({}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + console.info('>>>devicestatus results', results); + + results.length.should.equal(1); + results[0].openaps.enacted.eventualBG.should.equal(82); + + done(); + }); + }); + }); + + it('find treatments', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('treatments').find({}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(2); + results[0].eventType.should.equal('Temp Basal'); + + done(); + }); + }); + }); + + it('When no connection-string is given the storage-class should throw an error.', function (done) { + delete env.storageURI; + should.not.exist(env.storageURI); + + (function () { + return require('../lib/storage/openaps-storage')(env); + }).should.throw('openaps config uri is missing or invalid'); + + done(); + }); + + it('An invalid connection-string should throw an error.', function (done) { + env.storageURI = 'This is not an openaps config path'; + + (function () { + return require('../lib/storage/openaps-storage')(env); + }).should.throw(Error); + + done(); + }); + +}); + diff --git a/tests/openaps.test.js b/tests/openaps.test.js new file mode 100644 index 000000000..7a106041d --- /dev/null +++ b/tests/openaps.test.js @@ -0,0 +1,281 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var moment = require('moment'); + +var ctx = { + language: require('../lib/language')() +}; +var env = require('../env')(); +var openaps = require('../lib/plugins/openaps')(ctx); +var sandbox = require('../lib/sandbox')(); +var levels = require('../lib/levels'); + +var statuses = [{ + created_at: '2015-12-05T19:05:00.000Z', + device: 'openaps://abusypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + timestamp: '2015-12-05T18:59:37.000Z', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T10:58:47-08:00' + }, + mmtune: { + scanDetails: [ + ['916.640',4,-64] + , ['916.660',5,-55] + , ['916.680',5,-59] + ] + , setFreq: 916.66 + , timestamp:' 2015-12-05T18:59:37.000Z' + , usedDefault: false + }, + openaps: { + suggested: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + timestamp: '2015-12-05T19:02:42.000Z', + rate: 0.75, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + eventualBG: 125, + duration: 30, + tick: '+1' + }, + iob: { + timestamp: '2015-12-05T19:02:42.000Z', + bolusiob: 0, + iob: 0.6068340736133333, + activity: 0.016131569664902996 + }, + enacted: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + recieved: true, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + rate: 0.75, + eventualBG: 125, + timestamp: '2015-12-05T19:03:00.000Z', + duration: 30, + tick: '+1', + predBGs: { + IOB: [100, 100, 100, 100] + , aCOB: [100, 100, 100, 100] + , COB: [100, 100, 100, 100] + } + } + } +} +, { + created_at: '2015-12-05T18:05:00.000Z', + device: 'openaps://awaitingpi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + timestamp: '2015-12-05T16:59:37.000Z', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T08:58:47-08:00' + }, + openaps: { + suggested: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + timestamp: '2015-12-05T16:02:42.000Z', + rate: 0.75, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + eventualBG: 125, + duration: 30, + tick: '+1' + }, + iob: { + timestamp: '2015-12-05T16:02:42.000Z', + bolusiob: 0, + iob: 0.6068340736133333, + activity: 0.016131569664902996 + }, + enacted: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + recieved: true, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + rate: 0.75, + eventualBG: 125, + timestamp: '2015-12-05T16:03:00.000Z', + duration: 30, + tick: '+1' + } + } +}]; + +var now = moment(statuses[0].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +describe('openaps', function ( ) { + + it('set the property and update the pill and add forecast points', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('OpenAPS ⌁'); + options.value.should.equal('2m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('abusypi ⌁ Enacted @ -55dB'); + var last = _.last(options.info); + last.label.should.equal('1h ago'); + last.value.should.equal('awaitingpi ◉ Waiting'); + } + , addForecastPoints: function mockAddForecastPoints (points) { + points.length.should.equal(12); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('openaps'); + var result = setter(); + should.exist(result); + + result.status.symbol.should.equal('⌁'); + result.status.code.should.equal('enacted'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + + }; + + openaps.setProperties(sbx); + + openaps.updateVisualisation(sbx); + + }); + + it('check the recieved flag to see if it was received', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var notStatuses = _.cloneDeep(statuses); + notStatuses[0].openaps.enacted.recieved = false; + var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('openaps'); + var result = setter(); + should.exist(result); + result.status.symbol.should.equal('x'); + result.status.code.should.equal('notenacted'); + done(); + }; + + openaps.setProperties(sbx); + + }); + + it('generate an alert for a stuck loop', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(1, 'hours').valueOf(), {devicestatus: statuses}); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + openaps.setProperties(sbx); + openaps.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('OpenAPS'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('OpenAPS isn\'t looping'); + done(); + }); + + it('not generate an alert for a stuck loop, when there is an offline marker', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(1, 'hours').valueOf(), { + devicestatus: statuses + , treatments: [{eventType: 'OpenAPS Offline', mills: now.valueOf(), duration: 60}] + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + openaps.setProperties(sbx); + openaps.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('OpenAPS'); + should.not.exist(highest); + done(); + }); + + it('should handle alexa requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + , language: require('../lib/language')() + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + openaps.setProperties(sbx); + + openaps.alexa.intentHandlers.length.should.equal(2); + + openaps.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Loop Forecast'); + response.should.equal('The OpenAPS Eventual BG is 125'); + + openaps.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last loop'); + response.should.equal('The last successful loop was 2 minutes ago'); + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/pebble.test.js b/tests/pebble.test.js index 6b3b07430..d26e5e64e 100644 --- a/tests/pebble.test.js +++ b/tests/pebble.test.js @@ -1,101 +1,156 @@ +'use strict'; var request = require('supertest'); var should = require('should'); +var language = require('../lib/language')(); -//Mock entries -var entries = { - list: function(q, callback) { - var results = [ - { device: 'dexcom', - date: 1422727301000, - dateString: 'Sat Jan 31 10:01:41 PST 2015', - sgv: 82, - direction: 'Flat', - type: 'sgv', - filtered: 113984, - unfiltered: 111920, - rssi: 179, - noise: 1 - }, - { device: 'dexcom', - date: 1422647711000, - dateString: 'Fri Jan 30 11:55:11 PST 2015', - slope: 895.8571693029189, - intercept: 34281.06876195567, - scale: 1, - type: 'cal' - }, - { device: 'dexcom', - date: 1422727001000, - dateString: 'Sat Jan 31 09:56:41 PST 2015', - sgv: 84, - direction: 'Flat', - type: 'sgv', - filtered: 115680, - unfiltered: 113552, - rssi: 179, - noise: 1 - }, - { device: 'dexcom', - date: 1422726701000, - dateString: 'Sat Jan 31 09:51:41 PST 2015', - sgv: 86, - direction: 'Flat', - type: 'sgv', - filtered: 117808, - unfiltered: 114640, - rssi: 169, - noise: 1 - }, - { device: 'dexcom', - date: 1422726401000, - dateString: 'Sat Jan 31 09:46:41 PST 2015', - sgv: 88, - direction: 'Flat', - type: 'sgv', - filtered: 120464, - unfiltered: 116608, - rssi: 175, - noise: 1 - }, - { device: 'dexcom', - date: 1422726101000, - dateString: 'Sat Jan 31 09:41:41 PST 2015', - sgv: 91, - direction: 'Flat', - type: 'sgv', - filtered: 124048, - unfiltered: 118880, - rssi: 174, - noise: 1 - } - ]; - callback(null, results); +//Mocked ctx +var ctx = {}; +// var env = {}; Unused variable +var now = Date.now(); + +function updateMills (entries) { + //last is now, assume 5m between points + for (var i = 0; i < entries.length; i++) { + var entry = entries[entries.length - i - 1]; + entry.mills = now - (i * 5 * 60 * 1000); + } + return entries; +} + +ctx.ddata = require('../lib/data/ddata')(); +ctx.ddata.sgvs = updateMills([ + { device: 'dexcom', + mgdl: 91, + direction: 'Flat', + type: 'sgv', + filtered: 124048, + unfiltered: 118880, + rssi: 174, + noise: 1 + } + , { device: 'dexcom', + mgdl: 88, + direction: 'Flat', + type: 'sgv', + filtered: 120464, + unfiltered: 116608, + rssi: 175, + noise: 1 + } + , { device: 'dexcom', + mgdl: 86, + direction: 'Flat', + type: 'sgv', + filtered: 117808, + unfiltered: 114640, + rssi: 169, + noise: 1 + } + , { device: 'dexcom', + mgdl: 92, + direction: 'Flat', + type: 'sgv', + filtered: 115680, + unfiltered: 113552, + rssi: 179, + noise: 1 + } + , { device: 'dexcom', + mgdl: 90, + direction: 'Flat', + type: 'sgv', + filtered: 113984, + unfiltered: 111920, + rssi: 179, + noise: 1 } -}; +]); -//Mock devicestatus -var devicestatus = { - last: function(callback) { - callback(null, {uploaderBattery: 100}); +ctx.ddata.cals = updateMills([ + { device: 'dexcom', + slope: 895.8571693029189, + intercept: 34281.06876195567, + scale: 1, + type: 'cal' } -}; +]); -describe('Pebble Endpoint without Raw', function ( ) { +ctx.ddata.profiles = [{dia: 4, sens: 70, carbratio: 15, carbs_hr: 30}]; + +ctx.ddata.treatments = updateMills([ + { eventType: 'Snack Bolus', insulin: '1.50', carbs: '22' } +]); + +ctx.ddata.devicestatus = [{uploader: {battery: 100}}]; + +var bootevent = require('../lib/bootevent'); +describe('Pebble Endpoint', function ( ) { var pebble = require('../lib/pebble'); before(function (done) { var env = require('../env')( ); + env.settings.authDefaultRoles = 'readable'; this.app = require('express')( ); this.app.enable('api'); - this.app.use('/pebble', pebble(entries, devicestatus, env)); - done(); + var self = this; + bootevent(env, language).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.app.use('/pebble', pebble(env, context)); + done(); + }); }); - it('should be a module', function ( ) { - pebble.should.be.ok; + it('/pebble default(1) count', function (done) { + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.sgv.should.equal('90'); + bg.bgdelta.should.equal(-2); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + should.not.exist(bg.filtered); + should.not.exist(bg.unfiltered); + should.not.exist(bg.noise); + should.not.exist(bg.rssi); + should.not.exist(bg.iob); + should.not.exist(bg.cob); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(0); + done( ); + }); }); - it('/pebble', function (done) { + it('/pebble with mmol param', function (done) { + request(this.app) + .get('/pebble?units=mmol') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.sgv.should.equal('5.0'); + bg.bgdelta.should.equal('-0.1'); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + should.not.exist(bg.filtered); + should.not.exist(bg.unfiltered); + should.not.exist(bg.noise); + should.not.exist(bg.rssi); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble?count=2', function (done) { request(this.app) .get('/pebble?count=2') .expect(200) @@ -103,11 +158,11 @@ describe('Pebble Endpoint without Raw', function ( ) { var bgs = res.body.bgs; bgs.length.should.equal(2); var bg = bgs[0]; - bg.sgv.should.equal('82'); + bg.sgv.should.equal('90'); bg.bgdelta.should.equal(-2); bg.trend.should.equal(4); bg.direction.should.equal('Flat'); - bg.datetime.should.equal(1422727301000); + bg.datetime.should.equal(now); should.not.exist(bg.filtered); should.not.exist(bg.unfiltered); should.not.exist(bg.noise); @@ -118,25 +173,71 @@ describe('Pebble Endpoint without Raw', function ( ) { done( ); }); }); -}); + it('/pebble without battery', function (done) { + ctx.ddata.devicestatus = []; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble with a negative battery', function (done) { + ctx.ddata.devicestatus = [{uploader: {battery: -1}}]; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble with a false battery', function (done) { + ctx.ddata.devicestatus = [{uploader: {battery: false}}]; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); +}); -describe('Pebble Endpoint with Raw', function ( ) { +describe('Pebble Endpoint with Raw and IOB and COB', function ( ) { var pebbleRaw = require('../lib/pebble'); before(function (done) { - var envRaw = require('../env')( ); - envRaw.enable = "rawbg"; + var env = require('../env')( ); + env.settings.enable = ['rawbg', 'iob', 'cob']; + env.settings.authDefaultRoles = 'readable'; this.appRaw = require('express')( ); this.appRaw.enable('api'); - this.appRaw.use('/pebble', pebbleRaw(entries, devicestatus, envRaw)); - done(); - }); - - it('should be a module', function ( ) { - pebbleRaw.should.be.ok; + var self = this; + bootevent(env, language).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.appRaw.use('/pebble', pebbleRaw(env, context)); + done(); + }); }); it('/pebble', function (done) { + ctx.ddata.devicestatus = [{uploader: {battery: 100}}]; request(this.appRaw) .get('/pebble?count=2') .expect(200) @@ -144,24 +245,56 @@ describe('Pebble Endpoint with Raw', function ( ) { var bgs = res.body.bgs; bgs.length.should.equal(2); var bg = bgs[0]; - bg.sgv.should.equal('82'); + bg.sgv.should.equal('90'); bg.bgdelta.should.equal(-2); bg.trend.should.equal(4); bg.direction.should.equal('Flat'); - bg.datetime.should.equal(1422727301000); + bg.datetime.should.equal(now); bg.filtered.should.equal(113984); bg.unfiltered.should.equal(111920); bg.noise.should.equal(1); - bg.rssi.should.equal(179); bg.battery.should.equal('100'); + bg.iob.should.equal('1.50'); + bg.cob.should.equal(22); res.body.cals.length.should.equal(1); var cal = res.body.cals[0]; - cal.slope.should.equal(895.8571693029189); - cal.intercept.should.equal(34281.06876195567); + cal.slope.toFixed(3).should.equal('895.857'); + cal.intercept.toFixed(3).should.equal('34281.069'); cal.scale.should.equal(1); done( ); }); }); -}); \ No newline at end of file + it('/pebble with no treatments', function (done) { + ctx.ddata.treatments = []; + request(this.appRaw) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.iob.should.equal(0); + bg.cob.should.equal(0); + done(); + }); + }); + + it('/pebble with IOB from devicestatus', function (done) { + ctx.ddata.treatments = []; + ctx.ddata.devicestatus = updateMills([{pump: {iob: {bolusiob: 2.3}}}]); + request(this.appRaw) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.iob.should.equal('2.30'); + bg.cob.should.equal(0); + done(); + }); + }); + +}); diff --git a/tests/pluginbase.test.js b/tests/pluginbase.test.js new file mode 100644 index 000000000..c04f0c802 --- /dev/null +++ b/tests/pluginbase.test.js @@ -0,0 +1,58 @@ +'use strict'; + +require('should'); +var benv = require('benv'); + +describe('pluginbase', function ( ) { + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + headless.setup({ }, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + + it('does stuff', function() { + + function div (clazz) { + return $('
'); + } + + var container = div('container') + , bgStatus = div('bgStatus').appendTo(container) + , majorPills = div('majorPills').appendTo(bgStatus) + , minorPills = div('minorPills').appendTo(bgStatus) + , statusPills = div('statusPills').appendTo(bgStatus) + , tooltip = div('tooltip').appendTo(container) + ; + + var fake = { + name: 'fake' + , label: 'Insulin-on-Board' + , pluginType: 'pill-major' + }; + + var pluginbase = require('../lib/plugins/pluginbase')(majorPills, minorPills, statusPills, bgStatus, tooltip); + + pluginbase.updatePillText(fake, { + value: '123' + , label: 'TEST' + , info: [{label: 'Label', value: 'Value'}] + }); + + majorPills.length.should.equal(1); + }); + +}); diff --git a/tests/plugins.test.js b/tests/plugins.test.js new file mode 100644 index 000000000..38f6c1898 --- /dev/null +++ b/tests/plugins.test.js @@ -0,0 +1,39 @@ +'use strict'; + +var should = require('should'); + +describe('Plugins', function ( ) { + + + it('should find client plugins, but not server only plugins', function (done) { + var plugins = require('../lib/plugins/')({ + settings: { } + , language: require('../lib/language')() + }).registerClientDefaults(); + + plugins('bgnow').name.should.equal('bgnow'); + plugins('rawbg').name.should.equal('rawbg'); + + //server only plugin + should.not.exist(plugins('treatmentnotify')); + + done( ); + }); + + it('should find sever plugins, but not client only plugins', function (done) { + var plugins = require('../lib/plugins/')({ + settings: { } + , language: require('../lib/language')() + }).registerServerDefaults(); + + plugins('rawbg').name.should.equal('rawbg'); + plugins('treatmentnotify').name.should.equal('treatmentnotify'); + + //client only plugin + should.not.exist(plugins('cannulaage')); + + done( ); + }); + + +}); diff --git a/tests/profile.test.js b/tests/profile.test.js new file mode 100644 index 000000000..8171f459e --- /dev/null +++ b/tests/profile.test.js @@ -0,0 +1,190 @@ +var should = require('should'); +var moment = require('moment-timezone'); + +describe('Profile', function ( ) { + + var profile_empty = require('../lib/profilefunctions')(); + + it('should say it does not have data before it has data', function() { + var hasData = profile_empty.hasData(); + hasData.should.equal(false); + }); + + it('should return undefined if asking for keys before init', function() { + var dia = profile_empty.getDIA(now); + should.not.exist(dia); + }); + + it('should return undefined if asking for missing keys', function() { + var sens = profile_empty.getSensitivity(now); + should.not.exist(sens); + }); + + var profileData = { + 'dia': 3 + , 'carbs_hr': 30 + , 'carbratio': 7 + , 'sens': 35 + , 'target_low': 95 + , 'target_high': 120 + }; + + var profile = require('../lib/profilefunctions')([profileData]); +// console.log(profile); + + var now = Date.now(); + + it('should know what the DIA is with old style profiles', function() { + var dia = profile.getDIA(now); + dia.should.equal(3); + }); + + it('should know what the DIA is with old style profiles, with missing date argument', function() { + var dia = profile.getDIA(); + dia.should.equal(3); + }); + + it('should know what the carbs_hr is with old style profiles', function() { + var carbs_hr = profile.getCarbAbsorptionRate(now); + carbs_hr.should.equal(30); + }); + + it('should know what the carbratio is with old style profiles', function() { + var carbRatio = profile.getCarbRatio(now); + carbRatio.should.equal(7); + }); + + it('should know what the sensitivity is with old style profiles', function() { + var dia = profile.getSensitivity(now); + dia.should.equal(35); + }); + + it('should know what the low target is with old style profiles', function() { + var dia = profile.getLowBGTarget(now); + dia.should.equal(95); + }); + + it('should know what the high target is with old style profiles', function() { + var dia = profile.getHighBGTarget(now); + dia.should.equal(120); + }); + + it('should know how to reload data and still know what the low target is with old style profiles', function() { + + var profile2 = require('../lib/profilefunctions')([profileData]); + var profileData2 = { + 'dia': 3, + 'carbs_hr': 30, + 'carbratio': 7, + 'sens': 35, + 'target_low': 50, + 'target_high': 120 + }; + + profile2.loadData([profileData2]); + var dia = profile2.getLowBGTarget(now); + dia.should.equal(50); + }); + + var complexProfileData = + { + 'timezone': moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + 'sens': [ + { + 'time': '00:00', + 'value': 10 + }, + { + 'time': '02:00', + 'value': 10 + }, + { + 'time': '07:00', + 'value': 9 + } + ], + 'dia': 3, + 'carbratio': [ + { + 'time': '00:00', + 'value': 16 + }, + { + 'time': '06:00', + 'value': 15 + }, + { + 'time': '14:00', + 'value': 16 + } + ], + 'carbs_hr': 30, + 'startDate': '2015-06-21', + 'basal': [ + { + 'time': '00:00', + 'value': 0.175 + }, + { + 'time': '02:30', + 'value': 0.125 + }, + { + 'time': '05:00', + 'value': 0.075 + }, + { + 'time': '08:00', + 'value': 0.1 + }, + { + 'time': '14:00', + 'value': 0.125 + }, + { + 'time': '20:00', + 'value': 0.3 + }, + { + 'time': '22:00', + 'value': 0.225 + } + ], + 'target_low': 4.5, + 'target_high': 8, + 'units': 'mmol' +}; + + var complexProfile = require('../lib/profilefunctions')([complexProfileData]); + + var noon = new Date('2015-06-22 12:00:00').getTime(); + var threepm = new Date('2015-06-22 15:00:00').getTime(); + + it('should return profile units when configured', function() { + var value = complexProfile.getUnits(); + value.should.equal('mmol'); + }); + + + it('should know what the basal rate is at 12:00 with complex style profiles', function() { + var value = complexProfile.getBasal(noon); + value.should.equal(0.1); + }); + + it('should know what the basal rate is at 15:00 with complex style profiles', function() { + var value = complexProfile.getBasal(threepm); + value.should.equal(0.125); + }); + + it('should know what the carbratio is at 12:00 with complex style profiles', function() { + var carbRatio = complexProfile.getCarbRatio(noon); + carbRatio.should.equal(15); + }); + + it('should know what the sensitivity is at 12:00 with complex style profiles', function() { + var dia = complexProfile.getSensitivity(noon); + dia.should.equal(9); + }); + + +}); \ No newline at end of file diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js new file mode 100644 index 000000000..ba68dde6c --- /dev/null +++ b/tests/profileeditor.test.js @@ -0,0 +1,181 @@ +'use strict'; + +require('should'); +var _ = require('lodash'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = require('../lib/data/ddata')(); +nowData.sgvs.push({ mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' }); + +var exampleProfile = { + defaultProfile : 'Default' + , store: { + 'Default' : { + //General values + 'dia':3, + + // Simple style values, 'from' are in minutes from midnight + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }], + 'carbs_hr':30, + 'delay': 20, + 'sens': [ + { + 'time': '00:00', + 'value': 100 + } + , { + 'time': '8:00', + 'value': 80 + }], + 'startDate': new Date(), + 'timezone': 'UTC', + + //perGIvalues style values + 'perGIvalues': false, + 'carbs_hr_high': 30, + 'carbs_hr_medium': 30, + 'carbs_hr_low': 30, + 'delay_high': 15, + 'delay_medium': 20, + 'delay_low': 20, + + 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }], + 'target_low':[ + { + 'time': '00:00', + 'value': 100 + }], + 'target_high':[ + { + 'time': '00:00', + 'value': 120 + }] + } + } +}; + + +var someData = { + '/api/v1/profile.json': [exampleProfile] + }; + + +describe('Profile editor', function ( ) { + var self = this; + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../static/profile/index.html' + , mockProfileEditor: true + , mockAjax: someData + , benvRequires: [ + __dirname + '/../bundle/bundle.source.js' + , __dirname + '/../static/profile/js/profileeditor.js' + ] + }; + headless.setup(opts, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + it ('should produce some html', function (done) { + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm (text) { + console.log('Confirm:', text); + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + client.init(); + client.dataUpdate(nowData); + + //var result = $('body').html(); + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.html', { flags: 'a'} ) + //logfile.write($('body').html()); + + // database records manipulation + $('#pe_databaserecords option').length.should.be.equal(1); + $('#pe_records_add').click(); + $('#pe_databaserecords option').length.should.be.equal(2); + $('#pe_records_remove').click(); + $('#pe_databaserecords option').length.should.be.equal(1); + $('#pe_records_clone').click(); + $('#pe_databaserecords option').length.should.be.equal(2); + $('#pe_databaserecords option').val(0); + + //console.log($('#pe_databaserecords').html()); + //console.log($('#pe_databaserecords').val()); + + // database records manipulation + $('#pe_profiles option').length.should.be.equal(1); + $('#pe_profile_add').click(); + $('#pe_profiles option').length.should.be.equal(2); + $('#pe_profile_name').val('Test'); + $('#pe_profiles option').val('Default'); + $('#pe_profiles option').val('Test'); + $('#pe_profile_remove').click(); + $('#pe_profiles option').length.should.be.equal(1); + $('#pe_profile_clone').click(); + $('#pe_profiles option').length.should.be.equal(2); + $('#pe_profiles option').val('Default'); + + //console.log($('#pe_profiles').html()); + //console.log($('#pe_profiles').val()); + + + // I:C range + $('#pe_ic_val_0').val().should.be.equal('30'); + $('#pe_ic_placeholder').find('img.addsingle').click(); + $('#pe_ic_val_0').val().should.be.equal('0'); + $('#pe_ic_val_1').val().should.be.equal('30'); + $('#pe_ic_placeholder').find('img.delsingle').click(); + $('#pe_ic_val_0').val().should.be.equal('30'); + + // traget bg range + $('#pe_targetbg_low_0').val().should.be.equal('100'); + $('#pe_targetbg_placeholder').find('img.addtargetbg').click(); + $('#pe_targetbg_low_0').val().should.be.equal('0'); + $('#pe_targetbg_low_1').val().should.be.equal('100'); + $('#pe_targetbg_placeholder').find('img.deltargetbg').click(); + $('#pe_targetbg_low_0').val().should.be.equal('100'); + + + $('#pe_submit').click(); + done(); + }); + +}); diff --git a/tests/pump.test.js b/tests/pump.test.js new file mode 100644 index 000000000..deb2b359a --- /dev/null +++ b/tests/pump.test.js @@ -0,0 +1,276 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var moment = require('moment'); + +var ctx = { + language: require('../lib/language')() +}; +var env = require('../env')(); +var pump = require('../lib/plugins/pump')(ctx); +var sandbox = require('../lib/sandbox')(); +var levels = require('../lib/levels'); + +var statuses = [{ + created_at: '2015-12-05T17:35:00.000Z' + , device: 'openaps://farawaypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T17:32:00.000Z' + } +}, { + created_at: '2015-12-05T19:05:00.000Z' + , device: 'openaps://abusypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T19:02:00.000Z' + } +}]; + +var now = moment(statuses[1].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +describe('pump', function ( ) { + + it('set the property and update the pill', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('Pump'); + options.value.should.equal('86.4U'); + done(); + } + } + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('pump'); + var result = setter(); + should.exist(result); + result.data.level.should.equal(levels.NONE); + result.data.battery.value.should.equal(1.52); + result.data.reservoir.value.should.equal(86.4); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + + }; + + pump.setProperties(sbx); + + pump.updateVisualisation(sbx); + + }); + + it('not generate an alert when pump is ok', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: statuses + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + should.not.exist(highest); + + done(); + }); + + it('generate an alert when reservoir is low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var lowResStatuses = _.cloneDeep(statuses); + lowResStatuses[1].pump.reservoir = 0.5; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowResStatuses + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Reservoir Low'); + + done(); + }); + + it('generate an alert when reservoir is 0', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var lowResStatuses = _.cloneDeep(statuses); + lowResStatuses[1].pump.reservoir = 0; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowResStatuses + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Reservoir Low'); + + done(); + }); + + + it('generate an alert when battery is low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var lowBattStatuses = _.cloneDeep(statuses); + lowBattStatuses[1].pump.battery.voltage = 1.33; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowBattStatuses + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, Pump Battery Low'); + + done(); + }); + + it('generate an urgent alarm when battery is really low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var lowBattStatuses = _.cloneDeep(statuses); + lowBattStatuses[1].pump.battery.voltage = 1.00; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowBattStatuses + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Battery Low'); + + done(); + }); + + it('not generate an alert for a stale pump data, when there is an offline marker', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.add(1, 'hours').valueOf(), { + devicestatus: statuses + , treatments: [{eventType: 'OpenAPS Offline', mills: now.valueOf(), duration: 60}] + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + should.not.exist(highest); + done(); + }); + + it('should handle alexa requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx) + , language: require('../lib/language')() + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + pump.setProperties(sbx); + + pump.alexa.intentHandlers.length.should.equal(2); + + pump.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Remaining insulin'); + response.should.equal('You have 86.4 units remaining'); + + pump.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Pump battery'); + response.should.equal('Your battery is at 1.52 volts'); + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/pushnotify.test.js b/tests/pushnotify.test.js new file mode 100644 index 000000000..dc0bc9151 --- /dev/null +++ b/tests/pushnotify.test.js @@ -0,0 +1,112 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +describe('pushnotify', function ( ) { + + it('send a pushover alarm, but only 1 time', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({receipt: 'abcd12345'})); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + ctx.pushnotify.emitNotification(notify); + + //call again, but should be deduped, or fail with 'done() called multiple times' + ctx.pushnotify.emitNotification(notify); + + }); + + it('send a pushover notification, but only 1 time', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Sent from a test' + , message: 'details details details details' + , level: levels.INFO + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({})); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + ctx.pushnotify.emitNotification(notify); + + //call again, but should be deduped, or fail with 'done() called multiple times' + ctx.pushnotify.emitNotification(notify); + + }); + + it('send a pushover alarm, and then cancel', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({receipt: 'abcd12345'})); + } + , cancelWithReceipt: function mockedCancel (receipt) { + receipt.should.equal('abcd12345'); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + //first send the warning + ctx.pushnotify.emitNotification(notify); + + //then pretend is was acked from the web + ctx.pushnotify.emitNotification({clear: true}); + + }); + + + +}); diff --git a/tests/pushover.test.js b/tests/pushover.test.js new file mode 100644 index 000000000..598086e51 --- /dev/null +++ b/tests/pushover.test.js @@ -0,0 +1,202 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +describe('pushover', function ( ) { + + var baseurl = 'https://nightscout.test'; + + var env = { + settings: { + baseURL: baseurl + } + , extendedSettings: { + pushover: { + userKey: '12345' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('convert a warning to a message and send it', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + should.not.exist(msg.message); + msg.priority.should.equal(2); + msg.retry.should.equal(15 * 60); + msg.sound.should.equal(notify.pushoverSound); + msg.callback.indexOf(baseurl).should.equal(0); + done(); + }; + + pushover.send(notify); + }); + + it('convert an urgent to a message and send it', function (done) { + + var notify = { + title: 'Urgent, this is a test!' + , message: 'details details details details' + , level: levels.URGENT + , pushoverSound: 'persistent' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.message.should.equal(notify.message); + msg.priority.should.equal(2); + msg.retry.should.equal(2 * 60); + msg.sound.should.equal(notify.pushoverSound); + done(); + }; + + pushover.send(notify); + }); + +}); + +describe('support legacy pushover groupkey', function ( ) { + var env = { + extendedSettings: { + pushover: { + groupKey: 'abcd' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + done(); + }; + + pushover.send(notify); + }); + +}); + +describe('multi announcement pushover', function ( ) { + var env = { + extendedSettings: { + pushover: { + userKey: 'use announcementKey instead' + , announcementKey: 'abcd efgh' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send multiple pushes if there are multiple keys', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + var key1Found = false; + var key2Found = false; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + + key1Found = key1Found || msg.user === 'abcd'; + key2Found = key2Found || msg.user === 'efgh'; + + if (key1Found && key2Found) { + done(); + } + }; + + pushover.send(notify); + }); + +}); + +describe('announcement only pushover', function ( ) { + var env = { + extendedSettings: { + pushover: { + announcementKey: 'abcd' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send push if announcement', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + + done(); + }; + + pushover.send(notify); + }); + + it('not send push if not announcement and no user key', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function failIfSend ( ) { + done(); + }; + + pushover.send(notify); + done(); + }); + +}); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js new file mode 100644 index 000000000..4697bea90 --- /dev/null +++ b/tests/rawbg.test.js @@ -0,0 +1,57 @@ +'use strict'; + +require('should'); + +describe('Raw BG', function ( ) { + var rawbg = require('../lib/plugins/rawbg')({ + settings: {} + , language: require('../lib/language')() + }); + + var now = Date.now(); + var data = { + sgvs: [{unfiltered: 113680, filtered: 111232, mgdl: 110, noise: 1, mills: now}] + , cals: [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}] + }; + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: {} + }; + + it('should calculate Raw BG', function (done) { + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('rawbg'); + var result = setter(); + result.mgdl.should.equal(113); + result.noiseLabel.should.equal('Clean'); + done(); + }; + + rawbg.setProperties(sbx); + + }); + + it('should handle alexa requests', function (done) { + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + rawbg.setProperties(sbx); + + rawbg.alexa.intentHandlers.length.should.equal(1); + + rawbg.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current Raw BG'); + response.should.equal('Your raw bg is 113'); + + done(); + }, [], sbx); + + }); + +}); diff --git a/tests/reports.test.js b/tests/reports.test.js new file mode 100644 index 000000000..b49ccd065 --- /dev/null +++ b/tests/reports.test.js @@ -0,0 +1,279 @@ +'use strict'; + +require('should'); +var _ = require('lodash'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] + , treatments: [] +}; + +var someData = { + '/api/v1/entries.json?find[date][$gte]=1438992000000&find[date][$lt]=1439078400000&count=10000': [{'_id':'55c697f9459cf1fa5ed71cd8','unfiltered':213888,'filtered':218560,'direction':'Flat','device':'dexcom','rssi':172,'sgv':208,'dateString':'Sat Aug 08 16:58:44 PDT 2015','type':'sgv','date':1439078324000,'noise':1},{'_id':'55c696cc459cf1fa5ed71cd7','unfiltered':217952,'filtered':220864,'direction':'Flat','device':'dexcom','rssi':430,'sgv':212,'dateString':'Sat Aug 08 16:53:45 PDT 2015','type':'sgv','date':1439078025000,'noise':1},{'_id':'55c5d0c6459cf1fa5ed71a04','device':'dexcom','scale':1.1,'dateString':'Sat Aug 08 02:48:05 PDT 2015','date':1439027285000,'type':'cal','intercept':31102.323470336833,'slope':776.9097574914869},{'_id':'55c5d0c5459cf1fa5ed71a03','device':'dexcom','dateString':'Sat Aug 08 02:48:03 PDT 2015','mbg':120,'date':1439027283000,'type':'mbg'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], + '/api/v1/entries.json?find[date][$gte]=1439078400000&find[date][$lt]=1439164800000&count=10000': [{'_id':'55c7e85f459cf1fa5ed71dc8','unfiltered':183520,'filtered':193120,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':161,'sgv':149,'dateString':'Sun Aug 09 16:53:40 PDT 2015','type':'sgv','date':1439164420000,'noise':1},{'_id':'55c7e270459cf1fa5ed71dc7','unfiltered':199328,'filtered':192608,'direction':'Flat','device':'dexcom','rssi':161,'sgv':166,'dateString':'Sun Aug 09 16:28:40 PDT 2015','type':'sgv','date':1439162920000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], + '/api/v1/entries.json?find[date][$gte]=1439164800000&find[date][$lt]=1439251200000&count=10000': [{'_id':'55c93af4459cf1fa5ed71ecc','unfiltered':193248,'filtered':188384,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':194,'sgv':193,'dateString':'Mon Aug 10 16:58:36 PDT 2015','type':'sgv','date':1439251116000,'noise':1},{'_id':'55c939d8459cf1fa5ed71ecb','unfiltered':189888,'filtered':184960,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':931,'sgv':188,'dateString':'Mon Aug 10 16:53:38 PDT 2015','type':'sgv','date':1439250818000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], + '/api/v1/entries.json?find[date][$gte]=1439251200000&find[date][$lt]=1439337600000&count=10000': [{'_id':'55ca8c6e459cf1fa5ed71fe2','unfiltered':174080,'filtered':184576,'direction':'FortyFiveDown','device':'dexcom','rssi':169,'sgv':156,'dateString':'Tue Aug 11 16:58:32 PDT 2015','type':'sgv','date':1439337512000,'noise':1},{'_id':'55ca8b42459cf1fa5ed71fe1','unfiltered':180192,'filtered':192768,'direction':'FortyFiveDown','device':'dexcom','rssi':182,'sgv':163,'dateString':'Tue Aug 11 16:53:32 PDT 2015','type':'sgv','date':1439337212000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], + '/api/v1/entries.json?find[date][$gte]=1439337600000&find[date][$lt]=1439424000000&count=10000': [{'_id':'55cbddee38a8d88ad1b48647','unfiltered':165760,'filtered':167488,'direction':'Flat','device':'dexcom','rssi':165,'sgv':157,'dateString':'Wed Aug 12 16:58:28 PDT 2015','type':'sgv','date':1439423908000,'noise':1},{'_id':'55cbdccc38a8d88ad1b48644','unfiltered':167456,'filtered':169312,'direction':'Flat','device':'dexcom','rssi':168,'sgv':159,'dateString':'Wed Aug 12 16:53:28 PDT 2015','type':'sgv','date':1439423608000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/entries.json?find[date][$gte]=1439424000000&find[date][$lt]=1439510400000&count=10000': [{'_id':'55cd2f6738a8d88ad1b48ca1','unfiltered':209792,'filtered':229344,'direction':'SingleDown','device':'dexcom','rssi':436,'sgv':205,'dateString':'Thu Aug 13 16:58:24 PDT 2015','type':'sgv','date':1439510304000,'noise':1},{'_id':'55cd2e3b38a8d88ad1b48c95','unfiltered':220928,'filtered':237472,'direction':'FortyFiveDown','device':'dexcom','rssi':418,'sgv':219,'dateString':'Thu Aug 13 16:53:24 PDT 2015','type':'sgv','date':1439510004000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], + '/api/v1/entries.json?find[date][$gte]=1439510400000&find[date][$lt]=1439596800000&count=10000': [{'_id':'55ce80e338a8d88ad1b49397','unfiltered':179936,'filtered':202080,'direction':'SingleDown','device':'dexcom','rssi':179,'sgv':182,'dateString':'Fri Aug 14 16:58:20 PDT 2015','type':'sgv','date':1439596700000,'noise':1},{'_id':'55ce7fb738a8d88ad1b4938d','unfiltered':192288,'filtered':213792,'direction':'SingleDown','device':'dexcom','rssi':180,'sgv':197,'dateString':'Fri Aug 14 16:53:20 PDT 2015','type':'sgv','date':1439596400000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], + '/api/v1/entries.json?find[date][$gte]=1439596800000&find[date][$lt]=1439683200000&count=10000': [{'_id':'55cfd25f38a8d88ad1b49931','unfiltered':283136,'filtered':304768,'direction':'SingleDown','device':'dexcom','rssi':185,'sgv':306,'dateString':'Sat Aug 15 16:58:16 PDT 2015','type':'sgv','date':1439683096000,'noise':1},{'_id':'55cfd13338a8d88ad1b4992e','unfiltered':302528,'filtered':312576,'direction':'FortyFiveDown','device':'dexcom','rssi':179,'sgv':329,'dateString':'Sat Aug 15 16:53:16 PDT 2015','type':'sgv','date':1439682796000,'noise':1}], + '/api/v1/food/regular.json': [{'_id':'552ece84a6947ea011db35bb','type':'food','category':'Zakladni','subcategory':'Sladkosti','name':'Bebe male','portion':18,'carbs':12,'gi':1,'unit':'pcs','created_at':'2015-04-15T20:48:04.966Z'}], + '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/devicestatus.json&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z?find[openaps][$exists]=true&count=1000': [ + { + 'openaps': { + 'suggested': { + 'temp': 'absolute', + 'bg': 67, + 'tick': '+6', + 'eventualBG': 145, + 'snoozeBG': 145, + 'reason': 'BG 67<74.5, delta 6>0; no high-temp to cancel', + 'timestamp': '2015-08-31T00:00:00.000Z' + } + }, + 'created_at': '2015-08-31T00:00:00.000Z' + } + ] + + }; + +var exampleProfile = [ + { + //General values + 'dia':3, + + // Simple style values, 'from' are in minutes from midnight + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }], + 'carbs_hr':30, + 'delay': 20, + 'sens': [ + { + 'time': '00:00', + 'value': 100 + } + , { + 'time': '8:00', + 'value': 80 + }], + 'startDate': new Date(), + 'timezone': 'UTC', + + //perGIvalues style values + 'perGIvalues': false, + 'carbs_hr_high': 30, + 'carbs_hr_medium': 30, + 'carbs_hr_low': 30, + 'delay_high': 15, + 'delay_medium': 20, + 'delay_low': 20, + + 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }], + 'target_low':[ + { + 'time': '00:00', + 'value': 0 + }], + 'target_high':[ + { + 'time': '00:00', + 'value': 0 + }] + } +]; + +exampleProfile[0].startDate.setSeconds(0); +exampleProfile[0].startDate.setMilliseconds(0); + + +describe('reports', function ( ) { + var self = this; + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + done( ); + }); + + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../static/report/index.html' + , mockProfileEditor: true + , serverSettings: serverSettings + , mockSimpleAjax: someData + , benvRequires: [ + __dirname + '/../bundle/bundle.source.js' + , __dirname + '/../static/report/js/report.js' + ] + }; + headless.setup(opts, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + + it ('should produce some html', function (done) { + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm () { + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + window.setTimeout = function mockSetTimeout (call) { + call(); + }; + + client.init(function afterInit ( ) { + client.dataUpdate(nowData); + + // Load profile, we need to operate in UTC + client.sbx.data.profile.loadData(exampleProfile); + + $('#treatments').addClass('selected'); + $('a.presetdates :first').click(); + $('#rp_notes').val('something'); + $('#rp_eventtype').val('BG Check'); + $('#rp_from').val('2015/08/08'); + $('#rp_to').val('2015/09/07'); + $('#rp_optionsraw').prop('checked', true); + $('#rp_optionsiob').prop('checked', true); + $('#rp_optionscob').prop('checked', true); + $('#rp_enableeventtype').click(); + $('#rp_enablenotes').click(); + $('#rp_enablefood').click(); + $('#rp_enablefood').click(); + $('#rp_log').prop('checked', true); + $('#rp_optionsopenaps').prop('checked', true); + $('#rp_show').click(); + + $('#rp_linear').prop('checked', true); + $('#rp_show').click(); + $('#dailystats').click(); + + $('img.deleteTreatment:first').click(); + $('img.editTreatment:first').click(); + $('.ui-button:contains("Save")').click(); + + var result = $('body').html(); + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + //logfile.write($('body').html()); + + // console.log(result); + + result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday + result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday + result.indexOf('
0%100%0%238%616 (100%)Correction Bolus250 (Sensor)0.75