Repository: JanDeDobbeleer/oh-my-posh
Pull request #895 (2021-08-13 13:32:03)

Added segment for Open Weather Map.
### 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)

### Description

Added a new segment that pulls data from [Open Weather Map](https://openweathermap.org) to display current weather and temperature.

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


JanDeDobbeleer: I'm currently on holiday so I'll be reviewing this a bit slower than usual.

Patch:
diff --git a/docs/docs/segment-owm.md b/docs/docs/segment-owm.md
new file mode 100644
index 00000000..a85ba2c4
--- /dev/null
+++ b/docs/docs/segment-owm.md
@@ -0,0 +1,42 @@
+---
+id: owm
+title: Open Weather Map
+sidebar_label: Open Weather Map 
+---
+
+## What
+
+Shows the current weather of a given location.
+
+:::caution
+
+You **must** request an API key at the [Open Weather Map](https://openweathermap.org/price) website.
+The free tier for *Current weather and forecasts collection* is sufficient.
+
+:::
+
+## Sample Configuration
+
+```json
+{
+  "type": "owm",
+  "style": "powerline",
+  "powerline_symbol": "\uE0B0",
+  "foreground": "#ffffff",
+  "background": "#FF0000",
+  "properties": {
+    "apikey": "<YOUR_API_KEY>",
+    "location": "AMSTERDAM,NL",
+    "units": "metric"
+  }
+}
+```
+
+## Properties
+
+- apikey: `string` - Your apikey from [Open Weather Map](https://openweathermap.org)
+- location: `string` - The requested location.
+                        Formatted as <City,STATE,COUNTRY_CODE>. City name, state code and country code divided by comma.
+                        Please, refer to ISO 3166 for the state codes or country codes - defaults to `DE BILT,NL`
+- units: `string` - Units of measurement.
+Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit) - defaults to `standard`
diff --git a/docs/sidebars.js b/docs/sidebars.js
index e8d06455..a1da862e 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -44,6 +44,7 @@ module.exports = {
         "nbgv",
         "node",
         "os",
+        "owm",
         "path",
         "python",
         "root",
diff --git a/src/segment.go b/src/segment.go
index 58f6d1a4..35b30800 100644
--- a/src/segment.go
+++ b/src/segment.go
@@ -121,6 +121,7 @@ const (
 	Nbgv SegmentType = "nbgv"
 	// Rust writes the cargo version information if cargo.toml is present
 	Rust SegmentType = "rust"
+	OWM  SegmentType = "owm"
 )
 
 func (segment *Segment) string() string {
@@ -229,6 +230,7 @@ func (segment *Segment) background() string {
 func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
 	segment.env = env
 	functions := map[SegmentType]SegmentWriter{
+		OWM:           &owm{},
 		Session:       &session{},
 		Path:          &path{},
 		Git:           &git{},
diff --git a/src/segment_owd.go b/src/segment_owd.go
new file mode 100644
index 00000000..6a7ed280
--- /dev/null
+++ b/src/segment_owd.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type owm struct {
+	props       *properties
+	env         environmentInfo
+	temperature float64
+	weather     string
+}
+
+const (
+	APIKEY   Property = "apikey"
+	LOCATION Property = "location"
+	UNITS    Property = "units"
+)
+
+type weather struct {
+	ShortDescription string `json:"main"`
+	Description      string `json:"description"`
+	TypeID           string `json:"icon"`
+}
+type temperature struct {
+	Value float64 `json:"temp"`
+}
+
+type OWMDataResponse struct {
+	Data        []weather `json:"weather"`
+	temperature `json:"main"`
+}
+
+func (d *owm) enabled() bool {
+	err := d.setStatus()
+	return err == nil
+}
+
+func (d *owm) string() string {
+	return fmt.Sprintf("%s (%g\ue33e)", d.weather, d.temperature)
+}
+
+func (d *owm) setStatus() error {
+	apikey := d.props.getString(APIKEY, ".")
+	location := d.props.getString(LOCATION, "De Bilt,NL")
+	units := d.props.getString(UNITS, "standard")
+
+	url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", location, units, apikey)
+	body, err := d.env.doGet(url)
+	if err != nil {
+		return err
+	}
+	q := new(OWMDataResponse)
+	err = json.Unmarshal(body, &q)
+	if err != nil {
+		return err
+	}
+	d.temperature = q.temperature.Value
+	icon := ""
+	switch q.Data[0].TypeID {
+	case "01d":
+		icon = "\ufa98"
+	case "02d":
+		icon = "\ufa94"
+	case "03d":
+		icon = "\ue33d"
+	case "04d":
+		icon = "\ue312"
+	case "09d":
+		icon = "\ufa95"
+	case "10d":
+		icon = "\ue308"
+	case "11d":
+		icon = "\ue31d"
+	case "13d":
+		icon = "\ue31a"
+	case "50d":
+		icon = "\ue313"
+	}
+	d.weather = icon
+	return nil
+}
+
+func (d *owm) init(props *properties, env environmentInfo) {
+	d.props = props
+	d.env = env
+}
diff --git a/themes/schema.json b/themes/schema.json
index 6825fc8c..04dedf46 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -1518,6 +1518,41 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "owm" }
+            }
+          },
+          "then": {
+            "title": "Open Weather Map Segment",
+            "description": "Displays the current weather from the Open Weather Map system",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "apikey": {
+                    "type": "string",
+                    "title": "apikey",
+                    "description": "The apikey used for the api call (Required)",
+                    "default": "."
+                  },
+                  "location": {
+                    "type": "string",
+                    "title": "location",
+                    "description": "Location to use for the api call. Formatted as <City>,<STATE>,<COUNTRY_CODE>. City name, state code and country code divided by comma. Please, refer to ISO 3166 for the state codes or country codes.",
+                    "default": "De Bilt,NL"
+                  },
+                  "units": {
+                    "type": "string",
+                    "title": "units",
+                    "description": "Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit). Default is standard",
+                    "default": "standard"
+                  }
+                }
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
@@ -1652,4 +1687,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file


Test patch:
diff --git a/src/segment_owd_test.go b/src/segment_owd_test.go
new file mode 100644
index 00000000..11adc992
--- /dev/null
+++ b/src/segment_owd_test.go
@@ -0,0 +1,139 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOWDSegmentSingle(t *testing.T) {
+	cases := []struct {
+		Case            string
+		JSONResponse    string
+		ExpectedString  string
+		ExpectedEnabled bool
+		Error           error
+	}{
+		{
+			Case:            "Sunny Display",
+			JSONResponse:    `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
+			ExpectedString:  "\ufa98 (20\ue33e)",
+			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{}{
+				APIKEY:   "key",
+				LOCATION: "AMSTERDAM,NL",
+				UNITS:    "metric",
+			},
+		}
+
+		url := "http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key"
+
+		env.On("doGet", url).Return([]byte(tc.JSONResponse), tc.Error)
+
+		o := &owm{
+			props: props,
+			env:   env,
+		}
+
+		enabled := o.enabled()
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if !enabled {
+			continue
+		}
+
+		assert.Equal(t, tc.ExpectedString, o.string(), tc.Case)
+	}
+}
+
+func TestOWDSegmentIcons(t *testing.T) {
+	cases := []struct {
+		Case               string
+		IconID             string
+		ExpectedIconString string
+	}{
+		{
+			Case:               "Sunny Display",
+			IconID:             "01d",
+			ExpectedIconString: "\ufa98",
+		},
+		{
+			Case:               "Light clouds Display",
+			IconID:             "02d",
+			ExpectedIconString: "\ufa94",
+		},
+		{
+			Case:               "Cloudy Display",
+			IconID:             "03d",
+			ExpectedIconString: "\ue33d",
+		},
+		{
+			Case:               "Broken Clouds Display",
+			IconID:             "04d",
+			ExpectedIconString: "\ue312",
+		},
+		{
+			Case:               "Shower Rain Display",
+			IconID:             "09d",
+			ExpectedIconString: "\ufa95",
+		},
+		{
+			Case:               "Rain Display",
+			IconID:             "10d",
+			ExpectedIconString: "\ue308",
+		},
+		{
+			Case:               "Thunderstorm Display",
+			IconID:             "11d",
+			ExpectedIconString: "\ue31d",
+		},
+		{
+			Case:               "Snow Display",
+			IconID:             "13d",
+			ExpectedIconString: "\ue31a",
+		},
+		{
+			Case:               "Fog Display",
+			IconID:             "50d",
+			ExpectedIconString: "\ue313",
+		},
+	}
+
+	for _, tc := range cases {
+		env := &MockedEnvironment{}
+		props := &properties{
+			values: map[Property]interface{}{
+				APIKEY:   "key",
+				LOCATION: "AMSTERDAM,NL",
+				UNITS:    "metric",
+			},
+		}
+
+		url := "http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key"
+		response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20}}`, tc.IconID)
+		expectedString := fmt.Sprintf("%s (20\ue33e)", tc.ExpectedIconString)
+
+		env.On("doGet", url).Return([]byte(response), nil)
+
+		o := &owm{
+			props: props,
+			env:   env,
+		}
+
+		assert.Nil(t, o.setStatus())
+		assert.Equal(t, expectedString, o.string(), tc.Case)
+	}
+}