mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-02-21 02:55:37 -08:00
feat: carbon intensity segment
This commit is contained in:
parent
092a0f9bcd
commit
5655bb4e6d
|
@ -108,6 +108,8 @@ const (
|
|||
BREWFATHER SegmentType = "brewfather"
|
||||
// Buf segment writes the active buf version
|
||||
BUF SegmentType = "buf"
|
||||
// CARBONINTENSITY writes the actual and forecast carbon intensity in gCO2/kWh
|
||||
CARBONINTENSITY SegmentType = "carbonintensity"
|
||||
// cds (SAP CAP) version
|
||||
CDS SegmentType = "cds"
|
||||
// Cloud Foundry segment
|
||||
|
@ -263,6 +265,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
|
|||
BATTERY: func() SegmentWriter { return &segments.Battery{} },
|
||||
BREWFATHER: func() SegmentWriter { return &segments.Brewfather{} },
|
||||
BUF: func() SegmentWriter { return &segments.Buf{} },
|
||||
CARBONINTENSITY: func() SegmentWriter { return &segments.CarbonIntensity{} },
|
||||
CDS: func() SegmentWriter { return &segments.Cds{} },
|
||||
CF: func() SegmentWriter { return &segments.Cf{} },
|
||||
CFTARGET: func() SegmentWriter { return &segments.CfTarget{} },
|
||||
|
|
155
src/segments/carbon_intensity.go
Normal file
155
src/segments/carbon_intensity.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/platform"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/properties"
|
||||
)
|
||||
|
||||
type CarbonIntensity struct {
|
||||
props properties.Properties
|
||||
env platform.Environment
|
||||
|
||||
TrendIcon string
|
||||
|
||||
CarbonIntensityData
|
||||
}
|
||||
|
||||
type CarbonIntensityResponse struct {
|
||||
Data []CarbonIntensityPeriod `json:"data"`
|
||||
}
|
||||
|
||||
type CarbonIntensityPeriod struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Intensity *CarbonIntensityData `json:"intensity"`
|
||||
}
|
||||
|
||||
type CarbonIntensityData struct {
|
||||
Forecast Number `json:"forecast"`
|
||||
Actual Number `json:"actual"`
|
||||
Index Index `json:"index"`
|
||||
}
|
||||
|
||||
type Number int
|
||||
|
||||
func (n Number) String() string {
|
||||
if n == 0 {
|
||||
return "??"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
type Index string
|
||||
|
||||
func (i Index) Icon() string {
|
||||
switch i {
|
||||
case "very low":
|
||||
return "↓↓"
|
||||
case "low":
|
||||
return "↓"
|
||||
case "moderate":
|
||||
return "•"
|
||||
case "high":
|
||||
return "↑"
|
||||
case "very high":
|
||||
return "↑↑"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) Enabled() bool {
|
||||
err := d.setStatus()
|
||||
|
||||
if err != nil {
|
||||
d.env.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) Template() string {
|
||||
return " CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} "
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) updateCache(responseBody []byte, url string, cacheTimeoutInMinutes int) {
|
||||
if cacheTimeoutInMinutes > 0 {
|
||||
d.env.Cache().Set(url, string(responseBody), cacheTimeoutInMinutes)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) getResult() (*CarbonIntensityResponse, error) {
|
||||
cacheTimeoutInMinutes := d.props.GetInt(properties.CacheTimeout, properties.DefaultCacheTimeout)
|
||||
|
||||
response := new(CarbonIntensityResponse)
|
||||
url := "https://api.carbonintensity.org.uk/intensity"
|
||||
|
||||
if cacheTimeoutInMinutes > 0 {
|
||||
cachedValue, foundInCache := d.env.Cache().Get(url)
|
||||
|
||||
if foundInCache {
|
||||
err := json.Unmarshal([]byte(cachedValue), response)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
// If there was an error, just fall through to refetching
|
||||
}
|
||||
}
|
||||
|
||||
httpTimeout := d.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
|
||||
|
||||
body, err := d.env.HTTPRequest(url, nil, httpTimeout)
|
||||
if err != nil {
|
||||
d.updateCache(body, url, cacheTimeoutInMinutes)
|
||||
return new(CarbonIntensityResponse), err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
d.updateCache(body, url, cacheTimeoutInMinutes)
|
||||
return new(CarbonIntensityResponse), err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) setStatus() error {
|
||||
response, err := d.getResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(response.Data) == 0 {
|
||||
d.Actual = 0
|
||||
d.Forecast = 0
|
||||
d.Index = "??"
|
||||
d.TrendIcon = "→"
|
||||
return nil
|
||||
}
|
||||
|
||||
d.CarbonIntensityData = *response.Data[0].Intensity
|
||||
|
||||
if d.Forecast > d.Actual {
|
||||
d.TrendIcon = "↗"
|
||||
}
|
||||
|
||||
if d.Forecast < d.Actual {
|
||||
d.TrendIcon = "↘"
|
||||
}
|
||||
|
||||
if d.Forecast == d.Actual || d.Actual == 0 || d.Forecast == 0 {
|
||||
d.TrendIcon = "→"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CarbonIntensity) Init(props properties.Properties, env platform.Environment) {
|
||||
d.props = props
|
||||
d.env = env
|
||||
}
|
266
src/segments/carbon_intensity_test.go
Normal file
266
src/segments/carbon_intensity_test.go
Normal file
|
@ -0,0 +1,266 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/mock"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/properties"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
mock2 "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
CARBONINTENSITYURL = "https://api.carbonintensity.org.uk/intensity"
|
||||
)
|
||||
|
||||
func TestCarbonIntensitySegmentSingle(t *testing.T) {
|
||||
cases := []struct {
|
||||
Case string
|
||||
HasError bool
|
||||
HasData bool
|
||||
Actual int
|
||||
Forecast int
|
||||
Index string
|
||||
ExpectedString string
|
||||
ExpectedEnabled bool
|
||||
Template string
|
||||
}{
|
||||
{
|
||||
Case: "Very Low, Going Down",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 20,
|
||||
Forecast: 10,
|
||||
Index: "very low",
|
||||
ExpectedString: "CO₂ ↓↓20 ↘ 10",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Very Low, Staying Same",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 20,
|
||||
Forecast: 20,
|
||||
Index: "very low",
|
||||
ExpectedString: "CO₂ ↓↓20 → 20",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Very Low, Going Up",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 20,
|
||||
Forecast: 30,
|
||||
Index: "very low",
|
||||
ExpectedString: "CO₂ ↓↓20 ↗ 30",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Low, Going Down",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 100,
|
||||
Forecast: 50,
|
||||
Index: "low",
|
||||
ExpectedString: "CO₂ ↓100 ↘ 50",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Low, Staying Same",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 100,
|
||||
Forecast: 100,
|
||||
Index: "low",
|
||||
ExpectedString: "CO₂ ↓100 → 100",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Low, Going Up",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 100,
|
||||
Forecast: 150,
|
||||
Index: "low",
|
||||
ExpectedString: "CO₂ ↓100 ↗ 150",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Moderate, Going Down",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 150,
|
||||
Forecast: 100,
|
||||
Index: "moderate",
|
||||
ExpectedString: "CO₂ •150 ↘ 100",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Moderate, Staying Same",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 150,
|
||||
Forecast: 150,
|
||||
Index: "moderate",
|
||||
ExpectedString: "CO₂ •150 → 150",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Moderate, Going Up",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 150,
|
||||
Forecast: 200,
|
||||
Index: "moderate",
|
||||
ExpectedString: "CO₂ •150 ↗ 200",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "High, Going Down",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 200,
|
||||
Forecast: 150,
|
||||
Index: "high",
|
||||
ExpectedString: "CO₂ ↑200 ↘ 150",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "High, Staying Same",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 200,
|
||||
Forecast: 200,
|
||||
Index: "high",
|
||||
ExpectedString: "CO₂ ↑200 → 200",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "High, Going Up",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 200,
|
||||
Forecast: 300,
|
||||
Index: "high",
|
||||
ExpectedString: "CO₂ ↑200 ↗ 300",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Missing Actual",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 0, // Missing data will be parsed to the default value of 0
|
||||
Forecast: 300,
|
||||
Index: "high",
|
||||
ExpectedString: "CO₂ ↑?? → 300",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Missing Forecast",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 200,
|
||||
Forecast: 0, // Missing data will be parsed to the default value of 0
|
||||
Index: "high",
|
||||
ExpectedString: "CO₂ ↑200 → ??",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Missing Index",
|
||||
HasError: false,
|
||||
HasData: true,
|
||||
Actual: 200,
|
||||
Forecast: 300,
|
||||
Index: "", // Missing data will be parsed to the default value of ""
|
||||
ExpectedString: "CO₂ 200 ↗ 300",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Missing Data",
|
||||
HasError: false,
|
||||
HasData: false,
|
||||
Actual: 0,
|
||||
Forecast: 0,
|
||||
Index: "",
|
||||
ExpectedString: "CO₂ ?? → ??",
|
||||
ExpectedEnabled: true,
|
||||
},
|
||||
{
|
||||
Case: "Error",
|
||||
HasError: true,
|
||||
HasData: false,
|
||||
Actual: 0,
|
||||
Forecast: 0,
|
||||
Index: "",
|
||||
ExpectedString: "",
|
||||
ExpectedEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
env := &mock.MockedEnvironment{}
|
||||
var props = properties.Map{
|
||||
properties.HTTPTimeout: 5000,
|
||||
properties.CacheTimeout: 0,
|
||||
}
|
||||
|
||||
jsonResponse := fmt.Sprintf(
|
||||
`{ "data": [ { "from": "2023-10-27T12:30Z", "to": "2023-10-27T13:00Z", "intensity": { "forecast": %d, "actual": %d, "index": "%s" } } ] }`,
|
||||
tc.Forecast, tc.Actual, tc.Index,
|
||||
)
|
||||
|
||||
if !tc.HasData {
|
||||
jsonResponse = `{ "data": [] }`
|
||||
}
|
||||
|
||||
if tc.HasError {
|
||||
jsonResponse = `{ "error": "Something went wrong" }`
|
||||
}
|
||||
|
||||
responseError := errors.New("Something went wrong")
|
||||
if !tc.HasError {
|
||||
responseError = nil
|
||||
}
|
||||
|
||||
env.On("HTTPRequest", CARBONINTENSITYURL).Return([]byte(jsonResponse), responseError)
|
||||
env.On("Error", mock2.Anything)
|
||||
|
||||
d := &CarbonIntensity{
|
||||
props: props,
|
||||
env: env,
|
||||
}
|
||||
|
||||
enabled := d.Enabled()
|
||||
assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if tc.Template == "" {
|
||||
tc.Template = d.Template()
|
||||
}
|
||||
assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, d), tc.Case)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarbonIntensitySegmentFromCache(t *testing.T) {
|
||||
response := `{ "data": [ { "from": "2023-10-27T12:30Z", "to": "2023-10-27T13:00Z", "intensity": { "forecast": 199, "actual": 193, "index": "moderate" } } ] }`
|
||||
expectedString := "CO₂ •193 ↗ 199"
|
||||
|
||||
env := &mock.MockedEnvironment{}
|
||||
cache := &mock.MockedCache{}
|
||||
|
||||
d := &CarbonIntensity{
|
||||
props: properties.Map{},
|
||||
env: env,
|
||||
}
|
||||
cache.On("Get", CARBONINTENSITYURL).Return(response, true)
|
||||
cache.On("Set").Return()
|
||||
env.On("Cache").Return(cache)
|
||||
|
||||
assert.Nil(t, d.setStatus())
|
||||
assert.Equal(t, expectedString, renderTemplate(env, d.Template(), d), "should return the cached response")
|
||||
}
|
|
@ -254,6 +254,7 @@
|
|||
"azfunc",
|
||||
"angular",
|
||||
"battery",
|
||||
"carbonintensity",
|
||||
"command",
|
||||
"connection",
|
||||
"crystal",
|
||||
|
@ -608,6 +609,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "carbonintensity"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"title": "Carbon Intensity Segment",
|
||||
"description": "Displays the actual and forecast carbon intensity in gCO2/kWh using the Carbon Intensity API",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"properties": {
|
||||
"http_timeout": {
|
||||
"$ref": "#/definitions/http_timeout"
|
||||
},
|
||||
"cache_timeout": {
|
||||
"$ref": "#/definitions/cache_timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
|
|
85
website/docs/segments/carbonintensity.mdx
Normal file
85
website/docs/segments/carbonintensity.mdx
Normal file
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
id: carbonintensity
|
||||
title: Carbon Intensity
|
||||
sidebar_label: Carbon Intensity
|
||||
---
|
||||
|
||||
## What
|
||||
|
||||
Shows the actual and forecast carbon intensity in gCO2/kWh using data from the [Carbon Intensity API][carbonintensity-api].
|
||||
|
||||
:::note
|
||||
Note that this segment only provides data for Great Britain at the moment. Support for other countries may become available in the future.
|
||||
:::
|
||||
|
||||
## Sample Configuration
|
||||
|
||||
:::caution
|
||||
The API can be slow. It's recommended to set the `http_timeout` property to a large value (e.g. `5000`).
|
||||
:::
|
||||
|
||||
import Config from "@site/src/components/Config.js";
|
||||
|
||||
<Config
|
||||
data={{
|
||||
type: "carbonintensity",
|
||||
style: "powerline",
|
||||
powerline_symbol: "\uE0B0",
|
||||
foreground: "#000000",
|
||||
background: "#ffffff",
|
||||
background_templates: [
|
||||
'{{if eq "very low" .Index}}#a3e635{{end}}',
|
||||
'{{if eq "low" .Index}}#bef264{{end}}',
|
||||
'{{if eq "moderate" .Index}}#fbbf24{{end}}',
|
||||
'{{if eq "high" .Index}}#ef4444{{end}}',
|
||||
'{{if eq "very high" .Index}}#dc2626{{end}}',
|
||||
],
|
||||
template:
|
||||
" CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} ",
|
||||
properties: {
|
||||
http_timeout: 5000,
|
||||
cache_timeout: 10,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## Properties
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `http_timeout` | `int` | Timeout (in milliseconds) for HTTP requests. The default is 20ms, but you may need to set this to as high as 5000ms to handle slow API requests. |
|
||||
| `cache_timeout` | `int` | Timeout (in minutes) for the response cache. The default is 10m. A value of 0 disables the cache. |
|
||||
|
||||
## Template ([info][templates])
|
||||
|
||||
:::note default template
|
||||
|
||||
```template
|
||||
CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `.Forecast` | `Number` | The forecast carbon intensity in gCO2/kWh. Equal to `0` if no data is available. |
|
||||
| `.Actual` | `Number` | The actual carbon intensity in gCO2/kWh. Equal to `0` if no data is available. |
|
||||
| `.Index` | `Index` | A rating of the current carbon intensity. Possible values are `"very low"`, `"low"`, `"moderate"`, `"high"`, or `"very high"`. Equal to `"??"` if no data is available. |
|
||||
| `.TrendIcon` | `string` | An icon representation of the predicted trend in carbon intensity based on the Actual and Forecast values. Possible values are `"↗"`, `"↘"`, or `"→"`. |
|
||||
|
||||
#### Number
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------- | -------- | ---------------------------------- |
|
||||
| `.String` | `string` | string representation of the value |
|
||||
|
||||
#### Index
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | -------- | -------------------------------- |
|
||||
| `.Icon` | `string` | icon representation of the value |
|
||||
|
||||
[templates]: /docs/configuration/templates
|
||||
[carbonintensity-api]: https://carbon-intensity.github.io/api-definitions
|
|
@ -59,6 +59,7 @@ module.exports = {
|
|||
"segments/battery",
|
||||
"segments/brewfather",
|
||||
"segments/buf",
|
||||
"segments/carbonintensity",
|
||||
"segments/cds",
|
||||
"segments/cf",
|
||||
"segments/cftarget",
|
||||
|
|
Loading…
Reference in a new issue