Repository: evcc-io/evcc
Pull request #8937 (2023-07-12 09:40:56)

i18n: energy format in subunit
- formatting energy price in currency submit where appropriate: `38 rp/kWh` instead of `0,38 CHF/kWh`
- minor layout fixes for long translation texts


Patch:
diff --git a/README.md b/README.md
index 9b188a5e..1aca7472 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ To build evcc from source, [Go][1] 1.20 and [Node][2] 18 are required.
 Build and run go backend. The UI becomes available at http://127.0.0.1:7070/
 
 ```sh
+make install-ui
 make ui
 make install
 make
diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue
index aff148ae..c094ef91 100644
--- a/assets/js/components/BatterySettingsModal.vue
+++ b/assets/js/components/BatterySettingsModal.vue
@@ -364,7 +364,6 @@ export default {
 		bufferStartOptionToSoc(option) {
 			const bufferSoc = this.selectedBufferSoc;
 			const bufferHeight = 100 - bufferSoc;
-			console.log("bufferStartOptionToSoc", { bufferSoc, bufferHeight, option });
 			switch (option) {
 				case "low":
 					return bufferSoc + bufferHeight * 0.2;
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index c0f2b67d..3164d0dc 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -23,20 +23,29 @@
 		<div class="details" :style="{ height: detailsHeight }">
 			<div ref="detailsInner" class="details-inner row">
 				<div class="col-12 d-flex justify-content-between pt-2 mb-4">
-					<div class="d-flex flex-nowrap align-items-center">
+					<div class="d-flex flex-nowrap align-items-center text-truncate">
 						<span class="color-self me-2"
 							><shopicon-filled-square></shopicon-filled-square
 						></span>
-						<span>{{ $t("main.energyflow.selfConsumption") }}</span>
+						<span class="text-nowrap text-truncate">
+							{{ $t("main.energyflow.selfConsumption") }}
+						</span>
 					</div>
-					<div v-if="gridImport > 0" class="d-flex flex-nowrap align-items-center">
-						<span>{{ $t("main.energyflow.gridImport") }}</span>
+					<div
+						v-if="gridImport > 0"
+						class="d-flex flex-nowrap align-items-center text-truncate"
+					>
+						<span class="text-nowrap text-truncate">
+							{{ $t("main.energyflow.gridImport") }}
+						</span>
 						<span class="color-grid ms-2"
 							><shopicon-filled-square></shopicon-filled-square
 						></span>
 					</div>
-					<div v-else class="d-flex flex-nowrap align-items-center">
-						<span>{{ $t("main.energyflow.pvExport") }}</span>
+					<div v-else class="d-flex flex-nowrap align-items-center text-truncate">
+						<span class="text-nowrap text-truncate">
+							{{ $t("main.energyflow.pvExport") }}
+						</span>
 						<span class="color-export ms-2"
 							><shopicon-filled-square></shopicon-filled-square
 						></span>
diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue
index 6ec3e20c..50c41bf3 100644
--- a/assets/js/components/Energyflow/EnergyflowEntry.vue
+++ b/assets/js/components/Energyflow/EnergyflowEntry.vue
@@ -5,7 +5,7 @@
 			<VehicleIcon v-else-if="isVehicle" :names="vehicleIcons" />
 			<component :is="`shopicon-regular-${icon}`" v-else></component>
 		</span>
-		<span class="text-nowrap flex-grow-1 ms-3">
+		<span class="text-nowrap flex-grow-1 ms-3 text-truncate">
 			{{ name }}
 		</span>
 		<span class="text-end text-nowrap ps-1 fw-bold d-flex">
diff --git a/assets/js/mixins/formatter.js b/assets/js/mixins/formatter.js
index f7f55fcb..624a4e55 100644
--- a/assets/js/mixins/formatter.js
+++ b/assets/js/mixins/formatter.js
@@ -1,3 +1,19 @@
+// list of currencies where energy price should be displayed in subunits (factor 100)
+const ENERGY_PRICE_IN_SUBUNIT = {
+  AUD: "c", // Australian cent
+  BGN: "st", // Bulgarian stotinka
+  BRL: "¢", // Brazilian centavo
+  CAD: "¢", // Canadian cent
+  CHF: "rp", // Swiss Rappen
+  CNY: "f", // Chinese fen
+  EUR: "ct", // Euro cent
+  GBP: "p", // GB pence
+  ILS: "ag", // Israeli agora
+  NZD: "c", // New Zealand cent
+  PLN: "gr", // Polish grosz
+  USD: "¢", // US cent
+};
+
 export default {
   data: function () {
     return {
@@ -201,7 +217,7 @@ export default {
       let value = amout;
       let minimumFractionDigits = 1;
       let maximumFractionDigits = 3;
-      if (["EUR", "USD"].includes(currency)) {
+      if (ENERGY_PRICE_IN_SUBUNIT[currency]) {
         value *= 100;
         minimumFractionDigits = 1;
         maximumFractionDigits = 1;
@@ -217,10 +233,7 @@ export default {
       return price;
     },
     pricePerKWhUnit: function (currency = "EUR", short = false) {
-      let unit = currency;
-      if (["EUR", "USD"].includes(currency)) {
-        unit = "ct";
-      }
+      const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || currency;
       return `${unit}${short ? "" : "/kWh"}`;
     },
     fmtTimeAgo: function (elapsed) {
diff --git a/templates/definition/meter/youless.yaml b/templates/definition/meter/youless.yaml
new file mode 100644
index 00000000..68d076b5
--- /dev/null
+++ b/templates/definition/meter/youless.yaml
@@ -0,0 +1,22 @@
+template: youless
+products:
+  - brand: Youless
+    description:
+      generic: Energy Monitor
+params:
+  - name: usage
+    choice: ["grid"]
+  - name: host
+    required: true
+render: |
+  type: custom
+  power:
+    source: http
+    uri: http://{{ .host }}/e
+    method: GET
+    jq: .[0]|.pwr
+  energy:
+    source: http
+    uri: http://{{ .host }}/e
+    method: GET
+    jq: .[0]|.p1+.p2
diff --git a/templates/docs/meter/youless_0.yaml b/templates/docs/meter/youless_0.yaml
new file mode 100644
index 00000000..ee9f7460
--- /dev/null
+++ b/templates/docs/meter/youless_0.yaml
@@ -0,0 +1,10 @@
+product:
+  brand: Youless
+  description: Energy Monitor
+render:
+  - usage: grid
+    default: |
+      type: template
+      template: youless
+      usage: grid
+      host: 192.0.2.2 # IP-Adresse oder Hostname
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index ed5dc7fe..ed42d065 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -127,6 +127,7 @@
   "VARTA",
   "Victron",
   "Volkszähler",
+  "Youless",
   "ZCS Azzurro"
  ],
  "PVBattery": [
diff --git a/vehicle/porsche.go b/vehicle/porsche.go
index acb0e5e2..b4288990 100644
--- a/vehicle/porsche.go
+++ b/vehicle/porsche.go
@@ -65,13 +65,7 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		return nil, errors.New("vehicle is not paired with the My Porsche account")
 	}
 
-	// get eMobility capabilities
-	emobIdentity := porsche.NewIdentity(log)
-	if err := emobIdentity.Login(porsche.EmobilityOAuth2Config, cc.User, cc.Password); err != nil {
-		return nil, fmt.Errorf("emobility login failed: %w", err)
-	}
-
-	emobApi := porsche.NewEmobilityAPI(log, emobIdentity)
+	emobApi := porsche.NewEmobilityAPI(log, identity)
 	capabilities, err := emobApi.Capabilities(cc.VIN)
 	if err != nil {
 		return nil, err
diff --git a/vehicle/porsche/api_emobility.go b/vehicle/porsche/api_emobility.go
index 7d8deacc..ba7935dc 100644
--- a/vehicle/porsche/api_emobility.go
+++ b/vehicle/porsche/api_emobility.go
@@ -27,7 +27,7 @@ func NewEmobilityAPI(log *util.Logger, identity oauth2.TokenSource) *EmobilityAP
 			Base:   v.Client.Transport,
 		},
 		Decorator: transport.DecorateHeaders(map[string]string{
-			"apikey": EmobilityOAuth2Config.ClientID,
+			"apikey": ClientID,
 		}),
 	}
 
diff --git a/vehicle/porsche/identity.go b/vehicle/porsche/identity.go
index 09aa7b98..4609d0e1 100644
--- a/vehicle/porsche/identity.go
+++ b/vehicle/porsche/identity.go
@@ -20,28 +20,20 @@ import (
 
 const (
 	OAuthURI = "https://identity.porsche.com"
+	ClientID = "UYsK00My6bCqJdbQhTQ0PbWmcSdIAMig"
 )
 
 // https://identity.porsche.com/.well-known/openid-configuration
 var (
-	endpoint = oauth2.Endpoint{
-		AuthURL:   OAuthURI + "/authorize",
-		TokenURL:  OAuthURI + "/oauth/token",
-		AuthStyle: oauth2.AuthStyleInParams,
-	}
-
 	OAuth2Config = &oauth2.Config{
-		ClientID:    "UYsK00My6bCqJdbQhTQ0PbWmcSdIAMig",
+		ClientID:    ClientID,
 		RedirectURL: "https://my.porsche.com/",
-		Endpoint:    endpoint,
-		Scopes:      []string{"openid", "offline_access"},
-	}
-
-	EmobilityOAuth2Config = &oauth2.Config{
-		ClientID:    OAuth2Config.ClientID,
-		RedirectURL: "https://my.porsche.com/myservices/auth/auth.html",
-		Endpoint:    endpoint,
-		Scopes:      []string{"openid", "offline_access"},
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   OAuthURI + "/authorize",
+			TokenURL:  OAuthURI + "/oauth/token",
+			AuthStyle: oauth2.AuthStyleInParams,
+		},
+		Scopes: []string{"openid", "offline_access"},
 	}
 )
 


Test patch:
diff --git a/assets/js/mixins/formatter.test.js b/assets/js/mixins/formatter.test.js
index b4b804ec..2162c451 100644
--- a/assets/js/mixins/formatter.test.js
+++ b/assets/js/mixins/formatter.test.js
@@ -31,9 +31,10 @@ describe("fmtPricePerKWh", () => {
   test("should format with units", () => {
     expect(fmt.fmtPricePerKWh(0.2, "EUR")).eq("20,0 ct/kWh");
     expect(fmt.fmtPricePerKWh(0.2, "EUR", true)).eq("20,0 ct");
-    expect(fmt.fmtPricePerKWh(0.234, "USD")).eq("23,4 ct/kWh");
+    expect(fmt.fmtPricePerKWh(0.234, "USD")).eq("23,4 ¢/kWh");
     expect(fmt.fmtPricePerKWh(1234, "SEK")).eq("1.234,0 SEK/kWh");
     expect(fmt.fmtPricePerKWh(0.2, "EUR", false, false)).eq("20,0");
+    expect(fmt.fmtPricePerKWh(0.123, "CHF")).eq("12,3 rp/kWh");
   });
 });
 
@@ -41,8 +42,9 @@ describe("pricePerKWhUnit", () => {
   test("should return correct unit", () => {
     expect(fmt.pricePerKWhUnit("EUR")).eq("ct/kWh");
     expect(fmt.pricePerKWhUnit("EUR", true)).eq("ct");
-    expect(fmt.pricePerKWhUnit("USD")).eq("ct/kWh");
+    expect(fmt.pricePerKWhUnit("USD")).eq("¢/kWh");
     expect(fmt.pricePerKWhUnit("SEK")).eq("SEK/kWh");
     expect(fmt.pricePerKWhUnit("SEK", true)).eq("SEK");
+    expect(fmt.pricePerKWhUnit("CHF")).eq("rp/kWh");
   });
 });