polish support for second VE.Direct MPPT charge controller
* fix compiler warning in SerialPortManager.cpp: function must not return void * clean up and simplify implementation of usesHwPort2() * make const * overrides are final * default implementation returns false * implement in header, as the implementation is very simple * rename PortManager to SerialPortManager. as "PortManager" is too generic, the static instance of the serial port manager is renamed to "SerialPortManager". the class is therefore renamed to SerialPortManagerClass, which is in line with other (static) classes withing OpenDTU(-OnBattery). * implement separate data ages for MPPT charge controllers * make sure MPPT data and live data time out * do not use invalid data of MPPT controlers for calculations * add :key binding to v-for iterating over MPPT instances
This commit is contained in:
parent
75541be248
commit
7d6b7252bf
@ -14,7 +14,7 @@ public:
|
||||
virtual void deinit() = 0;
|
||||
virtual void loop() = 0;
|
||||
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
|
||||
virtual bool usesHwPort2() = 0;
|
||||
virtual bool usesHwPort2() const { return false; }
|
||||
};
|
||||
|
||||
class BatteryClass {
|
||||
|
||||
@ -19,7 +19,7 @@ class Controller : public BatteryProvider {
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
bool usesHwPort2() override;
|
||||
bool usesHwPort2() const final { return true; }
|
||||
|
||||
private:
|
||||
enum class Status : unsigned {
|
||||
|
||||
@ -12,7 +12,6 @@ public:
|
||||
void deinit() final;
|
||||
void loop() final { return; } // this class is event-driven
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
bool usesHwPort2() override;
|
||||
|
||||
private:
|
||||
bool _verboseLogging = false;
|
||||
|
||||
@ -14,7 +14,6 @@ public:
|
||||
void deinit() final;
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
bool usesHwPort2() override;
|
||||
|
||||
private:
|
||||
uint16_t readUnsignedInt16(uint8_t *data);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
#include <map>
|
||||
|
||||
class SerialPortManager {
|
||||
class SerialPortManagerClass {
|
||||
public:
|
||||
bool allocateMpptPort(int port);
|
||||
bool allocateBatteryPort(int port);
|
||||
@ -24,4 +24,4 @@ private:
|
||||
static const char* print(Owner owner);
|
||||
};
|
||||
|
||||
extern SerialPortManager PortManager;
|
||||
extern SerialPortManagerClass SerialPortManager;
|
||||
|
||||
@ -22,6 +22,7 @@ public:
|
||||
// returns the data age of all controllers,
|
||||
// i.e, the youngest data's age is returned.
|
||||
uint32_t getDataAgeMillis() const;
|
||||
uint32_t getDataAgeMillis(size_t idx) const;
|
||||
|
||||
std::optional<VeDirectMpptController::spData_t> getData(size_t idx = 0) const;
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ public:
|
||||
void deinit() final { }
|
||||
void loop() final;
|
||||
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
|
||||
bool usesHwPort2() override;
|
||||
bool usesHwPort2() const final { return true; }
|
||||
|
||||
private:
|
||||
uint32_t _lastUpdate = 0;
|
||||
|
||||
@ -14,7 +14,7 @@ public:
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void generateJsonResponse(JsonVariant& root, bool fullUpdate);
|
||||
static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
@ -22,8 +22,8 @@ private:
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsPublish = 0;
|
||||
uint32_t _dataAgeMillis = 0;
|
||||
uint32_t _lastFullPublish = 0;
|
||||
uint32_t _dataAgeMillis[VICTRON_MAX_COUNT] = { 0 };
|
||||
static constexpr uint16_t _responseSize = VICTRON_MAX_COUNT * (1024 + 128);
|
||||
|
||||
std::mutex _mutex;
|
||||
|
||||
@ -281,13 +281,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
|
||||
}
|
||||
|
||||
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
|
||||
if (_lastUpdate == 0) {
|
||||
return false;
|
||||
}
|
||||
if (strlen(frame.SER) == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000);
|
||||
}
|
||||
|
||||
uint32_t VeDirectFrameHandler::getLastUpdate() const
|
||||
|
||||
@ -39,7 +39,7 @@ void BatteryClass::updateSettings()
|
||||
_upProvider->deinit();
|
||||
_upProvider = nullptr;
|
||||
}
|
||||
PortManager.invalidateBatteryPort();
|
||||
SerialPortManager.invalidateBatteryPort();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Battery.Enabled) { return; }
|
||||
@ -65,7 +65,7 @@ void BatteryClass::updateSettings()
|
||||
}
|
||||
|
||||
if(_upProvider->usesHwPort2()) {
|
||||
if (!PortManager.allocateBatteryPort(2)) {
|
||||
if (!SerialPortManager.allocateBatteryPort(2)) {
|
||||
MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2);
|
||||
_upProvider = nullptr;
|
||||
return;
|
||||
@ -73,7 +73,7 @@ void BatteryClass::updateSettings()
|
||||
}
|
||||
|
||||
if (!_upProvider->init(verboseLogging)) {
|
||||
PortManager.invalidateBatteryPort();
|
||||
SerialPortManager.invalidateBatteryPort();
|
||||
_upProvider = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -427,8 +427,4 @@ void Controller::processDataPoints(DataPointContainer const& dataPoints)
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::usesHwPort2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace JkBms */
|
||||
|
||||
@ -112,7 +112,3 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con
|
||||
*voltage, topic);
|
||||
}
|
||||
}
|
||||
|
||||
bool MqttBattery::usesHwPort2() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -266,10 +266,6 @@ bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit)
|
||||
return (value & (1 << bit)) >> bit;
|
||||
}
|
||||
|
||||
bool PylontechCanReceiver::usesHwPort2() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef PYLONTECH_DUMMY
|
||||
void PylontechCanReceiver::dummyData()
|
||||
{
|
||||
|
||||
@ -4,19 +4,19 @@
|
||||
|
||||
#define MAX_CONTROLLERS 3
|
||||
|
||||
SerialPortManager PortManager;
|
||||
SerialPortManagerClass SerialPortManager;
|
||||
|
||||
bool SerialPortManager::allocateBatteryPort(int port)
|
||||
bool SerialPortManagerClass::allocateBatteryPort(int port)
|
||||
{
|
||||
return allocatePort(port, Owner::BATTERY);
|
||||
}
|
||||
|
||||
bool SerialPortManager::allocateMpptPort(int port)
|
||||
bool SerialPortManagerClass::allocateMpptPort(int port)
|
||||
{
|
||||
return allocatePort(port, Owner::MPPT);
|
||||
}
|
||||
|
||||
bool SerialPortManager::allocatePort(uint8_t port, Owner owner)
|
||||
bool SerialPortManagerClass::allocatePort(uint8_t port, Owner owner)
|
||||
{
|
||||
if (port >= MAX_CONTROLLERS) {
|
||||
MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port);
|
||||
@ -26,17 +26,17 @@ bool SerialPortManager::allocatePort(uint8_t port, Owner owner)
|
||||
return allocatedPorts.insert({port, owner}).second;
|
||||
}
|
||||
|
||||
void SerialPortManager::invalidateBatteryPort()
|
||||
void SerialPortManagerClass::invalidateBatteryPort()
|
||||
{
|
||||
invalidate(Owner::BATTERY);
|
||||
}
|
||||
|
||||
void SerialPortManager::invalidateMpptPorts()
|
||||
void SerialPortManagerClass::invalidateMpptPorts()
|
||||
{
|
||||
invalidate(Owner::MPPT);
|
||||
}
|
||||
|
||||
void SerialPortManager::invalidate(Owner owner)
|
||||
void SerialPortManagerClass::invalidate(Owner owner)
|
||||
{
|
||||
for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) {
|
||||
if (it->second == owner) {
|
||||
@ -48,7 +48,7 @@ void SerialPortManager::invalidate(Owner owner)
|
||||
}
|
||||
}
|
||||
|
||||
const char* SerialPortManager::print(Owner owner)
|
||||
const char* SerialPortManagerClass::print(Owner owner)
|
||||
{
|
||||
switch (owner) {
|
||||
case BATTERY:
|
||||
@ -56,4 +56,5 @@ const char* SerialPortManager::print(Owner owner)
|
||||
case MPPT:
|
||||
return "MPPT";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ void VictronMpptClass::updateSettings()
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
_controllers.clear();
|
||||
PortManager.invalidateMpptPorts();
|
||||
SerialPortManager.invalidateMpptPorts();
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (!config.Vedirect.Enabled) { return; }
|
||||
@ -47,7 +47,7 @@ bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hw
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PortManager.allocateMpptPort(hwSerialPort)) {
|
||||
if (!SerialPortManager.allocateMpptPort(hwSerialPort)) {
|
||||
MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n",
|
||||
hwSerialPort);
|
||||
return false;
|
||||
@ -110,6 +110,15 @@ uint32_t VictronMpptClass::getDataAgeMillis() const
|
||||
return age;
|
||||
}
|
||||
|
||||
uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
if (_controllers.empty() || idx >= _controllers.size()) { return 0; }
|
||||
|
||||
return millis() - _controllers[idx]->getLastUpdate();
|
||||
}
|
||||
|
||||
std::optional<VeDirectMpptController::spData_t> VictronMpptClass::getData(size_t idx) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
@ -128,6 +137,7 @@ int32_t VictronMpptClass::getPowerOutputWatts() const
|
||||
int32_t sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { continue; }
|
||||
sum += upController->getData()->P;
|
||||
}
|
||||
|
||||
@ -139,6 +149,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const
|
||||
int32_t sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { continue; }
|
||||
sum += upController->getData()->PPV;
|
||||
}
|
||||
|
||||
@ -150,6 +161,7 @@ double VictronMpptClass::getYieldTotal() const
|
||||
double sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { continue; }
|
||||
sum += upController->getData()->H19;
|
||||
}
|
||||
|
||||
@ -161,6 +173,7 @@ double VictronMpptClass::getYieldDay() const
|
||||
double sum = 0;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { continue; }
|
||||
sum += upController->getData()->H20;
|
||||
}
|
||||
|
||||
@ -172,6 +185,7 @@ double VictronMpptClass::getOutputVoltage() const
|
||||
double min = -1;
|
||||
|
||||
for (const auto& upController : _controllers) {
|
||||
if (!upController->isDataValid()) { continue; }
|
||||
double volts = upController->getData()->V;
|
||||
if (min == -1) { min = volts; }
|
||||
min = std::min(min, volts);
|
||||
|
||||
@ -34,7 +34,3 @@ void VictronSmartShunt::loop()
|
||||
_stats->updateFrom(VeDirectShunt.veFrame);
|
||||
_lastUpdate = VeDirectShunt.getLastUpdate();
|
||||
}
|
||||
|
||||
bool VictronSmartShunt::usesHwPort2() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -55,25 +55,28 @@ void WebApiWsVedirectLiveClass::wsCleanupTaskCb()
|
||||
void WebApiWsVedirectLiveClass::sendDataTaskCb()
|
||||
{
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we assume this loop to be running at least twice for every
|
||||
// update from a VE.Direct MPPT data producer, so _dataAgeMillis
|
||||
// actually grows in between updates.
|
||||
auto lastDataAgeMillis = _dataAgeMillis;
|
||||
_dataAgeMillis = VictronMppt.getDataAgeMillis();
|
||||
if (_ws.count() == 0) { return; }
|
||||
|
||||
// Update on ve.direct change or at least after 10 seconds
|
||||
if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) {
|
||||
bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000));
|
||||
bool updateAvailable = false;
|
||||
if (!fullUpdate) {
|
||||
for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) {
|
||||
auto currentAgeMillis = VictronMppt.getDataAgeMillis(idx);
|
||||
if (currentAgeMillis > 0 && currentAgeMillis < _dataAgeMillis[idx]) {
|
||||
updateAvailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fullUpdate || updateAvailable) {
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
DynamicJsonDocument root(_responseSize);
|
||||
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
generateJsonResponse(var, fullUpdate);
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
@ -92,15 +95,17 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb()
|
||||
} catch (const std::exception& exc) {
|
||||
MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what());
|
||||
}
|
||||
}
|
||||
|
||||
_lastWsPublish = millis();
|
||||
if (fullUpdate) {
|
||||
_lastFullPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool fullUpdate)
|
||||
{
|
||||
root["vedirect"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000;
|
||||
const JsonArray &array = root["vedirect"].createNestedArray("devices");
|
||||
const JsonObject &array = root["vedirect"].createNestedObject("instances");
|
||||
root["vedirect"]["full_update"] = fullUpdate;
|
||||
|
||||
for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) {
|
||||
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
|
||||
@ -108,10 +113,16 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
continue;
|
||||
}
|
||||
|
||||
auto lastDataAgeMillis = _dataAgeMillis[idx];
|
||||
_dataAgeMillis[idx] = VictronMppt.getDataAgeMillis(idx);
|
||||
bool validAge = _dataAgeMillis[idx] > 0;
|
||||
bool updateAvailable = _dataAgeMillis[idx] < lastDataAgeMillis;
|
||||
if (!fullUpdate && !(validAge && updateAvailable)) { continue; }
|
||||
|
||||
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
|
||||
|
||||
const JsonObject &nested = array.createNestedObject();
|
||||
nested["age_critical"] = !VictronMppt.isDataValid(idx);
|
||||
const JsonObject &nested = array.createNestedObject(spMpptData->SER);
|
||||
nested["data_age_ms"] = _dataAgeMillis[idx];
|
||||
populateJson(nested, spMpptData);
|
||||
}
|
||||
|
||||
@ -122,8 +133,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
|
||||
}
|
||||
|
||||
void
|
||||
WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) {
|
||||
void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) {
|
||||
// device info
|
||||
root["device"]["PID"] = spMpptData->getPidAsString();
|
||||
root["device"]["SER"] = spMpptData->SER;
|
||||
@ -202,7 +212,7 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize);
|
||||
auto& root = response->getRoot();
|
||||
|
||||
generateJsonResponse(root);
|
||||
generateJsonResponse(root, true/*fullUpdate*/);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -9,11 +9,11 @@
|
||||
<template v-else>
|
||||
<div class="row gy-3">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card" v-for="item in vedirect.devices">
|
||||
<div class="card" v-for="(item, serial) in vedirect.instances" :key="serial">
|
||||
<div class="card-header d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'text-bg-danger': item.age_critical,
|
||||
'text-bg-primary': !item.age_critical,
|
||||
'text-bg-danger': item.data_age_ms >= 10000,
|
||||
'text-bg-primary': item.data_age_ms < 10000,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
@ -27,7 +27,7 @@
|
||||
{{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': vedirect.data_age }) }}
|
||||
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -199,7 +199,7 @@ export default defineComponent({
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataAgeTimers: {} as Record<string, number>,
|
||||
dataLoading: true,
|
||||
dplData: {} as DynamicPowerLimiter,
|
||||
vedirect: {} as Vedirect,
|
||||
@ -209,7 +209,6 @@ export default defineComponent({
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
@ -224,6 +223,7 @@ export default defineComponent({
|
||||
this.dplData = root["dpl"];
|
||||
this.vedirect = root["vedirect"];
|
||||
this.dataLoading = false;
|
||||
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
@ -240,7 +240,12 @@ export default defineComponent({
|
||||
console.log(event);
|
||||
var root = JSON.parse(event.data);
|
||||
this.dplData = root["dpl"];
|
||||
this.vedirect = root["vedirect"];
|
||||
if (root["vedirect"]["full_update"] === true) {
|
||||
this.vedirect = root["vedirect"];
|
||||
} else {
|
||||
Object.assign(this.vedirect.instances, root["vedirect"]["instances"]);
|
||||
}
|
||||
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
@ -255,11 +260,25 @@ export default defineComponent({
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.vedirect) {
|
||||
this.vedirect.data_age++;
|
||||
resetDataAging(serials: Array<string>) {
|
||||
serials.forEach((serial) => {
|
||||
if (this.dataAgeTimers[serial] !== undefined) {
|
||||
clearTimeout(this.dataAgeTimers[serial]);
|
||||
}
|
||||
|
||||
var nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000);
|
||||
this.dataAgeTimers[serial] = setTimeout(() => {
|
||||
this.doDataAging(serial);
|
||||
}, nextMs);
|
||||
});
|
||||
},
|
||||
doDataAging(serial: string) {
|
||||
if (this.vedirect?.instances?.[serial] === undefined) { return; }
|
||||
|
||||
this.vedirect.instances[serial].data_age_ms += 1000;
|
||||
|
||||
this.dataAgeTimers[serial] = setTimeout(() => {
|
||||
this.doDataAging(serial);
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
@ -280,11 +299,6 @@ export default defineComponent({
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -6,15 +6,15 @@ export interface DynamicPowerLimiter {
|
||||
}
|
||||
|
||||
export interface Vedirect {
|
||||
data_age: 0;
|
||||
devices: Array<VedirectDevices>;
|
||||
full_update: boolean;
|
||||
instances: { [key: string]: VedirectInstance };
|
||||
}
|
||||
|
||||
export interface VedirectDevices {
|
||||
age_critical: boolean;
|
||||
export interface VedirectInstance {
|
||||
data_age_ms: number;
|
||||
device: VedirectDevice;
|
||||
input: VedirectInput;
|
||||
output: VedirectOutput;
|
||||
input: VedirectInput;
|
||||
}
|
||||
|
||||
export interface VedirectDevice {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user