Curtailment devices: new device class for feed-in limitation (BC) (#32416)
This commit is contained in:
parent
7c0ddf1828
commit
8661c66fc7
40 changed files with 819 additions and 149 deletions
|
|
@ -64,6 +64,7 @@ type All struct {
|
||||||
Site map[string]any
|
Site map[string]any
|
||||||
Loadpoints []config.Named
|
Loadpoints []config.Named
|
||||||
Circuits []config.Named
|
Circuits []config.Named
|
||||||
|
Curtailers []config.Named
|
||||||
}
|
}
|
||||||
|
|
||||||
type Javascript struct {
|
type Javascript struct {
|
||||||
|
|
|
||||||
97
assets/js/components/Config/CurtailerModal.vue
Normal file
97
assets/js/components/Config/CurtailerModal.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<DeviceModalBase
|
||||||
|
:id="id"
|
||||||
|
name="curtailer"
|
||||||
|
device-type="curtailer"
|
||||||
|
:modal-title="$t(`config.curtailer.${isNew ? 'titleAdd' : 'titleEdit'}`)"
|
||||||
|
:provide-template-options="provideTemplateOptions"
|
||||||
|
:initial-values="initialValues"
|
||||||
|
:on-template-change="handleTemplateChange"
|
||||||
|
:preserve-on-template-change="preserveFields"
|
||||||
|
@added="(name) => $emit('changed', { action: 'added', name })"
|
||||||
|
@updated="$emit('changed', { action: 'updated' })"
|
||||||
|
@removed="$emit('changed', { action: 'removed' })"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<p class="mt-0 mb-4">{{ $t("config.curtailer.description") }}</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #before-template="{ values }">
|
||||||
|
<FormRow id="curtailerParamDeviceTitle" :label="$t('config.general.title')">
|
||||||
|
<PropertyField
|
||||||
|
id="curtailerParamDeviceTitle"
|
||||||
|
v-model.trim="values.deviceTitle"
|
||||||
|
type="String"
|
||||||
|
size="w-100"
|
||||||
|
class="me-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
</template>
|
||||||
|
</DeviceModalBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import DeviceModalBase from "./DeviceModal/DeviceModalBase.vue";
|
||||||
|
import FormRow from "./FormRow.vue";
|
||||||
|
import PropertyField from "./PropertyField.vue";
|
||||||
|
import type { DeviceValues, Product } from "./DeviceModal";
|
||||||
|
import { type TemplateGroup, customTemplateOption } from "./DeviceModal/TemplateSelector.vue";
|
||||||
|
import { ConfigType } from "@/types/evcc";
|
||||||
|
import defaultCurtailerYaml from "./defaultYaml/curtailer.yaml?raw";
|
||||||
|
import { getModal } from "@/configModal";
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
type: ConfigType.Template,
|
||||||
|
deviceTitle: "",
|
||||||
|
template: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "CurtailerModal",
|
||||||
|
components: {
|
||||||
|
DeviceModalBase,
|
||||||
|
FormRow,
|
||||||
|
PropertyField,
|
||||||
|
},
|
||||||
|
emits: ["changed"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initialValues,
|
||||||
|
preserveFields: ["deviceTitle"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
id(): number | undefined {
|
||||||
|
return getModal("curtailer")?.id;
|
||||||
|
},
|
||||||
|
isNew(): boolean {
|
||||||
|
return this.id === undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
provideTemplateOptions(products: Product[]): TemplateGroup[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "generic",
|
||||||
|
options: [
|
||||||
|
...products.filter((p: Product) => p.group === "generic"),
|
||||||
|
customTemplateOption(this.$t("config.general.customOption")),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "specific",
|
||||||
|
options: products.filter((p: Product) => p.group !== "generic"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
handleTemplateChange(value: string, values: DeviceValues) {
|
||||||
|
if (value === ConfigType.Custom) {
|
||||||
|
values.type = ConfigType.Custom;
|
||||||
|
values.yaml = defaultCurtailerYaml;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
<shopicon-regular-plus class="me-1"></shopicon-regular-plus>
|
<shopicon-regular-plus class="me-1 flex-shrink-0"></shopicon-regular-plus>
|
||||||
<span class="text-start">{{ title }}</span>
|
<span class="text-start">{{ title }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
28
assets/js/components/Config/defaultYaml/curtailer.yaml
Normal file
28
assets/js/components/Config/defaultYaml/curtailer.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
## required attributes
|
||||||
|
# feed-in limit in percent of nominal production power (0..100, 100 = uncurtailed)
|
||||||
|
curtail: # set feed-in limit
|
||||||
|
source: js
|
||||||
|
script: console.log(curtail)
|
||||||
|
curtailed: # current feed-in limit
|
||||||
|
source: const
|
||||||
|
value: 100
|
||||||
|
|
||||||
|
## alternative examples
|
||||||
|
|
||||||
|
#curtail:
|
||||||
|
# source: http
|
||||||
|
# uri: http://inverter.local/api/limit?percent={{ .curtail }}
|
||||||
|
# method: POST
|
||||||
|
#curtailed:
|
||||||
|
# source: http
|
||||||
|
# uri: http://inverter.local/api/limit
|
||||||
|
# jq: .percent
|
||||||
|
|
||||||
|
#curtail:
|
||||||
|
# source: modbus
|
||||||
|
# uri: 192.168.179.200:502
|
||||||
|
# id: 1
|
||||||
|
# register: { type: writesingle, address: 40016, encoding: uint16 }
|
||||||
|
#curtailed:
|
||||||
|
# source: error
|
||||||
|
# error: ErrNotAvailable
|
||||||
18
assets/js/components/MaterialIcon/Curtail.vue
Normal file
18
assets/js/components/MaterialIcon/Curtail.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<svg :style="svgStyle" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12.825 15.8q.75-.35 1.113-1.2t-.163-1.6q-1.1-1.575-2.212-3.062T9.3 6.95q-.275-.35-.687-.162t-.313.637q.4 1.825.85 3.625t1 3.65q.25.875 1.038 1.163t1.637-.063m-7.7 4.2q-.525 0-1.012-.238t-.738-.712q-.7-1.15-1.025-2.437T2.025 14q0-1.725.588-3.463t1.862-3.112q.375-.425.8-.412t.7.262t.35.625t-.225.725q-.95 1.05-1.512 2.475T4.025 14q0 1.025.263 2.038T5.125 18H18.9q.525-.9.813-1.925T20 14q0-1.875-.812-3.475T17.025 7.8q-1.05-.875-2.375-1.35T11.95 6q-.4 0-.712-.262t-.363-.663t.15-.725t.6-.35q1.825-.075 3.563.525T18.35 6.3q1.675 1.4 2.663 3.375T22 14q0 1.4-.35 2.663t-1 2.387q-.275.475-.75.713t-1 .237zm6.9-6.025"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import icon from "@/mixins/icon";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "Curtail",
|
||||||
|
mixins: [icon],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -460,6 +460,10 @@ export enum ConfigType {
|
||||||
export type ConfigVehicle = Entity;
|
export type ConfigVehicle = Entity;
|
||||||
export type ConfigMessenger = Entity;
|
export type ConfigMessenger = Entity;
|
||||||
|
|
||||||
|
export interface ConfigCurtailer extends Entity {
|
||||||
|
deviceTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigHems extends Entity {
|
export interface ConfigHems extends Entity {
|
||||||
deviceProduct?: string;
|
deviceProduct?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -1368,7 +1372,8 @@ export type DeviceType =
|
||||||
| "loadpoint"
|
| "loadpoint"
|
||||||
| "messenger"
|
| "messenger"
|
||||||
| "tariff"
|
| "tariff"
|
||||||
| "hems";
|
| "hems"
|
||||||
|
| "curtailer";
|
||||||
export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer";
|
export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer";
|
||||||
export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux";
|
export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux";
|
||||||
export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature";
|
export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature";
|
||||||
|
|
@ -1385,6 +1390,7 @@ export interface SiteConfig {
|
||||||
aux: string[] | null;
|
aux: string[] | null;
|
||||||
ext: string[] | null;
|
ext: string[] | null;
|
||||||
consumer: string[] | null;
|
consumer: string[] | null;
|
||||||
|
curtail: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValueOf<T> = T[keyof T];
|
export type ValueOf<T> = T[keyof T];
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,31 @@
|
||||||
data-testid="add-grid"
|
data-testid="add-grid"
|
||||||
@click="openModal('meter', { type: 'grid' })"
|
@click="openModal('meter', { type: 'grid' })"
|
||||||
/>
|
/>
|
||||||
|
<DeviceCard
|
||||||
|
v-for="curtailer in curtailerDevices"
|
||||||
|
:id="`curtailer_${curtailer.name}`"
|
||||||
|
:key="curtailer.name"
|
||||||
|
:name="curtailer.name"
|
||||||
|
:title="curtailerTitle(curtailer)"
|
||||||
|
:editable="!!curtailer.id"
|
||||||
|
:error="hasDeviceError('curtailer', curtailer.name)"
|
||||||
|
:banner="curtailmentBanner('curtailer', curtailer.name)"
|
||||||
|
data-testid="curtailer"
|
||||||
|
@edit="openModal('curtailer', { id: curtailer.id })"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<CurtailIcon />
|
||||||
|
</template>
|
||||||
|
<template #tags>
|
||||||
|
<DeviceTags :tags="deviceTags('curtailer', curtailer.name)" />
|
||||||
|
</template>
|
||||||
|
</DeviceCard>
|
||||||
|
<NewDeviceButton
|
||||||
|
v-if="pvMeters.length > 0"
|
||||||
|
:title="$t('config.main.addCurtailer')"
|
||||||
|
data-testid="add-curtailer"
|
||||||
|
@click="openModal('curtailer')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfigSection>
|
</ConfigSection>
|
||||||
|
|
||||||
|
|
@ -162,7 +187,7 @@
|
||||||
meter-type="pv"
|
meter-type="pv"
|
||||||
:has-error="hasDeviceError('meter', meter.name)"
|
:has-error="hasDeviceError('meter', meter.name)"
|
||||||
:tags="deviceTags('meter', meter.name)"
|
:tags="deviceTags('meter', meter.name)"
|
||||||
:banner="meterBanner(meter.name)"
|
:banner="curtailmentBanner('meter', meter.name)"
|
||||||
@edit="(type, id) => openModal('meter', { type, id })"
|
@edit="(type, id) => openModal('meter', { type, id })"
|
||||||
@enable="handleDisable('meter', meter.id, false)"
|
@enable="handleDisable('meter', meter.id, false)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -525,6 +550,7 @@
|
||||||
<MessagingLegacyModal @changed="loadDirty" />
|
<MessagingLegacyModal @changed="loadDirty" />
|
||||||
<MessagingModal :messengers="messengers" @changed="loadDirty" />
|
<MessagingModal :messengers="messengers" @changed="loadDirty" />
|
||||||
<MessengerModal @changed="messengerChanged" />
|
<MessengerModal @changed="messengerChanged" />
|
||||||
|
<CurtailerModal @changed="curtailerChanged" />
|
||||||
<TariffsLegacyModal @changed="loadDirty" />
|
<TariffsLegacyModal @changed="loadDirty" />
|
||||||
<TariffModal
|
<TariffModal
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
|
|
@ -579,6 +605,8 @@ import CircuitsModal from "../components/Config/CircuitsModal.vue";
|
||||||
import CircuitTags from "../components/Config/CircuitTags.vue";
|
import CircuitTags from "../components/Config/CircuitTags.vue";
|
||||||
import collector from "../mixins/collector";
|
import collector from "../mixins/collector";
|
||||||
import ControlModal from "../components/Config/ControlModal.vue";
|
import ControlModal from "../components/Config/ControlModal.vue";
|
||||||
|
import CurtailerModal from "../components/Config/CurtailerModal.vue";
|
||||||
|
import CurtailIcon from "../components/MaterialIcon/Curtail.vue";
|
||||||
import DeviceCard from "../components/Config/DeviceCard.vue";
|
import DeviceCard from "../components/Config/DeviceCard.vue";
|
||||||
import DeviceTags from "../components/Config/DeviceTags.vue";
|
import DeviceTags from "../components/Config/DeviceTags.vue";
|
||||||
import EebusIcon from "../components/MaterialIcon/Eebus.vue";
|
import EebusIcon from "../components/MaterialIcon/Eebus.vue";
|
||||||
|
|
@ -633,6 +661,7 @@ import type {
|
||||||
ConfigCharger,
|
ConfigCharger,
|
||||||
ConfigVehicle,
|
ConfigVehicle,
|
||||||
ConfigCircuit,
|
ConfigCircuit,
|
||||||
|
ConfigCurtailer,
|
||||||
ConfigMessenger,
|
ConfigMessenger,
|
||||||
ConfigHems,
|
ConfigHems,
|
||||||
ConfigLoadpoint,
|
ConfigLoadpoint,
|
||||||
|
|
@ -691,6 +720,8 @@ export default defineComponent({
|
||||||
CircuitsModal,
|
CircuitsModal,
|
||||||
CircuitTags,
|
CircuitTags,
|
||||||
ControlModal,
|
ControlModal,
|
||||||
|
CurtailerModal,
|
||||||
|
CurtailIcon,
|
||||||
DeviceCard,
|
DeviceCard,
|
||||||
DeviceTags,
|
DeviceTags,
|
||||||
EebusIcon,
|
EebusIcon,
|
||||||
|
|
@ -749,6 +780,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
messengers: [] as ConfigMessenger[],
|
messengers: [] as ConfigMessenger[],
|
||||||
|
curtailers: [] as ConfigCurtailer[],
|
||||||
vehicles: [] as ConfigVehicle[],
|
vehicles: [] as ConfigVehicle[],
|
||||||
meters: [] as ConfigMeter[],
|
meters: [] as ConfigMeter[],
|
||||||
loadpoints: [] as ConfigLoadpoint[],
|
loadpoints: [] as ConfigLoadpoint[],
|
||||||
|
|
@ -772,6 +804,7 @@ export default defineComponent({
|
||||||
aux: null as string[] | null,
|
aux: null as string[] | null,
|
||||||
ext: null as string[] | null,
|
ext: null as string[] | null,
|
||||||
consumer: null as string[] | null,
|
consumer: null as string[] | null,
|
||||||
|
curtail: null as string[] | null,
|
||||||
} as SiteConfig,
|
} as SiteConfig,
|
||||||
deviceValueTimeout: null as Timeout,
|
deviceValueTimeout: null as Timeout,
|
||||||
deviceValues: {
|
deviceValues: {
|
||||||
|
|
@ -781,6 +814,7 @@ export default defineComponent({
|
||||||
loadpoint: {},
|
loadpoint: {},
|
||||||
messenger: {},
|
messenger: {},
|
||||||
tariff: {},
|
tariff: {},
|
||||||
|
curtailer: {},
|
||||||
} as DeviceValuesMap,
|
} as DeviceValuesMap,
|
||||||
isComponentMounted: true,
|
isComponentMounted: true,
|
||||||
isPageVisible: true,
|
isPageVisible: true,
|
||||||
|
|
@ -868,8 +902,10 @@ export default defineComponent({
|
||||||
{
|
{
|
||||||
slug: "grid",
|
slug: "grid",
|
||||||
icon: "shopicon-regular-powersupply",
|
icon: "shopicon-regular-powersupply",
|
||||||
count: this.gridMeter ? 1 : 0,
|
count: (this.gridMeter ? 1 : 0) + this.curtailerDevices.length,
|
||||||
error: !!this.gridMeter && this.hasDeviceError("meter", this.gridMeter.name),
|
error:
|
||||||
|
(!!this.gridMeter && this.hasDeviceError("meter", this.gridMeter.name)) ||
|
||||||
|
this.curtailerDevices.some((c) => this.hasDeviceError("curtailer", c.name)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "pv-battery",
|
slug: "pv-battery",
|
||||||
|
|
@ -877,7 +913,7 @@ export default defineComponent({
|
||||||
count: pvAndBattery.length,
|
count: pvAndBattery.length,
|
||||||
error: meterError(pvAndBattery),
|
error: meterError(pvAndBattery),
|
||||||
warning:
|
warning:
|
||||||
this.pvMeters.some((m) => this.meterBanner(m.name)) ||
|
this.pvMeters.some((m) => this.curtailmentBanner("meter", m.name)) ||
|
||||||
meterDisabled(pvAndBattery),
|
meterDisabled(pvAndBattery),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -956,6 +992,12 @@ export default defineComponent({
|
||||||
const names = this.site?.ext;
|
const names = this.site?.ext;
|
||||||
return this.getMetersByNames(names);
|
return this.getMetersByNames(names);
|
||||||
},
|
},
|
||||||
|
curtailerDevices(): ConfigCurtailer[] {
|
||||||
|
const names = this.site?.curtail || [];
|
||||||
|
return names
|
||||||
|
.map((name) => this.curtailers.find((c) => c.name === name))
|
||||||
|
.filter((c): c is ConfigCurtailer => c !== undefined);
|
||||||
|
},
|
||||||
consumerMeters() {
|
consumerMeters() {
|
||||||
return this.getMetersByNames(this.site?.consumer);
|
return this.getMetersByNames(this.site?.consumer);
|
||||||
},
|
},
|
||||||
|
|
@ -1291,6 +1333,7 @@ export default defineComponent({
|
||||||
await this.loadLoadpoints();
|
await this.loadLoadpoints();
|
||||||
await this.loadCircuits();
|
await this.loadCircuits();
|
||||||
await this.loadMessengers();
|
await this.loadMessengers();
|
||||||
|
await this.loadCurtailers();
|
||||||
await this.loadTariffs();
|
await this.loadTariffs();
|
||||||
await this.loadTariffRefs();
|
await this.loadTariffRefs();
|
||||||
await this.loadHems();
|
await this.loadHems();
|
||||||
|
|
@ -1311,6 +1354,9 @@ export default defineComponent({
|
||||||
async loadMessengers() {
|
async loadMessengers() {
|
||||||
this.messengers = (await this.loadConfig("devices/messenger")) || [];
|
this.messengers = (await this.loadConfig("devices/messenger")) || [];
|
||||||
},
|
},
|
||||||
|
async loadCurtailers() {
|
||||||
|
this.curtailers = (await this.loadConfig("devices/curtailer")) || [];
|
||||||
|
},
|
||||||
async loadVehicles() {
|
async loadVehicles() {
|
||||||
this.vehicles = (await this.loadConfig("devices/vehicle")) || [];
|
this.vehicles = (await this.loadConfig("devices/vehicle")) || [];
|
||||||
},
|
},
|
||||||
|
|
@ -1464,6 +1510,25 @@ export default defineComponent({
|
||||||
this.loadMessengers();
|
this.loadMessengers();
|
||||||
this.loadDirty();
|
this.loadDirty();
|
||||||
},
|
},
|
||||||
|
curtailerTitle(curtailer: ConfigCurtailer): string {
|
||||||
|
return (
|
||||||
|
curtailer.deviceTitle ||
|
||||||
|
curtailer.config?.template ||
|
||||||
|
this.$t("config.curtailer.title")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async curtailerChanged(result: ModalResult) {
|
||||||
|
if (result.action === "added" && result.name) {
|
||||||
|
this.site.curtail = [...(this.site.curtail || []), result.name];
|
||||||
|
await this.saveSite("curtail");
|
||||||
|
}
|
||||||
|
if (result.action === "removed") {
|
||||||
|
await this.loadSite();
|
||||||
|
}
|
||||||
|
await this.loadCurtailers();
|
||||||
|
await this.loadDirty();
|
||||||
|
this.updateValues();
|
||||||
|
},
|
||||||
siteChanged() {
|
siteChanged() {
|
||||||
this.loadDirty();
|
this.loadDirty();
|
||||||
},
|
},
|
||||||
|
|
@ -1504,6 +1569,7 @@ export default defineComponent({
|
||||||
vehicle: this.vehicles,
|
vehicle: this.vehicles,
|
||||||
charger: this.chargers,
|
charger: this.chargers,
|
||||||
tariff: this.tariffs,
|
tariff: this.tariffs,
|
||||||
|
curtailer: this.curtailers,
|
||||||
} as Record<DeviceType, any[]>;
|
} as Record<DeviceType, any[]>;
|
||||||
for (const type in devices) {
|
for (const type in devices) {
|
||||||
for (const device of devices[type as DeviceType]) {
|
for (const device of devices[type as DeviceType]) {
|
||||||
|
|
@ -1523,9 +1589,10 @@ export default defineComponent({
|
||||||
deviceTags(type: DeviceType, id: string) {
|
deviceTags(type: DeviceType, id: string) {
|
||||||
return this.deviceValues[type][id] || {};
|
return this.deviceValues[type][id] || {};
|
||||||
},
|
},
|
||||||
meterBanner(name: string): string | undefined {
|
curtailmentBanner(type: DeviceType, name: string): string | undefined {
|
||||||
// the tag is only present while curtailing, a zero percent limit is still one
|
// devices report the allowed feed-in percent, 100 = uncurtailed
|
||||||
return this.deviceTags("meter", name)["curtailed"]?.value !== undefined
|
const value = this.deviceTags(type, name)["curtailed"]?.value;
|
||||||
|
return typeof value === "number" && value < 100
|
||||||
? this.$t("config.deviceValue.productionLimited")
|
? this.$t("config.deviceValue.productionLimited")
|
||||||
: undefined;
|
: undefined;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _ClassName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsshminfluxmessengersponsorshiploadpoint"
|
const _ClassName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsshminfluxmessengersponsorshiploadpointcurtailer"
|
||||||
|
|
||||||
var _ClassIndex = [...]uint8{0, 10, 15, 22, 29, 35, 42, 46, 50, 58, 69, 74, 84, 86, 90, 93, 99, 108, 119, 128}
|
var _ClassIndex = [...]uint8{0, 10, 15, 22, 29, 35, 42, 46, 50, 58, 69, 74, 84, 86, 90, 93, 99, 108, 119, 128, 137}
|
||||||
|
|
||||||
const _ClassLowerName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsshminfluxmessengersponsorshiploadpoint"
|
const _ClassLowerName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsshminfluxmessengersponsorshiploadpointcurtailer"
|
||||||
|
|
||||||
func (i Class) String() string {
|
func (i Class) String() string {
|
||||||
i -= 1
|
i -= 1
|
||||||
|
|
@ -44,9 +44,10 @@ func _ClassNoOp() {
|
||||||
_ = x[ClassMessenger-(17)]
|
_ = x[ClassMessenger-(17)]
|
||||||
_ = x[ClassSponsorship-(18)]
|
_ = x[ClassSponsorship-(18)]
|
||||||
_ = x[ClassLoadpoint-(19)]
|
_ = x[ClassLoadpoint-(19)]
|
||||||
|
_ = x[ClassCurtailer-(20)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassValues = []Class{ClassConfigFile, ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassSHM, ClassInflux, ClassMessenger, ClassSponsorship, ClassLoadpoint}
|
var _ClassValues = []Class{ClassConfigFile, ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassSHM, ClassInflux, ClassMessenger, ClassSponsorship, ClassLoadpoint, ClassCurtailer}
|
||||||
|
|
||||||
var _ClassNameToValueMap = map[string]Class{
|
var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassName[0:10]: ClassConfigFile,
|
_ClassName[0:10]: ClassConfigFile,
|
||||||
|
|
@ -87,6 +88,8 @@ var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassLowerName[108:119]: ClassSponsorship,
|
_ClassLowerName[108:119]: ClassSponsorship,
|
||||||
_ClassName[119:128]: ClassLoadpoint,
|
_ClassName[119:128]: ClassLoadpoint,
|
||||||
_ClassLowerName[119:128]: ClassLoadpoint,
|
_ClassLowerName[119:128]: ClassLoadpoint,
|
||||||
|
_ClassName[128:137]: ClassCurtailer,
|
||||||
|
_ClassLowerName[128:137]: ClassCurtailer,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassNames = []string{
|
var _ClassNames = []string{
|
||||||
|
|
@ -109,6 +112,7 @@ var _ClassNames = []string{
|
||||||
_ClassName[99:108],
|
_ClassName[99:108],
|
||||||
_ClassName[108:119],
|
_ClassName[108:119],
|
||||||
_ClassName[119:128],
|
_ClassName[119:128],
|
||||||
|
_ClassName[128:137],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClassString retrieves an enum value from the enum constants string name.
|
// ClassString retrieves an enum value from the enum constants string name.
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ func runConfigDelete(cmd *cobra.Command, args []string) {
|
||||||
deleteDevice[api.Tariff](c)
|
deleteDevice[api.Tariff](c)
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
deleteDevice[api.Circuit](c)
|
deleteDevice[api.Circuit](c)
|
||||||
|
case templates.Curtailer:
|
||||||
|
deleteDevice[api.Curtailer](c)
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
deleteDevice[hems.API](c)
|
deleteDevice[hems.API](c)
|
||||||
case templates.Loadpoint:
|
case templates.Loadpoint:
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const (
|
||||||
ClassMessenger
|
ClassMessenger
|
||||||
ClassSponsorship
|
ClassSponsorship
|
||||||
ClassLoadpoint
|
ClassLoadpoint
|
||||||
|
ClassCurtailer
|
||||||
)
|
)
|
||||||
|
|
||||||
// FatalError is an error that can be marshaled
|
// FatalError is an error that can be marshaled
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var references struct {
|
var references struct {
|
||||||
meter, charger, vehicle, circuit, tariff []string
|
meter, charger, vehicle, circuit, tariff, curtailer []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectRefs(conf globalconfig.All) error {
|
func collectRefs(conf globalconfig.All) error {
|
||||||
|
|
@ -57,6 +57,7 @@ func collectRefs(conf globalconfig.All) error {
|
||||||
func collectSiteRefs(conf globalconfig.All) error {
|
func collectSiteRefs(conf globalconfig.All) error {
|
||||||
var refs struct {
|
var refs struct {
|
||||||
Meters core.MetersConfig `mapstructure:"meters"` // Meter references
|
Meters core.MetersConfig `mapstructure:"meters"` // Meter references
|
||||||
|
Curtailers []string `mapstructure:"curtailers"` // Curtailment device references
|
||||||
Other map[string]any `mapstructure:",remain"`
|
Other map[string]any `mapstructure:",remain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +71,7 @@ func collectSiteRefs(conf globalconfig.All) error {
|
||||||
references.meter = append(references.meter, refs.Meters.ExtMetersRef...)
|
references.meter = append(references.meter, refs.Meters.ExtMetersRef...)
|
||||||
references.meter = append(references.meter, refs.Meters.AuxMetersRef...)
|
references.meter = append(references.meter, refs.Meters.AuxMetersRef...)
|
||||||
references.meter = append(references.meter, refs.Meters.ConsumerMetersRef...)
|
references.meter = append(references.meter, refs.Meters.ConsumerMetersRef...)
|
||||||
|
references.curtailer = append(references.curtailer, refs.Curtailers...)
|
||||||
|
|
||||||
// append devices from settings
|
// append devices from settings
|
||||||
if v, err := settings.String(keys.GridMeter); err == nil && v != "" {
|
if v, err := settings.String(keys.GridMeter); err == nil && v != "" {
|
||||||
|
|
@ -82,6 +84,10 @@ func collectSiteRefs(conf globalconfig.All) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, err := settings.String(keys.Curtailers); err == nil && v != "" {
|
||||||
|
references.curtailer = append(references.curtailer, strings.Split(v, ",")...)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
cmd/setup.go
49
cmd/setup.go
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/evcc-io/evcc/core/loadpoint"
|
"github.com/evcc-io/evcc/core/loadpoint"
|
||||||
"github.com/evcc-io/evcc/core/metrics"
|
"github.com/evcc-io/evcc/core/metrics"
|
||||||
coresettings "github.com/evcc-io/evcc/core/settings"
|
coresettings "github.com/evcc-io/evcc/core/settings"
|
||||||
|
"github.com/evcc-io/evcc/curtailer"
|
||||||
"github.com/evcc-io/evcc/hems"
|
"github.com/evcc-io/evcc/hems"
|
||||||
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
||||||
"github.com/evcc-io/evcc/hems/shm"
|
"github.com/evcc-io/evcc/hems/shm"
|
||||||
|
|
@ -400,6 +401,50 @@ func configureMeters(static []config.Named, names ...string) error {
|
||||||
return eg.Wait()
|
return eg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureCurtailers(static []config.Named, names ...string) error {
|
||||||
|
var eg errgroup.Group
|
||||||
|
|
||||||
|
for i, cc := range static {
|
||||||
|
if cc.Name == "" {
|
||||||
|
return fmt.Errorf("cannot create curtailer %d: missing name", i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure all, if no name refs are given
|
||||||
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nameValid(cc.Name); err != nil {
|
||||||
|
log.WARN.Printf("create curtailer %d: %v", i+1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
return staticInstance("curtailer", cc, curtailer.NewFromConfig, config.Curtailers())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// append devices from database
|
||||||
|
configurable, err := config.ConfigurationsByClass(templates.Curtailer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range configurable {
|
||||||
|
eg.Go(func() error {
|
||||||
|
cc := conf.Named()
|
||||||
|
|
||||||
|
// always skip unreferenced db devices
|
||||||
|
if !slices.Contains(names, cc.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurableInstance("curtailer", &conf, curtailer.NewFromConfig, config.Curtailers())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func configureChargers(static []config.Named, names ...string) error {
|
func configureChargers(static []config.Named, names ...string) error {
|
||||||
var eg errgroup.Group
|
var eg errgroup.Group
|
||||||
|
|
||||||
|
|
@ -1287,6 +1332,10 @@ func configureDevices(conf globalconfig.All) error {
|
||||||
errs = append(errs, &ClassError{ClassCircuit, err})
|
errs = append(errs, &ClassError{ClassCircuit, err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := configureCurtailers(conf.Curtailers, references.curtailer...); err != nil {
|
||||||
|
errs = append(errs, &ClassError{ClassCurtailer, err})
|
||||||
|
}
|
||||||
|
|
||||||
return joinErrors(errs...)
|
return joinErrors(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ const (
|
||||||
AuxMeters = "auxMeters"
|
AuxMeters = "auxMeters"
|
||||||
ConsumerMeters = "consumerMeters"
|
ConsumerMeters = "consumerMeters"
|
||||||
|
|
||||||
|
// curtailment devices
|
||||||
|
Curtailers = "curtailers"
|
||||||
|
|
||||||
// battery settings
|
// battery settings
|
||||||
BatteryDischargeControl = "batteryDischargeControl"
|
BatteryDischargeControl = "batteryDischargeControl"
|
||||||
BatteryGridChargeLimit = "batteryGridChargeLimit"
|
BatteryGridChargeLimit = "batteryGridChargeLimit"
|
||||||
|
|
|
||||||
24
core/site.go
24
core/site.go
|
|
@ -65,6 +65,7 @@ type Site struct {
|
||||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||||
Meters MetersConfig `mapstructure:"meters"` // Meter references
|
Meters MetersConfig `mapstructure:"meters"` // Meter references
|
||||||
|
CurtailersRef []string `mapstructure:"curtailers"` // Curtailment device references
|
||||||
|
|
||||||
// meters
|
// meters
|
||||||
circuit api.Circuit // Circuit
|
circuit api.Circuit // Circuit
|
||||||
|
|
@ -75,6 +76,7 @@ type Site struct {
|
||||||
extMeters []config.Device[api.Meter] // External meters - for monitoring only
|
extMeters []config.Device[api.Meter] // External meters - for monitoring only
|
||||||
auxMeters []config.Device[api.Meter] // Auxiliary meters
|
auxMeters []config.Device[api.Meter] // Auxiliary meters
|
||||||
consumerMeters []config.Device[api.Meter] // Consumer meters
|
consumerMeters []config.Device[api.Meter] // Consumer meters
|
||||||
|
curtailers []config.Device[api.Curtailer]
|
||||||
|
|
||||||
// last applied HEMS state, nil until applied or after a failed attempt
|
// last applied HEMS state, nil until applied or after a failed attempt
|
||||||
dimmed *bool
|
dimmed *bool
|
||||||
|
|
@ -351,6 +353,15 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif
|
||||||
site.collectors[ref] = me
|
site.collectors[ref] = me
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// curtailment devices
|
||||||
|
for _, ref := range site.CurtailersRef {
|
||||||
|
dev, err := config.Curtailers().ByName(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
site.curtailers = append(site.curtailers, dev)
|
||||||
|
}
|
||||||
|
|
||||||
// revert battery mode on shutdown
|
// revert battery mode on shutdown
|
||||||
shutdown.Register(func() {
|
shutdown.Register(func() {
|
||||||
if mode := site.GetBatteryMode(); batteryModeModified(mode) {
|
if mode := site.GetBatteryMode(); batteryModeModified(mode) {
|
||||||
|
|
@ -395,19 +406,22 @@ func (site *Site) restoreMetersAndTitle() {
|
||||||
site.Meters.GridMeterRef = v
|
site.Meters.GridMeterRef = v
|
||||||
}
|
}
|
||||||
if v, err := settings.String(keys.PvMeters); err == nil && v != "" {
|
if v, err := settings.String(keys.PvMeters); err == nil && v != "" {
|
||||||
site.Meters.PVMetersRef = append(site.Meters.PVMetersRef, filterConfigurable(strings.Split(v, ","))...)
|
site.Meters.PVMetersRef = append(site.Meters.PVMetersRef, filterConfigurableMeter(strings.Split(v, ","))...)
|
||||||
}
|
}
|
||||||
if v, err := settings.String(keys.BatteryMeters); err == nil && v != "" {
|
if v, err := settings.String(keys.BatteryMeters); err == nil && v != "" {
|
||||||
site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, filterConfigurable(strings.Split(v, ","))...)
|
site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, filterConfigurableMeter(strings.Split(v, ","))...)
|
||||||
}
|
}
|
||||||
if v, err := settings.String(keys.ExtMeters); err == nil && v != "" {
|
if v, err := settings.String(keys.ExtMeters); err == nil && v != "" {
|
||||||
site.Meters.ExtMetersRef = append(site.Meters.ExtMetersRef, filterConfigurable(strings.Split(v, ","))...)
|
site.Meters.ExtMetersRef = append(site.Meters.ExtMetersRef, filterConfigurableMeter(strings.Split(v, ","))...)
|
||||||
}
|
}
|
||||||
if v, err := settings.String(keys.AuxMeters); err == nil && v != "" {
|
if v, err := settings.String(keys.AuxMeters); err == nil && v != "" {
|
||||||
site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, filterConfigurable(strings.Split(v, ","))...)
|
site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, filterConfigurableMeter(strings.Split(v, ","))...)
|
||||||
}
|
}
|
||||||
if v, err := settings.String(keys.ConsumerMeters); err == nil && v != "" {
|
if v, err := settings.String(keys.ConsumerMeters); err == nil && v != "" {
|
||||||
site.Meters.ConsumerMetersRef = append(site.Meters.ConsumerMetersRef, filterConfigurable(strings.Split(v, ","))...)
|
site.Meters.ConsumerMetersRef = append(site.Meters.ConsumerMetersRef, filterConfigurableMeter(strings.Split(v, ","))...)
|
||||||
|
}
|
||||||
|
if v, err := settings.String(keys.Curtailers); err == nil && v != "" {
|
||||||
|
site.CurtailersRef = append(site.CurtailersRef, filterConfigurableCurtailers(strings.Split(v, ","))...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ type API interface {
|
||||||
// Config
|
// Config
|
||||||
GetGridMeterRef() string
|
GetGridMeterRef() string
|
||||||
SetGridMeterRef(string)
|
SetGridMeterRef(string)
|
||||||
|
GetCurtailerRefs() []string
|
||||||
|
SetCurtailerRefs([]string)
|
||||||
GetPVMeterRefs() []string
|
GetPVMeterRefs() []string
|
||||||
SetPVMeterRefs([]string)
|
SetPVMeterRefs([]string)
|
||||||
GetBatteryMeterRefs() []string
|
GetBatteryMeterRefs() []string
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,23 @@ var (
|
||||||
ErrBatteryControlNotAvailable = errors.New("battery control not available")
|
ErrBatteryControlNotAvailable = errors.New("battery control not available")
|
||||||
)
|
)
|
||||||
|
|
||||||
// isConfigurable checks if the meter is configurable
|
// filterConfigurableDevices filters references to configurable devices of the given handler
|
||||||
func isConfigurable(ref string) bool {
|
func filterConfigurableDevices[T any](h config.Handler[T], ref []string) []string {
|
||||||
dev, _ := config.Meters().ByName(ref)
|
return lo.Filter(ref, func(ref string, _ int) bool {
|
||||||
_, ok := dev.(config.ConfigurableDevice[api.Meter])
|
dev, _ := h.ByName(ref)
|
||||||
|
_, ok := dev.(config.ConfigurableDevice[T])
|
||||||
return ok
|
return ok
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterConfigurable filters configurable meters
|
// filterConfigurableMeter filters configurable meters
|
||||||
func filterConfigurable(ref []string) []string {
|
func filterConfigurableMeter(ref []string) []string {
|
||||||
return lo.Filter(ref, func(ref string, _ int) bool {
|
return filterConfigurableDevices(config.Meters(), ref)
|
||||||
return isConfigurable(ref)
|
}
|
||||||
})
|
|
||||||
|
// filterConfigurableCurtailers filters configurable curtailment devices
|
||||||
|
func filterConfigurableCurtailers(ref []string) []string {
|
||||||
|
return filterConfigurableDevices(config.Curtailers(), ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimize updates the optimizer
|
// Optimize updates the optimizer
|
||||||
|
|
@ -89,7 +94,7 @@ func (site *Site) SetPVMeterRefs(ref []string) {
|
||||||
defer site.Unlock()
|
defer site.Unlock()
|
||||||
|
|
||||||
site.Meters.PVMetersRef = ref
|
site.Meters.PVMetersRef = ref
|
||||||
settings.SetString(keys.PvMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.PvMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBatteryMeterRefs returns the BatteryMeterRef
|
// GetBatteryMeterRefs returns the BatteryMeterRef
|
||||||
|
|
@ -105,7 +110,7 @@ func (site *Site) SetBatteryMeterRefs(ref []string) {
|
||||||
defer site.Unlock()
|
defer site.Unlock()
|
||||||
|
|
||||||
site.Meters.BatteryMetersRef = ref
|
site.Meters.BatteryMetersRef = ref
|
||||||
settings.SetString(keys.BatteryMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.BatteryMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuxMeterRefs returns the AuxMeterRef
|
// GetAuxMeterRefs returns the AuxMeterRef
|
||||||
|
|
@ -121,7 +126,7 @@ func (site *Site) SetAuxMeterRefs(ref []string) {
|
||||||
defer site.Unlock()
|
defer site.Unlock()
|
||||||
|
|
||||||
site.Meters.AuxMetersRef = ref
|
site.Meters.AuxMetersRef = ref
|
||||||
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConsumerMeterRefs returns the ConsumerMeterRef
|
// GetConsumerMeterRefs returns the ConsumerMeterRef
|
||||||
|
|
@ -137,7 +142,7 @@ func (site *Site) SetConsumerMeterRefs(ref []string) {
|
||||||
defer site.Unlock()
|
defer site.Unlock()
|
||||||
|
|
||||||
site.Meters.ConsumerMetersRef = ref
|
site.Meters.ConsumerMetersRef = ref
|
||||||
settings.SetString(keys.ConsumerMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.ConsumerMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtMeterRefs returns the ExtMeterRef
|
// GetExtMeterRefs returns the ExtMeterRef
|
||||||
|
|
@ -153,7 +158,23 @@ func (site *Site) SetExtMeterRefs(ref []string) {
|
||||||
defer site.Unlock()
|
defer site.Unlock()
|
||||||
|
|
||||||
site.Meters.ExtMetersRef = ref
|
site.Meters.ExtMetersRef = ref
|
||||||
settings.SetString(keys.ExtMeters, strings.Join(filterConfigurable(ref), ","))
|
settings.SetString(keys.ExtMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurtailerRefs returns the curtailment device references
|
||||||
|
func (site *Site) GetCurtailerRefs() []string {
|
||||||
|
site.RLock()
|
||||||
|
defer site.RUnlock()
|
||||||
|
return site.CurtailersRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurtailerRefs sets the curtailment device references
|
||||||
|
func (site *Site) SetCurtailerRefs(ref []string) {
|
||||||
|
site.Lock()
|
||||||
|
defer site.Unlock()
|
||||||
|
|
||||||
|
site.CurtailersRef = ref
|
||||||
|
settings.SetString(keys.Curtailers, strings.Join(filterConfigurableCurtailers(ref), ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBatterySoc returns the current battery soc
|
// GetBatterySoc returns the current battery soc
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,30 @@ func (site *Site) dimMeters(dim bool) error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// curtailPV applies the HEMS curtailment percent to all curtailable pv or grid meters.
|
// curtailable is a named curtailment device
|
||||||
|
type curtailable struct {
|
||||||
|
name string
|
||||||
|
api.Curtailer
|
||||||
|
}
|
||||||
|
|
||||||
|
// curtailables returns the curtailment devices and the curtailable pv meters
|
||||||
|
func (site *Site) curtailables() []curtailable {
|
||||||
|
var res []curtailable
|
||||||
|
|
||||||
|
for _, dev := range site.pvMeters {
|
||||||
|
if m, ok := api.Cap[api.Curtailer](dev.Instance()); ok {
|
||||||
|
res = append(res, curtailable{deviceTitleOrName(dev), m})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range site.curtailers {
|
||||||
|
res = append(res, curtailable{deviceTitleOrName(dev), dev.Instance()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// curtailPV applies the HEMS curtailment percent to all curtailment devices and curtailable pv meters.
|
||||||
// Devices are only queried when the percent changes or after a failed attempt.
|
// Devices are only queried when the percent changes or after a failed attempt.
|
||||||
func (site *Site) curtailPV(percent *int) error {
|
func (site *Site) curtailPV(percent *int) error {
|
||||||
if percent == nil || site.curtailPercent != nil && *site.curtailPercent == *percent {
|
if percent == nil || site.curtailPercent != nil && *site.curtailPercent == *percent {
|
||||||
|
|
@ -149,22 +172,12 @@ func (site *Site) curtailPV(percent *int) error {
|
||||||
// invalidate until successfully applied
|
// invalidate until successfully applied
|
||||||
site.curtailPercent = nil
|
site.curtailPercent = nil
|
||||||
|
|
||||||
meters := slices.Clone(site.pvMeters)
|
|
||||||
if site.gridMeter != nil {
|
|
||||||
meters = append(meters, site.gridMeter)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs error
|
var errs error
|
||||||
for _, dev := range meters {
|
for _, m := range site.curtailables() {
|
||||||
m, ok := api.Cap[api.Curtailer](dev.Instance())
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// unreadable state: apply unconditionally
|
// unreadable state: apply unconditionally
|
||||||
curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff())
|
curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff())
|
||||||
if err != nil && !errors.Is(err, api.ErrNotAvailable) {
|
if err != nil && !errors.Is(err, api.ErrNotAvailable) {
|
||||||
errs = errors.Join(errs, fmt.Errorf("%s curtailed: %w", deviceTitleOrName(dev), err))
|
errs = errors.Join(errs, fmt.Errorf("%s curtailed: %w", m.name, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err == nil && curtailed == *percent {
|
if err == nil && curtailed == *percent {
|
||||||
|
|
@ -172,9 +185,9 @@ func (site *Site) curtailPV(percent *int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.SetCurtailPercent(*percent); err == nil {
|
if err := m.SetCurtailPercent(*percent); err == nil {
|
||||||
site.log.DEBUG.Printf("%s curtail: %d%%", deviceTitleOrName(dev), *percent)
|
site.log.DEBUG.Printf("%s curtail: %d%%", m.name, *percent)
|
||||||
} else if !errors.Is(err, api.ErrNotAvailable) {
|
} else if !errors.Is(err, api.ErrNotAvailable) {
|
||||||
errs = errors.Join(errs, fmt.Errorf("%s curtail: %w", deviceTitleOrName(dev), err))
|
errs = errors.Join(errs, fmt.Errorf("%s curtail: %w", m.name, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,24 @@ func TestCurtailPVNotAvailable(t *testing.T) {
|
||||||
assert.Equal(t, []int{60, 30}, m.setCalls)
|
assert.Equal(t, []int{60, 30}, m.setCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A curtailment device is applied like a curtailable pv meter.
|
||||||
|
func TestCurtailDevice(t *testing.T) {
|
||||||
|
m := &curtailableMeter{percent: 100}
|
||||||
|
site := &Site{
|
||||||
|
log: util.NewLogger("foo"),
|
||||||
|
curtailers: []config.Device[api.Curtailer]{config.NewStaticDevice[api.Curtailer](config.Named{}, m)},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, site.curtailPV(new(60)))
|
||||||
|
assert.Equal(t, []int{60}, m.setCalls)
|
||||||
|
assert.Equal(t, 1, m.gets)
|
||||||
|
|
||||||
|
// unchanged: device is not queried again
|
||||||
|
require.NoError(t, site.curtailPV(new(60)))
|
||||||
|
assert.Equal(t, []int{60}, m.setCalls)
|
||||||
|
assert.Equal(t, 1, m.gets)
|
||||||
|
}
|
||||||
|
|
||||||
// dimmableMeter counts device interactions to verify caching.
|
// dimmableMeter counts device interactions to verify caching.
|
||||||
type dimmableMeter struct {
|
type dimmableMeter struct {
|
||||||
api.Meter
|
api.Meter
|
||||||
|
|
|
||||||
25
core/site_curtailer_refs_test.go
Normal file
25
core/site_curtailer_refs_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFilterConfigurableCurtailers ensures curtailment device refs are not filtered
|
||||||
|
// against the meter registry, which would drop them from the persisted settings
|
||||||
|
func TestFilterConfigurableCurtailers(t *testing.T) {
|
||||||
|
name := config.NameForID(1)
|
||||||
|
|
||||||
|
conf := &config.Config{ID: 1}
|
||||||
|
|
||||||
|
var m api.Curtailer = &curtailableMeter{}
|
||||||
|
dev := config.NewConfigurableDevice(conf, m)
|
||||||
|
require.NoError(t, config.Curtailers().Add(dev))
|
||||||
|
t.Cleanup(func() { _ = config.Curtailers().Delete(name) })
|
||||||
|
|
||||||
|
require.Equal(t, []string{name}, filterConfigurableCurtailers([]string{name}))
|
||||||
|
require.Empty(t, filterConfigurableMeter([]string{name}), "curtailer must not resolve as meter")
|
||||||
|
}
|
||||||
28
curtailer/curtailer.go
Normal file
28
curtailer/curtailer.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package curtailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
reg "github.com/evcc-io/evcc/util/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
var registry = reg.New[api.Curtailer]("curtailer")
|
||||||
|
|
||||||
|
// NewFromConfig creates curtailer from configuration
|
||||||
|
func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.Curtailer, error) {
|
||||||
|
factory, err := registry.Get(strings.ToLower(typ))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := factory(ctx, other)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("cannot create curtailer type '%s': %w", util.TypeWithTemplateName(typ, other), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
45
curtailer/custom.go
Normal file
45
curtailer/custom.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package curtailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/api/implement"
|
||||||
|
"github.com/evcc-io/evcc/meter/measurement"
|
||||||
|
"github.com/evcc-io/evcc/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.AddCtx(api.Custom, NewConfigurableFromConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigurableFromConfig creates a curtailer from config
|
||||||
|
func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Curtailer, error) {
|
||||||
|
cc := struct {
|
||||||
|
measurement.Curtailer `mapstructure:",squash"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := util.DecodeOther(other, &cc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
curtailS, curtailedG, err := cc.Curtailer.Configure(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if curtailS == nil {
|
||||||
|
return nil, errors.New("missing curtail")
|
||||||
|
}
|
||||||
|
|
||||||
|
return implement.Curtailer(
|
||||||
|
func() (int, error) {
|
||||||
|
percent, err := curtailedG()
|
||||||
|
return int(percent), err
|
||||||
|
},
|
||||||
|
func(percent int) error {
|
||||||
|
return curtailS(int64(percent))
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
21
curtailer/template.go
Normal file
21
curtailer/template.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package curtailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.AddCtx("template", NewFromTemplateConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromTemplateConfig(ctx context.Context, other map[string]any) (api.Curtailer, error) {
|
||||||
|
instance, err := templates.RenderInstance(templates.Curtailer, other)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFromConfig(ctx, instance.Type, instance.Other)
|
||||||
|
}
|
||||||
21
curtailer/template_test.go
Normal file
21
curtailer/template_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package curtailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
|
"github.com/evcc-io/evcc/util/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var acceptable = []string{}
|
||||||
|
|
||||||
|
func TestTemplates(t *testing.T) {
|
||||||
|
templates.TestClass(t, templates.Curtailer, func(t *testing.T, values map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if _, err := NewFromConfig(t.Context(), "template", values); err != nil && !test.Acceptable(err, acceptable) {
|
||||||
|
t.Log(values)
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
i18n/de.json
13
i18n/de.json
|
|
@ -188,6 +188,15 @@
|
||||||
"label": "Währung",
|
"label": "Währung",
|
||||||
"title": "Währung"
|
"title": "Währung"
|
||||||
},
|
},
|
||||||
|
"curtailer": {
|
||||||
|
"description": "Gerät, das die Einspeisung auf Anforderung des Netzbetreibers begrenzt (§ 9 EEG). Viele Wechselrichter unterstützen dies direkt über ihre PV-Zähler-Konfiguration. Eine separate Konfiguration ist nur bei manchen Herstellern oder Anlagen mit mehreren Wechselrichtern nötig.",
|
||||||
|
"generic": "Generische Integrationen",
|
||||||
|
"specific": "Spezifische Integrationen",
|
||||||
|
"template": "Hersteller",
|
||||||
|
"title": "Einspeisebegrenzung",
|
||||||
|
"titleAdd": "Einspeisebegrenzung hinzufügen",
|
||||||
|
"titleEdit": "Einspeisebegrenzung bearbeiten"
|
||||||
|
},
|
||||||
"deviceValue": {
|
"deviceValue": {
|
||||||
"activeClients": "Aktive Clients",
|
"activeClients": "Aktive Clients",
|
||||||
"amount": "Anzahl",
|
"amount": "Anzahl",
|
||||||
|
|
@ -212,7 +221,7 @@
|
||||||
"currentRange": "Strom",
|
"currentRange": "Strom",
|
||||||
"curtailLimit": "Einspeiselimit",
|
"curtailLimit": "Einspeiselimit",
|
||||||
"curtailable": "Abregelbar",
|
"curtailable": "Abregelbar",
|
||||||
"curtailed": "Einspeisung begrenzt",
|
"curtailed": "Einspeiselimit",
|
||||||
"detected": "Erkannt",
|
"detected": "Erkannt",
|
||||||
"dimLimit": "Verbrauchslimit",
|
"dimLimit": "Verbrauchslimit",
|
||||||
"dimmed": "Verbrauch begrenzt",
|
"dimmed": "Verbrauch begrenzt",
|
||||||
|
|
@ -539,6 +548,7 @@
|
||||||
"main": {
|
"main": {
|
||||||
"addAdditional": "Zusätzlichen Zähler hinzufügen",
|
"addAdditional": "Zusätzlichen Zähler hinzufügen",
|
||||||
"addConsumer": "Verbraucher hinzufügen",
|
"addConsumer": "Verbraucher hinzufügen",
|
||||||
|
"addCurtailer": "Einspeisebegrenzung hinzufügen",
|
||||||
"addGrid": "Netzzähler hinzufügen",
|
"addGrid": "Netzzähler hinzufügen",
|
||||||
"addLoadpoint": "Ladepunkt oder Heizung hinzufügen",
|
"addLoadpoint": "Ladepunkt oder Heizung hinzufügen",
|
||||||
"addPvBattery": "PV oder Speicher hinzufügen",
|
"addPvBattery": "PV oder Speicher hinzufügen",
|
||||||
|
|
@ -639,6 +649,7 @@
|
||||||
"aux": "Selbstregelnden Verbraucher hinzufügen",
|
"aux": "Selbstregelnden Verbraucher hinzufügen",
|
||||||
"battery": "Hausbatterie hinzufügen",
|
"battery": "Hausbatterie hinzufügen",
|
||||||
"consumer": "Regulären Verbraucher hinzufügen",
|
"consumer": "Regulären Verbraucher hinzufügen",
|
||||||
|
"grid": "Netzzähler hinzufügen",
|
||||||
"pv": "PV-Anlage hinzufügen"
|
"pv": "PV-Anlage hinzufügen"
|
||||||
},
|
},
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
|
|
|
||||||
13
i18n/en.json
13
i18n/en.json
|
|
@ -188,6 +188,15 @@
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"title": "Currency"
|
"title": "Currency"
|
||||||
},
|
},
|
||||||
|
"curtailer": {
|
||||||
|
"description": "Device that limits feed-in on request of the grid operator (§ 9 EEG). Many solar inverters support this directly via their solar meter configuration. A separate configuration is only needed for some manufacturers or multi-inverter systems.",
|
||||||
|
"generic": "Generic integrations",
|
||||||
|
"specific": "Specific integrations",
|
||||||
|
"template": "Manufacturer",
|
||||||
|
"title": "Curtailment",
|
||||||
|
"titleAdd": "Add Curtailment Device",
|
||||||
|
"titleEdit": "Edit Curtailment Device"
|
||||||
|
},
|
||||||
"deviceValue": {
|
"deviceValue": {
|
||||||
"activeClients": "Active clients",
|
"activeClients": "Active clients",
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
|
|
@ -212,7 +221,7 @@
|
||||||
"currentRange": "Current",
|
"currentRange": "Current",
|
||||||
"curtailLimit": "Feed-in limit",
|
"curtailLimit": "Feed-in limit",
|
||||||
"curtailable": "Curtailable",
|
"curtailable": "Curtailable",
|
||||||
"curtailed": "Feed-in limited",
|
"curtailed": "Feed-in limit",
|
||||||
"detected": "Detected",
|
"detected": "Detected",
|
||||||
"dimLimit": "Consumption limit",
|
"dimLimit": "Consumption limit",
|
||||||
"dimmed": "Consumption limited",
|
"dimmed": "Consumption limited",
|
||||||
|
|
@ -539,6 +548,7 @@
|
||||||
"main": {
|
"main": {
|
||||||
"addAdditional": "Add additional meter",
|
"addAdditional": "Add additional meter",
|
||||||
"addConsumer": "Add consumer",
|
"addConsumer": "Add consumer",
|
||||||
|
"addCurtailer": "Add curtailment device",
|
||||||
"addGrid": "Add grid meter",
|
"addGrid": "Add grid meter",
|
||||||
"addLoadpoint": "Add charging point or heater",
|
"addLoadpoint": "Add charging point or heater",
|
||||||
"addPvBattery": "Add solar or battery",
|
"addPvBattery": "Add solar or battery",
|
||||||
|
|
@ -639,6 +649,7 @@
|
||||||
"aux": "Add self-regulating consumer",
|
"aux": "Add self-regulating consumer",
|
||||||
"battery": "Add battery meter",
|
"battery": "Add battery meter",
|
||||||
"consumer": "Add regular consumer",
|
"consumer": "Add regular consumer",
|
||||||
|
"grid": "Add grid meter",
|
||||||
"pv": "Add solar meter"
|
"pv": "Add solar meter"
|
||||||
},
|
},
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/evcc-io/evcc/core/circuit"
|
"github.com/evcc-io/evcc/core/circuit"
|
||||||
"github.com/evcc-io/evcc/core/keys"
|
"github.com/evcc-io/evcc/core/keys"
|
||||||
"github.com/evcc-io/evcc/core/site"
|
"github.com/evcc-io/evcc/core/site"
|
||||||
|
"github.com/evcc-io/evcc/curtailer"
|
||||||
"github.com/evcc-io/evcc/hems"
|
"github.com/evcc-io/evcc/hems"
|
||||||
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
||||||
"github.com/evcc-io/evcc/messenger"
|
"github.com/evcc-io/evcc/messenger"
|
||||||
|
|
@ -81,6 +82,9 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
res, err = devicesConfig(class, config.Circuits(), hidePrivate)
|
res, err = devicesConfig(class, config.Circuits(), hidePrivate)
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
res, err = devicesConfig(class, config.Curtailers(), hidePrivate)
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
res, err = devicesConfig(class, config.Hems(), hidePrivate)
|
res, err = devicesConfig(class, config.Hems(), hidePrivate)
|
||||||
|
|
||||||
|
|
@ -210,6 +214,9 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
res, err = deviceConfig(class, id, config.Circuits(), hidePrivate)
|
res, err = deviceConfig(class, id, config.Circuits(), hidePrivate)
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
res, err = deviceConfig(class, id, config.Curtailers(), hidePrivate)
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
res, err = deviceConfig(class, id, config.Hems(), hidePrivate)
|
res, err = deviceConfig(class, id, config.Hems(), hidePrivate)
|
||||||
|
|
||||||
|
|
@ -275,6 +282,9 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
instance, err = deviceStatus(name, config.Circuits())
|
instance, err = deviceStatus(name, config.Circuits())
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
instance, err = deviceStatus(name, config.Curtailers())
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
err = api.ErrNotAvailable
|
err = api.ErrNotAvailable
|
||||||
|
|
||||||
|
|
@ -375,6 +385,9 @@ func newDeviceHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
conf, err = newDevice(ctx, class, req, circuit.NewFromConfig, config.Circuits(), force)
|
conf, err = newDevice(ctx, class, req, circuit.NewFromConfig, config.Circuits(), force)
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
conf, err = newDevice(ctx, class, req, curtailer.NewFromConfig, config.Curtailers(), force)
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
if existing, _ := config.ConfigurationByClass(templates.Hems); existing != nil {
|
if existing, _ := config.ConfigurationByClass(templates.Hems); existing != nil {
|
||||||
err = errors.New("hems already configured")
|
err = errors.New("hems already configured")
|
||||||
|
|
@ -477,6 +490,9 @@ func updateDeviceHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
err = updateDevice(ctx, id, class, req, circuit.NewFromConfig, config.Circuits(), force)
|
err = updateDevice(ctx, id, class, req, circuit.NewFromConfig, config.Circuits(), force)
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
err = updateDevice(ctx, id, class, req, curtailer.NewFromConfig, config.Curtailers(), force)
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
err = updateDevice(ctx, id, class, req, newHemsFactory(site), config.Hems(), force)
|
err = updateDevice(ctx, id, class, req, newHemsFactory(site), config.Hems(), force)
|
||||||
|
|
||||||
|
|
@ -655,6 +671,12 @@ func deleteDeviceHandler(site site.API) func(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
err = deleteDevice(id, config.Curtailers())
|
||||||
|
|
||||||
|
// cleanup references
|
||||||
|
cleanupSiteMeterRef(config.NameForID(id), site.GetCurtailerRefs, site.SetCurtailerRefs)
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
err = deleteDevice(id, config.Hems())
|
err = deleteDevice(id, config.Hems())
|
||||||
|
|
||||||
|
|
@ -759,6 +781,9 @@ func testConfigHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
||||||
case templates.Circuit:
|
case templates.Circuit:
|
||||||
instance, err = testConfig(ctx, id, class, req, circuit.NewFromConfig, config.Circuits())
|
instance, err = testConfig(ctx, id, class, req, circuit.NewFromConfig, config.Circuits())
|
||||||
|
|
||||||
|
case templates.Curtailer:
|
||||||
|
instance, err = testConfig(ctx, id, class, req, curtailer.NewFromConfig, config.Curtailers())
|
||||||
|
|
||||||
case templates.Hems:
|
case templates.Hems:
|
||||||
instance, err = testConfig(ctx, id, class, req, newHemsFactory(site), config.Hems())
|
instance, err = testConfig(ctx, id, class, req, newHemsFactory(site), config.Hems())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -446,11 +446,17 @@ func testInstance(ctx context.Context, instance any) map[string]testResult {
|
||||||
|
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
if dev, ok := api.Cap[api.Curtailer](instance); ok {
|
if dev, ok := api.Cap[api.Curtailer](instance); ok {
|
||||||
|
if _, isMeter := instance.(api.Meter); isMeter {
|
||||||
|
// curtailment as meter capability, limit only reported while actually curtailing
|
||||||
makeResult("curtailable", true, nil)
|
makeResult("curtailable", true, nil)
|
||||||
// only reported while actually curtailing
|
|
||||||
if val, err := dev.CurtailedPercent(); err != nil || val < 100 {
|
if val, err := dev.CurtailedPercent(); err != nil || val < 100 {
|
||||||
makeResult("curtailed", val, err)
|
makeResult("curtailed", val, err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// dedicated curtailment device, always report the limit
|
||||||
|
val, err := dev.CurtailedPercent()
|
||||||
|
makeResult("curtailed", val, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ func siteHandler(site site.API) http.HandlerFunc {
|
||||||
Aux []string `json:"aux"`
|
Aux []string `json:"aux"`
|
||||||
Ext []string `json:"ext"`
|
Ext []string `json:"ext"`
|
||||||
Consumer []string `json:"consumer"`
|
Consumer []string `json:"consumer"`
|
||||||
|
Curtail []string `json:"curtail"`
|
||||||
}{
|
}{
|
||||||
Title: site.GetTitle(),
|
Title: site.GetTitle(),
|
||||||
Grid: site.GetGridMeterRef(),
|
Grid: site.GetGridMeterRef(),
|
||||||
|
Curtail: site.GetCurtailerRefs(),
|
||||||
PV: site.GetPVMeterRefs(),
|
PV: site.GetPVMeterRefs(),
|
||||||
Battery: site.GetBatteryMeterRefs(),
|
Battery: site.GetBatteryMeterRefs(),
|
||||||
Aux: site.GetAuxMeterRefs(),
|
Aux: site.GetAuxMeterRefs(),
|
||||||
|
|
@ -54,6 +56,7 @@ func updateSiteHandler(site site.API) http.HandlerFunc {
|
||||||
Aux *[]string
|
Aux *[]string
|
||||||
Ext *[]string
|
Ext *[]string
|
||||||
Consumer *[]string
|
Consumer *[]string
|
||||||
|
Curtail *[]string
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := jsonDecoder(r.Body).Decode(&payload); err != nil {
|
if err := jsonDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
|
@ -119,6 +122,18 @@ func updateSiteHandler(site site.API) http.HandlerFunc {
|
||||||
setConfigDirty()
|
setConfigDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Curtail != nil {
|
||||||
|
for _, m := range *payload.Curtail {
|
||||||
|
if _, err := config.Curtailers().ByName(m); err != nil {
|
||||||
|
jsonError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
site.SetCurtailerRefs(*payload.Curtail)
|
||||||
|
setConfigDirty()
|
||||||
|
}
|
||||||
|
|
||||||
// persist immediately to keep meter refs consistent with device config on unclean shutdown
|
// persist immediately to keep meter refs consistent with device config on unclean shutdown
|
||||||
if err := settings.Persist(); err != nil {
|
if err := settings.Persist(); err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, err)
|
jsonError(w, http.StatusInternalServerError, err)
|
||||||
|
|
|
||||||
25
templates/definition/curtailer/sma-homemanager.yaml
Normal file
25
templates/definition/curtailer/sma-homemanager.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
template: sma-homemanager
|
||||||
|
products:
|
||||||
|
- brand: SMA
|
||||||
|
description:
|
||||||
|
generic: Sunny Home Manager 2.0
|
||||||
|
params:
|
||||||
|
- name: modbus
|
||||||
|
choice: ["tcpip"]
|
||||||
|
port: 502
|
||||||
|
id: 2
|
||||||
|
help:
|
||||||
|
en: ModbusTCP server needs to be enabled.
|
||||||
|
de: Der ModbusTCP Server muss aktiviert sein.
|
||||||
|
render: |
|
||||||
|
type: custom
|
||||||
|
curtail:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 2 }}
|
||||||
|
register:
|
||||||
|
address: 40016 # Wirkleistungssollwert P, in % der maximalen Wirkleistung (PMAX) der PV-Anlage
|
||||||
|
type: writesingle
|
||||||
|
encoding: uint16
|
||||||
|
curtailed:
|
||||||
|
source: error
|
||||||
|
error: ErrNotAvailable
|
||||||
135
templates/definition/curtailer/solaredge.yaml
Normal file
135
templates/definition/curtailer/solaredge.yaml
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
template: solaredge
|
||||||
|
products:
|
||||||
|
- brand: SolarEdge
|
||||||
|
description:
|
||||||
|
de: Wechselrichter (Einspeisebegrenzung)
|
||||||
|
en: Inverter (feed-in limitation)
|
||||||
|
requirements:
|
||||||
|
description:
|
||||||
|
de: |
|
||||||
|
Nur ein System kann und darf zeitgleich eine Modbus TCP-Verbindung zum Wechselrichter haben!
|
||||||
|
Die Einspeisebegrenzung begrenzt die Netzeinspeisung am Netzanschlusspunkt auf den geforderten Prozentsatz der installierten Generatorleistung.
|
||||||
|
en: |
|
||||||
|
Only one system can and may have a Modbus TCP connection to the inverter at the same time!
|
||||||
|
Feed-in limitation limits the export power at the grid connection point to the requested percentage of the installed generator power.
|
||||||
|
params:
|
||||||
|
- name: modbus
|
||||||
|
choice: ["tcpip", "rs485"]
|
||||||
|
id: 1
|
||||||
|
port: 1502
|
||||||
|
- name: productionnominalmax
|
||||||
|
type: float
|
||||||
|
unit: Wp
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
en: Installed generator power
|
||||||
|
de: Installierte Generatorleistung
|
||||||
|
help:
|
||||||
|
en: Rated generator/module power of the installation (reference value according to § 9 EEG).
|
||||||
|
de: Gesamtnennleistung bzw. Modul- oder Generatorleistung der Anlage (Bezugsgröße nach § 9 EEG).
|
||||||
|
render: |
|
||||||
|
type: custom
|
||||||
|
curtail:
|
||||||
|
source: sequence
|
||||||
|
set:
|
||||||
|
- source: switch
|
||||||
|
switch:
|
||||||
|
- case: 100
|
||||||
|
set:
|
||||||
|
source: go
|
||||||
|
in:
|
||||||
|
- name: mode
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 18 }}
|
||||||
|
register:
|
||||||
|
address: 57344 # Export Control Mode
|
||||||
|
type: holding
|
||||||
|
decode: uint16
|
||||||
|
out:
|
||||||
|
- name: out
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 18 }}
|
||||||
|
register:
|
||||||
|
address: 57344 # Export Control Mode
|
||||||
|
type: writesingle
|
||||||
|
encoding: uint16
|
||||||
|
script: |
|
||||||
|
mode &^ 1 // clear bit 0 (export limitation), preserve other bits (e.g. indirect production limitation)
|
||||||
|
default:
|
||||||
|
source: go
|
||||||
|
in:
|
||||||
|
- name: mode
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 14 }}
|
||||||
|
register:
|
||||||
|
address: 57344 # Export Control Mode
|
||||||
|
type: holding
|
||||||
|
decode: uint16
|
||||||
|
out:
|
||||||
|
- name: out
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 14 }}
|
||||||
|
register:
|
||||||
|
address: 57344 # Export Control Mode
|
||||||
|
type: writesingle
|
||||||
|
encoding: uint16
|
||||||
|
script: |
|
||||||
|
mode | 1 // set bit 0 (export limitation via meter), preserve other bits
|
||||||
|
- source: const
|
||||||
|
value: 0 # Total
|
||||||
|
set:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 8 }}
|
||||||
|
register:
|
||||||
|
address: 57345 # Export Control Limit Mode
|
||||||
|
type: writesingle
|
||||||
|
encoding: uint16
|
||||||
|
- source: modbus
|
||||||
|
{{- include "modbus" . | indent 6 }}
|
||||||
|
register:
|
||||||
|
address: 57346 # Export Control Site Limit
|
||||||
|
type: writemultiple
|
||||||
|
encoding: float32s
|
||||||
|
scale: {{ divf (maxf .productionnominalmax 1) 100 }} # % to W
|
||||||
|
curtailed:
|
||||||
|
source: go
|
||||||
|
in:
|
||||||
|
- name: mode
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 8 }}
|
||||||
|
register:
|
||||||
|
address: 57344 # Export Control Mode
|
||||||
|
type: holding
|
||||||
|
decode: uint16
|
||||||
|
- name: limit
|
||||||
|
type: float
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 8 }}
|
||||||
|
register:
|
||||||
|
address: 57346 # Export Control Site Limit
|
||||||
|
type: holding
|
||||||
|
decode: float32s
|
||||||
|
script: |
|
||||||
|
percent := 100.0
|
||||||
|
if mode&1 != 0 {
|
||||||
|
percent = limit * 100 / {{ maxf .productionnominalmax 1 }}
|
||||||
|
}
|
||||||
|
p := int(percent + 0.5)
|
||||||
|
if p < 0 {
|
||||||
|
p = 0
|
||||||
|
}
|
||||||
|
if p > 100 {
|
||||||
|
p = 100
|
||||||
|
}
|
||||||
|
p
|
||||||
|
|
@ -2,7 +2,7 @@ package definition
|
||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml messenger/*.yaml circuit/*.yaml hems/*.yaml
|
//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml messenger/*.yaml circuit/*.yaml hems/*.yaml curtailer/*.yaml
|
||||||
var YamlTemplates embed.FS
|
var YamlTemplates embed.FS
|
||||||
|
|
||||||
//go:embed */*.tpl
|
//go:embed */*.tpl
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ render: |
|
||||||
power:
|
power:
|
||||||
source: const
|
source: const
|
||||||
value: {{ .power }}
|
value: {{ .power }}
|
||||||
{{- if .curtailable }}
|
{{- if eq .curtailable "true" }}
|
||||||
curtail:
|
curtail:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
template: sma-homemanager-modbus
|
|
||||||
products:
|
|
||||||
- brand: SMA
|
|
||||||
description:
|
|
||||||
generic: Sunny Home Manager 2.0 Modbus
|
|
||||||
capabilities: ["curtail"]
|
|
||||||
params:
|
|
||||||
- name: usage
|
|
||||||
choice: ["grid"]
|
|
||||||
- name: modbus
|
|
||||||
choice: ["tcpip"]
|
|
||||||
port: 502
|
|
||||||
id: 2
|
|
||||||
help:
|
|
||||||
en: ModbusTCP server needs to be enabled.
|
|
||||||
de: Der ModbusTCP Server muss aktiviert sein.
|
|
||||||
render: |
|
|
||||||
type: custom
|
|
||||||
power:
|
|
||||||
source: calc
|
|
||||||
add:
|
|
||||||
- source: modbus
|
|
||||||
{{- include "modbus" . | indent 6 }}
|
|
||||||
register:
|
|
||||||
address: 30865 # SMA Modbus Profile: Metering.GridMs.TotWIn
|
|
||||||
type: input
|
|
||||||
decode: int32nan
|
|
||||||
- source: modbus
|
|
||||||
{{- include "modbus" . | indent 6 }}
|
|
||||||
register:
|
|
||||||
address: 30867 # SMA Modbus Profile: Metering.GridMs.TotWOut
|
|
||||||
type: input
|
|
||||||
decode: int32nan
|
|
||||||
scale: -1
|
|
||||||
energy:
|
|
||||||
source: modbus
|
|
||||||
{{- include "modbus" . | indent 2 }}
|
|
||||||
register:
|
|
||||||
address: 30581 # SMA Modbus Profile: Metering.GridMs.TotWhIn
|
|
||||||
type: holding
|
|
||||||
decode: int32nan
|
|
||||||
scale: 0.001
|
|
||||||
returnenergy:
|
|
||||||
source: modbus
|
|
||||||
{{- include "modbus" . | indent 2 }}
|
|
||||||
register:
|
|
||||||
address: 30583 # SMA Modbus Profile: Metering.GridMs.TotWhOut
|
|
||||||
type: holding
|
|
||||||
decode: int32nan
|
|
||||||
scale: 0.001
|
|
||||||
curtail:
|
|
||||||
source: modbus
|
|
||||||
{{- include "modbus" . | indent 2 }}
|
|
||||||
register:
|
|
||||||
address: 40016 # Wirkleistungssollwert P, in % der maximalen Wirkleistung (PMAX) der PV-Anlage.
|
|
||||||
type: writesingle
|
|
||||||
decode: uint16
|
|
||||||
curtailed:
|
|
||||||
source: error
|
|
||||||
error: ErrNotAvailable
|
|
||||||
|
|
@ -345,9 +345,7 @@ w4:${signalSource("w4")}`
|
||||||
|
|
||||||
// no signals: no limits
|
// no signals: no limits
|
||||||
await page.goto("/#/config");
|
await page.goto("/#/config");
|
||||||
await expect(hems).toContainText(
|
await expect(hems).toContainText(["Consumption limited", "no", "Feed-in limit", "no"].join(""));
|
||||||
["Consumption limited", "no", "Feed-in limited", "no"].join("")
|
|
||||||
);
|
|
||||||
await expect(hint).not.toBeVisible();
|
await expect(hint).not.toBeVisible();
|
||||||
|
|
||||||
// dim (W4): consumption limited
|
// dim (W4): consumption limited
|
||||||
|
|
@ -391,9 +389,7 @@ w4:${signalSource("w4")}`
|
||||||
await expect(hint).not.toBeVisible();
|
await expect(hint).not.toBeVisible();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(pvBanner).not.toBeVisible();
|
await expect(pvBanner).not.toBeVisible();
|
||||||
await expect(hems).toContainText(
|
await expect(hems).toContainText(["Consumption limited", "no", "Feed-in limit", "no"].join(""));
|
||||||
["Consumption limited", "no", "Feed-in limited", "no"].join("")
|
|
||||||
);
|
|
||||||
|
|
||||||
await stopSimulator();
|
await stopSimulator();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ var instance struct {
|
||||||
chargers *handler[api.Charger]
|
chargers *handler[api.Charger]
|
||||||
vehicles *handler[api.Vehicle]
|
vehicles *handler[api.Vehicle]
|
||||||
circuits *handler[api.Circuit]
|
circuits *handler[api.Circuit]
|
||||||
|
curtailers *handler[api.Curtailer]
|
||||||
hems *handler[hems.API]
|
hems *handler[hems.API]
|
||||||
messengers *handler[api.Messenger]
|
messengers *handler[api.Messenger]
|
||||||
loadpoints *handler[loadpoint.API]
|
loadpoints *handler[loadpoint.API]
|
||||||
|
|
@ -29,6 +30,7 @@ func Reset() {
|
||||||
instance.chargers = &handler[api.Charger]{topic: "charger"}
|
instance.chargers = &handler[api.Charger]{topic: "charger"}
|
||||||
instance.vehicles = &handler[api.Vehicle]{topic: "vehicle"}
|
instance.vehicles = &handler[api.Vehicle]{topic: "vehicle"}
|
||||||
instance.circuits = &handler[api.Circuit]{topic: "circuit"}
|
instance.circuits = &handler[api.Circuit]{topic: "circuit"}
|
||||||
|
instance.curtailers = &handler[api.Curtailer]{topic: "curtailer"}
|
||||||
instance.hems = &handler[hems.API]{topic: "hems"}
|
instance.hems = &handler[hems.API]{topic: "hems"}
|
||||||
instance.messengers = &handler[api.Messenger]{topic: "messenger"}
|
instance.messengers = &handler[api.Messenger]{topic: "messenger"}
|
||||||
instance.loadpoints = &handler[loadpoint.API]{topic: "loadpoint"}
|
instance.loadpoints = &handler[loadpoint.API]{topic: "loadpoint"}
|
||||||
|
|
@ -63,6 +65,10 @@ func Circuits() Handler[api.Circuit] {
|
||||||
return instance.circuits
|
return instance.circuits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Curtailers() Handler[api.Curtailer] {
|
||||||
|
return instance.curtailers
|
||||||
|
}
|
||||||
|
|
||||||
func Hems() Handler[hems.API] {
|
func Hems() Handler[hems.API] {
|
||||||
return instance.hems
|
return instance.hems
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ const (
|
||||||
Circuit
|
Circuit
|
||||||
Messenger
|
Messenger
|
||||||
Hems
|
Hems
|
||||||
|
Curtailer
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _ClassName = "chargermetervehicletariffloadpointcircuitmessengerhems"
|
const _ClassName = "chargermetervehicletariffloadpointcircuitmessengerhemscurtailer"
|
||||||
|
|
||||||
var _ClassIndex = [...]uint8{0, 7, 12, 19, 25, 34, 41, 50, 54}
|
var _ClassIndex = [...]uint8{0, 7, 12, 19, 25, 34, 41, 50, 54, 63}
|
||||||
|
|
||||||
const _ClassLowerName = "chargermetervehicletariffloadpointcircuitmessengerhems"
|
const _ClassLowerName = "chargermetervehicletariffloadpointcircuitmessengerhemscurtailer"
|
||||||
|
|
||||||
func (i Class) String() string {
|
func (i Class) String() string {
|
||||||
i -= 1
|
i -= 1
|
||||||
|
|
@ -33,9 +33,10 @@ func _ClassNoOp() {
|
||||||
_ = x[Circuit-(6)]
|
_ = x[Circuit-(6)]
|
||||||
_ = x[Messenger-(7)]
|
_ = x[Messenger-(7)]
|
||||||
_ = x[Hems-(8)]
|
_ = x[Hems-(8)]
|
||||||
|
_ = x[Curtailer-(9)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff, Loadpoint, Circuit, Messenger, Hems}
|
var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff, Loadpoint, Circuit, Messenger, Hems, Curtailer}
|
||||||
|
|
||||||
var _ClassNameToValueMap = map[string]Class{
|
var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassName[0:7]: Charger,
|
_ClassName[0:7]: Charger,
|
||||||
|
|
@ -54,6 +55,8 @@ var _ClassNameToValueMap = map[string]Class{
|
||||||
_ClassLowerName[41:50]: Messenger,
|
_ClassLowerName[41:50]: Messenger,
|
||||||
_ClassName[50:54]: Hems,
|
_ClassName[50:54]: Hems,
|
||||||
_ClassLowerName[50:54]: Hems,
|
_ClassLowerName[50:54]: Hems,
|
||||||
|
_ClassName[54:63]: Curtailer,
|
||||||
|
_ClassLowerName[54:63]: Curtailer,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ClassNames = []string{
|
var _ClassNames = []string{
|
||||||
|
|
@ -65,6 +68,7 @@ var _ClassNames = []string{
|
||||||
_ClassName[34:41],
|
_ClassName[34:41],
|
||||||
_ClassName[41:50],
|
_ClassName[41:50],
|
||||||
_ClassName[50:54],
|
_ClassName[50:54],
|
||||||
|
_ClassName[54:63],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClassString retrieves an enum value from the enum constants string name.
|
// ClassString retrieves an enum value from the enum constants string name.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func init() {
|
||||||
// must not collide with an include name (`$` cannot appear in a file name)
|
// must not collide with an include name (`$` cannot appear in a file name)
|
||||||
baseTmpl = template.Must(FuncMap(template.New("$root")).ParseFS(includeFS, "includes/*.tpl"))
|
baseTmpl = template.Must(FuncMap(template.New("$root")).ParseFS(includeFS, "includes/*.tpl"))
|
||||||
|
|
||||||
for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit, Hems} {
|
for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit, Hems, Curtailer} {
|
||||||
if err := loadIncludes(definition.TemplateIncludes, class); err != nil {
|
if err := loadIncludes(definition.TemplateIncludes, class); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ type Usage int
|
||||||
//go:generate go tool enumer -type Usage -trimprefix Usage -transform=lower -text
|
//go:generate go tool enumer -type Usage -trimprefix Usage -transform=lower -text
|
||||||
const (
|
const (
|
||||||
UsageGrid Usage = iota
|
UsageGrid Usage = iota
|
||||||
|
UsageCurtail
|
||||||
UsagePV
|
UsagePV
|
||||||
UsageBattery
|
UsageBattery
|
||||||
UsageCharge
|
UsageCharge
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _UsageName = "gridpvbatterychargeaux"
|
const _UsageName = "gridcurtailpvbatterychargeaux"
|
||||||
|
|
||||||
var _UsageIndex = [...]uint8{0, 4, 6, 13, 19, 22}
|
var _UsageIndex = [...]uint8{0, 4, 11, 13, 20, 26, 29}
|
||||||
|
|
||||||
const _UsageLowerName = "gridpvbatterychargeaux"
|
const _UsageLowerName = "gridcurtailpvbatterychargeaux"
|
||||||
|
|
||||||
func (i Usage) String() string {
|
func (i Usage) String() string {
|
||||||
if i < 0 || i >= Usage(len(_UsageIndex)-1) {
|
if i < 0 || i >= Usage(len(_UsageIndex)-1) {
|
||||||
|
|
@ -25,33 +25,37 @@ func (i Usage) String() string {
|
||||||
func _UsageNoOp() {
|
func _UsageNoOp() {
|
||||||
var x [1]struct{}
|
var x [1]struct{}
|
||||||
_ = x[UsageGrid-(0)]
|
_ = x[UsageGrid-(0)]
|
||||||
_ = x[UsagePV-(1)]
|
_ = x[UsageCurtail-(1)]
|
||||||
_ = x[UsageBattery-(2)]
|
_ = x[UsagePV-(2)]
|
||||||
_ = x[UsageCharge-(3)]
|
_ = x[UsageBattery-(3)]
|
||||||
_ = x[UsageAux-(4)]
|
_ = x[UsageCharge-(4)]
|
||||||
|
_ = x[UsageAux-(5)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _UsageValues = []Usage{UsageGrid, UsagePV, UsageBattery, UsageCharge, UsageAux}
|
var _UsageValues = []Usage{UsageGrid, UsageCurtail, UsagePV, UsageBattery, UsageCharge, UsageAux}
|
||||||
|
|
||||||
var _UsageNameToValueMap = map[string]Usage{
|
var _UsageNameToValueMap = map[string]Usage{
|
||||||
_UsageName[0:4]: UsageGrid,
|
_UsageName[0:4]: UsageGrid,
|
||||||
_UsageLowerName[0:4]: UsageGrid,
|
_UsageLowerName[0:4]: UsageGrid,
|
||||||
_UsageName[4:6]: UsagePV,
|
_UsageName[4:11]: UsageCurtail,
|
||||||
_UsageLowerName[4:6]: UsagePV,
|
_UsageLowerName[4:11]: UsageCurtail,
|
||||||
_UsageName[6:13]: UsageBattery,
|
_UsageName[11:13]: UsagePV,
|
||||||
_UsageLowerName[6:13]: UsageBattery,
|
_UsageLowerName[11:13]: UsagePV,
|
||||||
_UsageName[13:19]: UsageCharge,
|
_UsageName[13:20]: UsageBattery,
|
||||||
_UsageLowerName[13:19]: UsageCharge,
|
_UsageLowerName[13:20]: UsageBattery,
|
||||||
_UsageName[19:22]: UsageAux,
|
_UsageName[20:26]: UsageCharge,
|
||||||
_UsageLowerName[19:22]: UsageAux,
|
_UsageLowerName[20:26]: UsageCharge,
|
||||||
|
_UsageName[26:29]: UsageAux,
|
||||||
|
_UsageLowerName[26:29]: UsageAux,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _UsageNames = []string{
|
var _UsageNames = []string{
|
||||||
_UsageName[0:4],
|
_UsageName[0:4],
|
||||||
_UsageName[4:6],
|
_UsageName[4:11],
|
||||||
_UsageName[6:13],
|
_UsageName[11:13],
|
||||||
_UsageName[13:19],
|
_UsageName[13:20],
|
||||||
_UsageName[19:22],
|
_UsageName[20:26],
|
||||||
|
_UsageName[26:29],
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsageString retrieves an enum value from the enum constants string name.
|
// UsageString retrieves an enum value from the enum constants string name.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue