chore: remove dead code and unused abstractions (#31437)

This commit is contained in:
andig 2026-07-05 10:03:23 +02:00 • committed by GitHub
parent f0bc3e4ddf
commit 67f7f286ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 35 additions and 470 deletions

View file

@ -79,7 +79,6 @@ import formatter from "@/mixins/formatter";
import collector from "@/mixins/collector";
import api from "@/api";
import deepEqual from "@/utils/deepEqual";
import convertRates from "@/utils/convertRates";
import { debounceLeading } from "@/utils/debounceLeading";
import { defineComponent, type PropType } from "vue";
import type { Vehicle, CURRENCY, Forecast } from "@/types/evcc";
@ -153,8 +152,11 @@ export default defineComponent({
return this.collectProps(Warnings);
},
chargingPlanPreviewProps(): any {
const forecastSlots = this.forecast?.planner || [];
const rates = convertRates(forecastSlots);
const rates = (this.forecast?.planner || []).map(({ start, end, value }) => ({
start: new Date(start),
end: new Date(end),
value,
}));
const { duration, plan, power, planTime } = this.plan;
const targetTime = planTime ? new Date(planTime) : null;
const { currency, smartCostType } = this;

View file

@ -1,47 +0,0 @@
<template>
<div v-if="isSlot" class="text-end tabular">
<span class="text-nowrap">{{ day }} {{ start }}</span
>{{ " " }}<span class="text-nowrap">– {{ end }}</span>
</div>
<div v-if="isTimeseries" class="text-end tabular">
<span class="text-nowrap">{{ time }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import formatter from "@/mixins/formatter";
import { isForecastSlot, type ForecastSlot, type TimeseriesEntry } from "./types";
export default defineComponent({
name: "ForecastActiveSlot",
mixins: [formatter],
props: {
activeSlot: { type: Object as PropType<ForecastSlot | TimeseriesEntry | null> },
},
computed: {
isSlot() {
return this.activeSlot !== null && isForecastSlot(this.activeSlot);
},
isTimeseries() {
return this.activeSlot !== null && !isForecastSlot(this.activeSlot);
},
day() {
const startDate = new Date((this.activeSlot! as ForecastSlot).start);
return this.weekdayShort(startDate);
},
start() {
const startDate = new Date((this.activeSlot! as ForecastSlot).start);
return this.fmtHourMinute(startDate);
},
end() {
const endDate = new Date((this.activeSlot! as ForecastSlot).end);
return this.fmtHourMinute(endDate);
},
time() {
const time = new Date((this.activeSlot! as TimeseriesEntry).ts);
return `${this.weekdayShort(time)} ${this.fmtHourMinute(time)}`;
},
},
});
</script>

View file

@ -1,226 +0,0 @@
<template>
<div v-if="isSolar && solar" class="row">
<div class="col-6 col-sm-4 mb-3 d-flex flex-column">
<div class="label">{{ label("today") }}</div>
<div class="value d-flex flex-column flex-lg-row gap-lg-2 align-items-lg-baseline">
<div class="text-primary text-nowrap">
<AnimatedNumber :to="solar.today?.energy" :format="fmtEnergy" />
</div>
<div class="extraValue text-nowrap">{{ label("remaining") }}</div>
</div>
</div>
<div class="col-6 col-sm-4 mb-3 d-flex flex-column align-items-end align-items-sm-center">
<div class="label">{{ label("tomorrow") }}</div>
<div
class="value d-flex flex-column flex-lg-row gap-lg-2 align-items-end align-items-sm-center align-items-lg-baseline"
>
<div class="text-primary text-nowrap">
<AnimatedNumber :to="solar.tomorrow?.energy" :format="fmtEnergy" />
</div>
<div v-if="!solar.tomorrow?.complete" class="extraValue text-nowrap">
{{ label("partly") }}
</div>
</div>
</div>
<div class="col-6 col-sm-4 mb-3 d-flex flex-column align-items-start align-items-sm-end">
<div class="label">{{ label("dayAfterTomorrow") }}</div>
<div
class="value d-flex flex-column flex-lg-row gap-lg-2 align-items-start align-items-sm-end align-items-lg-baseline"
>
<div class="text-primary text-nowrap">
<AnimatedNumber :to="solar.dayAfterTomorrow?.energy" :format="fmtEnergy" />
</div>
<div v-if="!solar.dayAfterTomorrow?.complete" class="extraValue text-nowrap">
{{ label("partly") }}
</div>
</div>
</div>
</div>
<div v-else-if="isConstantValue" class="row">
<div class="col-6 col-sm-4 mb-3 d-flex flex-column">
<div class="label">{{ label("constant") }}</div>
<div class="value text-price text-nowrap" :class="highlightColor">
{{ constantValue }}
</div>
</div>
</div>
<div v-else class="row">
<div class="col-12 col-sm-6 col-lg-4 mb-3 d-flex flex-column">
<div class="label">{{ label("range") }}</div>
<div class="value text-price text-nowrap" :class="highlightColor">
{{ priceRange }}
</div>
</div>
<div
class="col-12 col-sm-6 col-lg-4 mb-3 d-flex flex-column align-items-sm-end align-items-lg-center"
>
<div class="label">{{ label("average") }}</div>
<div class="value text-price text-nowrap" :class="highlightColor">
{{ averagePrice }}
</div>
</div>
<div
class="col-12 col-sm-6 col-lg-4 mb-3 d-flex flex-column align-items-sm-start align-items-lg-end"
>
<div class="label">{{ label("lowestHour") }}</div>
<div class="value text-price text-nowrap" :class="highlightColor">
{{ lowestPriceHour }}
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import formatter, { POWER_UNIT } from "@/mixins/formatter";
import minuteTicker from "@/mixins/minuteTicker";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import type { CURRENCY } from "@/types/evcc";
import { ForecastType, findLowestSumSlotIndex } from "@/utils/forecast";
import type { ForecastSlot, SolarDetails } from "./types";
const LOCALES_WITHOUT_DAY_AFTER_TOMORROW = ["en", "tr"];
const FORECASTED_HOURS = 96;
const SLOTS_PER_HOUR = 4;
export interface Energy {
energy: string;
incomplete: boolean;
}
export interface EnergyByDay {
today: Energy;
tomorrow: Energy;
dayAfterTomorrow: Energy;
}
export default defineComponent({
name: "ForecastDetails",
components: {
AnimatedNumber,
},
mixins: [formatter, minuteTicker],
props: {
type: { type: String as () => ForecastType, required: true },
grid: { type: Array as PropType<ForecastSlot[]> },
co2: { type: Array as PropType<ForecastSlot[]> },
solar: { type: Object as PropType<SolarDetails> },
currency: { type: String as PropType<CURRENCY> },
},
data() {
return {
now: new Date(),
};
},
computed: {
isSolar() {
return this.type === ForecastType.Solar;
},
isPrice() {
return this.type === ForecastType.Price;
},
upcomingSlots(): ForecastSlot[] {
const now = this.now;
const slots = this.isPrice ? this.grid || [] : this.co2 || [];
return slots
.filter((slot) => new Date(slot.end) > now)
.slice(0, FORECASTED_HOURS * SLOTS_PER_HOUR);
},
isConstantValue(): boolean {
if (this.isSolar) return false;
const slots = this.upcomingSlots;
if (slots.length === 0) return false;
const firstValue = slots[0]?.value;
return slots.every((slot) => slot.value === firstValue);
},
constantValue(): string {
return this.fmtValue(this.upcomingSlots[0]!.value, true);
},
averagePrice() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const price = slots.reduce((acc, slot) => acc + slot.value, 0) / slots.length;
return this.fmtValue(price, true);
},
priceRange() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const min = Math.min(...slots.map((slot) => slot.value));
const max = Math.max(...slots.map((slot) => slot.value));
return `${this.fmtValue(min, false)} – ${this.fmtValue(max, true)}`;
},
lowestPriceHour() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const index = findLowestSumSlotIndex(slots, SLOTS_PER_HOUR);
if (index === -1) return "";
const startSlot = slots[index];
const endSlot = slots[index + SLOTS_PER_HOUR - 1];
if (!startSlot || !endSlot) return "";
const start = new Date(startSlot.start);
const end = new Date(endSlot.end);
return `${this.weekdayShort(start)} ${this.fmtHourMinute(start)} – ${this.fmtHourMinute(end)}`;
},
highlightColor() {
switch (this.type) {
case ForecastType.Price:
return "text-price";
case ForecastType.Co2:
return "text-co2";
default:
return "";
}
},
},
watch: {
everyMinute(): void {
this.now = new Date();
},
},
mounted() {
this.now = new Date();
},
methods: {
label(key: string) {
// special case "day after tomorrow"
if (
key === "dayAfterTomorrow" &&
LOCALES_WITHOUT_DAY_AFTER_TOMORROW.includes(this.$i18n.locale)
) {
const date = new Date();
date.setDate(date.getDate() + 2);
return this.fmtDayMonth(date);
}
return this.$t(`forecast.${this.type}.${key}`);
},
fmtValue(value: number, withUnit = true) {
if (this.type === ForecastType.Price) {
return this.fmtPricePerKWh(value, this.currency, false, withUnit);
}
return withUnit ? this.fmtCo2Medium(value) : this.fmtNumber(value, 0);
},
fmtEnergy(energy: number | undefined) {
return !energy ? "-" : this.fmtWh(energy, POWER_UNIT.AUTO);
},
},
});
</script>
<style scoped>
.value {
font-size: 18px;
font-weight: bold;
}
.extraValue {
color: var(--evcc-gray);
font-size: 14px;
font-weight: normal;
}
.label {
color: var(--evcc-gray);
text-transform: uppercase;
}
</style>

View file

@ -1,57 +0,0 @@
<template>
<IconSelectGroup>
<IconSelectItem
v-for="type in types"
:key="type"
:active="selectedType === type"
:label="$t(`forecast.type.${type}`)"
:disabled="!availableTypes[type]"
hideLabelOnMobile
@click="$emit('update:modelValue', type)"
>
<component :is="typeIcons[type]"></component>
</IconSelectItem>
</IconSelectGroup>
</template>
<script lang="ts">
import "@h2d2/shopicons/es/regular/eco1";
import "@h2d2/shopicons/es/regular/sun";
import { defineComponent } from "vue";
import IconSelectItem from "../Helper/IconSelectItem.vue";
import IconSelectGroup from "../Helper/IconSelectGroup.vue";
import DynamicPriceIcon from "../MaterialIcon/DynamicPrice.vue";
import { ForecastType } from "@/utils/forecast";
export default defineComponent({
name: "ForecastTypeSelect",
components: { IconSelectItem, IconSelectGroup },
props: {
modelValue: { type: String as () => ForecastType, required: true },
forecast: { type: Object, required: true },
},
emits: ["update:modelValue"],
computed: {
selectedType() {
return this.modelValue;
},
types() {
return Object.values(ForecastType);
},
availableTypes() {
return {
[ForecastType.Solar]: !!this.forecast["solar"],
[ForecastType.Price]: !!this.forecast["grid"],
[ForecastType.Co2]: !!this.forecast["co2"],
};
},
typeIcons() {
return {
[ForecastType.Solar]: "shopicon-regular-sun",
[ForecastType.Price]: DynamicPriceIcon,
[ForecastType.Co2]: "shopicon-regular-eco1",
};
},
},
});
</script>

View file

@ -12,6 +12,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { copyWithFeedback } from "@/utils/clipboard";
import "@h2d2/shopicons/es/regular/copy";
import "@h2d2/shopicons/es/regular/checkmark";
@ -52,54 +53,7 @@ export default defineComponent({
},
methods: {
handleCopy() {
// Modern browser API, requires secure context (localhost or https)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(this.content)
.then(() => {
this.showCopiedFeedback();
})
.catch((err) => {
console.error("Failed to copy to clipboard:", err);
this.fallbackCopy();
});
} else {
// Fallback method for HTTP/non-secure contexts
this.fallbackCopy();
}
},
fallbackCopy() {
try {
// Create a temporary textarea element
const textarea = document.createElement("textarea");
textarea.value = this.content;
textarea.style.position = "fixed";
textarea.style.left = "-999999px";
textarea.style.top = "-999999px";
document.body.appendChild(textarea);
// Select and copy the text
textarea.focus();
textarea.select();
const successful = document.execCommand("copy");
document.body.removeChild(textarea);
if (successful) {
this.showCopiedFeedback();
} else {
alert("Copy failed. Please copy manually.");
}
} catch (err) {
console.error("Fallback copy failed:", err);
// Show user feedback that copy failed
alert("Copy failed. Please copy manually.");
}
},
showCopiedFeedback() {
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2000);
copyWithFeedback(this.content, (value) => (this.copied = value));
},
},
});

View file

@ -2,16 +2,36 @@ export function isClipboardSupported(): boolean {
return !!(navigator.clipboard && window.isSecureContext);
}
// textarea/execCommand fallback for HTTP/non-secure contexts
function fallbackCopy(text: string): boolean {
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-999999px";
textarea.style.top = "-999999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const successful = document.execCommand("copy");
document.body.removeChild(textarea);
return successful;
} catch (err) {
console.error("Fallback copy failed:", err);
return false;
}
}
export async function copyToClipboard(text: string): Promise<boolean> {
if (!isClipboardSupported()) {
return false;
return fallbackCopy(text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error("Failed to copy to clipboard:", err);
return false;
return fallbackCopy(text);
}
}

View file

@ -1,15 +0,0 @@
import type { Rate } from "../types/evcc";
import type { ForecastSlot } from "../components/Forecast/types";
function convertRate(slot: ForecastSlot): Rate {
return {
start: new Date(slot.start),
end: new Date(slot.end),
value: slot.value,
};
}
export default function convertRates(slots: ForecastSlot[] | null): Rate[] {
if (!slots) return [];
return slots.map(convertRate);
}

View file

@ -13,9 +13,6 @@ import (
var ErrLoadpointNotInitialized = errors.New("loadpoint not initialized")
// TODO remove when used
var _ = ensureCharger
// ensureCharger extracts ID from list of IDs returned from `list` function
func ensureCharger(id string, list func() ([]string, error)) (string, error) {
id, err := ensureChargerEx(id, list, func(v string) (string, error) {

View file

@ -15,20 +15,6 @@ type API interface {
// Name returns the vehicle name
Name() string
// // GetMode returns the charge mode
// GetMode() api.ChargeMode
// // SetMode sets the charge mode
// SetMode(api.ChargeMode)
// // GetPhases returns the phases
// GetPhases() int
// // SetPhases sets the phases
// SetPhases(phases int) error
// // GetPriority returns the priority
// GetPriority() int
// // SetPriority sets the priority
// SetPriority(priority int)
// GetMinSoc returns the min soc
GetMinSoc() int
// SetMinSoc sets the min soc
@ -52,13 +38,4 @@ type API interface {
GetPlanStrategy() api.PlanStrategy
// SetPlanStrategy sets the plan strategy
SetPlanStrategy(api.PlanStrategy) error
// // GetMinCurrent returns the min charging current
// GetMinCurrent() float64
// // SetMinCurrent sets the min charging current
// SetMinCurrent(float64)
// // GetMaxCurrent returns the max charging current
// GetMaxCurrent() float64
// // SetMaxCurrent sets the max charging current
// SetMaxCurrent(float64)
}

View file

@ -18,8 +18,8 @@ func (h *msgHandler) receive(payload string) {
h.val.Set(payload)
}
// hasValue returned the received and processed payload as string
func (h *msgHandler) hasValue() (string, error) {
// value returns the received and processed payload as string
func (h *msgHandler) value() (string, error) {
payload, err := h.val.GetContext(h.ctx)
if err != nil {
return "", err
@ -36,12 +36,3 @@ func (h *msgHandler) hasValue() (string, error) {
return payload, nil
}
func (h *msgHandler) value() (string, error) {
v, err := h.hasValue()
if err != nil {
return "", err
}
return v, nil
}

View file

@ -39,20 +39,6 @@ func (h *TimeoutHandler) FloatGetter(p FloatGetter) (func() (float64, error), er
}, nil
}
func (h *TimeoutHandler) StringGetter(p StringGetter) (func() (string, error), error) {
g, err := p.StringGetter()
if err != nil {
return nil, err
}
return func() (val string, err error) {
if val, err = g(); err == nil {
_, err = h.ticker()
}
return val, err
}, nil
}
func (h *TimeoutHandler) JsonGetter(p StringGetter) (func(any) error, error) {
g, err := p.StringGetter()
if err != nil {

View file

@ -30,13 +30,6 @@ func New(driver, dsn string) (*gorm.DB, error) {
switch driver {
case "sqlite":
// Example DSNs:
//"path/to/database.db"
// "~/database.db",
// "database.db?cache=shared&journal_mode=WAL"
// ":memory:"
// Split database path and connection parameters
dbPath, params, _ := strings.Cut(dsn, "?")

View file

@ -179,12 +179,6 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) {
"plan2": {"DELETE", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc", planSocRemoveHandler(site)},
"repeatingPlans": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/repeating", addRepeatingPlansHandler(site)},
"planStrategy": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/strategy", updatePlanStrategyHandler(site)},
// config ui
// "mode": {"POST", "/mode/{value:[a-z]+}", chargeModeHandler(v)},
// "mincurrent": {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMinCurrent), v.GetMinCurrent)},
// "maxcurrent": {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMaxCurrent), v.GetMaxCurrent)},
// "phases": {"POST", "/phases/{value:[0-9]+}", intHandler(pass(v.SetMinSoc), v.GetMinSoc)},
}
for _, r := range vehicles {

View file

@ -10,7 +10,7 @@ func WithLogger(ctx context.Context, log *Logger) context.Context {
return context.WithValue(ctx, CtxLogger, log)
}
func ContextLogger(ctx context.Context) *Logger {
func contextLogger(ctx context.Context) *Logger {
if ctx != nil {
if l, ok := ctx.Value(CtxLogger).(*Logger); ok {
return l
@ -21,7 +21,7 @@ func ContextLogger(ctx context.Context) *Logger {
}
func ContextLoggerWithDefault(ctx context.Context, log *Logger) *Logger {
if log := ContextLogger(ctx); log != nil {
if log := contextLogger(ctx); log != nil {
return log
}

View file

@ -6,11 +6,6 @@ import (
"github.com/evcc-io/evcc/api"
)
// TeeAttacher allows attaching a listener to a tee
type TeeAttacher interface {
Attach() <-chan Param
}
// Tee distributes parameters to subscribers
type Tee struct {
mu sync.Mutex

View file

@ -4,6 +4,7 @@ package urlvalues
import (
"errors"
"net/url"
"slices"
"strings"
)
@ -11,7 +12,7 @@ import (
func Copy(q url.Values) url.Values {
res := make(url.Values, len(q))
for k, v := range q {
res[k] = append([]string{}, v...)
res[k] = slices.Clone(v)
}
return res
}