Fix vedirect polling (#291)

* VE.Direct: remove polling interval

the polling interval was meant to limit the amount of MQTT updates.
however, that is already controlled by the global MQTT publish interval.
the removed interval was instead used to limit polling of the VE.Direct
UART for incoming data.

the Victron device sends data unsolicited. the VeDirectFrameHandler does
not implement any polling mechanism. no data is ever sent to the Victron
device.

what the removed polling interval did was cause a buffer overrun of the
HardwareSerial class, since the incoming data was not processed in time.
so every five seconds, we read a whole valid VE.Direct frame, plus some
old data, which was not a whole frame, leading to VE.Direct error
messages to pop up.

with the polling interval removed, no framing errors are reported, and
instead we gain new data from the charge controller approximately ever
two seconds -- for free.

* VE.Direct: change texts to correct VE.Direct capital letters

* VE.Direct: improve "UpdatesOnly" switch labels

especially since the publish interval setting is gone, the label makes
it hard to comprehend what the switch does. update the texts to better
explain what the switch is used for.

use the same text on the VE.Direct info view.

* VE.Direct: use StatusBadge on info view

there were custom badges to indicate the VE.Direct settings. replace
those by the common StatusBadge to make then look the same as the other
badged on the info views.
This commit is contained in:
Bernhard Kirchen 2023-07-04 12:04:38 +02:00 committed by GitHub
parent 006f63ed02
commit e457ab73f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 48 additions and 104 deletions

View File

@ -122,7 +122,6 @@ struct CONFIG_T {
bool Vedirect_Enabled; bool Vedirect_Enabled;
bool Vedirect_UpdatesOnly; bool Vedirect_UpdatesOnly;
uint32_t Vedirect_PollInterval;
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];

View File

@ -64,8 +64,7 @@ VeDirectFrameHandler::VeDirectFrameHandler() :
_name(""), _name(""),
_value(""), _value(""),
_tmpFrame(), _tmpFrame(),
_pollInterval(5), _lastUpdate(0)
_lastPoll(0)
{ {
} }
@ -75,17 +74,8 @@ void VeDirectFrameHandler::init(int8_t rx, int8_t tx)
VedirectSerial.flush(); VedirectSerial.flush();
} }
void VeDirectFrameHandler::setPollInterval(unsigned long interval)
{
_pollInterval = interval;
}
void VeDirectFrameHandler::loop() void VeDirectFrameHandler::loop()
{ {
if ((millis() - getLastUpdate()) < _pollInterval * 1000) {
return;
}
while ( VedirectSerial.available()) { while ( VedirectSerial.available()) {
rxData(VedirectSerial.read()); rxData(VedirectSerial.read());
} }
@ -273,7 +263,7 @@ void VeDirectFrameHandler::frameEndEvent(bool valid) {
} }
veFrame = _tmpFrame; veFrame = _tmpFrame;
setLastUpdate(); _lastUpdate = millis();
} }
_tmpFrame = {}; _tmpFrame = {};
} }
@ -316,7 +306,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
} }
bool VeDirectFrameHandler::isDataValid() { bool VeDirectFrameHandler::isDataValid() {
if ((millis() - getLastUpdate()) / 1000 > _pollInterval * 5) { if (_lastUpdate == 0) {
return false; return false;
} }
if (strlen(veFrame.SER) == 0) { if (strlen(veFrame.SER) == 0) {
@ -327,16 +317,7 @@ bool VeDirectFrameHandler::isDataValid() {
unsigned long VeDirectFrameHandler::getLastUpdate() unsigned long VeDirectFrameHandler::getLastUpdate()
{ {
return _lastPoll; return _lastUpdate;
}
/*
* setLastUpdate
* This function is called every time a new ve.direct frame was read.
*/
void VeDirectFrameHandler::setLastUpdate()
{
_lastPoll = millis();
} }
/* /*

View File

@ -88,7 +88,6 @@ public:
VeDirectFrameHandler(); VeDirectFrameHandler();
void init(int8_t rx, int8_t tx); // initialize HardewareSerial void init(int8_t rx, int8_t tx); // initialize HardewareSerial
void setPollInterval(unsigned long interval); // set poll intervall in seconds
void loop(); // main loop to read ve.direct data void loop(); // main loop to read ve.direct data
unsigned long getLastUpdate(); // timestamp of last successful frame read unsigned long getLastUpdate(); // timestamp of last successful frame read
bool isDataValid(); // return true if data valid and not outdated bool isDataValid(); // return true if data valid and not outdated
@ -118,8 +117,7 @@ private:
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
veStruct _tmpFrame{}; // private struct for received name and value pairs veStruct _tmpFrame{}; // private struct for received name and value pairs
MovingAverage<double, 5> _efficiency; MovingAverage<double, 5> _efficiency;
unsigned long _pollInterval; unsigned long _lastUpdate;
unsigned long _lastPoll;
}; };
extern VeDirectFrameHandler VeDirect; extern VeDirectFrameHandler VeDirect;

View File

@ -121,7 +121,6 @@ bool ConfigurationClass::write()
JsonObject vedirect = doc.createNestedObject("vedirect"); JsonObject vedirect = doc.createNestedObject("vedirect");
vedirect["enabled"] = config.Vedirect_Enabled; vedirect["enabled"] = config.Vedirect_Enabled;
vedirect["updates_only"] = config.Vedirect_UpdatesOnly; vedirect["updates_only"] = config.Vedirect_UpdatesOnly;
vedirect["poll_interval"] = config.Vedirect_PollInterval;
JsonObject powermeter = doc.createNestedObject("powermeter"); JsonObject powermeter = doc.createNestedObject("powermeter");
powermeter["enabled"] = config.PowerMeter_Enabled; powermeter["enabled"] = config.PowerMeter_Enabled;
@ -330,7 +329,6 @@ bool ConfigurationClass::read()
JsonObject vedirect = doc["vedirect"]; JsonObject vedirect = doc["vedirect"];
config.Vedirect_Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; config.Vedirect_Enabled = vedirect["enabled"] | VEDIRECT_ENABLED;
config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY;
config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL;
JsonObject powermeter = doc["powermeter"]; JsonObject powermeter = doc["powermeter"];
config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED; config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED;

View File

@ -37,7 +37,6 @@ void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request)
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("vedirect_enabled")] = config.Vedirect_Enabled; root[F("vedirect_enabled")] = config.Vedirect_Enabled;
root[F("vedirect_pollinterval")] = config.Vedirect_PollInterval;
root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly; root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly;
response->setLength(); response->setLength();
@ -55,7 +54,6 @@ void WebApiVedirectClass::onVedirectAdminGet(AsyncWebServerRequest* request)
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("vedirect_enabled")] = config.Vedirect_Enabled; root[F("vedirect_enabled")] = config.Vedirect_Enabled;
root[F("vedirect_pollinterval")] = config.Vedirect_PollInterval;
root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly; root[F("vedirect_updatesonly")] = config.Vedirect_UpdatesOnly;
response->setLength(); response->setLength();
@ -101,7 +99,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (!(root.containsKey("vedirect_enabled") && root.containsKey("vedirect_pollinterval") && root.containsKey("vedirect_updatesonly")) ) { if (!(root.containsKey("vedirect_enabled") && root.containsKey("vedirect_updatesonly")) ) {
retMsg[F("message")] = F("Values are missing!"); retMsg[F("message")] = F("Values are missing!");
retMsg[F("code")] = WebApiError::GenericValueMissing; retMsg[F("code")] = WebApiError::GenericValueMissing;
response->setLength(); response->setLength();
@ -109,21 +107,9 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (root[F("vedirect_pollinterval")].as<uint32_t>() == 0) {
retMsg[F("message")] = F("Poll interval must be a number between 5 and 65535!");
retMsg[F("code")] = WebApiError::MqttPublishInterval;
retMsg[F("param")][F("min")] = 5;
retMsg[F("param")][F("max")] = 65535;
response->setLength();
request->send(response);
return;
}
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.Vedirect_Enabled = root[F("vedirect_enabled")].as<bool>(); config.Vedirect_Enabled = root[F("vedirect_enabled")].as<bool>();
config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as<bool>(); config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as<bool>();
config.Vedirect_PollInterval = root[F("vedirect_pollinterval")].as<uint32_t>();
Configuration.write(); Configuration.write();
retMsg[F("type")] = F("success"); retMsg[F("type")] = F("success");
@ -132,6 +118,4 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
response->setLength(); response->setLength();
request->send(response); request->send(response);
VeDirect.setPollInterval(config.Vedirect_PollInterval);
} }

View File

@ -165,7 +165,6 @@ void setup()
if (PinMapping.isValidVictronConfig()) { if (PinMapping.isValidVictronConfig()) {
MessageOutput.printf("ve.direct rx = %d, tx = %d\r\n", pin.victron_rx, pin.victron_tx); MessageOutput.printf("ve.direct rx = %d, tx = %d\r\n", pin.victron_rx, pin.victron_tx);
VeDirect.init(pin.victron_rx, pin.victron_tx); VeDirect.init(pin.victron_rx, pin.victron_tx);
VeDirect.setPollInterval(config.Vedirect_PollInterval);
MessageOutput.println(F("done")); MessageOutput.println(F("done"));
} else { } else {
MessageOutput.println(F("Invalid pin config")); MessageOutput.println(F("Invalid pin config"));

View File

@ -9,7 +9,7 @@
"SecuritySettings": "Sicherheit", "SecuritySettings": "Sicherheit",
"DTUSettings": "DTU", "DTUSettings": "DTU",
"DeviceManager": "Hardware", "DeviceManager": "Hardware",
"VedirectSettings": "Ve.direct", "VedirectSettings": "VE.Direct",
"PowerMeterSettings": "Power Meter", "PowerMeterSettings": "Power Meter",
"BatterySettings": "Batterie", "BatterySettings": "Batterie",
"AcChargerSettings": "AC Ladegerät", "AcChargerSettings": "AC Ladegerät",
@ -22,7 +22,7 @@
"NTP": "NTP", "NTP": "NTP",
"MQTT": "MQTT", "MQTT": "MQTT",
"Console": "Konsole", "Console": "Konsole",
"Vedirect": "Ve.direct", "Vedirect": "VE.Direct",
"About": "Über", "About": "Über",
"Logout": "Abmelden", "Logout": "Abmelden",
"Login": "Anmelden" "Login": "Anmelden"
@ -318,12 +318,12 @@
"Disconnected": "getrennt" "Disconnected": "getrennt"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "Ve.direct Info", "VedirectInformation" : "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
"Disabled": "@:mqttinfo.Disabled", "Disabled": "@:mqttinfo.Disabled",
"UpdatesOnly": "Nur Änderungen senden", "UpdatesOnly": "@:vedirectadmin.UpdatesOnly",
"UpdatesEnabled": "@:mqttinfo.Enabled", "UpdatesEnabled": "@:mqttinfo.Enabled",
"UpdatesDisabled": "@:mqttinfo.Disabled" "UpdatesDisabled": "@:mqttinfo.Disabled"
}, },
@ -485,13 +485,12 @@
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"vedirectadmin": { "vedirectadmin": {
"VedirectSettings": "Ve.direct Einstellungen", "VedirectSettings": "VE.Direct Einstellungen",
"VedirectConfiguration": "Ve.direct Konfiguration", "VedirectConfiguration": "VE.Direct Konfiguration",
"EnableVedirect": "Aktiviere Ve.direct", "EnableVedirect": "Aktiviere VE.Direct",
"VedirectParameter": "Ve.direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"PublishInterval": "Veröffentlichungsintervall:",
"Seconds": "Sekunden", "Seconds": "Sekunden",
"UpdatesOnly": "Nur Änderungen senden:", "UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{ "powermeteradmin":{

View File

@ -9,7 +9,7 @@
"SecuritySettings": "Security", "SecuritySettings": "Security",
"DTUSettings": "DTU", "DTUSettings": "DTU",
"DeviceManager": "Device-Manager", "DeviceManager": "Device-Manager",
"VedirectSettings": "Ve.direct", "VedirectSettings": "VE.Direct",
"PowerMeterSettings": "Power Meter", "PowerMeterSettings": "Power Meter",
"BatterySettings": "Battery", "BatterySettings": "Battery",
"AcChargerSettings": "AC Charger", "AcChargerSettings": "AC Charger",
@ -22,7 +22,7 @@
"NTP": "NTP", "NTP": "NTP",
"MQTT": "MQTT", "MQTT": "MQTT",
"Console": "Console", "Console": "Console",
"Vedirect": "Ve.direct", "Vedirect": "VE.Direct",
"About": "About", "About": "About",
"Logout": "Logout", "Logout": "Logout",
"Login": "Login" "Login": "Login"
@ -318,12 +318,12 @@
"Disconnected": "disconnected" "Disconnected": "disconnected"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "Ve.direct Info", "VedirectInformation" : "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
"Disabled": "@:mqttinfo.Disabled", "Disabled": "@:mqttinfo.Disabled",
"UpdatesOnly": "Send updates only", "UpdatesOnly": "@:vedirectadmin.UpdatesOnly",
"UpdatesEnabled": "@:mqttinfo.Enabled", "UpdatesEnabled": "@:mqttinfo.Enabled",
"UpdatesDisabled": "@:mqttinfo.Disabled" "UpdatesDisabled": "@:mqttinfo.Disabled"
}, },
@ -485,13 +485,12 @@
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"vedirectadmin": { "vedirectadmin": {
"VedirectSettings": "Ve.direct Settings", "VedirectSettings": "VE.Direct Settings",
"VedirectConfiguration": "Ve.direct Configuration", "VedirectConfiguration": "VE.Direct Configuration",
"EnableVedirect": "Enable Ve.direct", "EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "Ve.direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"PublishInterval": "Publish Interval:",
"Seconds": "seconds", "Seconds": "seconds",
"UpdatesOnly": "Send only updates:", "UpdatesOnly": "Publish values to MQTT only when they change",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{ "powermeteradmin":{

View File

@ -9,7 +9,7 @@
"SecuritySettings": "Sécurité", "SecuritySettings": "Sécurité",
"DTUSettings": "DTU", "DTUSettings": "DTU",
"DeviceManager": "Périphériques", "DeviceManager": "Périphériques",
"VedirectSettings": "Ve.direct", "VedirectSettings": "VE.Direct",
"PowerMeterSettings": "Power Meter", "PowerMeterSettings": "Power Meter",
"BatterySettings": "Battery", "BatterySettings": "Battery",
"AcChargerSettings": "AC Charger", "AcChargerSettings": "AC Charger",
@ -22,7 +22,7 @@
"NTP": "NTP", "NTP": "NTP",
"MQTT": "MQTT", "MQTT": "MQTT",
"Console": "Console", "Console": "Console",
"Vedirect": "Ve.direct", "Vedirect": "VE.Direct",
"About": "A propos", "About": "A propos",
"Logout": "Déconnexion", "Logout": "Déconnexion",
"Login": "Connexion" "Login": "Connexion"
@ -318,12 +318,12 @@
"Disconnected": "déconnecté" "Disconnected": "déconnecté"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "Ve.direct Info", "VedirectInformation" : "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
"Disabled": "@:mqttinfo.Disabled", "Disabled": "@:mqttinfo.Disabled",
"UpdatesOnly": "Send updates only", "UpdatesOnly": "@:vedirectadmin.UpdatesOnly",
"UpdatesEnabled": "@:mqttinfo.Enabled", "UpdatesEnabled": "@:mqttinfo.Enabled",
"UpdatesDisabled": "@:mqttinfo.Disabled" "UpdatesDisabled": "@:mqttinfo.Disabled"
}, },
@ -485,13 +485,12 @@
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"vedirectadmin": { "vedirectadmin": {
"VedirectSettings": "Ve.direct Settings", "VedirectSettings": "VE.Direct Settings",
"VedirectConfiguration": "Ve.direct Configuration", "VedirectConfiguration": "VE.Direct Configuration",
"EnableVedirect": "Enable Ve.direct", "EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "Ve.direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"PublishInterval": "Publish Interval:",
"Seconds": "seconds", "Seconds": "seconds",
"UpdatesOnly": "Send only updates:", "UpdatesOnly": "Publish values to MQTT only when they change",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"inverteradmin": { "inverteradmin": {

View File

@ -70,7 +70,7 @@ const router = createRouter({
}, },
{ {
path: '/info/vedirect', path: '/info/vedirect',
name: 'Ve.direct', name: 'VE.Direct',
component: VedirectInfoView component: VedirectInfoView
}, },
{ {
@ -85,7 +85,7 @@ const router = createRouter({
}, },
{ {
path: '/settings/vedirect', path: '/settings/vedirect',
name: 'Ve.direct Settings', name: 'VE.Direct Settings',
component: VedirectAdminView component: VedirectAdminView
}, },
{ {

View File

@ -1,5 +1,4 @@
export interface VedirectConfig { export interface VedirectConfig {
vedirect_enabled: boolean; vedirect_enabled: boolean;
vedirect_pollinterval: number;
vedirect_updatesonly: boolean; vedirect_updatesonly: boolean;
} }

View File

@ -14,14 +14,9 @@
<CardElement :text="$t('vedirectadmin.VedirectParameter')" textVariant="text-bg-primary" add-space <CardElement :text="$t('vedirectadmin.VedirectParameter')" textVariant="text-bg-primary" add-space
v-show="vedirectConfigList.vedirect_enabled" v-show="vedirectConfigList.vedirect_enabled"
> >
<InputElement :label="$t('vedirectadmin.PublishInterval')"
v-model="vedirectConfigList.vedirect_pollinterval"
type="number" min="5" max="86400"
:postfix="$t('vedirectadmin.Seconds')"/>
<InputElement :label="$t('vedirectadmin.UpdatesOnly')" <InputElement :label="$t('vedirectadmin.UpdatesOnly')"
v-model="vedirectConfigList.vedirect_updatesonly" v-model="vedirectConfigList.vedirect_updatesonly"
type="checkbox"/> type="checkbox" wide/>
</CardElement> </CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('vedirectadmin.Save') }}</button> <button type="submit" class="btn btn-primary mb-3">{{ $t('vedirectadmin.Save') }}</button>

View File

@ -6,22 +6,14 @@
<tbody> <tbody>
<tr> <tr>
<th>{{ $t('vedirectinfo.Status') }}</th> <th>{{ $t('vedirectinfo.Status') }}</th>
<td class="badge" :class="{ <td>
'text-bg-danger': !vedirectDataList.vedirect_enabled, <StatusBadge :status="vedirectDataList.vedirect_enabled" true_text="vedirectinfo.Enabled" false_text="vedirectinfo.Disabled" />
'text-bg-success': vedirectDataList.vedirect_enabled,
}">
<span v-if="vedirectDataList.vedirect_enabled">{{ $t('vedirectinfo.Enabled') }}</span>
<span v-else>{{ $t('vedirectinfo.Disabled') }}</span>
</td> </td>
</tr> </tr>
<tr v-show="vedirectDataList.vedirect_enabled"> <tr>
<th>{{ $t('vedirectinfo.UpdatesOnly') }}</th> <th>{{ $t('vedirectinfo.UpdatesOnly') }}</th>
<td class="badge" :class="{ <td>
'text-bg-danger': !vedirectDataList.vedirect_updatesonly, <StatusBadge :status="vedirectDataList.vedirect_updatesonly" true_text="vedirectinfo.UpdatesEnabled" false_text="vedirectinfo.UpdatesDisabled" />
'text-bg-success': vedirectDataList.vedirect_updatesonly,
}">
<span v-if="vedirectDataList.vedirect_updatesonly">{{ $t('vedirectinfo.UpdatesEnabled') }}</span>
<span v-else>{{ $t('vedirectinfo.UpdatesDisabled') }}</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -34,6 +26,7 @@
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import StatusBadge from '@/components/StatusBadge.vue';
import type { VedirectStatus } from "@/types/VedirectStatus"; import type { VedirectStatus } from "@/types/VedirectStatus";
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -42,6 +35,7 @@ export default defineComponent({
components: { components: {
BasePage, BasePage,
CardElement, CardElement,
StatusBadge
}, },
data() { data() {
return { return {