feat: nightscout segment

This commit is contained in:
Scott Hanselman 2021-11-23 01:34:35 -08:00 committed by Jan De Dobbeleer
parent 7627a65c66
commit fd81f4a00c
5 changed files with 341 additions and 0 deletions

View file

@ -0,0 +1,88 @@
---
id: nightscout
title: Nightscout
sidebar_label: Nightscout
---
## What
[Nightscout][nightscout] (CGM in the Cloud) is an open source,
DIY project that allows real time access to a CGM data via an HTTP REST API. It
is used for secure remote viewing of blood sugar data from anywhere...including
OhMyPosh segments on the command line!
## Sample Configuration
This example is using mg/dl, you'll want change the numbers for mmol. Your idea
of "high" or "low" is different from others. You'll also want to think about
your background and foreground colors. Don't use white text on a yellow
background, for example.
The foreground_templates example below could be set to just a single color,
if that color is visible against any of your backgrounds.
```json
{
"type": "nightscout",
"style": "diamond",
"foreground": "#ffffff",
"background": "#ff0000",
"background_templates": [
"{{ if gt .Sgv 150 }}#FFFF00{{ end }}",
"{{ if lt .Sgv 60 }}#FF0000{{ end }}",
"#00FF00"
],
"foreground_templates": [
"{{ if gt .Sgv 150 }}#000000{{ end }}",
"{{ if lt .Sgv 60 }}#000000{{ end }}",
"#000000"
],
"leading_diamond": "",
"trailing_diamond": "\uE0B0",
"properties": {
"url": "https://YOURNIGHTSCOUTAPP.herokuapp.com/api/v1/entries.json?count=1&token=APITOKENFROMYOURADMIN",
"http_timeout": 1500,
"template": " {{.Sgv}}{{.TrendIcon}}"
}
},
```
## Properties
- url: `string` - Your Nightscout URL, inclding the full path to entries.json
AND count=1 AND token. Example above. You'll know this works if you can curl
it yourself and get a single value. - defaults to ``
- http_timeout: `int` - How long do you want to wait before you want to see
your prompt more than your sugar? I figure a half second is a good default -
defaults to 500ms
- template: `string` - a go [text/template][go-text-template] template extended
with [sprig][sprig] utilizing the properties below.
See the example above where I added a syringe.
You can change the icon, put the trend elsewhere, add text, however you like!
Make sure your NerdFont has the glyph you want or search for one
at nerdfonts.com
- NSCacheTimeout: `int` in minutes - How long do you want your numbers cached? -
defaults to 5 min
- NOTE: You can change the icons for trend, put the trend elsewhere, add text,
however you like!
Make sure your NerdFont has the glyph you want or search for one at
nerdfonts.com
- DoubleUpIcon - defaults to ↑↑
- SingleUpIcon - defaults to ↑
- FortyFiveUpIcon - defaults to ↗
- FlatIcon - defaults to →
- FortyFiveDownIcon - defaults to ↘
- SingleDownIcon - defaults to ↓
- DoubleDownIcon - defaults to ↓↓
## Template Properties
- .Sgv: `int` - Your Serum Glucose Value (your sugar)
- .TrendIcon: `string` - By default, this will be something like ↑↑ or ↘ etc but you can
override them with any glpyh as seen above
[go-text-template]: https://golang.org/pkg/text/template/
[sprig]: https://masterminds.github.io/sprig/
[nightscout]: http://www.nightscout.info/

View file

@ -55,6 +55,7 @@ module.exports = {
"julia",
"kubectl",
"nbgv",
"nightscout",
"node",
"os",
"owm",

View file

@ -130,6 +130,8 @@ const (
Angular SegmentType = "angular"
// PHP writes which php version is currently active
PHP SegmentType = "php"
// Nightscout is an open source diabetes system
Nightscout SegmentType = "nightscout"
)
func (segment *Segment) string() string {
@ -263,6 +265,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
SysInfo: &sysinfo{},
Angular: &angular{},
PHP: &php{},
Nightscout: &nightscout{},
}
if writer, ok := functions[segment.Type]; ok {
props := &properties{

134
src/segment_nightscout.go Normal file
View file

@ -0,0 +1,134 @@
package main
import (
"encoding/json"
)
// segment struct, makes templating easier
type nightscout struct {
props *properties
env environmentInfo
// array of nightscoutData (is often just 1, and we will pick the ZEROeth)
Sgv int64
Direction string
TrendIcon string
}
const (
// Your complete Nightscout URL and APIKey like this
URL Property = "url"
DoubleUpIcon Property = "doubleup_icon"
SingleUpIcon Property = "singleup_icon"
FortyFiveUpIcon Property = "fortyfiveup_icon"
FlatIcon Property = "flat_icon"
FortyFiveDownIcon Property = "fortyfivedown_icon"
SingleDownIcon Property = "singledown_icon"
DoubleDownIcon Property = "doubledown_icon"
NSCacheTimeout Property = "cache_timeout"
)
type nightscoutData struct {
Sgv int64 `json:"sgv"`
Direction string `json:"direction"`
}
func (ns *nightscout) enabled() bool {
data, err := ns.getResult()
if err != nil {
return false
}
ns.Sgv = data.Sgv
ns.Direction = data.Direction
ns.TrendIcon = ns.getTrendIcon()
return true
}
func (ns *nightscout) getTrendIcon() string {
switch ns.Direction {
case "DoubleUp":
return ns.props.getString(DoubleUpIcon, "↑↑")
case "SingleUp":
return ns.props.getString(SingleUpIcon, "↑")
case "FortyFiveUp":
return ns.props.getString(FortyFiveUpIcon, "↗")
case "Flat":
return ns.props.getString(FlatIcon, "→")
case "FortyFiveDown":
return ns.props.getString(FortyFiveDownIcon, "↘")
case "SingleDown":
return ns.props.getString(SingleDownIcon, "↓")
case "DoubleDown":
return ns.props.getString(DoubleDownIcon, "↓↓")
default:
return ""
}
}
func (ns *nightscout) string() string {
segmentTemplate := ns.props.getString(SegmentTemplate, "{{.Sgv}}")
template := &textTemplate{
Template: segmentTemplate,
Context: ns,
Env: ns.env,
}
text, err := template.render()
if err != nil {
return err.Error()
}
return text
}
func (ns *nightscout) getResult() (*nightscoutData, error) {
url := ns.props.getString(URL, "")
// natural and understood NS timeout is 5, anything else is unusual
cacheTimeout := ns.props.getInt(NSCacheTimeout, 5)
response := &nightscoutData{}
if cacheTimeout > 0 {
// check if data stored in cache
val, found := ns.env.cache().get(url)
// we got something from the cache
if found {
err := json.Unmarshal([]byte(val), response)
if err != nil {
return nil, err
}
return response, nil
}
}
httpTimeout := ns.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
body, err := ns.env.doGet(url, httpTimeout)
if err != nil {
return &nightscoutData{}, err
}
var arr []*nightscoutData
err = json.Unmarshal(body, &arr)
if err != nil {
return &nightscoutData{}, err
}
firstelement := arr[0]
firstData, err := json.Marshal(firstelement)
if err != nil {
return &nightscoutData{}, err
}
if cacheTimeout > 0 {
// persist new sugars in cache
ns.env.cache().set(url, string(firstData), cacheTimeout)
}
return firstelement, nil
}
func (ns *nightscout) init(props *properties, env environmentInfo) {
ns.props = props
ns.env = env
}

View file

@ -0,0 +1,115 @@
package main
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
const (
NSAPIURL = "FAKE"
)
func TestNSSegment(t *testing.T) {
cases := []struct {
Case string
JSONResponse string
ExpectedString string
ExpectedEnabled bool
Template string
Error error
}{
{
Case: "Flat 150",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":150,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"Flat","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 150→",
ExpectedEnabled: true,
},
{
Case: "DoubleDown 50",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 50↓↓",
ExpectedEnabled: true,
},
{
Case: "DoubleUp 250",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":250,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 250↑↑",
ExpectedEnabled: true,
},
{
Case: "SingleUp 130",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":130,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"SingleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 130↑",
ExpectedEnabled: true,
},
{
Case: "FortyFiveUp 174",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":174,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 174↗",
ExpectedEnabled: true,
},
{
Case: "FortyFiveDown 61",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":61,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 61↘",
ExpectedEnabled: true,
},
{
Case: "DoubleDown 50",
JSONResponse: `
[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
Template: " {{.Sgv}}{{.TrendIcon}}",
ExpectedString: " 50↓↓",
ExpectedEnabled: true,
},
{
Case: "Error in retrieving data",
JSONResponse: "nonsense",
Error: errors.New("Something went wrong"),
ExpectedEnabled: false,
},
}
for _, tc := range cases {
env := &MockedEnvironment{}
props := &properties{
values: map[Property]interface{}{
CacheTimeout: 0,
URL: "FAKE",
},
}
env.On("doGet", NSAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
if tc.Template != "" {
props.values[SegmentTemplate] = tc.Template
}
ns := &nightscout{
props: props,
env: env,
}
enabled := ns.enabled()
assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
if !enabled {
continue
}
assert.Equal(t, tc.ExpectedString, ns.string(), tc.Case)
}
}