Optimizer: add feasible status (#32632)

This commit is contained in:
andig 2026-08-09 10:37:22 +02:00 • committed by GitHub
parent b29b49e305
commit d09a952cba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 32 additions and 17 deletions

View file

@ -16,7 +16,7 @@
import { defineComponent, type PropType } from "vue";
import Tooltip from "bootstrap/js/dist/tooltip";
type Variant = "success" | "warning" | "muted";
type Variant = "success" | "warning" | "danger" | "muted";
export default defineComponent({
name: "StatusIndicator",
@ -34,6 +34,8 @@ export default defineComponent({
return "bg-success";
case "warning":
return "bg-warning";
case "danger":
return "bg-danger";
default:
return "border border-secondary";
}

View file

@ -41,6 +41,10 @@ Payment.args = { ...base, status: OptimizationStatus.OPTIMAL, netCost: 12.4 };
export const Credit = Template.bind({});
Credit.args = { ...base, status: OptimizationStatus.OPTIMAL, netCost: -3.8 };
// usable but not proven optimal plan
export const Feasible = Template.bind({});
Feasible.args = { ...base, status: OptimizationStatus.FEASIBLE, netCost: 12.4 };
// solver could not produce a plan
export const Infeasible = Template.bind({});
Infeasible.args = { ...base, status: OptimizationStatus.INFEASIBLE, netCost: 0 };

View file

@ -11,7 +11,9 @@
<div class="field-caption small evcc-gray mt-1">always, fixed</div>
</div>
<div class="field-value gap-2">
<span class="large fw-bold text-lowercase evcc-default-text">Lowest cost</span>
<span class="value-text fw-bold text-lowercase evcc-default-text"
>Lowest cost</span
>
<LockIcon class="value-icon" />
</div>
</div>
@ -32,7 +34,7 @@
@change="onStrategyChange"
>
<span
class="large fw-bold text-lowercase evcc-default-text secondary-goal"
class="value-text fw-bold text-lowercase evcc-default-text secondary-goal"
>{{ secondaryGoalLabel }}</span
>
</CustomSelect>
@ -60,7 +62,7 @@
</div>
<div class="field-value gap-2">
<StatusIndicator :variant="statusVariant">
<span class="large fw-bold text-lowercase evcc-default-text">{{
<span class="value-text fw-bold text-lowercase evcc-default-text">{{
status
}}</span>
</StatusIndicator>
@ -88,7 +90,7 @@
</div>
<div class="field-value gap-2">
<span
class="fs-4 fw-bold tabular"
class="value-text fw-bold tabular"
:class="isCredit ? 'text-primary' : 'evcc-default-text'"
>
{{ netCostDisplay }}
@ -132,6 +134,7 @@ const STRATEGY_LABELS: Record<string, string> = {
const STATUS_TOOLTIP =
"The optimizer result:<br><br>" +
"<strong>Optimal</strong>: the best plan was found.<br>" +
"<strong>Feasible</strong>: a usable plan was found, but not proven optimal.<br>" +
"<strong>Infeasible</strong>: the constraints cannot all be met.<br>" +
"<strong>Not solved</strong>: optimization hasn't finished yet.";
@ -180,13 +183,15 @@ export default defineComponent({
if (Math.abs(elapsed) < 60 * 1000) return "just now";
return this.fmtTimeAgo(elapsed);
},
statusVariant(): "success" | "warning" | "muted" {
statusVariant(): "success" | "warning" | "danger" | "muted" {
switch (this.status) {
case OptimizationStatus.OPTIMAL:
return "success";
case OptimizationStatus.FEASIBLE:
return "warning";
case OptimizationStatus.INFEASIBLE:
case OptimizationStatus.UNBOUNDED:
return "warning";
return "danger";
default:
return "muted";
}
@ -276,20 +281,22 @@ export default defineComponent({
.info-icon {
cursor: help;
}
/* body size on small screens, larger from md up; bold carries the emphasis */
.value-text {
font-size: 1rem;
line-height: 1.2;
}
@media (min-width: 768px) {
.value-text {
font-size: 1.25rem;
}
}
.secondary-goal {
text-decoration: underline;
text-decoration-color: var(--evcc-gray);
text-underline-offset: 3px;
}
/* small: slightly reduced value size, rows are tighter */
@media (max-width: 767.98px) {
.field-value .large,
.field-value .fs-4 {
font-size: 1.125rem !important;
}
}
/* md and up: each field a vertical stack (label, value, caption); columns via Bootstrap grid */
@media (min-width: 768px) {
.field {

View file

@ -1429,6 +1429,7 @@ export interface TimeSeries {
// Solver status enum
export enum OptimizationStatus {
OPTIMAL = "Optimal",
FEASIBLE = "Feasible",
INFEASIBLE = "Infeasible",
UNBOUNDED = "Unbounded",
UNDEFINED = "Undefined",

View file

@ -599,8 +599,9 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error {
Details: details,
})
if resp.JSON200.Status != optimizer.Optimal {
return errors.New(string(resp.JSON200.Status))
// feasible results are usable, they are just not proven optimal
if status := resp.JSON200.Status; status != optimizer.Optimal && status != optimizer.Feasible {
return errors.New(string(status))
}
site.applyOptimizerResult(req, details.BatteryDetails, *resp.JSON200)