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)