HEMS: add custom implementation (#32187)
This commit is contained in:
parent
ce75e49192
commit
4c26af7f5c
10 changed files with 436 additions and 38 deletions
|
|
@ -71,11 +71,15 @@ import { ConfigType, type YamlSource } from "@/types/evcc";
|
|||
import { type DeviceValues } from "./DeviceModal";
|
||||
import { customTemplateOption, type TemplateGroup } from "./DeviceModal/TemplateSelector.vue";
|
||||
import customHemsYaml from "./defaultYaml/customHems.yaml?raw";
|
||||
import relayHemsYaml from "./defaultYaml/relayHems.yaml?raw";
|
||||
import api from "../../api";
|
||||
import { docsPrefix } from "@/i18n";
|
||||
import DownloadButton from "../Helper/DownloadButton.vue";
|
||||
import formatter from "../../mixins/formatter";
|
||||
|
||||
// selector value for the relay variant; both variants save as type custom
|
||||
const RELAY_OPTION = "relay";
|
||||
|
||||
const initialValues = {
|
||||
type: ConfigType.Template,
|
||||
icon: undefined,
|
||||
|
|
@ -147,7 +151,10 @@ export default defineComponent({
|
|||
return [
|
||||
{
|
||||
label: "generic",
|
||||
options: [customTemplateOption(this.$t("config.hems.customOption"))],
|
||||
options: [
|
||||
customTemplateOption(this.$t("config.hems.type.custom")),
|
||||
customTemplateOption(this.$t("config.hems.type.relay"), RELAY_OPTION),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "integrations",
|
||||
|
|
@ -156,12 +163,12 @@ export default defineComponent({
|
|||
];
|
||||
},
|
||||
isYamlInputType(type: ConfigType): boolean {
|
||||
return type === ConfigType.Custom;
|
||||
return type === ConfigType.Custom || (type as string) === RELAY_OPTION;
|
||||
},
|
||||
handleTemplateChange(value: string, values: DeviceValues) {
|
||||
if (value === ConfigType.Custom) {
|
||||
if (this.isYamlInputType(value as ConfigType)) {
|
||||
values.type = ConfigType.Custom;
|
||||
values.yaml = customHemsYaml;
|
||||
values.yaml = value === RELAY_OPTION ? relayHemsYaml : customHemsYaml;
|
||||
}
|
||||
},
|
||||
onAdded(name: string) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,12 @@
|
|||
## external limit via relay contact (binary on/off)
|
||||
type: relay
|
||||
maxpower: 4200 # total load limit while signal is active (W)
|
||||
limit: # input signal, plugin
|
||||
## required attributes
|
||||
type: custom
|
||||
maxconsumptionpower: # total load limit (W), 0 = no limit
|
||||
source: const
|
||||
value: false # 0/false = normal, 1/true = limit active
|
||||
#interval: 10s # polling interval
|
||||
#passthrough: # output, mirrors limit to a downstream system
|
||||
# source: http
|
||||
# uri: http://another-hems.local/api/limit?active={{ .dim }}
|
||||
# method: POST
|
||||
value: 0
|
||||
|
||||
## alternative limit sources examples
|
||||
#limit:
|
||||
# source: gpio
|
||||
# pin: 17
|
||||
# function: read
|
||||
#limit:
|
||||
# source: mqtt
|
||||
# topic: hems/limit/status
|
||||
#limit:
|
||||
# source: http
|
||||
# uri: http://steuerbox.local/api/limit
|
||||
# jq: .limited
|
||||
#limit:
|
||||
# source: modbus
|
||||
# uri: 192.168.179.200:4703
|
||||
# id: 3
|
||||
# register: { type: holding, decode: uint16, address: 8056 }
|
||||
## optional attributes
|
||||
#curtailedpercent: # allowed feed-in (0..100), 100 = uncurtailed
|
||||
# source: const
|
||||
# value: 100
|
||||
#productionnominalmax: 10000 # nominal production power (W), required with curtailedpercent
|
||||
#interval: 10s # polling interval
|
||||
|
|
|
|||
32
assets/js/components/Config/defaultYaml/relayHems.yaml
Normal file
32
assets/js/components/Config/defaultYaml/relayHems.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
## required attributes
|
||||
type: relay
|
||||
maxpower: 4200 # total load limit while signal is active (W)
|
||||
limit: # input signal, plugin
|
||||
source: const
|
||||
value: false # 0/false = normal, 1/true = limit active
|
||||
|
||||
|
||||
## optional attributes
|
||||
#interval: 10s # polling interval
|
||||
#passthrough: # output, mirrors limit to a downstream system
|
||||
# source: http
|
||||
# uri: http://another-hems.local/api/limit?active={{ .dim }}
|
||||
# method: POST
|
||||
|
||||
## alternative limit sources examples
|
||||
#limit:
|
||||
# source: gpio
|
||||
# pin: 17
|
||||
# function: read
|
||||
#limit:
|
||||
# source: mqtt
|
||||
# topic: hems/limit/status
|
||||
#limit:
|
||||
# source: http
|
||||
# uri: http://steuerbox.local/api/limit
|
||||
# jq: .limited
|
||||
#limit:
|
||||
# source: modbus
|
||||
# uri: 192.168.179.200:4703
|
||||
# id: 3
|
||||
# register: { type: holding, decode: uint16, address: 8056 }
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/hems/config"
|
||||
_ "github.com/evcc-io/evcc/hems/custom"
|
||||
_ "github.com/evcc-io/evcc/hems/eebus"
|
||||
_ "github.com/evcc-io/evcc/hems/fnn"
|
||||
"github.com/evcc-io/evcc/hems/hems"
|
||||
|
|
|
|||
242
hems/custom/custom.go
Normal file
242
hems/custom/custom.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package custom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/hems/config"
|
||||
"github.com/evcc-io/evcc/hems/smartgrid"
|
||||
"github.com/evcc-io/evcc/plugin"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
config.AddCtx(api.Custom, NewFromConfig)
|
||||
}
|
||||
|
||||
// Custom implements a plugin-configurable HEMS.
|
||||
type Custom struct {
|
||||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
|
||||
site site.API
|
||||
publishFunc func()
|
||||
|
||||
maxConsumptionPower func() (float64, error)
|
||||
curtailedPercent func() (int64, error)
|
||||
|
||||
productionNominalMax float64
|
||||
interval time.Duration
|
||||
|
||||
smartgridConsumptionID uint
|
||||
smartgridProductionID uint
|
||||
|
||||
consumptionLimit *float64
|
||||
productionPercent int // allowed feed-in percent (0..100), 100 = uncurtailed
|
||||
}
|
||||
|
||||
// NewFromConfig creates a custom HEMS from generic config.
|
||||
func NewFromConfig(ctx context.Context, other map[string]any, site site.API) (*Custom, error) {
|
||||
cc := struct {
|
||||
MaxConsumptionPower *plugin.Config
|
||||
CurtailedPercent *plugin.Config
|
||||
ProductionNominalMax float64
|
||||
Interval time.Duration
|
||||
}{
|
||||
Interval: 10 * time.Second,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxConsumptionPowerG, err := cc.MaxConsumptionPower.FloatGetter(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max consumption power: %w", err)
|
||||
}
|
||||
|
||||
curtailedPercentG, err := cc.CurtailedPercent.IntGetter(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("curtailed percent: %w", err)
|
||||
}
|
||||
|
||||
return NewCustom(site, maxConsumptionPowerG, curtailedPercentG, math.Abs(cc.ProductionNominalMax), cc.Interval)
|
||||
}
|
||||
|
||||
// NewCustom creates a custom HEMS.
|
||||
func NewCustom(site site.API, maxConsumptionPower func() (float64, error), curtailedPercent func() (int64, error), productionNominalMax float64, interval time.Duration) (*Custom, error) {
|
||||
if maxConsumptionPower == nil && curtailedPercent == nil {
|
||||
return nil, errors.New("must have either maxconsumptionpower or curtailedpercent")
|
||||
}
|
||||
|
||||
if curtailedPercent != nil && productionNominalMax == 0 {
|
||||
return nil, errors.New("cannot have curtailedpercent without productionnominalmax")
|
||||
}
|
||||
|
||||
c := &Custom{
|
||||
log: util.NewLogger("custom"),
|
||||
site: site,
|
||||
maxConsumptionPower: maxConsumptionPower,
|
||||
curtailedPercent: curtailedPercent,
|
||||
productionNominalMax: productionNominalMax,
|
||||
productionPercent: 100,
|
||||
interval: interval,
|
||||
}
|
||||
|
||||
// read the plugins once synchronously so limits are valid as soon as NewCustom returns
|
||||
if err := c.run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Custom) SetUpdated(f func()) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.publishFunc = f
|
||||
}
|
||||
|
||||
// Run starts the control loop. NewCustom already ran the first pass.
|
||||
func (c *Custom) Run() {
|
||||
for range time.Tick(c.interval) {
|
||||
if err := c.run(); err != nil {
|
||||
c.log.ERROR.Println(err)
|
||||
}
|
||||
|
||||
if c.publishFunc != nil {
|
||||
c.publishFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Custom) run() error {
|
||||
return errors.Join(c.runDim(), c.runCurtail())
|
||||
}
|
||||
|
||||
// runDim reads the consumption limit. No-op if not configured.
|
||||
// The previous limit is retained if reading fails.
|
||||
func (c *Custom) runDim() error {
|
||||
if c.maxConsumptionPower == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
limit, err := c.maxConsumptionPower()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if limit < 0 {
|
||||
return fmt.Errorf("invalid consumption limit: %.0fW", limit)
|
||||
}
|
||||
|
||||
c.setConsumptionLimit(limit)
|
||||
|
||||
if err := smartgrid.UpdateSession(&c.smartgridConsumptionID, smartgrid.Dim, c.site.GetGridPower(), limit, limit > 0); err != nil {
|
||||
return fmt.Errorf("smartgrid session: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runCurtail reads the curtailment percentage. No-op if not configured.
|
||||
// The previous percentage is retained if reading fails.
|
||||
func (c *Custom) runCurtail() error {
|
||||
if c.curtailedPercent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
percent, err := c.curtailedPercent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if percent < 0 || percent > 100 {
|
||||
return fmt.Errorf("invalid curtailment percent: %d", percent)
|
||||
}
|
||||
|
||||
c.setProductionLimit(int(percent))
|
||||
|
||||
active := percent < 100
|
||||
var limit float64
|
||||
if active {
|
||||
limit = float64(percent) / 100 * c.productionNominalMax
|
||||
}
|
||||
|
||||
if err := smartgrid.UpdateSession(&c.smartgridProductionID, smartgrid.Curtail, c.site.GetGridPower(), limit, active); err != nil {
|
||||
return fmt.Errorf("smartgrid session: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setConsumptionLimit applies the dimming limit.
|
||||
func (c *Custom) setConsumptionLimit(limit float64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.consumptionLimit = nil
|
||||
if limit > 0 {
|
||||
c.consumptionLimit = &limit
|
||||
}
|
||||
}
|
||||
|
||||
// setProductionLimit applies the curtailment limit.
|
||||
func (c *Custom) setProductionLimit(percent int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.productionPercent = percent
|
||||
}
|
||||
|
||||
var _ api.HEMS = (*Custom)(nil)
|
||||
|
||||
// CurtailedPercent implements api.HEMS, returning the allowed production percent.
|
||||
func (c *Custom) CurtailedPercent() *int {
|
||||
if c.curtailedPercent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return new(c.productionPercent)
|
||||
}
|
||||
|
||||
// MaxConsumptionPower implements api.HEMS, returning the active wattage cap.
|
||||
func (c *Custom) MaxConsumptionPower() *float64 {
|
||||
if c.maxConsumptionPower == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.consumptionLimit == nil {
|
||||
return new(0.0)
|
||||
}
|
||||
|
||||
return new(*c.consumptionLimit)
|
||||
}
|
||||
|
||||
// MaxProductionPower implements api.HEMS.
|
||||
func (c *Custom) MaxProductionPower() *float64 {
|
||||
if c.curtailedPercent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.productionPercent >= 100 {
|
||||
return new(0.0)
|
||||
}
|
||||
|
||||
return new(float64(c.productionPercent) / 100 * c.productionNominalMax)
|
||||
}
|
||||
78
hems/custom/custom_test.go
Normal file
78
hems/custom/custom_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package custom
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/hems/hems"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestConsumptionOnly verifies that a custom HEMS without curtailment plugin
|
||||
// makes no curtailment statement while dimming to the configured limit.
|
||||
func TestConsumptionOnly(t *testing.T) {
|
||||
c := &Custom{
|
||||
maxConsumptionPower: func() (float64, error) { return 0, nil },
|
||||
productionPercent: 100,
|
||||
}
|
||||
|
||||
assert.Nil(t, c.CurtailedPercent())
|
||||
assert.Nil(t, c.MaxProductionPower())
|
||||
assert.Nil(t, hems.Curtailed(c))
|
||||
|
||||
assert.Equal(t, new(0.0), c.MaxConsumptionPower())
|
||||
assert.Equal(t, new(false), hems.Dimmed(c))
|
||||
|
||||
c.setConsumptionLimit(1e3)
|
||||
assert.Equal(t, new(1e3), c.MaxConsumptionPower())
|
||||
assert.Equal(t, new(true), hems.Dimmed(c))
|
||||
|
||||
c.setConsumptionLimit(0)
|
||||
assert.Equal(t, new(0.0), c.MaxConsumptionPower())
|
||||
assert.Equal(t, new(false), hems.Dimmed(c))
|
||||
}
|
||||
|
||||
// TestCurtailmentOnly verifies that a custom HEMS without consumption plugin
|
||||
// makes no dimming statement while curtailing production.
|
||||
func TestCurtailmentOnly(t *testing.T) {
|
||||
c := &Custom{
|
||||
curtailedPercent: func() (int64, error) { return 100, nil },
|
||||
productionNominalMax: 1e4,
|
||||
productionPercent: 100,
|
||||
}
|
||||
|
||||
assert.Nil(t, c.MaxConsumptionPower())
|
||||
assert.Nil(t, hems.Dimmed(c))
|
||||
|
||||
assert.Equal(t, new(100), c.CurtailedPercent())
|
||||
assert.Equal(t, new(0.0), c.MaxProductionPower())
|
||||
assert.Equal(t, new(false), hems.Curtailed(c))
|
||||
|
||||
c.setProductionLimit(60)
|
||||
assert.Equal(t, new(60), c.CurtailedPercent())
|
||||
assert.Equal(t, new(6e3), c.MaxProductionPower())
|
||||
assert.Equal(t, new(true), hems.Curtailed(c))
|
||||
}
|
||||
|
||||
// TestInvalidPluginValues verifies that out-of-range values are rejected and
|
||||
// the previous limits retained.
|
||||
func TestInvalidPluginValues(t *testing.T) {
|
||||
c := &Custom{
|
||||
maxConsumptionPower: func() (float64, error) { return -1, nil },
|
||||
curtailedPercent: func() (int64, error) { return 101, nil },
|
||||
productionPercent: 100,
|
||||
}
|
||||
|
||||
assert.Error(t, c.runDim())
|
||||
assert.Equal(t, new(0.0), c.MaxConsumptionPower())
|
||||
|
||||
assert.Error(t, c.runCurtail())
|
||||
assert.Equal(t, new(100), c.CurtailedPercent())
|
||||
}
|
||||
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
_, err := NewCustom(nil, nil, nil, 0, 0)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = NewCustom(nil, nil, func() (int64, error) { return 100, nil }, 0, 0)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
@ -374,7 +374,11 @@
|
|||
"noEvents": "Es wurden noch keine Ereignisse aufgezeichnet.",
|
||||
"recordedEvents": "Aufgezeichnete Ereignisse",
|
||||
"template": "Integration",
|
||||
"title": "Externe Begrenzung"
|
||||
"title": "Externe Begrenzung",
|
||||
"type": {
|
||||
"custom": "Benutzerdefinierte Integration",
|
||||
"relay": "Benutzerdefinierte Integration (Relais)"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"change": "ändern",
|
||||
|
|
|
|||
|
|
@ -378,7 +378,11 @@
|
|||
"noEvents": "No events recorded yet.",
|
||||
"recordedEvents": "Recorded events",
|
||||
"template": "Integration",
|
||||
"title": "External Limit"
|
||||
"title": "External Limit",
|
||||
"type": {
|
||||
"custom": "User-defined integration",
|
||||
"relay": "User-defined integration (relay)"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"change": "change",
|
||||
|
|
|
|||
|
|
@ -448,6 +448,19 @@ func testInstance(ctx context.Context, instance any) map[string]testResult {
|
|||
}
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
if dev, ok := api.Cap[api.HEMS](instance); ok {
|
||||
if power := dev.MaxConsumptionPower(); power != nil && *power > 0 {
|
||||
makeResult("dimLimit", *power, nil)
|
||||
}
|
||||
if percent := dev.CurtailedPercent(); percent != nil && *percent < 100 {
|
||||
if limit := dev.MaxProductionPower(); limit != nil {
|
||||
makeResult("curtailLimit", *limit, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
if dev, ok := api.Cap[api.Identifier](instance); ok {
|
||||
val, err := dev.Identify()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,30 @@ test.describe("HEMS", () => {
|
|||
await expect(hemsModal.getByTestId("yaml-editor")).not.toBeVisible();
|
||||
await expect(hemsModal).not.toContainText("Configured via evcc.yaml");
|
||||
|
||||
// both user-defined variants prefill their own yaml
|
||||
const editor = hemsModal.getByTestId("yaml-editor");
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
await expect(editor).toContainText("type: relay");
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await expect(editor).toContainText("type: custom");
|
||||
|
||||
// validate reports active limits
|
||||
const testResult = hemsModal.getByTestId("test-result");
|
||||
await editorClear(editor);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`type: custom
|
||||
maxconsumptionpower:
|
||||
source: const
|
||||
value: 4200`
|
||||
);
|
||||
await testResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(testResult).toContainText("Status: successful");
|
||||
await expect(testResult).toContainText(["Consumption limit", "4.2 kW"].join(""));
|
||||
|
||||
await hemsModal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(hemsModal);
|
||||
});
|
||||
|
|
@ -99,7 +123,9 @@ test.describe("HEMS", () => {
|
|||
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
|
||||
const hemsModal = page.getByTestId("hems-modal");
|
||||
await expectModalVisible(hemsModal);
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
const hemsEditor = hemsModal.getByTestId("yaml-editor");
|
||||
await expect(hemsEditor).toBeVisible();
|
||||
await editorClear(hemsEditor);
|
||||
|
|
@ -151,7 +177,9 @@ limit:
|
|||
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
|
||||
const hemsModal = page.getByTestId("hems-modal");
|
||||
await expectModalVisible(hemsModal);
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
const hemsEditor = hemsModal.getByTestId("yaml-editor");
|
||||
await editorClear(hemsEditor);
|
||||
await editorPaste(
|
||||
|
|
@ -211,7 +239,9 @@ limit:
|
|||
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
|
||||
const hemsModal = page.getByTestId("hems-modal");
|
||||
await expectModalVisible(hemsModal);
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
const hemsEditor = hemsModal.getByTestId("yaml-editor");
|
||||
await expect(hemsEditor).toBeVisible();
|
||||
await editorClear(hemsEditor);
|
||||
|
|
@ -311,7 +341,9 @@ w4:${signalSource("w4")}`
|
|||
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
|
||||
const hemsModal = page.getByTestId("hems-modal");
|
||||
await expectModalVisible(hemsModal);
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
const hemsEditor = hemsModal.getByTestId("yaml-editor");
|
||||
await expect(hemsEditor).toBeVisible();
|
||||
await editorClear(hemsEditor);
|
||||
|
|
@ -376,7 +408,9 @@ w3:
|
|||
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
|
||||
const hemsModal = page.getByTestId("hems-modal");
|
||||
await expectModalVisible(hemsModal);
|
||||
await hemsModal.getByLabel("Integration").selectOption({ label: "User-defined integration" });
|
||||
await hemsModal
|
||||
.getByLabel("Integration")
|
||||
.selectOption({ label: "User-defined integration (relay)" });
|
||||
const hemsEditor = hemsModal.getByTestId("yaml-editor");
|
||||
await expect(hemsEditor).toBeVisible();
|
||||
await editorClear(hemsEditor);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue