Repository: JanDeDobbeleer/oh-my-posh
Pull request #1471 (2021-12-19 22:41:17)

feat(brewfather): unit conversion routines
### Prerequisites

- [X] I have read and understood the `CONTRIBUTING` guide
- [X] The commit message follows the [conventional commits][cc] guidelines
- [X] Tests for the changes have been added (for bug fixes/features)
- [X] Docs have been added / updated (for bug fixes/features)

feat(brewfather): unit conversion helper functions made available to template

degrees C to F and Kelvin
SG to Brix and Plato

#1457
<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
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


Test patch:
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 {