Skip to content

Commit 5120c8d

Browse files
authored
Battery: optimizer suggestion state icons (#31748)
1 parent 3bb943c commit 5120c8d

8 files changed

Lines changed: 209 additions & 11 deletions

File tree

assets/js/components/Battery/BatteryStatusCard.stories.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import BatteryStatusCard from "./BatteryStatusCard.vue";
2+
import { BATTERY_MODE } from "@/types/evcc";
23
import type { Meta, StoryFn } from "@storybook/vue3";
34

45
export default {
@@ -86,3 +87,58 @@ SuggestionAndForecast.args = {
8687
suggestion: { action: "charge" },
8788
forecast: { highest: { soc: 100, time: "2026-07-01T18:00:00+02:00", limit: true } },
8889
};
90+
91+
// grid of cards whose current mode (gauge) deliberately differs from the suggested action,
92+
// so the suggestion icon always signals a change.
93+
// current mode: charge/holdcharge/hold are locked modes; discharging is power-derived (no mode)
94+
const cards = [
95+
{
96+
title: "Sungrow",
97+
soc: 76,
98+
power: 0,
99+
capacity: 13.5,
100+
color: "#0BA631",
101+
controllable: true,
102+
batteryMode: BATTERY_MODE.CHARGE, // current: charge
103+
suggestion: { action: "normal", actionable: true },
104+
},
105+
{
106+
title: "Anker",
107+
soc: 40,
108+
power: 1200, // current: discharging
109+
capacity: 7.5,
110+
color: "#7FC41B",
111+
controllable: true,
112+
suggestion: { action: "hold", actionable: true },
113+
},
114+
{
115+
title: "Fox ESS",
116+
soc: 88,
117+
power: 0,
118+
capacity: 10.4,
119+
color: "#0FD0BF",
120+
controllable: true,
121+
batteryMode: BATTERY_MODE.HOLDCHARGE, // current: holdcharge
122+
suggestion: { action: "hold", actionable: true },
123+
},
124+
{
125+
title: "Huawei",
126+
soc: 30,
127+
power: 0,
128+
capacity: 5,
129+
color: "#4EABE6",
130+
controllable: true,
131+
batteryMode: BATTERY_MODE.HOLD, // current: hold
132+
suggestion: { action: "charge", actionable: true },
133+
},
134+
];
135+
136+
export const CurrentVsSuggested = () => ({
137+
components: { BatteryStatusCard },
138+
setup() {
139+
return { cards };
140+
},
141+
template: `<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 1rem">
142+
<BatteryStatusCard v-for="c in cards" :key="c.title" v-bind="c" />
143+
</div>`,
144+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import BatterySuggestionIcon from "./BatterySuggestionIcon.vue";
2+
import { ICON_SIZE } from "@/types/evcc";
3+
import type { Meta, StoryFn } from "@storybook/vue3";
4+
5+
const actions = ["normal", "hold", "charge", "holdcharge"] as const;
6+
7+
export default {
8+
title: "Battery/BatterySuggestionIcon",
9+
component: BatterySuggestionIcon,
10+
argTypes: {
11+
action: { control: "select", options: actions },
12+
size: { control: "select", options: Object.values(ICON_SIZE) },
13+
},
14+
} as Meta<typeof BatterySuggestionIcon>;
15+
16+
const Single: StoryFn<typeof BatterySuggestionIcon> = (args) => ({
17+
components: { BatterySuggestionIcon },
18+
setup() {
19+
return { args };
20+
},
21+
template: '<BatterySuggestionIcon v-bind="args" />',
22+
});
23+
24+
export const Single_ = Single.bind({});
25+
Single_.args = { action: "normal", size: ICON_SIZE.L };
26+
27+
// all states side by side, reusing the MaterialIcon grid layout
28+
export const AllStates = () => ({
29+
components: { BatterySuggestionIcon },
30+
setup() {
31+
return { actions };
32+
},
33+
template: `
34+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 20px;">
35+
<div v-for="action in actions" :key="action" style="display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 15px;">
36+
<BatterySuggestionIcon :action="action" size="xl" />
37+
<small style="font-family: monospace; color: #666; text-align: center; font-size: 12px;">{{ action }}</small>
38+
</div>
39+
</div>
40+
`,
41+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<template>
2+
<!-- normal: free charge/discharge -->
3+
<BatteryNormal v-if="action === 'normal'" :size="size" />
4+
<!-- charge: grid charge -->
5+
<shopicon-regular-powersupply
6+
v-else-if="action === 'charge'"
7+
:size="size"
8+
></shopicon-regular-powersupply>
9+
<!-- holdcharge: prevent charging -->
10+
<BatteryHoldCharge v-else-if="action === 'holdcharge'" :size="size" />
11+
<!-- hold: prevent discharging -->
12+
<BatteryHold v-else :size="size" />
13+
</template>
14+
15+
<script lang="ts">
16+
import { defineComponent, type PropType } from "vue";
17+
import "@h2d2/shopicons/es/regular/powersupply";
18+
import { ICON_SIZE } from "@/types/evcc";
19+
import type { BatterySuggestion } from "@/types/evcc";
20+
import BatteryNormal from "../MaterialIcon/BatteryNormal.vue";
21+
import BatteryHold from "../MaterialIcon/BatteryHold.vue";
22+
import BatteryHoldCharge from "../MaterialIcon/BatteryHoldCharge.vue";
23+
24+
export default defineComponent({
25+
name: "BatterySuggestionIcon",
26+
components: { BatteryNormal, BatteryHold, BatteryHoldCharge },
27+
props: {
28+
action: { type: String as PropType<BatterySuggestion["action"]>, required: true },
29+
size: { type: String as PropType<ICON_SIZE>, default: ICON_SIZE.S },
30+
},
31+
});
32+
</script>

assets/js/components/Battery/OptimizerInfo.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<template>
22
<div class="d-flex flex-column gap-2">
3-
<div v-if="recommendation" class="d-flex justify-content-between gap-3">
3+
<div v-if="suggestion && suggestionValue" class="d-flex justify-content-between gap-3">
44
<span class="d-flex align-items-center gap-2 fw-bold">
55
<Optimizer />
66
<router-link to="/optimize" class="evcc-default-text text-decoration-underline">
77
{{ $t("battery.optimizer.suggestion") }}
88
</router-link>
99
</span>
10-
<span class="text-muted text-end">{{ recommendation }}</span>
10+
<span class="d-flex align-items-center gap-1 text-muted text-end">
11+
{{ suggestionValue }}
12+
<BatterySuggestionIcon :action="suggestion.action" />
13+
</span>
1114
</div>
1215
<div v-if="forecast?.highest" class="d-flex justify-content-between gap-3">
1316
<span class="d-flex align-items-center gap-2 fw-bold">
@@ -36,19 +39,20 @@ import formatter from "@/mixins/formatter";
3639
import type { BatteryForecast, BatteryForecastPoint, BatterySuggestion } from "@/types/evcc";
3740
import BatteryIcon from "../Energyflow/BatteryIcon.vue";
3841
import Optimizer from "../MaterialIcon/Optimizer.vue";
42+
import BatterySuggestionIcon from "./BatterySuggestionIcon.vue";
3943
4044
// Optimizer rows: suggestion plus the battery high/low soc forecast.
4145
// Simple bold-label / muted-value layout.
4246
export default defineComponent({
4347
name: "OptimizerInfo",
44-
components: { BatteryIcon, Optimizer },
48+
components: { BatteryIcon, Optimizer, BatterySuggestionIcon },
4549
mixins: [formatter],
4650
props: {
4751
suggestion: { type: Object as PropType<BatterySuggestion | null>, default: null },
4852
forecast: { type: Object as PropType<BatteryForecast | null>, default: null },
4953
},
5054
computed: {
51-
recommendation(): string | null {
55+
suggestionValue(): string | null {
5256
const action = this.suggestion?.action;
5357
if (!action) return null;
5458
return this.$t(`battery.optimizer.action.${action}`);

assets/js/components/Battery/SocGauge.vue

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@
3535
:class="{ 'layer--active': isCharging || isDischarging }"
3636
:style="{ '--rotate': arrowRotate }"
3737
/>
38-
<Pause
39-
:size="ICON_SIZE.S"
38+
<BatteryHold
39+
:size="ICON_SIZE.M"
40+
class="layer"
41+
:class="{ 'layer--active': mode === BATTERY_MODE.HOLD }"
42+
/>
43+
<BatteryHoldCharge
44+
:size="ICON_SIZE.M"
4045
class="layer"
41-
:class="{
42-
'layer--active': mode === BATTERY_MODE.HOLD || mode === BATTERY_MODE.HOLDCHARGE,
43-
}"
46+
:class="{ 'layer--active': mode === BATTERY_MODE.HOLDCHARGE }"
4447
/>
4548
<Dot :size="ICON_SIZE.S" class="layer" :class="{ 'layer--active': isIdle }" />
4649
</span>
@@ -52,7 +55,8 @@ import { defineComponent, type PropType } from "vue";
5255
import "@h2d2/shopicons/es/regular/powersupply";
5356
import { BATTERY_MODE, ICON_SIZE } from "@/types/evcc";
5457
import ArrowDown from "../MaterialIcon/ArrowDown.vue";
55-
import Pause from "../MaterialIcon/Pause.vue";
58+
import BatteryHold from "../MaterialIcon/BatteryHold.vue";
59+
import BatteryHoldCharge from "../MaterialIcon/BatteryHoldCharge.vue";
5660
import Dot from "../MaterialIcon/Dot.vue";
5761
5862
// fixed ring geometry, computed once rather than per instance
@@ -72,7 +76,7 @@ const LOCKED_MODES: BATTERY_MODE[] = [
7276
// tweens instead of hard-swapping
7377
export default defineComponent({
7478
name: "SocGauge",
75-
components: { ArrowDown, Pause, Dot },
79+
components: { ArrowDown, BatteryHold, BatteryHoldCharge, Dot },
7680
props: {
7781
soc: { type: Number, default: 0 },
7882
color: { type: String, default: "" },
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<svg :style="svgStyle" viewBox="0 0 134 134">
3+
<path
4+
fill="currentColor"
5+
fill-rule="nonzero"
6+
d="M72.222,73.548l-0,21.73l16.111,-16.111c1.019,-1.019 2.315,-1.528 3.889,-1.528c1.574,0 2.87,0.509 3.889,1.528c1.019,1.019 1.528,2.315 1.528,3.889c0,1.574 -0.509,2.87 -1.528,3.889l-25.556,25.556c-1.111,1.111 -2.407,1.667 -3.889,1.667c-1.481,0 -2.778,-0.556 -3.889,-1.667l-25.556,-25.556c-1.019,-1.019 -1.528,-2.315 -1.528,-3.889c0,-1.574 0.509,-2.87 1.528,-3.889c1.019,-1.019 2.315,-1.528 3.889,-1.528c1.574,0 2.87,0.509 3.889,1.528l16.111,16.111l0,-32.841l-10.2,-10.2c-1.019,-1.019 -1.528,-2.315 -1.528,-3.889c0,-1.574 0.509,-2.87 1.528,-3.889c1.019,-1.019 2.315,-1.528 3.889,-1.528c1.574,0 2.87,0.509 3.889,1.528l23.734,23.734c1.019,1.019 1.528,2.315 1.528,3.889c0,1.574 -0.509,2.87 -1.528,3.889c-1.019,1.019 -2.315,1.528 -3.889,1.528c-1.574,0 -2.87,-0.509 -3.889,-1.528l-2.422,-2.422Zm-11.111,-41.833l0,-6.993c0,-1.574 0.533,-2.893 1.6,-3.956c1.067,-1.063 2.385,-1.596 3.956,-1.6c1.57,-0.004 2.891,0.53 3.961,1.6c1.07,1.07 1.602,2.389 1.594,3.956l-0,18.104l-11.111,-11.111Z"
7+
/>
8+
</svg>
9+
</template>
10+
11+
<script lang="ts">
12+
import { defineComponent } from "vue";
13+
import icon from "@/mixins/icon";
14+
15+
export default defineComponent({
16+
name: "BatteryHold",
17+
mixins: [icon],
18+
});
19+
</script>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<svg :style="svgStyle" viewBox="0 0 134 134">
3+
<path
4+
fill="currentColor"
5+
fill-rule="nonzero"
6+
d="M61.111,52.549l0,-14.493l-16.111,16.111c-1.019,1.019 -2.315,1.528 -3.889,1.528c-1.574,0 -2.87,-0.509 -3.889,-1.528c-1.019,-1.019 -1.528,-2.315 -1.528,-3.889c0,-1.574 0.509,-2.87 1.528,-3.889l25.556,-25.556c1.111,-1.111 2.407,-1.667 3.889,-1.667c1.481,0 2.778,0.556 3.889,1.667l25.556,25.556c1.019,1.019 1.528,2.315 1.528,3.889c0,1.574 -0.509,2.87 -1.528,3.889c-1.019,1.019 -2.315,1.528 -3.889,1.528c-1.574,0 -2.87,-0.509 -3.889,-1.528l-16.111,-16.111l0,25.604l-11.111,-11.111Zm11.111,26.667l-0,29.396c0,1.574 -0.533,2.893 -1.6,3.956c-1.067,1.063 -2.385,1.596 -3.956,1.6c-1.57,0.004 -2.891,-0.53 -3.961,-1.6c-1.07,-1.07 -1.602,-2.389 -1.594,-3.956l0,-40.507l11.111,11.111Z"
7+
/>
8+
<path
9+
fill="currentColor"
10+
fill-rule="nonzero"
11+
d="M82.422,96.804c-1.019,1.019 -2.315,1.528 -3.889,1.528c-1.574,0 -2.87,-0.509 -3.889,-1.528l-23.734,-23.734c-1.019,-1.019 -1.528,-2.315 -1.528,-3.889c0,-1.574 0.509,-2.87 1.528,-3.889c1.019,-1.019 2.315,-1.528 3.889,-1.528c1.574,0 2.87,0.509 3.889,1.528l23.734,23.734c1.019,1.019 1.528,2.315 1.528,3.889c0,1.574 -0.509,2.87 -1.528,3.889"
12+
/>
13+
</svg>
14+
</template>
15+
16+
<script lang="ts">
17+
import { defineComponent } from "vue";
18+
import icon from "@/mixins/icon";
19+
20+
export default defineComponent({
21+
name: "BatteryHoldCharge",
22+
mixins: [icon],
23+
});
24+
</script>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<svg :style="svgStyle" viewBox="0 0 24 24">
3+
<path
4+
fill="currentColor"
5+
d="M9 13q-.425 0-.712-.288T8 12V5.825L6.125 7.7q-.275.275-.687.275T4.725 7.7q-.3-.3-.3-.712t.3-.713L8.3 2.7q.15-.15.325-.213T9 2.425t.375.062t.325.213l3.6 3.6q.3.3.287.7t-.312.7q-.3.275-.7.288t-.7-.288L10 5.825V12q0 .425-.288.713T9 13m6 8.575q-.2 0-.375-.062T14.3 21.3l-3.6-3.6q-.3-.3-.287-.7t.312-.7q.3-.275.7-.288t.7.288L14 18.175V12q0-.425.288-.712T15 11t.713.288T16 12v6.175l1.875-1.875q.275-.275.688-.275t.712.275q.3.3.3.713t-.3.712L15.7 21.3q-.15.15-.325.213t-.375.062"
6+
/>
7+
</svg>
8+
</template>
9+
10+
<script lang="ts">
11+
import { defineComponent } from "vue";
12+
import icon from "@/mixins/icon";
13+
14+
export default defineComponent({
15+
name: "BatteryNormal",
16+
mixins: [icon],
17+
});
18+
</script>

0 commit comments

Comments
 (0)