diff --git a/assets/js/components/Config/StatusIndicator.vue b/assets/js/components/Config/StatusIndicator.vue index 17c92d037..d27f33fb2 100644 --- a/assets/js/components/Config/StatusIndicator.vue +++ b/assets/js/components/Config/StatusIndicator.vue @@ -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"; } diff --git a/assets/js/components/Optimize/OptimizeHeader.stories.ts b/assets/js/components/Optimize/OptimizeHeader.stories.ts index f443ac6a5..601eb24a2 100644 --- a/assets/js/components/Optimize/OptimizeHeader.stories.ts +++ b/assets/js/components/Optimize/OptimizeHeader.stories.ts @@ -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 }; diff --git a/assets/js/components/Optimize/OptimizeHeader.vue b/assets/js/components/Optimize/OptimizeHeader.vue index a7cd81b65..5736ecd75 100644 --- a/assets/js/components/Optimize/OptimizeHeader.vue +++ b/assets/js/components/Optimize/OptimizeHeader.vue @@ -11,7 +11,9 @@
always, fixed
- Lowest cost + Lowest cost
@@ -32,7 +34,7 @@ @change="onStrategyChange" > {{ secondaryGoalLabel }} @@ -60,7 +62,7 @@
- {{ + {{ status }} @@ -88,7 +90,7 @@
{{ netCostDisplay }} @@ -132,6 +134,7 @@ const STRATEGY_LABELS: Record = { const STATUS_TOOLTIP = "The optimizer result:

" + "Optimal: the best plan was found.
" + + "Feasible: a usable plan was found, but not proven optimal.
" + "Infeasible: the constraints cannot all be met.
" + "Not solved: 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 { diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index 30a2bb32a..97cd268e5 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -1429,6 +1429,7 @@ export interface TimeSeries { // Solver status enum export enum OptimizationStatus { OPTIMAL = "Optimal", + FEASIBLE = "Feasible", INFEASIBLE = "Infeasible", UNBOUNDED = "Unbounded", UNDEFINED = "Undefined", diff --git a/core/site_optimizer.go b/core/site_optimizer.go index bdd34a44b..ef1a7b983 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -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)