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
|
||||
Loadpoints []config.Named
|
||||
Circuits []config.Named
|
||||
Curtailers []config.Named
|
||||
}
|
||||
|
||||
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"
|
||||
@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>
|
||||
</button>
|
||||
</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 ConfigMessenger = Entity;
|
||||
|
||||
export interface ConfigCurtailer extends Entity {
|
||||
deviceTitle?: string;
|
||||
}
|
||||
|
||||
export interface ConfigHems extends Entity {
|
||||
deviceProduct?: string;
|
||||
}
|
||||
|
|
@ -1368,7 +1372,8 @@ export type DeviceType =
|
|||
| "loadpoint"
|
||||
| "messenger"
|
||||
| "tariff"
|
||||
| "hems";
|
||||
| "hems"
|
||||
| "curtailer";
|
||||
export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer";
|
||||
export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux";
|
||||
export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature";
|
||||
|
|
@ -1385,6 +1390,7 @@ export interface SiteConfig {
|
|||
aux: string[] | null;
|
||||
ext: string[] | null;
|
||||
consumer: string[] | null;
|
||||
curtail: string[] | null;
|
||||
}
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
|
|
|||
|
|
@ -150,6 +150,31 @@
|
|||
data-testid="add-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>
|
||||
</ConfigSection>
|
||||
|
||||
|
|
@ -162,7 +187,7 @@
|
|||
meter-type="pv"
|
||||
:has-error="hasDeviceError('meter', meter.name)"
|
||||
:tags="deviceTags('meter', meter.name)"
|
||||
:banner="meterBanner(meter.name)"
|
||||
:banner="curtailmentBanner('meter', meter.name)"
|
||||
@edit="(type, id) => openModal('meter', { type, id })"
|
||||
@enable="handleDisable('meter', meter.id, false)"
|
||||
/>
|
||||
|
|
@ -525,6 +550,7 @@
|
|||
<MessagingLegacyModal @changed="loadDirty" />
|
||||
<MessagingModal :messengers="messengers" @changed="loadDirty" />
|
||||
<MessengerModal @changed="messengerChanged" />
|
||||
<CurtailerModal @changed="curtailerChanged" />
|
||||
<TariffsLegacyModal @changed="loadDirty" />
|
||||
<TariffModal
|
||||
:currency="currency"
|
||||
|
|
@ -579,6 +605,8 @@ import CircuitsModal from "../components/Config/CircuitsModal.vue";
|
|||
import CircuitTags from "../components/Config/CircuitTags.vue";
|
||||
import collector from "../mixins/collector";
|
||||
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 DeviceTags from "../components/Config/DeviceTags.vue";
|
||||
import EebusIcon from "../components/MaterialIcon/Eebus.vue";
|
||||
|
|
@ -633,6 +661,7 @@ import type {
|
|||
ConfigCharger,
|
||||
ConfigVehicle,
|
||||
ConfigCircuit,
|
||||
ConfigCurtailer,
|
||||
ConfigMessenger,
|
||||
ConfigHems,
|
||||
ConfigLoadpoint,
|
||||
|
|
@ -691,6 +720,8 @@ export default defineComponent({
|
|||
CircuitsModal,
|
||||
CircuitTags,
|
||||
ControlModal,
|
||||
CurtailerModal,
|
||||
CurtailIcon,
|
||||
DeviceCard,
|
||||
DeviceTags,
|
||||
EebusIcon,
|
||||
|
|
@ -749,6 +780,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
messengers: [] as ConfigMessenger[],
|
||||
curtailers: [] as ConfigCurtailer[],
|
||||
vehicles: [] as ConfigVehicle[],
|
||||
meters: [] as ConfigMeter[],
|
||||
loadpoints: [] as ConfigLoadpoint[],
|
||||
|
|
@ -772,6 +804,7 @@ export default defineComponent({
|
|||
aux: null as string[] | null,
|
||||
ext: null as string[] | null,
|
||||
consumer: null as string[] | null,
|
||||
curtail: null as string[] | null,
|
||||
} as SiteConfig,
|
||||
deviceValueTimeout: null as Timeout,
|
||||
deviceValues: {
|
||||
|
|
@ -781,6 +814,7 @@ export default defineComponent({
|
|||
loadpoint: {},
|
||||
messenger: {},
|
||||
tariff: {},
|
||||
curtailer: {},
|
||||
} as DeviceValuesMap,
|
||||
isComponentMounted: true,
|
||||
isPageVisible: true,
|
||||
|
|
@ -868,8 +902,10 @@ export default defineComponent({
|
|||
{
|
||||
slug: "grid",
|
||||
icon: "shopicon-regular-powersupply",
|
||||
count: this.gridMeter ? 1 : 0,
|
||||
error: !!this.gridMeter && this.hasDeviceError("meter", this.gridMeter.name),
|
||||
count: (this.gridMeter ? 1 : 0) + this.curtailerDevices.length,
|
||||
error:
|
||||
(!!this.gridMeter && this.hasDeviceError("meter", this.gridMeter.name)) ||
|
||||
this.curtailerDevices.some((c) => this.hasDeviceError("curtailer", c.name)),
|
||||
},
|
||||
{
|
||||
slug: "pv-battery",
|
||||
|
|
@ -877,7 +913,7 @@ export default defineComponent({
|
|||
count: pvAndBattery.length,
|
||||
error: meterError(pvAndBattery),
|
||||
warning:
|
||||
this.pvMeters.some((m) => this.meterBanner(m.name)) ||
|
||||
this.pvMeters.some((m) => this.curtailmentBanner("meter", m.name)) ||
|
||||
meterDisabled(pvAndBattery),
|
||||
},
|
||||
{
|
||||
|
|
@ -956,6 +992,12 @@ export default defineComponent({
|
|||
const names = this.site?.ext;
|
||||
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() {
|
||||
return this.getMetersByNames(this.site?.consumer);
|
||||
},
|
||||
|
|
@ -1291,6 +1333,7 @@ export default defineComponent({
|
|||
await this.loadLoadpoints();
|
||||
await this.loadCircuits();
|
||||
await this.loadMessengers();
|
||||
await this.loadCurtailers();
|
||||
await this.loadTariffs();
|
||||
await this.loadTariffRefs();
|
||||
await this.loadHems();
|
||||
|
|
@ -1311,6 +1354,9 @@ export default defineComponent({
|
|||
async loadMessengers() {
|
||||
this.messengers = (await this.loadConfig("devices/messenger")) || [];
|
||||
},
|
||||
async loadCurtailers() {
|
||||
this.curtailers = (await this.loadConfig("devices/curtailer")) || [];
|
||||
},
|
||||
async loadVehicles() {
|
||||
this.vehicles = (await this.loadConfig("devices/vehicle")) || [];
|
||||
},
|
||||
|
|
@ -1464,6 +1510,25 @@ export default defineComponent({
|
|||
this.loadMessengers();
|
||||
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() {
|
||||
this.loadDirty();
|
||||
},
|
||||
|
|
@ -1504,6 +1569,7 @@ export default defineComponent({
|
|||
vehicle: this.vehicles,
|
||||
charger: this.chargers,
|
||||
tariff: this.tariffs,
|
||||
curtailer: this.curtailers,
|
||||
} as Record<DeviceType, any[]>;
|
||||
for (const type in devices) {
|
||||
for (const device of devices[type as DeviceType]) {
|
||||
|
|
@ -1523,9 +1589,10 @@ export default defineComponent({
|
|||
deviceTags(type: DeviceType, id: string) {
|
||||
return this.deviceValues[type][id] || {};
|
||||
},
|
||||
meterBanner(name: string): string | undefined {
|
||||
// the tag is only present while curtailing, a zero percent limit is still one
|
||||
return this.deviceTags("meter", name)["curtailed"]?.value !== undefined
|
||||
curtailmentBanner(type: DeviceType, name: string): string | undefined {
|
||||
// devices report the allowed feed-in percent, 100 = uncurtailed
|
||||
const value = this.deviceTags(type, name)["curtailed"]?.value;
|
||||
return typeof value === "number" && value < 100
|
||||
? this.$t("config.deviceValue.productionLimited")
|
||||
: undefined;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"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 {
|
||||
i -= 1
|
||||
|
|
@ -44,9 +44,10 @@ func _ClassNoOp() {
|
|||
_ = x[ClassMessenger-(17)]
|
||||
_ = x[ClassSponsorship-(18)]
|
||||
_ = 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{
|
||||
_ClassName[0:10]: ClassConfigFile,
|
||||
|
|
@ -87,6 +88,8 @@ var _ClassNameToValueMap = map[string]Class{
|
|||
_ClassLowerName[108:119]: ClassSponsorship,
|
||||
_ClassName[119:128]: ClassLoadpoint,
|
||||
_ClassLowerName[119:128]: ClassLoadpoint,
|
||||
_ClassName[128:137]: ClassCurtailer,
|
||||
_ClassLowerName[128:137]: ClassCurtailer,
|
||||
}
|
||||
|
||||
var _ClassNames = []string{
|
||||
|
|
@ -109,6 +112,7 @@ var _ClassNames = []string{
|
|||
_ClassName[99:108],
|
||||
_ClassName[108:119],
|
||||
_ClassName[119:128],
|
||||
_ClassName[128:137],
|
||||
}
|
||||
|
||||
// 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)
|
||||
case templates.Circuit:
|
||||
deleteDevice[api.Circuit](c)
|
||||
case templates.Curtailer:
|
||||
deleteDevice[api.Curtailer](c)
|
||||
case templates.Hems:
|
||||
deleteDevice[hems.API](c)
|
||||
case templates.Loadpoint:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const (
|
|||
ClassMessenger
|
||||
ClassSponsorship
|
||||
ClassLoadpoint
|
||||
ClassCurtailer
|
||||
)
|
||||
|
||||
// FatalError is an error that can be marshaled
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
)
|
||||
|
||||
var references struct {
|
||||
meter, charger, vehicle, circuit, tariff []string
|
||||
meter, charger, vehicle, circuit, tariff, curtailer []string
|
||||
}
|
||||
|
||||
func collectRefs(conf globalconfig.All) error {
|
||||
|
|
@ -57,6 +57,7 @@ func collectRefs(conf globalconfig.All) error {
|
|||
func collectSiteRefs(conf globalconfig.All) error {
|
||||
var refs struct {
|
||||
Meters core.MetersConfig `mapstructure:"meters"` // Meter references
|
||||
Curtailers []string `mapstructure:"curtailers"` // Curtailment device references
|
||||
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.AuxMetersRef...)
|
||||
references.meter = append(references.meter, refs.Meters.ConsumerMetersRef...)
|
||||
references.curtailer = append(references.curtailer, refs.Curtailers...)
|
||||
|
||||
// append devices from settings
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
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/metrics"
|
||||
coresettings "github.com/evcc-io/evcc/core/settings"
|
||||
"github.com/evcc-io/evcc/curtailer"
|
||||
"github.com/evcc-io/evcc/hems"
|
||||
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
||||
"github.com/evcc-io/evcc/hems/shm"
|
||||
|
|
@ -400,6 +401,50 @@ func configureMeters(static []config.Named, names ...string) error {
|
|||
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 {
|
||||
var eg errgroup.Group
|
||||
|
||||
|
|
@ -1287,6 +1332,10 @@ func configureDevices(conf globalconfig.All) error {
|
|||
errs = append(errs, &ClassError{ClassCircuit, err})
|
||||
}
|
||||
|
||||
if err := configureCurtailers(conf.Curtailers, references.curtailer...); err != nil {
|
||||
errs = append(errs, &ClassError{ClassCurtailer, err})
|
||||
}
|
||||
|
||||
return joinErrors(errs...)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ const (
|
|||
AuxMeters = "auxMeters"
|
||||
ConsumerMeters = "consumerMeters"
|
||||
|
||||
// curtailment devices
|
||||
Curtailers = "curtailers"
|
||||
|
||||
// battery settings
|
||||
BatteryDischargeControl = "batteryDischargeControl"
|
||||
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.
|
||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
Meters MetersConfig `mapstructure:"meters"` // Meter references
|
||||
CurtailersRef []string `mapstructure:"curtailers"` // Curtailment device references
|
||||
|
||||
// meters
|
||||
circuit api.Circuit // Circuit
|
||||
|
|
@ -75,6 +76,7 @@ type Site struct {
|
|||
extMeters []config.Device[api.Meter] // External meters - for monitoring only
|
||||
auxMeters []config.Device[api.Meter] // Auxiliary 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
|
||||
dimmed *bool
|
||||
|
|
@ -351,6 +353,15 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif
|
|||
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
|
||||
shutdown.Register(func() {
|
||||
if mode := site.GetBatteryMode(); batteryModeModified(mode) {
|
||||
|
|
@ -395,19 +406,22 @@ func (site *Site) restoreMetersAndTitle() {
|
|||
site.Meters.GridMeterRef = 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 != "" {
|
||||
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 != "" {
|
||||
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 != "" {
|
||||
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 != "" {
|
||||
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
|
||||
GetGridMeterRef() string
|
||||
SetGridMeterRef(string)
|
||||
GetCurtailerRefs() []string
|
||||
SetCurtailerRefs([]string)
|
||||
GetPVMeterRefs() []string
|
||||
SetPVMeterRefs([]string)
|
||||
GetBatteryMeterRefs() []string
|
||||
|
|
|
|||
|
|
@ -24,18 +24,23 @@ var (
|
|||
ErrBatteryControlNotAvailable = errors.New("battery control not available")
|
||||
)
|
||||
|
||||
// isConfigurable checks if the meter is configurable
|
||||
func isConfigurable(ref string) bool {
|
||||
dev, _ := config.Meters().ByName(ref)
|
||||
_, ok := dev.(config.ConfigurableDevice[api.Meter])
|
||||
// filterConfigurableDevices filters references to configurable devices of the given handler
|
||||
func filterConfigurableDevices[T any](h config.Handler[T], ref []string) []string {
|
||||
return lo.Filter(ref, func(ref string, _ int) bool {
|
||||
dev, _ := h.ByName(ref)
|
||||
_, ok := dev.(config.ConfigurableDevice[T])
|
||||
return ok
|
||||
})
|
||||
}
|
||||
|
||||
// filterConfigurable filters configurable meters
|
||||
func filterConfigurable(ref []string) []string {
|
||||
return lo.Filter(ref, func(ref string, _ int) bool {
|
||||
return isConfigurable(ref)
|
||||
})
|
||||
// filterConfigurableMeter filters configurable meters
|
||||
func filterConfigurableMeter(ref []string) []string {
|
||||
return filterConfigurableDevices(config.Meters(), ref)
|
||||
}
|
||||
|
||||
// filterConfigurableCurtailers filters configurable curtailment devices
|
||||
func filterConfigurableCurtailers(ref []string) []string {
|
||||
return filterConfigurableDevices(config.Curtailers(), ref)
|
||||
}
|
||||
|
||||
// Optimize updates the optimizer
|
||||
|
|
@ -89,7 +94,7 @@ func (site *Site) SetPVMeterRefs(ref []string) {
|
|||
defer site.Unlock()
|
||||
|
||||
site.Meters.PVMetersRef = ref
|
||||
settings.SetString(keys.PvMeters, strings.Join(filterConfigurable(ref), ","))
|
||||
settings.SetString(keys.PvMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||
}
|
||||
|
||||
// GetBatteryMeterRefs returns the BatteryMeterRef
|
||||
|
|
@ -105,7 +110,7 @@ func (site *Site) SetBatteryMeterRefs(ref []string) {
|
|||
defer site.Unlock()
|
||||
|
||||
site.Meters.BatteryMetersRef = ref
|
||||
settings.SetString(keys.BatteryMeters, strings.Join(filterConfigurable(ref), ","))
|
||||
settings.SetString(keys.BatteryMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||
}
|
||||
|
||||
// GetAuxMeterRefs returns the AuxMeterRef
|
||||
|
|
@ -121,7 +126,7 @@ func (site *Site) SetAuxMeterRefs(ref []string) {
|
|||
defer site.Unlock()
|
||||
|
||||
site.Meters.AuxMetersRef = ref
|
||||
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ","))
|
||||
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||
}
|
||||
|
||||
// GetConsumerMeterRefs returns the ConsumerMeterRef
|
||||
|
|
@ -137,7 +142,7 @@ func (site *Site) SetConsumerMeterRefs(ref []string) {
|
|||
defer site.Unlock()
|
||||
|
||||
site.Meters.ConsumerMetersRef = ref
|
||||
settings.SetString(keys.ConsumerMeters, strings.Join(filterConfigurable(ref), ","))
|
||||
settings.SetString(keys.ConsumerMeters, strings.Join(filterConfigurableMeter(ref), ","))
|
||||
}
|
||||
|
||||
// GetExtMeterRefs returns the ExtMeterRef
|
||||
|
|
@ -153,7 +158,23 @@ func (site *Site) SetExtMeterRefs(ref []string) {
|
|||
defer site.Unlock()
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -139,7 +139,30 @@ func (site *Site) dimMeters(dim bool) error {
|
|||
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.
|
||||
func (site *Site) curtailPV(percent *int) error {
|
||||
if percent == nil || site.curtailPercent != nil && *site.curtailPercent == *percent {
|
||||
|
|
@ -149,22 +172,12 @@ func (site *Site) curtailPV(percent *int) error {
|
|||
// invalidate until successfully applied
|
||||
site.curtailPercent = nil
|
||||
|
||||
meters := slices.Clone(site.pvMeters)
|
||||
if site.gridMeter != nil {
|
||||
meters = append(meters, site.gridMeter)
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, dev := range meters {
|
||||
m, ok := api.Cap[api.Curtailer](dev.Instance())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range site.curtailables() {
|
||||
// unreadable state: apply unconditionally
|
||||
curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff())
|
||||
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
|
||||
}
|
||||
if err == nil && curtailed == *percent {
|
||||
|
|
@ -172,9 +185,9 @@ func (site *Site) curtailPV(percent *int) error {
|
|||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type dimmableMeter struct {
|
||||
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",
|
||||
"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": {
|
||||
"activeClients": "Aktive Clients",
|
||||
"amount": "Anzahl",
|
||||
|
|
@ -212,7 +221,7 @@
|
|||
"currentRange": "Strom",
|
||||
"curtailLimit": "Einspeiselimit",
|
||||
"curtailable": "Abregelbar",
|
||||
"curtailed": "Einspeisung begrenzt",
|
||||
"curtailed": "Einspeiselimit",
|
||||
"detected": "Erkannt",
|
||||
"dimLimit": "Verbrauchslimit",
|
||||
"dimmed": "Verbrauch begrenzt",
|
||||
|
|
@ -539,6 +548,7 @@
|
|||
"main": {
|
||||
"addAdditional": "Zusätzlichen Zähler hinzufügen",
|
||||
"addConsumer": "Verbraucher hinzufügen",
|
||||
"addCurtailer": "Einspeisebegrenzung hinzufügen",
|
||||
"addGrid": "Netzzähler hinzufügen",
|
||||
"addLoadpoint": "Ladepunkt oder Heizung hinzufügen",
|
||||
"addPvBattery": "PV oder Speicher hinzufügen",
|
||||
|
|
@ -639,6 +649,7 @@
|
|||
"aux": "Selbstregelnden Verbraucher hinzufügen",
|
||||
"battery": "Hausbatterie hinzufügen",
|
||||
"consumer": "Regulären Verbraucher hinzufügen",
|
||||
"grid": "Netzzähler hinzufügen",
|
||||
"pv": "PV-Anlage hinzufügen"
|
||||
},
|
||||
"save": "Speichern",
|
||||
|
|
|
|||
13
i18n/en.json
13
i18n/en.json
|
|
@ -188,6 +188,15 @@
|
|||
"label": "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": {
|
||||
"activeClients": "Active clients",
|
||||
"amount": "Amount",
|
||||
|
|
@ -212,7 +221,7 @@
|
|||
"currentRange": "Current",
|
||||
"curtailLimit": "Feed-in limit",
|
||||
"curtailable": "Curtailable",
|
||||
"curtailed": "Feed-in limited",
|
||||
"curtailed": "Feed-in limit",
|
||||
"detected": "Detected",
|
||||
"dimLimit": "Consumption limit",
|
||||
"dimmed": "Consumption limited",
|
||||
|
|
@ -539,6 +548,7 @@
|
|||
"main": {
|
||||
"addAdditional": "Add additional meter",
|
||||
"addConsumer": "Add consumer",
|
||||
"addCurtailer": "Add curtailment device",
|
||||
"addGrid": "Add grid meter",
|
||||
"addLoadpoint": "Add charging point or heater",
|
||||
"addPvBattery": "Add solar or battery",
|
||||
|
|
@ -639,6 +649,7 @@
|
|||
"aux": "Add self-regulating consumer",
|
||||
"battery": "Add battery meter",
|
||||
"consumer": "Add regular consumer",
|
||||
"grid": "Add grid meter",
|
||||
"pv": "Add solar meter"
|
||||
},
|
||||
"save": "Save",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/circuit"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/curtailer"
|
||||
"github.com/evcc-io/evcc/hems"
|
||||
hemsapi "github.com/evcc-io/evcc/hems/hems"
|
||||
"github.com/evcc-io/evcc/messenger"
|
||||
|
|
@ -81,6 +82,9 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||
case templates.Circuit:
|
||||
res, err = devicesConfig(class, config.Circuits(), hidePrivate)
|
||||
|
||||
case templates.Curtailer:
|
||||
res, err = devicesConfig(class, config.Curtailers(), hidePrivate)
|
||||
|
||||
case templates.Hems:
|
||||
res, err = devicesConfig(class, config.Hems(), hidePrivate)
|
||||
|
||||
|
|
@ -210,6 +214,9 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||
case templates.Circuit:
|
||||
res, err = deviceConfig(class, id, config.Circuits(), hidePrivate)
|
||||
|
||||
case templates.Curtailer:
|
||||
res, err = deviceConfig(class, id, config.Curtailers(), hidePrivate)
|
||||
|
||||
case templates.Hems:
|
||||
res, err = deviceConfig(class, id, config.Hems(), hidePrivate)
|
||||
|
||||
|
|
@ -275,6 +282,9 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||
case templates.Circuit:
|
||||
instance, err = deviceStatus(name, config.Circuits())
|
||||
|
||||
case templates.Curtailer:
|
||||
instance, err = deviceStatus(name, config.Curtailers())
|
||||
|
||||
case templates.Hems:
|
||||
err = api.ErrNotAvailable
|
||||
|
||||
|
|
@ -375,6 +385,9 @@ func newDeviceHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
|||
case templates.Circuit:
|
||||
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:
|
||||
if existing, _ := config.ConfigurationByClass(templates.Hems); existing != nil {
|
||||
err = errors.New("hems already configured")
|
||||
|
|
@ -477,6 +490,9 @@ func updateDeviceHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
|||
case templates.Circuit:
|
||||
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:
|
||||
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:
|
||||
err = deleteDevice(id, config.Hems())
|
||||
|
||||
|
|
@ -759,6 +781,9 @@ func testConfigHandler(site site.API, authObject auth.Auth) http.HandlerFunc {
|
|||
case templates.Circuit:
|
||||
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:
|
||||
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() {
|
||||
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)
|
||||
// only reported while actually curtailing
|
||||
if val, err := dev.CurtailedPercent(); err != nil || val < 100 {
|
||||
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"`
|
||||
Ext []string `json:"ext"`
|
||||
Consumer []string `json:"consumer"`
|
||||
Curtail []string `json:"curtail"`
|
||||
}{
|
||||
Title: site.GetTitle(),
|
||||
Grid: site.GetGridMeterRef(),
|
||||
Curtail: site.GetCurtailerRefs(),
|
||||
PV: site.GetPVMeterRefs(),
|
||||
Battery: site.GetBatteryMeterRefs(),
|
||||
Aux: site.GetAuxMeterRefs(),
|
||||
|
|
@ -54,6 +56,7 @@ func updateSiteHandler(site site.API) http.HandlerFunc {
|
|||
Aux *[]string
|
||||
Ext *[]string
|
||||
Consumer *[]string
|
||||
Curtail *[]string
|
||||
}
|
||||
|
||||
if err := jsonDecoder(r.Body).Decode(&payload); err != nil {
|
||||
|
|
@ -119,6 +122,18 @@ func updateSiteHandler(site site.API) http.HandlerFunc {
|
|||
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
|
||||
if err := settings.Persist(); err != nil {
|
||||
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"
|
||||
|
||||
//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
|
||||
|
||||
//go:embed */*.tpl
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ render: |
|
|||
power:
|
||||
source: const
|
||||
value: {{ .power }}
|
||||
{{- if .curtailable }}
|
||||
{{- if eq .curtailable "true" }}
|
||||
curtail:
|
||||
source: js
|
||||
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
|
||||
await page.goto("/#/config");
|
||||
await expect(hems).toContainText(
|
||||
["Consumption limited", "no", "Feed-in limited", "no"].join("")
|
||||
);
|
||||
await expect(hems).toContainText(["Consumption limited", "no", "Feed-in limit", "no"].join(""));
|
||||
await expect(hint).not.toBeVisible();
|
||||
|
||||
// dim (W4): consumption limited
|
||||
|
|
@ -391,9 +389,7 @@ w4:${signalSource("w4")}`
|
|||
await expect(hint).not.toBeVisible();
|
||||
await page.reload();
|
||||
await expect(pvBanner).not.toBeVisible();
|
||||
await expect(hems).toContainText(
|
||||
["Consumption limited", "no", "Feed-in limited", "no"].join("")
|
||||
);
|
||||
await expect(hems).toContainText(["Consumption limited", "no", "Feed-in limit", "no"].join(""));
|
||||
|
||||
await stopSimulator();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ var instance struct {
|
|||
chargers *handler[api.Charger]
|
||||
vehicles *handler[api.Vehicle]
|
||||
circuits *handler[api.Circuit]
|
||||
curtailers *handler[api.Curtailer]
|
||||
hems *handler[hems.API]
|
||||
messengers *handler[api.Messenger]
|
||||
loadpoints *handler[loadpoint.API]
|
||||
|
|
@ -29,6 +30,7 @@ func Reset() {
|
|||
instance.chargers = &handler[api.Charger]{topic: "charger"}
|
||||
instance.vehicles = &handler[api.Vehicle]{topic: "vehicle"}
|
||||
instance.circuits = &handler[api.Circuit]{topic: "circuit"}
|
||||
instance.curtailers = &handler[api.Curtailer]{topic: "curtailer"}
|
||||
instance.hems = &handler[hems.API]{topic: "hems"}
|
||||
instance.messengers = &handler[api.Messenger]{topic: "messenger"}
|
||||
instance.loadpoints = &handler[loadpoint.API]{topic: "loadpoint"}
|
||||
|
|
@ -63,6 +65,10 @@ func Circuits() Handler[api.Circuit] {
|
|||
return instance.circuits
|
||||
}
|
||||
|
||||
func Curtailers() Handler[api.Curtailer] {
|
||||
return instance.curtailers
|
||||
}
|
||||
|
||||
func Hems() Handler[hems.API] {
|
||||
return instance.hems
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ const (
|
|||
Circuit
|
||||
Messenger
|
||||
Hems
|
||||
Curtailer
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"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 {
|
||||
i -= 1
|
||||
|
|
@ -33,9 +33,10 @@ func _ClassNoOp() {
|
|||
_ = x[Circuit-(6)]
|
||||
_ = x[Messenger-(7)]
|
||||
_ = 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{
|
||||
_ClassName[0:7]: Charger,
|
||||
|
|
@ -54,6 +55,8 @@ var _ClassNameToValueMap = map[string]Class{
|
|||
_ClassLowerName[41:50]: Messenger,
|
||||
_ClassName[50:54]: Hems,
|
||||
_ClassLowerName[50:54]: Hems,
|
||||
_ClassName[54:63]: Curtailer,
|
||||
_ClassLowerName[54:63]: Curtailer,
|
||||
}
|
||||
|
||||
var _ClassNames = []string{
|
||||
|
|
@ -65,6 +68,7 @@ var _ClassNames = []string{
|
|||
_ClassName[34:41],
|
||||
_ClassName[41:50],
|
||||
_ClassName[50:54],
|
||||
_ClassName[54:63],
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ type Usage int
|
|||
//go:generate go tool enumer -type Usage -trimprefix Usage -transform=lower -text
|
||||
const (
|
||||
UsageGrid Usage = iota
|
||||
UsageCurtail
|
||||
UsagePV
|
||||
UsageBattery
|
||||
UsageCharge
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"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 {
|
||||
if i < 0 || i >= Usage(len(_UsageIndex)-1) {
|
||||
|
|
@ -25,33 +25,37 @@ func (i Usage) String() string {
|
|||
func _UsageNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[UsageGrid-(0)]
|
||||
_ = x[UsagePV-(1)]
|
||||
_ = x[UsageBattery-(2)]
|
||||
_ = x[UsageCharge-(3)]
|
||||
_ = x[UsageAux-(4)]
|
||||
_ = x[UsageCurtail-(1)]
|
||||
_ = x[UsagePV-(2)]
|
||||
_ = x[UsageBattery-(3)]
|
||||
_ = 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{
|
||||
_UsageName[0:4]: UsageGrid,
|
||||
_UsageLowerName[0:4]: UsageGrid,
|
||||
_UsageName[4:6]: UsagePV,
|
||||
_UsageLowerName[4:6]: UsagePV,
|
||||
_UsageName[6:13]: UsageBattery,
|
||||
_UsageLowerName[6:13]: UsageBattery,
|
||||
_UsageName[13:19]: UsageCharge,
|
||||
_UsageLowerName[13:19]: UsageCharge,
|
||||
_UsageName[19:22]: UsageAux,
|
||||
_UsageLowerName[19:22]: UsageAux,
|
||||
_UsageName[4:11]: UsageCurtail,
|
||||
_UsageLowerName[4:11]: UsageCurtail,
|
||||
_UsageName[11:13]: UsagePV,
|
||||
_UsageLowerName[11:13]: UsagePV,
|
||||
_UsageName[13:20]: UsageBattery,
|
||||
_UsageLowerName[13:20]: UsageBattery,
|
||||
_UsageName[20:26]: UsageCharge,
|
||||
_UsageLowerName[20:26]: UsageCharge,
|
||||
_UsageName[26:29]: UsageAux,
|
||||
_UsageLowerName[26:29]: UsageAux,
|
||||
}
|
||||
|
||||
var _UsageNames = []string{
|
||||
_UsageName[0:4],
|
||||
_UsageName[4:6],
|
||||
_UsageName[6:13],
|
||||
_UsageName[13:19],
|
||||
_UsageName[19:22],
|
||||
_UsageName[4:11],
|
||||
_UsageName[11:13],
|
||||
_UsageName[13:20],
|
||||
_UsageName[20:26],
|
||||
_UsageName[26:29],
|
||||
}
|
||||
|
||||
// UsageString retrieves an enum value from the enum constants string name.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue