mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-12-27 03:49:40 -08:00
feat(brewfather): add segment
This commit is contained in:
parent
a595278f5f
commit
0d079a4d8a
136
docs/docs/segment-brewfather.md
Normal file
136
docs/docs/segment-brewfather.md
Normal file
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
id: brewfather
|
||||
title: Brewfather
|
||||
sidebar_label: Brewfather
|
||||
---
|
||||
|
||||
## What
|
||||
|
||||
Calling all brewers! Keep up-to-date with the status of your [Brewfather][brewfather] batch directly in your
|
||||
commandline prompt using the brewfather segment!
|
||||
|
||||
You will need your User ID and API Key as generated in
|
||||
Brewfather's Settings screen, enabled with **batches.read** and **recipes.read** scopes.
|
||||
|
||||
## Sample Configuration
|
||||
|
||||
This example uses the default segment template to show a rendition of detail appropriate to the status of the batch
|
||||
|
||||
Additionally, the background of the segment will turn red if the latest reading is over 4 hours old - possibly helping indicate
|
||||
an issue if, for example there is a Tilt or similar device that is supposed to be logging to Brewfather every 15 minutes.
|
||||
|
||||
NOTE: Temperature units are in degrees C and specific gravity is expressed as `1.XYZ` values.
|
||||
|
||||
```json
|
||||
{
|
||||
"type":"brewfather",
|
||||
"style": "powerline",
|
||||
"powerline_symbol": "\uE0B0",
|
||||
"foreground": "#ffffff",
|
||||
"background": "#33158A",
|
||||
"background_templates": [
|
||||
"{{ if and (.Reading) (eq .Status \"Fermenting\") (gt .ReadingAge 4) }}#cc1515{{end}}"
|
||||
],
|
||||
"properties": {
|
||||
"user_id":"abcdefg123456",
|
||||
"api_key":"qrstuvw78910",
|
||||
"batch_id":"hijklmno098765",
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
- user_id: `string` - as provided by Brewfather's Generate API Key screen.
|
||||
- api_key: `string` - as provided by Brewfather's Generate API Key screen.
|
||||
- batch_id: `string` - Get this by navigating to the desired batch on the brewfather website,
|
||||
the batch id is at the end of the URL in the address bar.
|
||||
- http_timeout: `int` in milliseconds - How long to wait for the Brewfather service to answer the request. Default 2 seconds.
|
||||
- template: `string` - a go [text/template][go-text-template] template extended
|
||||
with [sprig][sprig] utilizing the properties below.
|
||||
- cache_timeout: `int` in minutes - How long to wait before updating the data from Brewfather. Default is 5 minutes.
|
||||
- day_icon: `string` - icon or letter to use to indicate days. Default is "d".
|
||||
|
||||
You can override the icons for temperature trend as used by template property `.TemperatureTrendIcon` with:
|
||||
|
||||
- doubleup_icon - for increases of more than 4°C, default is ↑↑
|
||||
- singleup_icon - increase 2-4°C, default is ↑
|
||||
- fortyfiveup_icon - increase 0.5-2°C, default is ↗
|
||||
- flat_icon -change less than 0.5°C, default is →
|
||||
- fortyfivedown_icon - decrease 0.5-2°C, default is ↘
|
||||
- singledown_icon - decrease 2-4°C, default is ↓
|
||||
- doubledown_icon - decrease more than 4°C, default is ↓↓
|
||||
|
||||
You can override the default icons for batch status as used by template property `.StatusIcon` with:
|
||||
|
||||
- planning_status_icon
|
||||
- brewing_status_icon
|
||||
- fermenting_status_icon
|
||||
- conditioning_status_icon
|
||||
- completed_status_icon
|
||||
- archived_status_icon
|
||||
|
||||
## Template Properties
|
||||
|
||||
Commonly used fields
|
||||
|
||||
- .Status: `string` - One of "Planning", "Brewing", "Fermenting", "Conditioning", "Completed" or "Archived"
|
||||
- .StatusIcon `string` - Icon representing above stats. Can be overridden with properties shown above
|
||||
- .TemperatureTrendIcon `string` - Icon showing temperature trend based on latest and previous reading
|
||||
- .DaysFermenting `int` - days since start of fermentation
|
||||
- .DaysBottled `int` - days since bottled/kegged
|
||||
- .DaysBottledOrFermented `int` - one of the above, chosen automatically based on batch status
|
||||
- .Recipe.Name: `string` - The recipe being brewed in this batch
|
||||
- .MeasuredAbv: `float` - The ABV for the batch - either estimated from recipe or calculated from entered OG and FG values
|
||||
- .ReadingAge `int` - age in hours of most recent reading or -1 if there are no readings available
|
||||
|
||||
.Reading contains the most recent data from devices or manual entry as visible on the Brewfather's batch Readings graph.
|
||||
If there are no readings available, .Reading will be null.
|
||||
|
||||
- .Reading.Gravity: `float` - specific gravity (in decimal point format)
|
||||
- .Reading.Temperature `float` - temperature in °C
|
||||
- .Reading.Time `int` - unix timestamp of reading
|
||||
- .Reading.Comment `string` - comment attached to this reading
|
||||
- .Reading.DeviceType `string` - source of the reading, e.g. "Tilt"
|
||||
- .Reading.DeviceID `string` - id of the device, e.g. "PINK"
|
||||
|
||||
Additional template properties
|
||||
|
||||
- .MeasuredOg: `float` - The OG for the batch as manually entered into Brewfather
|
||||
- .MeasuredFg: `float` -The FG for the batch as manually entered into Brewfather
|
||||
- .BrewDate: `int` - The unix timestamp of the brew day
|
||||
- .FermentStartDate: `int` The unix timestamp when fermentation was started
|
||||
- .BottlingDate: `time` - The unix timestamp when bottled/kegged
|
||||
- .TemperatureTrend `float` - The difference between the most recent and previous temperature in °C
|
||||
- .DayIcon `string` - given by "day_icon", or "d" by default
|
||||
|
||||
Hyperlink support
|
||||
|
||||
- .URL `string` - the URL for the batch in the Brewfather app. You can use this to add a hyperlink to the segment
|
||||
if you are using a terminal that supports it and the segment has `"enable_hyperlink":true` in it's properties. `.DefaultString`
|
||||
has this by default.
|
||||
|
||||
Hyperlink formatting example
|
||||
|
||||
````json
|
||||
{
|
||||
// General format: [Text](Url)
|
||||
"template":"[{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d{{end}} {{.Recipe.Name}}]({{.URL}})"
|
||||
}
|
||||
|
||||
````
|
||||
|
||||
### Advanced Templating
|
||||
|
||||
The built in template will provides key useful information. However, you can use the properties about the batch
|
||||
to build your own. For reference, the built-in template looks like this:
|
||||
|
||||
````json
|
||||
{
|
||||
"template":"{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}{{.DayIcon}} {{end}}[{{.Recipe.Name}}]({{.URL}}) {{printf \"%.1f\" .MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{printf \"%.3f\" .Reading.Gravity}} {{.Reading.Temperature}}\u00b0 {{.TemperatureTrendIcon}}{{end}}"
|
||||
}
|
||||
````
|
||||
|
||||
[go-text-template]: https://golang.org/pkg/text/template/
|
||||
[sprig]: https://masterminds.github.io/sprig/
|
||||
[brewfather]: http://brewfather.app
|
|
@ -56,6 +56,8 @@ type cache interface {
|
|||
set(key, value string, ttl int)
|
||||
}
|
||||
|
||||
type HTTPRequestModifier func(request *http.Request)
|
||||
|
||||
type windowsRegistryValueType int
|
||||
|
||||
const (
|
||||
|
@ -96,7 +98,7 @@ type environmentInfo interface {
|
|||
getShellName() string
|
||||
getWindowTitle(imageName, windowTitleRegex string) (string, error)
|
||||
getWindowsRegistryKeyValue(path string) (*windowsRegistryValue, error)
|
||||
doGet(url string, timeout int) ([]byte, error)
|
||||
doGet(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
|
||||
hasParentFilePath(path string) (fileInfo *fileInfo, err error)
|
||||
isWsl() bool
|
||||
stackCount() int
|
||||
|
@ -480,7 +482,7 @@ func (env *environment) getShellName() string {
|
|||
return *env.args.Shell
|
||||
}
|
||||
|
||||
func (env *environment) doGet(url string, timeout int) ([]byte, error) {
|
||||
func (env *environment) doGet(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
|
||||
defer env.trace(time.Now(), "doGet", url)
|
||||
ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
|
||||
defer cncl()
|
||||
|
@ -488,6 +490,9 @@ func (env *environment) doGet(url string, timeout int) ([]byte, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, modifier := range requestModifiers {
|
||||
modifier(request)
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
env.log(Error, "doGet", err.Error())
|
||||
|
|
|
@ -139,6 +139,8 @@ const (
|
|||
WiFi SegmentType = "wifi"
|
||||
// WinReg queries the Windows registry.
|
||||
WinReg SegmentType = "winreg"
|
||||
// Brewfather segment
|
||||
BrewFather SegmentType = "brewfather"
|
||||
)
|
||||
|
||||
func (segment *Segment) string() string {
|
||||
|
@ -271,6 +273,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
|
|||
Wakatime: &wakatime{},
|
||||
WiFi: &wifi{},
|
||||
WinReg: &winreg{},
|
||||
BrewFather: &brewfather{},
|
||||
}
|
||||
if segment.Properties == nil {
|
||||
segment.Properties = make(properties)
|
||||
|
|
319
src/segment_brewfather.go
Normal file
319
src/segment_brewfather.go
Normal file
|
@ -0,0 +1,319 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// segment struct, makes templating easier
|
||||
type brewfather struct {
|
||||
props properties
|
||||
env environmentInfo
|
||||
|
||||
Batch
|
||||
TemperatureTrendIcon string
|
||||
StatusIcon string
|
||||
DayIcon string // populated from day_icon for use in template
|
||||
|
||||
ReadingAge int // age in hours of the most recent reading included in the batch, -1 if none
|
||||
DaysFermenting uint
|
||||
DaysBottled uint
|
||||
DaysBottledOrFermented *uint // help avoid chronic template logic - code will point this to one of above or be nil depending on status
|
||||
|
||||
URL string // URL of batch page to open if hyperlink enabled on the segment and URL formatting used in template: [name](link)
|
||||
}
|
||||
|
||||
const (
|
||||
BFUserID Property = "user_id"
|
||||
BFAPIKey Property = "api_key"
|
||||
BFBatchID Property = "batch_id"
|
||||
|
||||
BFDoubleUpIcon Property = "doubleup_icon"
|
||||
BFSingleUpIcon Property = "singleup_icon"
|
||||
BFFortyFiveUpIcon Property = "fortyfiveup_icon"
|
||||
BFFlatIcon Property = "flat_icon"
|
||||
BFFortyFiveDownIcon Property = "fortyfivedown_icon"
|
||||
BFSingleDownIcon Property = "singledown_icon"
|
||||
BFDoubleDownIcon Property = "doubledown_icon"
|
||||
|
||||
BFPlanningStatusIcon Property = "planning_status_icon"
|
||||
BFBrewingStatusIcon Property = "brewing_status_icon"
|
||||
BFFermentingStatusIcon Property = "fermenting_status_icon"
|
||||
BFConditioningStatusIcon Property = "conditioning_status_icon"
|
||||
BFCompletedStatusIcon Property = "completed_status_icon"
|
||||
BFArchivedStatusIcon Property = "archived_status_icon"
|
||||
|
||||
BFDayIcon Property = "day_icon"
|
||||
|
||||
BFCacheTimeout Property = "cache_timeout"
|
||||
|
||||
DefaultTemplate string = "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}{{.DayIcon}} {{end}}[{{.Recipe.Name}}]({{.URL}})" +
|
||||
" {{printf \"%.1f\" .MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}} " +
|
||||
"{{printf \"%.3f\" .Reading.Gravity}} {{.Reading.Temperature}}\u00b0 {{.TemperatureTrendIcon}}{{end}}"
|
||||
|
||||
BFStatusPlanning string = "Planning"
|
||||
BFStatusBrewing string = "Brewing"
|
||||
BFStatusFermenting string = "Fermenting"
|
||||
BFStatusConditioning string = "Conditioning"
|
||||
BFStatusCompleted string = "Completed"
|
||||
BFStatusArchived string = "Archived"
|
||||
)
|
||||
|
||||
// Returned from https://api.brewfather.app/v1/batches/batch_id/readings
|
||||
type BatchReading struct {
|
||||
Comment string `json:"comment"`
|
||||
Gravity float64 `json:"sg"`
|
||||
DeviceType string `json:"type"`
|
||||
DeviceID string `json:"id"`
|
||||
Temperature float64 `json:"temp"` // celsius - need to add F conversion
|
||||
Timepoint int64 `json:"timepoint"` // << check what these are...
|
||||
Time int64 `json:"time"` // <<
|
||||
}
|
||||
type Batch struct {
|
||||
// Json tagged values returned from https://api.brewfather.app/v1/batches/batch_id
|
||||
Status string `json:"status"`
|
||||
Recipe struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"recipe"`
|
||||
BrewDate int64 `json:"brewDate"`
|
||||
FermentStartDate int64 `json:"fermentationStartDate"`
|
||||
BottlingDate int64 `json:"bottlingDate"`
|
||||
|
||||
MeasuredOg float64 `json:"measuredOg"`
|
||||
MeasuredFg float64 `json:"measuredFg"`
|
||||
MeasuredAbv float64 `json:"measuredAbv"`
|
||||
|
||||
// copy of the latest BatchReading in here.
|
||||
Reading *BatchReading
|
||||
|
||||
// Calculated values we need to cache because they require the rest query to reproduce
|
||||
TemperatureTrend float64 // diff between this and last, short term trend
|
||||
}
|
||||
|
||||
func (bf *brewfather) enabled() bool {
|
||||
data, err := bf.getResult()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bf.Batch = *data
|
||||
|
||||
if bf.Batch.Reading != nil {
|
||||
readingDate := time.UnixMilli(bf.Batch.Reading.Time)
|
||||
bf.ReadingAge = int(time.Since(readingDate).Hours())
|
||||
} else {
|
||||
bf.ReadingAge = -1
|
||||
}
|
||||
|
||||
bf.TemperatureTrendIcon = bf.getTrendIcon(bf.TemperatureTrend)
|
||||
bf.StatusIcon = bf.getBatchStatusIcon(data.Status)
|
||||
|
||||
fermStartDate := time.UnixMilli(bf.Batch.FermentStartDate)
|
||||
bottlingDate := time.UnixMilli(bf.Batch.BottlingDate)
|
||||
|
||||
switch bf.Batch.Status {
|
||||
case BFStatusFermenting:
|
||||
// in the fermenter now, so relative to today.
|
||||
bf.DaysFermenting = uint(time.Since(fermStartDate).Hours() / 24)
|
||||
bf.DaysBottled = 0
|
||||
bf.DaysBottledOrFermented = &bf.DaysFermenting
|
||||
case BFStatusConditioning, BFStatusCompleted, BFStatusArchived:
|
||||
bf.DaysFermenting = uint(bottlingDate.Sub(fermStartDate).Hours() / 24)
|
||||
bf.DaysBottled = uint(time.Since(bottlingDate).Hours() / 24)
|
||||
bf.DaysBottledOrFermented = &bf.DaysBottled
|
||||
default:
|
||||
bf.DaysFermenting = 0
|
||||
bf.DaysBottled = 0
|
||||
bf.DaysBottledOrFermented = nil
|
||||
}
|
||||
|
||||
// URL property set to weblink to the full batch page
|
||||
batchID := bf.props.getString(BFBatchID, "")
|
||||
if len(batchID) > 0 {
|
||||
bf.URL = fmt.Sprintf("https://web.brewfather.app/tabs/batches/batch/%s", batchID)
|
||||
}
|
||||
|
||||
bf.DayIcon = bf.props.getString(BFDayIcon, "d")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (bf *brewfather) getTrendIcon(trend float64) string {
|
||||
// Not a fan of this logic - wondering if Go lets us do something cleaner...
|
||||
if trend >= 0 {
|
||||
if trend > 4 {
|
||||
return bf.props.getString(BFDoubleUpIcon, "↑↑")
|
||||
}
|
||||
|
||||
if trend > 2 {
|
||||
return bf.props.getString(BFSingleUpIcon, "↑")
|
||||
}
|
||||
|
||||
if trend > 0.5 {
|
||||
return bf.props.getString(BFFortyFiveUpIcon, "↗")
|
||||
}
|
||||
|
||||
return bf.props.getString(BFFlatIcon, "→")
|
||||
}
|
||||
|
||||
if trend < -4 {
|
||||
return bf.props.getString(BFDoubleDownIcon, "↓↓")
|
||||
}
|
||||
|
||||
if trend < -2 {
|
||||
return bf.props.getString(BFSingleDownIcon, "↓")
|
||||
}
|
||||
|
||||
if trend < -0.5 {
|
||||
return bf.props.getString(BFFortyFiveDownIcon, "↘")
|
||||
}
|
||||
|
||||
return bf.props.getString(BFFlatIcon, "→")
|
||||
}
|
||||
|
||||
func (bf *brewfather) getBatchStatusIcon(batchStatus string) string {
|
||||
switch batchStatus {
|
||||
case BFStatusPlanning:
|
||||
return bf.props.getString(BFPlanningStatusIcon, "\uF8EA")
|
||||
case BFStatusBrewing:
|
||||
return bf.props.getString(BFBrewingStatusIcon, "\uF7DE")
|
||||
case BFStatusFermenting:
|
||||
return bf.props.getString(BFFermentingStatusIcon, "\uF499")
|
||||
case BFStatusConditioning:
|
||||
return bf.props.getString(BFConditioningStatusIcon, "\uE372")
|
||||
case BFStatusCompleted:
|
||||
return bf.props.getString(BFCompletedStatusIcon, "\uF7A5")
|
||||
case BFStatusArchived:
|
||||
return bf.props.getString(BFArchivedStatusIcon, "\uF187")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (bf *brewfather) string() string {
|
||||
segmentTemplate := bf.props.getString(SegmentTemplate, DefaultTemplate)
|
||||
template := &textTemplate{
|
||||
Template: segmentTemplate,
|
||||
Context: bf,
|
||||
Env: bf.env,
|
||||
}
|
||||
text, err := template.render()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
func (bf *brewfather) getResult() (*Batch, error) {
|
||||
getFromCache := func(key string) (*Batch, error) {
|
||||
val, found := bf.env.cache().get(key)
|
||||
// we got something from the cache
|
||||
if found {
|
||||
var result Batch
|
||||
err := json.Unmarshal([]byte(val), &result)
|
||||
if err == nil {
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no data in cache")
|
||||
}
|
||||
|
||||
putToCache := func(key string, batch *Batch, cacheTimeout int) error {
|
||||
cacheJSON, err := json.Marshal(batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bf.env.cache().set(key, string(cacheJSON), cacheTimeout)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
userID := bf.props.getString(BFUserID, "")
|
||||
if len(userID) == 0 {
|
||||
return nil, errors.New("missing Brewfather user id (user_id)")
|
||||
}
|
||||
|
||||
apiKey := bf.props.getString(BFAPIKey, "")
|
||||
if len(apiKey) == 0 {
|
||||
return nil, errors.New("missing Brewfather api key (api_key)")
|
||||
}
|
||||
|
||||
batchID := bf.props.getString(BFBatchID, "")
|
||||
if len(batchID) == 0 {
|
||||
return nil, errors.New("missing Brewfather batch id (batch_id)")
|
||||
}
|
||||
|
||||
authString := fmt.Sprintf("%s:%s", userID, apiKey)
|
||||
authStringb64 := base64.StdEncoding.EncodeToString([]byte(authString))
|
||||
authHeader := fmt.Sprintf("Basic %s", authStringb64)
|
||||
batchURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s", batchID)
|
||||
batchReadingsURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s/readings", batchID)
|
||||
|
||||
httpTimeout := bf.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
|
||||
cacheTimeout := bf.props.getInt(BFCacheTimeout, 5)
|
||||
|
||||
if cacheTimeout > 0 {
|
||||
if data, err := getFromCache(batchURL); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// batch
|
||||
addAuthHeader := func(request *http.Request) {
|
||||
request.Header.Add("authorization", authHeader)
|
||||
}
|
||||
body, err := bf.env.doGet(batchURL, httpTimeout, addAuthHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var batch Batch
|
||||
err = json.Unmarshal(body, &batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// readings
|
||||
body, err = bf.env.doGet(batchReadingsURL, httpTimeout, addAuthHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var arr []*BatchReading
|
||||
err = json.Unmarshal(body, &arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(arr) > 0 {
|
||||
// could just take latest reading using their API, but that won't allow us to see trend - get 'em all and sort by time,
|
||||
// using two most recent for trend
|
||||
sort.Slice(arr, func(i, j int) bool {
|
||||
return arr[i].Time > arr[j].Time
|
||||
})
|
||||
|
||||
// Keep the latest one
|
||||
batch.Reading = arr[0]
|
||||
|
||||
if len(arr) > 1 {
|
||||
batch.TemperatureTrend = arr[0].Temperature - arr[1].Temperature
|
||||
}
|
||||
}
|
||||
|
||||
if cacheTimeout > 0 {
|
||||
_ = putToCache(batchURL, &batch, cacheTimeout)
|
||||
}
|
||||
|
||||
return &batch, nil
|
||||
}
|
||||
|
||||
func (bf *brewfather) init(props properties, env environmentInfo) {
|
||||
bf.props = props
|
||||
bf.env = env
|
||||
}
|
154
src/segment_brewfather_test.go
Normal file
154
src/segment_brewfather_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
BFFakeBatchID = "FAKE"
|
||||
BFBatchURL = "https://api.brewfather.app/v1/batches/" + BFFakeBatchID
|
||||
BFCacheKey = BFBatchURL
|
||||
BFBatchReadingsURL = "https://api.brewfather.app/v1/batches/" + BFFakeBatchID + "/readings"
|
||||
)
|
||||
|
||||
func TestBrewfatherSegment(t *testing.T) {
|
||||
TimeNow := time.Now()
|
||||
|
||||
// Create a fake timeline for the fake json, all in Unix milliseconds, to be used in all fake json responses
|
||||
FakeBrewDate := TimeNow.Add(-time.Hour * 24 * 20)
|
||||
FakeFermentationStartDate := FakeBrewDate.Add(time.Hour * 24) // 1 day after brew date = 19 days ago
|
||||
FakeReading1Date := FakeFermentationStartDate.Add(time.Minute * 35) // first reading 35 minutes
|
||||
FakeReading2Date := FakeReading1Date.Add(time.Hour) // second reading 1 hour later
|
||||
FakeReading3Date := FakeReading2Date.Add(time.Hour * 3) // 3 hours after last reading, 454 hours ago
|
||||
FakeBottlingDate := FakeFermentationStartDate.Add(time.Hour * 24 * 14) // 14 days after ferm date = 5 days ago
|
||||
|
||||
FakeBrewDateString := fmt.Sprintf("%d", FakeBrewDate.UnixMilli())
|
||||
FakeFermStartDateString := fmt.Sprintf("%d", FakeFermentationStartDate.UnixMilli())
|
||||
FakeReading1DateString := fmt.Sprintf("%d", FakeReading1Date.UnixMilli())
|
||||
FakeReading2DateString := fmt.Sprintf("%d", FakeReading2Date.UnixMilli())
|
||||
FakeReading3DateString := fmt.Sprintf("%d", FakeReading3Date.UnixMilli())
|
||||
FakeBottlingDateString := fmt.Sprintf("%d", FakeBottlingDate.UnixMilli())
|
||||
|
||||
cases := []struct {
|
||||
Case string
|
||||
BatchJSONResponse string
|
||||
BatchReadingsJSONResponse string
|
||||
ExpectedString string
|
||||
ExpectedEnabled bool
|
||||
CacheTimeout int
|
||||
CacheFoundFail bool
|
||||
Template string
|
||||
Error error
|
||||
}{
|
||||
{
|
||||
Case: "Planning Status",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Planning","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[]`,
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " Fake Beer 1.3%",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Brewing Status",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Brewing","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[]`,
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " Fake Beer 1.3%",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Fermenting Status, no readings",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Fermenting","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[]`,
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " 19d Fake Beer 1.3%",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Fermenting Status, one reading",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Fermenting","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[{"id":"manual","temp":19.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}]`,
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " 19d Fake Beer 1.3%: 1.066 19.5 →",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Fermenting Status, two readings, temp trending up",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Fermenting","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[{"id":"manual","temp":19.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}, {"id":"manual","temp":21,"comment":"","sg":1.063,"time":` + FakeReading2DateString + `,"type":"manual"}]`, // nolint:lll
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " 19d Fake Beer 1.3%: 1.063 21 ↗",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Fermenting Status, three readings, temp trending hard down, include age of most recent reading",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Fermenting","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[{"id":"manual","temp":19.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}, {"id":"manual","temp":21,"comment":"","sg":1.063,"time":` + FakeReading2DateString + `,"type":"manual"}, {"id":"manual","temp":15,"comment":"","sg":1.050,"time":` + FakeReading3DateString + `,"type":"manual"}]`, // nolint:lll
|
||||
Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " 451 19d Fake Beer 1.3%: 1.05 15 ↓↓",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Bad batch json, readings fine",
|
||||
BatchJSONResponse: ``,
|
||||
BatchReadingsJSONResponse: `[{"id":"manual","temp":19.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}, {"id":"manual","temp":21,"comment":"","sg":1.063,"time":` + FakeReading2DateString + `,"type":"manual"}, {"id":"manual","temp":15,"comment":"","sg":1.050,"time":` + FakeReading3DateString + `,"type":"manual"}]`, // nolint:lll
|
||||
Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: "",
|
||||
ExpectedEnabled: false,
|
||||
},
|
||||
{
|
||||
Case: "Conditioning Status",
|
||||
BatchJSONResponse: `
|
||||
{"batchNo":18,"status":"Conditioning","brewDate":` + FakeBrewDateString + `,"bottlingDate":` + FakeBottlingDateString + `,"recipe":{"name":"Fake Beer"},"fermentationStartDate":` + FakeFermStartDateString + `,"name":"Batch","measuredAbv": 1.3}`, // nolint:lll
|
||||
BatchReadingsJSONResponse: `[{"id":"manual","temp":19.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}, {"id":"manual","temp":21,"comment":"","sg":1.063,"time":` + FakeReading2DateString + `,"type":"manual"}, {"id":"manual","temp":15,"comment":"","sg":1.050,"time":` + FakeReading3DateString + `,"type":"manual"}]`, // nolint:lll
|
||||
Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}\ue33e {{.TemperatureTrendIcon}}{{end}}", //nolint:lll
|
||||
ExpectedString: " 5d Fake Beer 1.3%",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
env := &MockedEnvironment{}
|
||||
var props properties = map[Property]interface{}{
|
||||
CacheTimeout: tc.CacheTimeout,
|
||||
BFBatchID: BFFakeBatchID,
|
||||
BFAPIKey: "FAKE",
|
||||
BFUserID: "FAKE",
|
||||
}
|
||||
|
||||
cache := &MockedCache{}
|
||||
cache.On("get", BFCacheKey).Return(nil, false) // cache testing later because cache is a little more complicated than just the single response.
|
||||
// cache.On("set", BFCacheKey, tc.JSONResponse, tc.CacheTimeout).Return()
|
||||
|
||||
env.On("doGet", BFBatchURL).Return([]byte(tc.BatchJSONResponse), tc.Error)
|
||||
env.On("doGet", BFBatchReadingsURL).Return([]byte(tc.BatchReadingsJSONResponse), tc.Error)
|
||||
env.On("cache", nil).Return(cache)
|
||||
|
||||
if tc.Template != "" {
|
||||
props[SegmentTemplate] = tc.Template
|
||||
}
|
||||
|
||||
ns := &brewfather{
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -134,7 +134,7 @@ func (env *MockedEnvironment) getWindowsRegistryKeyValue(path string) (*windowsR
|
|||
return args.Get(0).(*windowsRegistryValue), args.Error(1)
|
||||
}
|
||||
|
||||
func (env *MockedEnvironment) doGet(url string, timeout int) ([]byte, error) {
|
||||
func (env *MockedEnvironment) doGet(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error) {
|
||||
args := env.Called(url)
|
||||
return args.Get(0).([]byte), args.Error(1)
|
||||
}
|
||||
|
|
|
@ -1798,6 +1798,50 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": { "const": "brewfather" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"title": "Brewfather Batch Status",
|
||||
"description": "https://ohmyposh.dev/docs/brewfather",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"properties": {
|
||||
"template": {
|
||||
"$ref": "#/definitions/template"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"title": "Brewfather UserID (required)",
|
||||
"description": "Provided by Brewfather's Generate API Key settings option",
|
||||
"default": ""
|
||||
},
|
||||
"api_key": {
|
||||
"type":"string",
|
||||
"title":"Brewfather API Key (required)",
|
||||
"description":"Provided by Brewfather's Generate API Key settings option",
|
||||
"default":""
|
||||
},
|
||||
"batch_id": {
|
||||
"type":"string",
|
||||
"title":"ID of the batch in Brewfather (required)",
|
||||
"description":"At the end of the URL when viewing the batch on the Brewfather site",
|
||||
"default":""
|
||||
},
|
||||
"day_icon": {
|
||||
"type":"string",
|
||||
"title":"Icon to use to indicate days",
|
||||
"description":"Appended to a number to indicate days, e.g. 25d",
|
||||
"default":"d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue