feat(brewfather): add segment

This commit is contained in:
Will 2021-12-15 07:49:32 +00:00 committed by GitHub
parent a595278f5f
commit 0d079a4d8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 664 additions and 3 deletions

View 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

View file

@ -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())

View file

@ -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
View 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
}

View 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)
}
}

View file

@ -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)
}

View file

@ -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"
}
}
}
}
}
}
]
}