diff --git a/docs/docs/segment-brewfather.md b/docs/docs/segment-brewfather.md index 9f25e8cf..8969e666 100644 --- a/docs/docs/segment-brewfather.md +++ b/docs/docs/segment-brewfather.md @@ -19,7 +19,7 @@ This example uses the default segment template to show a rendition of detail app 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. +NOTE: Temperature units are in degrees C and specific gravity is expressed as `X.XXX` values. ```json { @@ -132,6 +132,41 @@ to build your own. For reference, the built-in template looks like this: } ```` +### Unit conversion + +By default temperature readings are provided in degrees C, gravity readings in decimal Specific Gravity unts (X.XXX). + +The following conversion functions are available to the template to convert to other units: + +#### Temperature + +- DegCToF - input: `float` degrees in C; output `float` degrees in F (1 decimal place). +- DegCToKelvin- input: `float` degrees in C; output `float` Kelvin (1 decimal place). + +#### Gravity + +- SGToBrix - input `float` SG in x.xxx decimal; output `float` Brix (2 decimal places) +- SGToPlato - input `float` SG in x.xxx decimal; output `float` Plato (2 decimal places) + + *(These use the polynomial conversions from [Wikipedia][wikipedia_gravity_page])* + +#### Example + +```` json +{ + "template":"{{if .Reading}}{{.SGToBrix .Reading.Gravity}}°Bx, {{.DegCToF .Reading.Temperature}}°F{{end}}" +} +```` + +To display gravity as SG in XXXX format (e.g. "1020" instead of "1.020"), use the `mulf` template function + +```` json +{ + "template":"{{if .Reading}}{{.mulf 1000 .Reading.Gravity}}, {{.DegCToF .Reading.Temperature}}°F{{end}}" +} +```` + [go-text-template]: https://golang.org/pkg/text/template/ [sprig]: https://masterminds.github.io/sprig/ [brewfather]: http://brewfather.app +[wikipedia_gravity_page]:https://en.wikipedia.org/wiki/Brix#Specific_gravity_2 diff --git a/src/segment_brewfather.go b/src/segment_brewfather.go index bd7a2b40..d8f29eb4 100644 --- a/src/segment_brewfather.go +++ b/src/segment_brewfather.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "sort" "time" @@ -315,6 +316,25 @@ func (bf *brewfather) getResult() (*Batch, error) { return &batch, nil } +// Unit conversion functions available to template. +func (bf *brewfather) DegCToF(degreesC float64) float64 { + return math.Round(10*((degreesC*1.8)+32)) / 10 // 1 decimal place +} + +func (bf *brewfather) DegCToKelvin(degreesC float64) float64 { + return math.Round(10*(degreesC+273.15)) / 10 // 1 decimal place, only addition, but just to be sure +} + +func (bf *brewfather) SGToBrix(sg float64) float64 { + // from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2 + return math.Round(100*((182.4601*sg*sg*sg)-(775.6821*sg*sg)+(1262.7794*sg)-669.5622)) / 100 +} + +func (bf *brewfather) SGToPlato(sg float64) float64 { + // from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2 + return math.Round(100*((135.997*sg*sg*sg)-(630.272*sg*sg)+(1111.14*sg)-616.868)) / 100 // 2 decimal places +} + func (bf *brewfather) init(props properties, env environmentInfo) { bf.props = props bf.env = env diff --git a/src/segment_brewfather_test.go b/src/segment_brewfather_test.go index 55a63630..62d2afa0 100644 --- a/src/segment_brewfather_test.go +++ b/src/segment_brewfather_test.go @@ -49,7 +49,7 @@ func TestBrewfatherSegment(t *testing.T) { 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 + Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " Fake Beer 1.3%", ExpectedEnabled: true, }, @@ -58,7 +58,7 @@ func TestBrewfatherSegment(t *testing.T) { 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 + Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " Fake Beer 1.3%", ExpectedEnabled: true, }, @@ -67,7 +67,7 @@ func TestBrewfatherSegment(t *testing.T) { 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 + Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 19d Fake Beer 1.3%", ExpectedEnabled: true, }, @@ -76,17 +76,17 @@ func TestBrewfatherSegment(t *testing.T) { 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 →", + Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.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 ↗", + 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}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll + ExpectedString: " 19d Fake Beer 1.3%: 1.063 21° ↗", ExpectedEnabled: true, }, { @@ -94,15 +94,15 @@ func TestBrewfatherSegment(t *testing.T) { 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 ↓↓", + Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.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 + Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: "", ExpectedEnabled: false, }, @@ -111,10 +111,28 @@ func TestBrewfatherSegment(t *testing.T) { 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 + Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 5d Fake Beer 1.3%", ExpectedEnabled: true, }, + { + Case: "Fermenting Status, test all unit conversions", + 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":34.5,"comment":"","sg":1.066,"time":` + FakeReading1DateString + `,"type":"manual"}]`, + Template: "{{ if and (.Reading) (eq .Status \"Fermenting\") }}SG: ({{.Reading.Gravity}} Bx:{{.SGToBrix .Reading.Gravity}} P:{{.SGToPlato .Reading.Gravity}}), Temp: (C:{{.Reading.Temperature}} F:{{.DegCToF .Reading.Temperature}} K:{{.DegCToKelvin .Reading.Temperature}}){{end}}", //nolint:lll + ExpectedString: "SG: (1.066 Bx:16.13 P:16.13), Temp: (C:34.5 F:94.1 K:307.7)", + ExpectedEnabled: true, + }, + { + Case: "Fermenting Status, test all unit conversions 2", + 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":3.5,"comment":"","sg":1.004,"time":` + FakeReading1DateString + `,"type":"manual"}]`, + Template: "{{ if and (.Reading) (eq .Status \"Fermenting\") }}SG: ({{.Reading.Gravity}} Bx:{{.SGToBrix .Reading.Gravity}} P:{{.SGToPlato .Reading.Gravity}}), Temp: (C:{{.Reading.Temperature}} F:{{.DegCToF .Reading.Temperature}} K:{{.DegCToKelvin .Reading.Temperature}}){{end}}", //nolint:lll + ExpectedString: "SG: (1.004 Bx:1.03 P:1.03), Temp: (C:3.5 F:38.3 K:276.7)", + ExpectedEnabled: true, + }, } for _, tc := range cases {