diff --git a/.vscode/settings.json b/.vscode/settings.json index ed596687..9642712e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,62 @@ { - "C_Cpp.clang_format_style": "WebKit" + "C_Cpp.clang_format_style": "WebKit", + "files.associations": { + "*.tcc": "cpp", + "unordered_map": "cpp", + "fstream": "cpp", + "istream": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "array": "cpp", + "deque": "cpp", + "list": "cpp", + "string": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp", + "regex": "cpp", + "chrono": "cpp", + "functional": "cpp", + "atomic": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "condition_variable": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp" + } } \ No newline at end of file diff --git a/README.md b/README.md index cb9f4ef4..3ce269cc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ [![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml) [![Yarn Linting](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml) -## !! IMPORTANT UPGRADE NOTES !! +# Fork + +This fork add support of [JSY-MK power meter modules](https://www.jsypowermeter.com/). + +## !! IMPORTANT UPGRADE NOTES If you are upgrading from a version before 15.03.2023 you have to upgrade the partition table of the ESP32. Please follow the [this](docs/UpgradePartition.md) documentation! diff --git a/include/Configuration.h b/include/Configuration.h index e13b558a..ca3ad3a6 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -30,6 +30,8 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 +#define PWRMTR_MAX_CHAN_COUNT 2 + struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; @@ -152,6 +154,18 @@ struct CONFIG_T { uint8_t Brightness; } Led_Single[PINMAPPING_LED_COUNT]; + struct { + uint32_t BaudRate; + uint32_t PollInterval; + } SerialModbus; + + struct { + struct { + bool InvertDirection; + bool NegativePower; + } channel[PWRMTR_MAX_CHAN_COUNT]; + } PowerMeter; + INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; }; diff --git a/include/JsyMk.h b/include/JsyMk.h new file mode 100644 index 00000000..84b95f1b --- /dev/null +++ b/include/JsyMk.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class JsyMkClass { +public: + enum class Field_t : size_t { + // Device informations + ADDRESS, + MANUFACTURER, + MODEL, + VERSION, + VOLTAGE_RANGE, + CURRENT_RANGE, + // Measures + VOLTAGE, + CURRENT, + POWER, + POWER_FACTOR, + FREQUENCY, + NEGATIVE, + TOTAL_POSITIVE_ENERGY, + TOTAL_NEGATIVE_ENERGY, + TODAY_POSITIVE_ENERGY, + TODAY_NEGATIVE_ENERGY + }; + + JsyMkClass(); + void init(Scheduler& scheduler); + + uint32_t getLastUpdate() const; + bool isInitialised() const; + + uint32_t getPollInterval() const; + void setPollInterval(const uint32_t interval); + + size_t getChannelNumber() const; + + String getFieldName(size_t channel, Field_t fieldId) const; + String getFieldString(size_t channel, Field_t fieldId) const; + float getFieldValue(size_t channel, Field_t fieldId) const; + + size_t getFieldDigits(Field_t fieldId) const; + String getFieldUnit(Field_t fieldId) const; + String getFieldDeviceClass(Field_t fieldId) const; + String getFieldStatusClass(Field_t fieldId) const; + + void reset(); + +private: + void loop(); + + JSY_MK _jsymk; + Task _loopTask; + bool _initialised = false; + uint32_t _lastUpdate = 0; + float _todayPositiveRef = 0; + float _todayNegativeRef = 0; +}; + +extern JsyMkClass JsyMk; \ No newline at end of file diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index a76cb0c7..8e09b7d3 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "MqttHandlePowerMeter.h" #include #include #include @@ -66,6 +67,9 @@ private: void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); + void publishPowerMeterField(size_t channel, JsyMkClass::Field_t fieldId, const bool clear = false); + + static void createPowerMeterInfo(JsonDocument& doc, size_t channel, JsyMkClass::Field_t fieldId); static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); static void createDtuInfo(JsonDocument& doc); diff --git a/include/MqttHandlePowerMeter.h b/include/MqttHandlePowerMeter.h new file mode 100644 index 00000000..910d2914 --- /dev/null +++ b/include/MqttHandlePowerMeter.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include "JsyMk.h" +#include + +class MqttHandlePowerMeterClass { +public: + using Field_t = JsyMkClass::Field_t; + + MqttHandlePowerMeterClass(); + void init(Scheduler& scheduler); + + static String getTopic(size_t channel, Field_t fieldId); + +private: + void + loop(); + void publishField(size_t channel, const Field_t fieldId); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); + + Task _loopTask; + uint32_t _lastUpdate = {}; +}; + +extern MqttHandlePowerMeterClass MqttHandlePowerMeter; diff --git a/include/PinMapping.h b/include/PinMapping.h index e0db88b6..5ead9185 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -39,6 +39,9 @@ struct PinMapping_t { uint8_t display_cs; uint8_t display_reset; int8_t led[PINMAPPING_LED_COUNT]; + + int8_t serial_modbus_tx; + int8_t serial_modbus_rx; }; class PinMappingClass { @@ -50,6 +53,7 @@ public: bool isValidNrf24Config() const; bool isValidCmt2300Config() const; bool isValidEthConfig() const; + bool isValidSerialModbusConfig() const; private: PinMapping_t _pinMapping; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 05f8ab8f..98f91ac9 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -16,6 +16,7 @@ private: static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv); static void generateCommonJsonResponse(JsonVariant& root); + static void generatePowerMeterJsonResponse(JsonObject& root); static void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); diff --git a/include/defaults.h b/include/defaults.h index fd41a3d0..21d0a44b 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -108,3 +108,9 @@ #define LED_BRIGHTNESS 100U #define MAX_INVERTER_LIMIT 2250 + +#define SERIAL_MODBUS_POLL_INTERVAL 1U + +#ifndef SERIAL_MODBUS_BAUDRATE +#define SERIAL_MODBUS_BAUDRATE 9600U +#endif \ No newline at end of file diff --git a/lib/JSY_MK/library.json b/lib/JSY_MK/library.json new file mode 100644 index 00000000..4447fb98 --- /dev/null +++ b/lib/JSY_MK/library.json @@ -0,0 +1,13 @@ +{ + "name": "JSY_MK", + "keywords": "modbus, RTU, energy", + "description": "JSY_MK", + "authors": { + "name": "Benoit Leforestier" + }, + "version": "0.1.0", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} \ No newline at end of file diff --git a/lib/JSY_MK/src/JSY_MK.cpp b/lib/JSY_MK/src/JSY_MK.cpp new file mode 100644 index 00000000..59aecef3 --- /dev/null +++ b/lib/JSY_MK/src/JSY_MK.cpp @@ -0,0 +1,258 @@ +#include "JSY_MK.h" +#include + +namespace { +constexpr uint8_t DEVICE_ADDRESS = 1; +constexpr std::chrono::seconds DEFAULT_UPDATE_PERIOD(1); + +struct parameterRaw { + uint8_t len; + uint16_t model; + uint16_t version; + uint16_t voltageRange; + uint16_t CurrentRange; +} __attribute__((__packed__)); + +struct measurementRaw { + uint8_t len; + uint16_t voltage; + uint16_t current; + uint16_t power; + uint32_t positiveEnergy; + uint16_t powerFactor; + uint32_t negativeEnergy; + uint16_t powerDirect; + uint16_t frequency; +} __attribute__((__packed__)); +} // namespace + +JSY_MK::JSY_MK(uint8_t uart_nr) + : m_serialRTU(uart_nr) + , m_updatePeriod(DEFAULT_UPDATE_PERIOD) + , m_lastUpdateTime(0) +{ +} + +void JSY_MK::begin(uint32_t baud, uint32_t config, int8_t rxPin, int8_t txPin) +{ + m_serialRTU.setRequestTimeout(std::chrono::milliseconds(500)); + m_serialRTU.setResponseCallback(std::bind( + &JSY_MK::responseHandler, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5)); + + m_serialRTU.begin(baud, config, rxPin, txPin); + m_serialRTU.readHoldingRegisters(DEVICE_ADDRESS, 0x0000, 4); +} + +void JSY_MK::end() +{ + m_serialRTU.end(); + m_model = 0; +} + +void JSY_MK::loop() +{ + m_serialRTU.loop(); + + if (m_updatePeriod > std::chrono::milliseconds::zero() && !m_serialRTU.isDeviceBusy(DEVICE_ADDRESS) && m_model != 0 && (m_lastUpdateTime == 0 || (millis() - m_lastUpdateTime) > m_updatePeriod.count())) { + m_serialRTU.readHoldingRegisters(DEVICE_ADDRESS, 0x0048, 10); + m_lastUpdateTime = millis(); + } +} + +bool JSY_MK::isBusy() const +{ + return m_serialRTU.isDeviceBusy(DEVICE_ADDRESS); +} + +void JSY_MK::setUpdatedCallback(updatedCb cb) +{ + m_updateCallback = cb; +} + +void JSY_MK::setErrorCallback(errorCb cb) +{ + m_errorCallback = cb; +} + +void JSY_MK::responseHandler( + SerialModbusRTU::ec exceptionCode, + uint8_t deviceAddress, + uint8_t functionCode, + const void* data, + size_t len) +{ + if (exceptionCode != SerialModbusRTU::ec::noError) { + if (m_model == 0) { + // Retry init + m_serialRTU.readHoldingRegisters(DEVICE_ADDRESS, 0x0000, 4); + } + + if (m_errorCallback) + m_errorCallback(exceptionCode); + + return; + } + + if (deviceAddress != DEVICE_ADDRESS) + return; // Ignore + + if (data == nullptr) + return; // Error + + if (len == sizeof(parameterRaw)) { + const auto* buffer = static_cast(data); + if (buffer->len != sizeof(parameterRaw) - 1) + return; // Error + + m_model = __bswap_16(buffer->model); + m_version = __bswap_16(buffer->version); + m_voltageRange = __bswap_16(buffer->voltageRange); + m_CurrentRange = __bswap_16(buffer->CurrentRange) / 10; + + if (m_version == 0) + m_version = 0x0100; + } else if (len == sizeof(measurementRaw)) { + const auto* buffer = static_cast(data); + if (buffer->len != sizeof(measurementRaw) - 1) + return; // Error + + m_voltage = static_cast(__bswap_16(buffer->voltage)) / 100.F; + m_current = static_cast(__bswap_16(buffer->current)) / 100.F; + m_power = static_cast(__bswap_16(buffer->power)); + m_powerFactor = static_cast(__bswap_16(buffer->powerFactor)) / 1000.F; + m_frequency = static_cast(__bswap_16(buffer->frequency)) / 100.F; + + m_isNegative = (buffer->powerDirect != 0); + m_positiveEnergy = static_cast(__bswap_32(buffer->positiveEnergy)) / 3200.F; + m_negativeEnergy = static_cast(__bswap_32(buffer->negativeEnergy)) / 3200.F; + + if (m_updateCallback) + m_updateCallback(); + } else if (len == 2) { + } +} + +std::chrono::milliseconds JSY_MK::getUpdatePeriod() const +{ + return m_updatePeriod; +} + +void JSY_MK::setUpdatePeriod(std::chrono::milliseconds period) +{ + m_updatePeriod = period; +} + +void JSY_MK::setBaudrate(baurate_t baudrate) +{ + if (m_serialRTU.isDeviceBusy(DEVICE_ADDRESS)) + return; + + std::array buffer = { DEVICE_ADDRESS, static_cast(baudrate) }; + m_serialRTU.writeMultipleRegisters(DEVICE_ADDRESS, 0x0004, 1, buffer.data(), buffer.size()); +} + +uint8_t JSY_MK::getAddress() const +{ + return DEVICE_ADDRESS; +} + +std::string JSY_MK::getManufacturer() const +{ + return "Shenzhen JianSiYan Technologies"; +} + +size_t JSY_MK::getChannelNumber() const +{ + return 1; +} + +size_t JSY_MK::getModel() const +{ + return m_model; +} + +std::string JSY_MK::getModelAsString() const +{ + std::array buffer = {}; + int len = std::snprintf(buffer.data(), buffer.size(), "JSY-MK-%XT", m_model); + return len > 0 ? std::string(buffer.data(), len) : std::string(); +} + +size_t JSY_MK::getVersion() const +{ + return m_version; +} + +std::string JSY_MK::getVersionAsString() const +{ + std::array buffer = {}; + int len = std::snprintf(buffer.data(), buffer.size(), "%X.%X", (m_version >> 8) & 0x0F, (m_version >> 4) & 0x0F); + return len > 0 ? std::string(buffer.data(), len) : std::string(); +} + +size_t JSY_MK::getVoltageRange() const +{ + return m_voltageRange; +} + +size_t JSY_MK::getCurrentRange() const +{ + return m_CurrentRange; +} + +bool JSY_MK::isNegative() const +{ + return m_isNegative; +} + +float JSY_MK::getVoltage() const +{ + return m_voltage; +} + +float JSY_MK::getCurrent() const +{ + return m_current; +} + +float JSY_MK::getPower() const +{ + return m_power; +} + +float JSY_MK::getPowerFactor() const +{ + return m_powerFactor; +} + +float JSY_MK::getFrequency() const +{ + return m_frequency; +} + +float JSY_MK::getPositiveEnergy() const +{ + return m_positiveEnergy; +} + +float JSY_MK::getNegativeEnergy() const +{ + return m_negativeEnergy; +} + +void JSY_MK::resetEnergy() +{ + if (m_serialRTU.isDeviceBusy(DEVICE_ADDRESS)) + return; + + // Registers 0x000C, 0x000D + // Data 00000000 + std::array buffer = {}; + m_serialRTU.writeMultipleRegisters(DEVICE_ADDRESS, 0x000C, 2, buffer.data(), buffer.size()); +} diff --git a/lib/JSY_MK/src/JSY_MK.h b/lib/JSY_MK/src/JSY_MK.h new file mode 100644 index 00000000..8bb64de5 --- /dev/null +++ b/lib/JSY_MK/src/JSY_MK.h @@ -0,0 +1,91 @@ +#pragma once +#include "SerialModbusRTU.h" + +// Jian Si Yan Metering module +// www.jsypowermeter.com + +class JSY_MK { +public: + enum class baurate_t : uint8_t { + _1200 = 3, // 1200bps + _2400 = 4, // 2400bps + _4800 = 5, // 4800bps + _9600 = 6, // 9600bps + _19200 = 7, // 19200bps + _38400 = 8 // 38400bps + }; + + typedef std::function updatedCb; + typedef std::function errorCb; + + JSY_MK(uint8_t uart_nr); + ~JSY_MK() = default; + + void begin(uint32_t baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1); + void end(); + void loop(); + + bool isBusy() const; + + void setUpdatedCallback(updatedCb cb); + void setErrorCallback(errorCb cb); + + std::chrono::milliseconds getUpdatePeriod() const; + void setUpdatePeriod(std::chrono::milliseconds period); + + void setBaudrate(baurate_t baudrate); + + uint8_t getAddress() const; + size_t getChannelNumber() const; + + size_t getModel() const; + std::string getModelAsString() const; + std::string getManufacturer() const; + + size_t getVersion() const; + std::string getVersionAsString() const; + + size_t getVoltageRange() const; + size_t getCurrentRange() const; + + bool isNegative() const; + float getVoltage() const; + float getCurrent() const; + float getPower() const; + float getPowerFactor() const; + float getFrequency() const; + + float getPositiveEnergy() const; + float getNegativeEnergy() const; + + void resetEnergy(); + +private: + void responseHandler( + SerialModbusRTU::ec exceptionCode, + uint8_t deviceAddress, + uint8_t functionCode, + const void* data, + size_t len); + +private: + SerialModbusRTU m_serialRTU; + std::chrono::milliseconds m_updatePeriod; + time_t m_lastUpdateTime; + updatedCb m_updateCallback; + errorCb m_errorCallback; + + uint16_t m_model {}; + uint16_t m_version {}; + size_t m_voltageRange {}; + size_t m_CurrentRange {}; + + bool m_isNegative {}; + float m_voltage {}; + float m_current {}; + float m_power {}; + float m_powerFactor {}; + float m_frequency {}; + float m_positiveEnergy {}; + float m_negativeEnergy {}; +}; diff --git a/lib/SerialModbusRTU/library.json b/lib/SerialModbusRTU/library.json new file mode 100644 index 00000000..5c23fa28 --- /dev/null +++ b/lib/SerialModbusRTU/library.json @@ -0,0 +1,13 @@ +{ + "name": "SerialModbusRTU", + "keywords": "serial, modbus, RTU", + "description": "Serial Modus RTU protocol", + "authors": { + "name": "Benoit Leforestier" + }, + "version": "0.1.0", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} \ No newline at end of file diff --git a/lib/SerialModbusRTU/src/SerialModbusRTU.cpp b/lib/SerialModbusRTU/src/SerialModbusRTU.cpp new file mode 100644 index 00000000..a9a36b70 --- /dev/null +++ b/lib/SerialModbusRTU/src/SerialModbusRTU.cpp @@ -0,0 +1,248 @@ +#include "SerialModbusRTU.h" + +#include + +namespace { +constexpr size_t MODBUS_ADU = 256; + +// Function codes +constexpr uint8_t FC_READ_COILS = 0x01; // Read Coils (Output) Status +constexpr uint8_t FC_READ_INPUT_STAT = 0x02; // Not implemented. Read Input Status (Discrete Inputs) +constexpr uint8_t FC_READ_REGS = 0x03; // Read Holding Registers +constexpr uint8_t FC_READ_INPUT_REGS = 0x04; // Not implemented. Read Input Registers +constexpr uint8_t FC_WRITE_COIL = 0x05; // Write Single Coil (Output) +constexpr uint8_t FC_WRITE_REG = 0x06; // Not implemented. Preset Single Register +constexpr uint8_t FC_DIAGNOSTICS = 0x08; // Not implemented. Diagnostics (Serial Line only) +constexpr uint8_t FC_WRITE_COILS = 0x0F; // Not implemented. Write Multiple Coils (Outputs) +constexpr uint8_t FC_WRITE_REGS = 0x10; // Write block of contiguous registers +constexpr uint8_t FC_READ_FILE_REC = 0x14; // Not implemented. Read File Record +constexpr uint8_t FC_WRITE_FILE_REC = 0x15; // Not implemented. Write File Record +constexpr uint8_t FC_MASKWRITE_REG = 0x16; // Not implemented. Mask Write Register +constexpr uint8_t FC_READWRITE_REGS = 0x17; // Not implemented. Read/Write Multiple registers + +// Code from https://github.com/LacobusVentura/MODBUS-CRC16 +// Copyright (c) 2019 Tiago Ventura +uint16_t computeModbusCRC(const uint8_t* buf, size_t len) +{ + constexpr uint16_t table[256] = { + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, + 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, + 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, + 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, + 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, + 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, + 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, + 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, + 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, + 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, + 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, + 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, + 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, + 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, + 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, + 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, + 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, + 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 + }; + + uint8_t xor8 = 0; + uint16_t crc16 = 0xFFFF; + + while (len--) { + xor8 = (*buf++) ^ crc16; + crc16 >>= 8; + crc16 ^= table[xor8]; + } + + return crc16; +} + +std::vector getTxBuffer(uint8_t address, uint8_t functionCode, size_t len) +{ + std::vector buffer; + buffer.reserve(len + 2); + + buffer.push_back(address); + buffer.push_back(functionCode); + + return buffer; +} +} // namespace + +SerialModbusRTU::SerialModbusRTU(uint8_t uart_nr) + : m_serial(uart_nr) + , m_requestTimeout(100) + , m_requestStartTime(0) +{ +} + +uint32_t SerialModbusRTU::baudRate() const +{ + return m_serial.baudRate(); +} + +void SerialModbusRTU::begin(uint32_t baud, uint32_t config, int8_t rxPin, int8_t txPin) +{ + m_serial.onReceive(std::bind(&SerialModbusRTU::onReceiveCb, this), true); + m_serial.setRxTimeout(3); + m_serial.setRxBufferSize(MODBUS_ADU); + m_serial.setTxBufferSize(MODBUS_ADU); + m_serial.begin(baud, config, rxPin, txPin); +} + +void SerialModbusRTU::end() +{ + m_rxReady = false; + m_requestStartTime = 0; + m_serial.end(); +} + +void SerialModbusRTU::setResponseCallback(cbResponse cb) +{ + m_callback = cb; +} + +void SerialModbusRTU::setRequestTimeout(std::chrono::milliseconds timeout) +{ + m_requestTimeout = timeout; +} + +bool SerialModbusRTU::setPins(int8_t rxPin, int8_t txPin, int8_t ctsPin, int8_t rtsPin) +{ + return m_serial.setPins(rxPin, txPin, ctsPin, rtsPin); +} + +bool SerialModbusRTU::isDeviceBusy(uint8_t /*address*/) const +{ + return (m_requestStartTime > 0); +} + +void SerialModbusRTU::readCoils(uint8_t address, uint16_t startCoil, uint16_t number) +{ + std::vector buffer = getTxBuffer(address, FC_READ_COILS, 4); + + buffer.push_back(startCoil >> 8); + buffer.push_back(startCoil & 0xFF); + buffer.push_back(number >> 8); + buffer.push_back(number & 0xFF); + + sendQuery(buffer.data(), buffer.size()); +} + +void SerialModbusRTU::writeSingleCoil(uint8_t address, uint16_t coil, bool enabled) +{ + std::vector buffer = getTxBuffer(address, FC_WRITE_COIL, 4); + + buffer.push_back(coil >> 8); + buffer.push_back(coil & 0xFF); + buffer.push_back(enabled ? 0xFF : 0x00); + buffer.push_back(0x00); + + sendQuery(buffer.data(), buffer.size()); +} + +void SerialModbusRTU::readHoldingRegisters(uint8_t address, uint16_t startReg, uint16_t number) +{ + std::vector buffer = getTxBuffer(address, FC_READ_REGS, 4); + + buffer.push_back(startReg >> 8); + buffer.push_back(startReg & 0xFF); + buffer.push_back(number >> 8); + buffer.push_back(number & 0xFF); + + sendQuery(buffer.data(), buffer.size()); +} + +void SerialModbusRTU::writeMultipleRegisters( + uint8_t address, + uint16_t startReg, + uint16_t number, + const void* data, + uint8_t len) +{ + std::vector buffer = getTxBuffer(address, FC_WRITE_REGS, len + 5); + + buffer.push_back(startReg >> 8); + buffer.push_back(startReg & 0xFF); + buffer.push_back(number >> 8); + buffer.push_back(number & 0xFF); + buffer.push_back(len); + buffer.insert(buffer.end(), reinterpret_cast(data), reinterpret_cast(data) + len); + + sendQuery(buffer.data(), buffer.size()); +} + +void SerialModbusRTU::onReceiveCb() +{ + m_rxReady = true; +} + +void SerialModbusRTU::sendQuery(const uint8_t* data, size_t len) +{ + if (m_requestStartTime > 0) { + // Device busy + if (m_callback) + m_callback(ec::deviceBusy, data[0], data[1], nullptr, 0); + + return; + } + + uint16_t crc16 = computeModbusCRC(data, len); + + m_serial.write(data, len); + m_serial.write(reinterpret_cast(&crc16), sizeof(crc16)); + + m_requestStartTime = millis(); +} + +void SerialModbusRTU::loop() +{ + if (!m_rxReady.exchange(false)) { + if (m_requestStartTime > 0 && (millis() - m_requestStartTime) > m_requestTimeout.count()) { + m_requestStartTime = 0; + if (m_callback) + m_callback(ec::requestTimeOut, 0, 0, nullptr, 0); + } + + return; + } + + m_requestStartTime = 0; + + // Read RX buffer + int rxLen = m_serial.available(); + + std::vector rxBuffer(rxLen); + size_t readLen = m_serial.read(rxBuffer.data(), rxBuffer.size()); + + if (readLen != rxBuffer.size() || readLen < 5) { + if (m_callback) + m_callback(ec::invalidFrameSize, 0, 0, nullptr, 0); + return; + } + + // Verify CRC + uint16_t crc16 = rxBuffer[rxBuffer.size() - 1] << 8; + crc16 |= rxBuffer[rxBuffer.size() - 2]; + + if (crc16 != computeModbusCRC(rxBuffer.data(), rxBuffer.size() - 2)) { + if (m_callback) + m_callback(ec::invalidCRC, 0, 0, nullptr, 0); + return; + } + + uint8_t address = rxBuffer[0]; + uint8_t functionCode = rxBuffer[1] & 0x7F; + + // Check error response + if ((rxBuffer[1] & 0x80) == 0x80) { + if (m_callback) + m_callback(static_cast(rxBuffer[2]), address, functionCode, nullptr, 0); + return; + } + + if (m_callback) + m_callback(ec::noError, address, functionCode, rxBuffer.data() + 2, rxBuffer.size() - 4); +} \ No newline at end of file diff --git a/lib/SerialModbusRTU/src/SerialModbusRTU.h b/lib/SerialModbusRTU/src/SerialModbusRTU.h new file mode 100644 index 00000000..74912cb7 --- /dev/null +++ b/lib/SerialModbusRTU/src/SerialModbusRTU.h @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include + +#include + +class SerialModbusRTU +{ +public: + enum class ec : uint8_t + { + noError = 0x00, + // MODBUS errors + illegalFunction = 0x01, + illegalDataAddress = 0x02, + illegalDataValue = 0x03, + serverDeviceFailure = 0x04, + acknowledge = 0x05, + serverDeviceBusy = 0x06, + memoryParityError = 0x08, + gatewayPathUnavailable = 0x0A, + gatewayTargetDeviceFailed = 0x0B, + // Class errors + invalidFrameSize = 0x20, + invalidCRC = 0x21, + requestTimeOut = 0x22, + deviceBusy = 0x23 + }; + + typedef std::function< + void(ec exceptionCode, uint8_t deviceAddress, uint8_t functionCode, const void* data, size_t len)> + cbResponse; + + SerialModbusRTU(uint8_t uart_nr); + ~SerialModbusRTU() = default; + + uint32_t baudRate() const; + + void begin(uint32_t baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1); + void end(); + void loop(); + + // setResponseCallback and setRequestTimeout shall be called before begin() + void setResponseCallback(cbResponse cb); + void setRequestTimeout(std::chrono::milliseconds timeout); + + // SetPins shall be called after begin() + bool setPins(int8_t rxPin, int8_t txPin, int8_t ctsPin = -1, int8_t rtsPin = -1); + + [[nodiscard]] bool isDeviceBusy(uint8_t address) const; + + void readCoils(uint8_t address, uint16_t startCoil, uint16_t number); + void writeSingleCoil(uint8_t address, uint16_t coil, bool enabled); + + void readHoldingRegisters(uint8_t address, uint16_t startReg, uint16_t number); + void writeMultipleRegisters(uint8_t address, uint16_t startReg, uint16_t number, const void* data, uint8_t len); + +private: + void onReceiveCb(); + void sendQuery(const uint8_t* data, size_t len); + +private: + mutable HardwareSerial m_serial; + std::atomic m_rxReady; + cbResponse m_callback; + std::chrono::milliseconds m_requestTimeout; + time_t m_requestStartTime; +}; diff --git a/platformio.ini b/platformio.ini index 48d9cf97..d3b47ce5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,11 +37,11 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESP Async WebServer @ 2.9.0 + mathieucarbou/ESP Async WebServer @ 2.9.3 bblanchon/ArduinoJson @ ^7.0.4 https://github.com/bertmelis/espMqttClient.git#v1.6.0 nrf24/RF24 @ ^1.4.8 - olikraus/U8g2 @ ^2.35.15 + olikraus/U8g2 @ ^2.35.17 buelowp/sunset @ ^1.1.7 https://github.com/arkhipenko/TaskScheduler#testing @@ -123,6 +123,15 @@ build_flags = ${env.build_flags} -DHOYMILES_PIN_CE=4 -DHOYMILES_PIN_CS=5 +[env:generic_cmt] +board = esp32dev +build_flags = ${env.build_flags} + -DCMT_CLK=18 + -DCMT_CS=4 + -DCMT_FCS=5 + -DCMT_SDIO=23 + -DCMT_GPIO2=19 + -DCMT_GPIO3=14 [env:olimex_esp32_poe] ; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware diff --git a/platformio_override.ini b/platformio_override.ini index e5d03411..a2c202d9 100644 --- a/platformio_override.ini +++ b/platformio_override.ini @@ -32,3 +32,15 @@ ; -DHOYMILES_PIN_CS=6 ;monitor_port = /dev/ttyACM0 ;upload_port = /dev/ttyACM0 + +[env:esp32_cmt_modbus] +board = esp32dev +build_flags = ${env.build_flags} + -DCMT_CLK=18 + -DCMT_CS=4 + -DCMT_FCS=5 + -DCMT_SDIO=23 + -DCMT_GPIO2=19 + -DCMT_GPIO3=14 + -DSERIAL_MODBUS_TX=17 + -DSERIAL_MODBUS_RX=16 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 8e803074..b0a24944 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -139,6 +139,18 @@ bool ConfigurationClass::write() } } + JsonObject serialModbus = doc["serial_modbus"].to(); + serialModbus["baudrate"] = config.SerialModbus.BaudRate; + serialModbus["poll_interval"] = config.SerialModbus.PollInterval; + + JsonObject powerMeter = doc["power_meter"].to(); + JsonArray channel = powerMeter["channel"].to(); + for (uint8_t c = 0; c < PWRMTR_MAX_CHAN_COUNT; c++) { + JsonObject chanData = channel.add(); + chanData["invert_direction"] = config.PowerMeter.channel[c].InvertDirection; + chanData["negative_power"] = config.PowerMeter.channel[c].NegativePower; + } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; } @@ -312,6 +324,17 @@ bool ConfigurationClass::read() } } + JsonObject serialModbus = doc["serial_modbus"]; + config.SerialModbus.BaudRate = serialModbus["baudrate"] | SERIAL_MODBUS_BAUDRATE; + config.SerialModbus.PollInterval = serialModbus["poll_interval"] | SERIAL_MODBUS_POLL_INTERVAL; + + JsonObject powerMeter = doc["power_meter"]; + JsonArray channel = powerMeter["channel"]; + for (uint8_t c = 0; c < PWRMTR_MAX_CHAN_COUNT; c++) { + config.PowerMeter.channel[c].InvertDirection = channel[c]["invert_direction"] | false; + config.PowerMeter.channel[c].NegativePower = channel[c]["negative_power"] | false; + } + f.close(); return true; } diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 4433c434..e219dacb 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -4,6 +4,7 @@ */ #include "Display_Graphic.h" #include "Datastore.h" +#include "JsyMk.h" #include #include #include @@ -41,6 +42,15 @@ static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %. static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" }; +static const char* const i18n_powermeter_power_w[] = { "%c %.0f W", "%c %.0f W", "%c %.0f W" }; +static const char* const i18n_powermeter_power_kw[] = { "%c %.1f kW", "%c %.1f kW", "%c %.1f kW" }; + +static const char* const i18n_pm_positive_today_wh[] = { "In: %4.0f Wh", "In: %4.0f Wh", "In: %4.0f Wh" }; +static const char* const i18n_pm_positive_today_kwh[] = { "In: %.1f kWh", "In: %.1f kWh", "In: %.1f kWh" }; + +static const char* const i18n_pm_negative_today_wh[] = { "Out: %4.0f Wh", "Out: %4.0f Wh", "Out: %4.0f Wh" }; +static const char* const i18n_pm_negative_today_kwh[] = { "Out: %.1f kWh", "Out: %.1f kWh", "Out: %.1f kWh" }; + DisplayGraphicClass::DisplayGraphicClass() : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&DisplayGraphicClass::loop, this)) { @@ -202,8 +212,21 @@ void DisplayGraphicClass::loop() bool displayPowerSave = false; bool showText = true; + bool displayPowerMeter = _mExtra % (10 * 2) < 10 && JsyMk.isInitialised(); // Every 10 seconds, swap screen + + if (displayPowerMeter) { + const float watts = std::fabs(JsyMk.getFieldValue(0, JsyMkClass::Field_t::POWER)); + const char direction = (JsyMk.getFieldValue(0, JsyMkClass::Field_t::NEGATIVE) > 0) ? 'O' : 'I'; + + if (watts > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_kw[_display_language], direction, watts / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_powermeter_power_w[_display_language], direction, watts); + } + printText(_fmtText, 0); + } //=====> Actual Production ========== - if (Datastore.getIsAtLeastOneReachable()) { + else if (Datastore.getIsAtLeastOneReachable()) { displayPowerSave = false; if (_isLarge) { uint8_t screenSaverOffsetX = enableScreensaver ? (_mExtra % 7) : 0; @@ -246,20 +269,40 @@ void DisplayGraphicClass::loop() //<======================= if (showText) { - // Daily production - float wattsToday = Datastore.getTotalAcYieldDayEnabled(); - if (wattsToday >= 10000) { - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); - } else { - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); - } - printText(_fmtText, 1); + if (displayPowerMeter) { + // Daily Input + float wattsInput = JsyMk.getFieldValue(0, JsyMkClass::Field_t::TODAY_POSITIVE_ENERGY); + if (wattsInput > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_pm_positive_today_kwh[_display_language], wattsInput / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_pm_positive_today_wh[_display_language], wattsInput); + } + printText(_fmtText, 1); - // Total production - const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); - auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; - snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); - printText(_fmtText, 2); + // Daily Output + float wattsOutput = JsyMk.getFieldValue(0, JsyMkClass::Field_t::TODAY_POSITIVE_ENERGY); + if (wattsOutput > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_pm_negative_today_kwh[_display_language], wattsOutput / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_pm_negative_today_wh[_display_language], wattsOutput); + } + printText(_fmtText, 2); + } else { + // Daily production + float wattsToday = Datastore.getTotalAcYieldDayEnabled(); + if (wattsToday >= 10000) { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); + } + printText(_fmtText, 1); + + // Total production + const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); + auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; + snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); + printText(_fmtText, 2); + } //=====> IP or Date-Time ======== // Change every 3 seconds diff --git a/src/JsyMk.cpp b/src/JsyMk.cpp new file mode 100644 index 00000000..11635611 --- /dev/null +++ b/src/JsyMk.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "JsyMk.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "PinMapping.h" + +JsyMkClass JsyMk; + +namespace { +// HA status classes +constexpr std::string_view scMeasurement("measurement"); +constexpr std::string_view scTotalIncreasing("total_increasing"); + +// Name, unit, digits, HA device class, HA status class +constexpr std::array, 16> fieldInfos = { // + { { "Address", {}, {}, {}, {} }, + { "Manufacturer", {}, {}, {}, {} }, + { "Model", {}, {}, {}, {} }, + { "Version", {}, {}, {}, {} }, + { "Voltage Range", "V", 0, "voltage", scMeasurement }, + { "Current Range", "A", 0, "current", scMeasurement }, + { "Voltage", "V", 2, "voltage", scMeasurement }, + { "Current", "A", 2, "current", scMeasurement }, + { "Power", "W", 0, "power", scMeasurement }, + { "Power Factor", "%", 2, "power_factor", scMeasurement }, + { "Frequency", "Hz", 2, "frequency", scMeasurement }, + { "Negative", {}, {}, {}, scMeasurement }, + { "Positive Energy", "kWh", 2, "energy", scTotalIncreasing }, + { "Negative Energy", "kWh", 2, "energy", scTotalIncreasing }, + { "Today Positive Energy", "kWh", 2, "energy", scTotalIncreasing }, + { "Today Negative Energy", "kWh", 2, "energy", scTotalIncreasing } } +}; +} +JsyMkClass::JsyMkClass() + : _jsymk(1) + , _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&JsyMkClass::loop, this)) +{ +} + +void JsyMkClass::init(Scheduler& scheduler) +{ + const CONFIG_T& config = Configuration.get(); + const PinMapping_t& pin = PinMapping.get(); + + if (PinMapping.isValidSerialModbusConfig()) { + // Initialize inverter communication + _jsymk.setUpdatedCallback( + [&] { + _lastUpdate = millis(); + }); + _jsymk.setErrorCallback([&](SerialModbusRTU::ec error_code) { + MessageOutput.printf("JSY-MK error %d\n", static_cast(error_code)); + }); + + _jsymk.setUpdatePeriod(std::chrono::seconds(config.SerialModbus.PollInterval)); + _jsymk.begin(config.SerialModbus.BaudRate, SERIAL_8N1, pin.serial_modbus_rx, pin.serial_modbus_tx); + + scheduler.addTask(_loopTask); + _loopTask.enable(); + } +} + +void JsyMkClass::loop() +{ + _jsymk.loop(); + + if (!_initialised && _jsymk.getModel() != 0) { + _initialised = true; + + MessageOutput.println("JSY-MK-xxxT Initialized"); + MessageOutput.printf("Model: %s %s\n", _jsymk.getModelAsString().c_str(), _jsymk.getVersionAsString().c_str()); + MessageOutput.printf("Ranges: %dV %dA\n", _jsymk.getVoltageRange(), _jsymk.getCurrentRange()); + } else { + // Check current time + time_t now = time(nullptr); + const auto* lt = localtime(&now); + + if (lt->tm_hour == 0 && lt->tm_min == 0 && lt->tm_sec <= Configuration.get().SerialModbus.PollInterval) { + _todayPositiveRef = 0; + _todayNegativeRef = 0; + } + + if (_todayPositiveRef == 0) { + _todayPositiveRef = _jsymk.getPositiveEnergy(); + } + if (_todayNegativeRef == 0) { + _todayNegativeRef = _jsymk.getNegativeEnergy(); + } + } +} + +bool JsyMkClass::isInitialised() const +{ + return _initialised; +} + +uint32_t JsyMkClass::getLastUpdate() const +{ + return _lastUpdate; +} + +uint32_t JsyMkClass::getPollInterval() const +{ + return std::chrono::duration_cast(_jsymk.getUpdatePeriod()).count(); +} + +void JsyMkClass::setPollInterval(const uint32_t interval) +{ + _jsymk.setUpdatePeriod(std::chrono::seconds(interval)); +} + +size_t JsyMkClass::getChannelNumber() const +{ + return _jsymk.getChannelNumber(); +} + +String JsyMkClass::getFieldName(size_t /*channel*/, Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + const auto& fieldName = std::get<0>(fieldInfos[static_cast(fieldId)]); + return String(fieldName.data(), fieldName.size()); +} + +String JsyMkClass::getFieldString(size_t channel, Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + switch (fieldId) { + case Field_t::ADDRESS: + return String(_jsymk.getAddress()); + + case Field_t::MANUFACTURER: + return String(_jsymk.getManufacturer().c_str()); + + case Field_t::MODEL: + return String(_jsymk.getModelAsString().c_str()); + + case Field_t::VERSION: + return String(_jsymk.getVersionAsString().c_str()); + + default: + break; + } + + return String(getFieldValue(channel, fieldId), getFieldDigits(fieldId)); +} + +float JsyMkClass::getFieldValue(size_t channel, Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return 0; + + const CONFIG_T& config = Configuration.get(); + auto isLogicalNegative = [&]() -> bool { + return config.PowerMeter.channel[channel].InvertDirection ? !_jsymk.isNegative() : _jsymk.isNegative(); + }; + + switch (fieldId) { + case Field_t::VOLTAGE_RANGE: + return static_cast(_jsymk.getVoltageRange()); + + case Field_t::CURRENT_RANGE: + return static_cast(_jsymk.getCurrentRange()); + + case Field_t::VOLTAGE: + return _jsymk.getVoltage(); + + case Field_t::CURRENT: + return _jsymk.getCurrent(); + + case Field_t::POWER: + if (config.PowerMeter.channel[channel].NegativePower) { + return (isLogicalNegative() ? -_jsymk.getPower() : _jsymk.getPower()); + } + return _jsymk.getPower(); + + case Field_t::POWER_FACTOR: + return _jsymk.getPowerFactor(); + + case Field_t::FREQUENCY: + return _jsymk.getFrequency(); + + case Field_t::NEGATIVE: + return isLogicalNegative() ? 1 : 0; + + case Field_t::TOTAL_POSITIVE_ENERGY: + return (config.PowerMeter.channel[channel].InvertDirection ? _jsymk.getNegativeEnergy() : _jsymk.getPositiveEnergy()); + + case Field_t::TOTAL_NEGATIVE_ENERGY: + return (config.PowerMeter.channel[channel].InvertDirection ? _jsymk.getPositiveEnergy() : _jsymk.getNegativeEnergy()); + + case Field_t::TODAY_POSITIVE_ENERGY: + return (config.PowerMeter.channel[channel].InvertDirection ? _jsymk.getNegativeEnergy() - _todayNegativeRef : _jsymk.getPositiveEnergy() - _todayPositiveRef); + + case Field_t::TODAY_NEGATIVE_ENERGY: + return (config.PowerMeter.channel[channel].InvertDirection ? _jsymk.getPositiveEnergy() - _todayPositiveRef : _jsymk.getNegativeEnergy() - _todayNegativeRef); + + default: + break; + } + + return 0; +} + +String JsyMkClass::getFieldUnit(Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + const auto& fielUnit = std::get<1>(fieldInfos[static_cast(fieldId)]); + return String(fielUnit.data(), fielUnit.size()); +} + +size_t JsyMkClass::getFieldDigits(Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + return std::get<2>(fieldInfos[static_cast(fieldId)]); +} + +String JsyMkClass::getFieldDeviceClass(Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + const auto& fieldDeviceClass = std::get<3>(fieldInfos[static_cast(fieldId)]); + return String(fieldDeviceClass.data(), fieldDeviceClass.size()); +} + +String JsyMkClass::getFieldStatusClass(Field_t fieldId) const +{ + if (static_cast(fieldId) >= fieldInfos.size()) + return {}; + + const auto& fieldStatusClass = std::get<4>(fieldInfos[static_cast(fieldId)]); + return String(fieldStatusClass.data(), fieldStatusClass.size()); +} + +void JsyMkClass::reset() +{ + _jsymk.resetEnergy(); +} diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index a2d998d1..3f215648 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -96,6 +96,27 @@ void MqttHandleHassClass::publishConfig() yield(); } + + if (!JsyMk.isInitialised()) + return; + + // Loop all power meter channels + // publishDtuButton("Reset energy", "", "config", "reset", "cmd/power_meter_reset", "1"); + for (size_t i = 0; i < JsyMk.getChannelNumber(); ++i) { + for (auto field : { + JsyMkClass::Field_t::VOLTAGE, + JsyMkClass::Field_t::CURRENT, + JsyMkClass::Field_t::POWER, + JsyMkClass::Field_t::POWER_FACTOR, + JsyMkClass::Field_t::FREQUENCY, + JsyMkClass::Field_t::NEGATIVE, + JsyMkClass::Field_t::TOTAL_POSITIVE_ENERGY, + JsyMkClass::Field_t::TOTAL_NEGATIVE_ENERGY }) { + publishPowerMeterField(i, field); + } + + yield(); + } } void MqttHandleHassClass::publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear) @@ -371,6 +392,75 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d publish(configTopic, buffer); } +void MqttHandleHassClass::publishPowerMeterField(size_t channel, JsyMkClass::Field_t fieldId, const bool clear) +{ + String modelUID = JsyMk.getFieldString(channel, JsyMkClass::Field_t::MODEL) + "-" + String(channel); + String name = JsyMk.getFieldName(channel, fieldId); + + String sensorId = name; + sensorId.replace(" ", "_"); + sensorId.toLowerCase(); + + String configTopic = "sensor/dtu_" + modelUID + + "/" + sensorId + + "/config"; + + if (!clear) { + JsonDocument root; + createPowerMeterInfo(root, channel, fieldId); + + root["name"] = name; + root["stat_t"] = MqttSettings.getPrefix() + MqttHandlePowerMeterClass::getTopic(channel, fieldId); + root["uniq_id"] = modelUID + "_" + sensorId; + + String unit_of_measure = JsyMk.getFieldUnit(fieldId); + if (!unit_of_measure.isEmpty()) { + root["unit_of_meas"] = unit_of_measure; + } + + if (Configuration.get().Mqtt.Hass.Expire) { + root["exp_aft"] = std::max(JsyMk.getPollInterval(), Configuration.get().Mqtt.PublishInterval) * 2; + } + + String deviceClass = JsyMk.getFieldDeviceClass(fieldId); + if (!deviceClass.isEmpty()) { + root["dev_cla"] = deviceClass; + } + + String statusClass = JsyMk.getFieldStatusClass(fieldId); + if (!statusClass.isEmpty()) { + root["stat_cla"] = statusClass; + } + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); + } else { + publish(configTopic, {}); + } +} + +void MqttHandleHassClass::createPowerMeterInfo(JsonDocument& root, size_t channel, JsyMkClass::Field_t fieldId) +{ + auto object = root["dev"].to(); + + String swVersion = JsyMk.getFieldString(channel, JsyMkClass::Field_t::VOLTAGE_RANGE) + JsyMk.getFieldUnit(JsyMkClass::Field_t::VOLTAGE_RANGE) + + " " + JsyMk.getFieldString(channel, JsyMkClass::Field_t::CURRENT_RANGE) + JsyMk.getFieldUnit(JsyMkClass::Field_t::CURRENT_RANGE); + + object["name"] = JsyMk.getFieldString(channel, JsyMkClass::Field_t::MODEL) + " Channel " + String(channel); + object["identifiers"] = JsyMk.getFieldString(channel, JsyMkClass::Field_t::MODEL) + "-" + String(channel); + object["configuration_url"] = getDtuUrl(); + object["manufacturer"] = JsyMk.getFieldString(channel, JsyMkClass::Field_t::MANUFACTURER); + object["model"] = JsyMk.getFieldString(channel, JsyMkClass::Field_t::MODEL); + object["sw_version"] = swVersion; + object["hw_version"] = JsyMk.getFieldString(channel, JsyMkClass::Field_t::VERSION); + object["via_device"] = getDtuUniqueId(); +} + void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr inv) { createDeviceInfo( diff --git a/src/MqttHandlePowerMeter.cpp b/src/MqttHandlePowerMeter.cpp new file mode 100644 index 00000000..800dcf06 --- /dev/null +++ b/src/MqttHandlePowerMeter.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "MqttHandlePowerMeter.h" +#include "MessageOutput.h" +#include "MqttSettings.h" + +#define TOPIC_SUB_RESET "power_meter_reset" + +MqttHandlePowerMeterClass MqttHandlePowerMeter; + +namespace { +} + +MqttHandlePowerMeterClass::MqttHandlePowerMeterClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandlePowerMeterClass::loop, this)) +{ +} + +void MqttHandlePowerMeterClass::init(Scheduler& scheduler) +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + String topic = MqttSettings.getPrefix(); + topic.concat("+/cmd/" TOPIC_SUB_RESET); + MqttSettings.subscribe(topic, 0, std::bind(&MqttHandlePowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + + scheduler.addTask(_loopTask); + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + _loopTask.enable(); +} + +void MqttHandlePowerMeterClass::loop() +{ + _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); + + if (!MqttSettings.getConnected() || !JsyMk.isInitialised() || JsyMk.getLastUpdate() == _lastUpdate) { + _loopTask.forceNextIteration(); + return; + } + + _lastUpdate = JsyMk.getLastUpdate(); + + // Loop all channels + for (size_t i = 0; i < JsyMk.getChannelNumber(); ++i) { + publishField(i, Field_t::VOLTAGE); + publishField(i, Field_t::CURRENT); + publishField(i, Field_t::POWER); + publishField(i, Field_t::POWER_FACTOR); + publishField(i, Field_t::FREQUENCY); + publishField(i, Field_t::NEGATIVE); + publishField(i, Field_t::TOTAL_POSITIVE_ENERGY); + publishField(i, Field_t::TOTAL_NEGATIVE_ENERGY); + + yield(); + } +} + +void MqttHandlePowerMeterClass::publishField(size_t channel, const Field_t fieldId) +{ + const String topic = getTopic(channel, fieldId); + if (topic.isEmpty()) + return; + + MqttSettings.publish(topic, JsyMk.getFieldString(channel, fieldId)); +} + +String MqttHandlePowerMeterClass::getTopic(size_t channel, const Field_t fieldId) +{ + String model = JsyMk.getFieldString(channel, Field_t::MODEL); + String name = JsyMk.getFieldName(channel, fieldId); + + if (model.isEmpty() || name.isEmpty()) + return {}; + + String sensorId = name; + sensorId.replace(" ", "_"); + sensorId.toLowerCase(); + + return model + "/" + String(channel) + "/" + sensorId; +} + +void MqttHandlePowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* /*payload*/, const size_t /*len*/, const size_t /*index*/, const size_t /*total*/) +{ + const CONFIG_T& config = Configuration.get(); + + std::string_view tokenTopic = topic; + tokenTopic.remove_prefix(strlen(config.Mqtt.Topic)); + + auto findNextToken = [&]() -> std::string_view { + std::string_view result; + + size_t pos = tokenTopic.find('/'); + if (pos == std::string_view::npos) { + result = tokenTopic; + tokenTopic = {}; + } else { + result = tokenTopic.substr(pos); + tokenTopic.remove_prefix(pos + 1); + } + + return result; + }; + + std::string_view model = findNextToken(); + std::string_view subtopic = findNextToken(); + std::string_view setting = findNextToken(); + + if (model.empty() || subtopic.empty() || setting.empty() || subtopic != "cmd") { + return; + } + + if (setting == TOPIC_SUB_RESET) { + MessageOutput.println(TOPIC_SUB_RESET); + if (JsyMk.isInitialised()) + JsyMk.reset(); + } +} diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 74f28285..e37f0dc1 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -84,6 +84,14 @@ #define CMT_SDIO -1 #endif +#ifndef SERIAL_MODBUS_TX +#define SERIAL_MODBUS_TX -1 +#endif + +#ifndef SERIAL_MODBUS_RX +#define SERIAL_MODBUS_RX -1 +#endif + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -124,6 +132,9 @@ PinMappingClass::PinMappingClass() _pinMapping.led[0] = LED0; _pinMapping.led[1] = LED1; + + _pinMapping.serial_modbus_tx = SERIAL_MODBUS_TX; + _pinMapping.serial_modbus_rx = SERIAL_MODBUS_RX; } PinMapping_t& PinMappingClass::get() @@ -186,6 +197,9 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.led[0] = doc[i]["led"]["led0"] | LED0; _pinMapping.led[1] = doc[i]["led"]["led1"] | LED1; + _pinMapping.serial_modbus_tx = doc[i]["serial_modbus"]["tx"] | SERIAL_MODBUS_TX; + _pinMapping.serial_modbus_rx = doc[i]["serial_modbus"]["rx"] | SERIAL_MODBUS_RX; + return true; } } @@ -215,3 +229,8 @@ bool PinMappingClass::isValidEthConfig() const { return _pinMapping.eth_enabled; } + +bool PinMappingClass::isValidSerialModbusConfig() const +{ + return _pinMapping.serial_modbus_tx >= 0 && _pinMapping.serial_modbus_rx >= 0; +} diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index a67be42f..759b6b24 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) requestFile = name; } else { request->send(404); + return; } } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 078d5b4a..3b821618 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -86,6 +86,10 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) led["brightness"] = config.Led_Single[i].Brightness; } + auto serialModbusPinObj = curPin["serial_modbus"].to(); + serialModbusPinObj["tx"] = pin.serial_modbus_tx; + serialModbusPinObj["rx"] = pin.serial_modbus_rx; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index a2893c82..580e7885 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_sysstatus.h" #include "Configuration.h" +#include "JsyMk.h" #include "NetworkSettings.h" #include "PinMapping.h" #include "WebApi.h" @@ -76,5 +77,11 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["cmt_configured"] = PinMapping.isValidCmt2300Config(); root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); + root["modbus_configured"] = PinMapping.isValidSerialModbusConfig(); + root["jsy_connected"] = JsyMk.isInitialised(); + root["jsy_variant"] = JsyMk.getFieldString(0, JsyMkClass::Field_t::MODEL) + " " + JsyMk.getFieldString(0, JsyMkClass::Field_t::VERSION) + + JsyMk.getFieldString(0, JsyMkClass::Field_t::VOLTAGE_RANGE) + JsyMk.getFieldUnit(JsyMkClass::Field_t::VOLTAGE_RANGE) + + " " + JsyMk.getFieldString(0, JsyMkClass::Field_t::CURRENT_RANGE) + JsyMk.getFieldUnit(JsyMkClass::Field_t::CURRENT_RANGE); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index eb1a83bf..eac4e144 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_ws_live.h" #include "Datastore.h" +#include "JsyMk.h" #include "MessageOutput.h" #include "Utils.h" #include "WebApi.h" @@ -83,6 +84,9 @@ void WebApiWsLiveClass::sendDataTaskCb() generateInverterCommonJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv); + auto powerMeter = var["power_meter"].to(); + generatePowerMeterJsonResponse(powerMeter); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { continue; } @@ -247,6 +251,9 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } } + auto powerMeter = root["power_meter"].to(); + generatePowerMeterJsonResponse(powerMeter); + generateCommonJsonResponse(root); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -259,3 +266,55 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) WebApi.sendTooManyRequests(request); } } + +void WebApiWsLiveClass::generatePowerMeterJsonResponse(JsonObject& root) +{ + if (!JsyMk.isInitialised()) + return; + + root["data_age"] = (millis() - JsyMk.getLastUpdate()) / 1000; + + for (auto field : { + JsyMkClass::Field_t::ADDRESS, + JsyMkClass::Field_t::MANUFACTURER, + JsyMkClass::Field_t::MODEL, + JsyMkClass::Field_t::VERSION }) { + + String name = JsyMk.getFieldName(0, field); + root[name]["v"] = JsyMk.getFieldString(0, field); + root[name]["u"] = JsyMk.getFieldUnit(field); + root[name]["d"] = JsyMk.getFieldDigits(field); + } + + for (auto field : { + JsyMkClass::Field_t::VOLTAGE_RANGE, + JsyMkClass::Field_t::CURRENT_RANGE }) { + + String name = JsyMk.getFieldName(0, field); + root[name]["v"] = JsyMk.getFieldValue(0, field); + root[name]["u"] = JsyMk.getFieldUnit(field); + root[name]["d"] = JsyMk.getFieldDigits(field); + } + + for (size_t i = 0; i < JsyMk.getChannelNumber(); ++i) { + for (auto field : { + JsyMkClass::Field_t::VOLTAGE, + JsyMkClass::Field_t::CURRENT, + JsyMkClass::Field_t::POWER, + JsyMkClass::Field_t::POWER_FACTOR, + JsyMkClass::Field_t::FREQUENCY, + JsyMkClass::Field_t::NEGATIVE, + JsyMkClass::Field_t::TODAY_POSITIVE_ENERGY, + JsyMkClass::Field_t::TODAY_NEGATIVE_ENERGY, + JsyMkClass::Field_t::TOTAL_POSITIVE_ENERGY, + JsyMkClass::Field_t::TOTAL_NEGATIVE_ENERGY }) { + + String channel(i); + String name = JsyMk.getFieldName(i, field); + + root[channel][name]["v"] = JsyMk.getFieldValue(i, field); + root[channel][name]["u"] = JsyMk.getFieldUnit(field); + root[channel][name]["d"] = JsyMk.getFieldDigits(field); + } + } +} diff --git a/src/main.cpp b/src/main.cpp index 433619e1..dabdc731 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,12 +6,14 @@ #include "Datastore.h" #include "Display_Graphic.h" #include "InverterSettings.h" +#include "JsyMk.h" #include "Led_Single.h" #include "MessageOutput.h" #include "MqttHandleDtu.h" #include "MqttHandleHass.h" #include "MqttHandleInverter.h" #include "MqttHandleInverterTotal.h" +#include "MqttHandlePowerMeter.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -107,6 +109,7 @@ void setup() MqttHandleDtu.init(scheduler); MqttHandleInverter.init(scheduler); MqttHandleInverterTotal.init(scheduler); + MqttHandlePowerMeter.init(scheduler); MqttHandleHass.init(scheduler); MessageOutput.println("done"); @@ -138,6 +141,11 @@ void setup() LedSingle.init(scheduler); MessageOutput.println("done"); + // Initialize JSY-MK-xxxT + MessageOutput.print("Initialize JSY-MK-xxxT... "); + JsyMk.init(scheduler); + MessageOutput.println("done"); + // Check for default DTU serial MessageOutput.print("Check for default DTU serial... "); if (config.Dtu.Serial == DTU_SERIAL) { diff --git a/webapp/package.json b/webapp/package.json index 86b30904..fbba65f1 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,9 +18,9 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.21", - "vue-i18n": "^9.12.0", - "vue-router": "^4.3.0" + "vue": "^3.4.25", + "vue-i18n": "^9.13.1", + "vue-router": "^4.3.2" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", @@ -33,16 +33,16 @@ "@vitejs/plugin-vue": "^5.0.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.0.0", - "eslint-plugin-vue": "^9.24.1", + "eslint": "^9.1.1", + "eslint-plugin-vue": "^9.25.0", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", "sass": "^1.75.0", - "terser": "^5.30.3", + "terser": "^5.30.4", "typescript": "^5.4.5", - "vite": "^5.2.8", + "vite": "^5.2.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.0", - "vue-tsc": "^2.0.13" + "vue-tsc": "^2.0.14" } } diff --git a/webapp/public/zones.json b/webapp/public/zones.json index 2d449fe2..ad90ea01 100644 --- a/webapp/public/zones.json +++ b/webapp/public/zones.json @@ -180,7 +180,7 @@ "America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24", "America/Santo_Domingo":"AST4", "America/Sao_Paulo":"<-03>3", -"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1", +"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0", "America/St_Barthelemy":"AST4", "America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0", @@ -200,7 +200,7 @@ "America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0", "America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0", "America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0", -"Antarctica/Casey":"<+11>-11", +"Antarctica/Casey":"<+08>-8", "Antarctica/Davis":"<+07>-7", "Antarctica/DumontDUrville":"<+10>-10", "Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3", @@ -210,10 +210,10 @@ "Antarctica/Rothera":"<-03>3", "Antarctica/Syowa":"<+03>-3", "Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3", -"Antarctica/Vostok":"<+06>-6", +"Antarctica/Vostok":"<+05>-5", "Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3", "Asia/Aden":"<+03>-3", -"Asia/Almaty":"<+06>-6", +"Asia/Almaty":"<+05>-5", "Asia/Amman":"<+03>-3", "Asia/Anadyr":"<+12>-12", "Asia/Aqtau":"<+05>-5", diff --git a/webapp/src/utils/authentication.ts b/webapp/src/utils/authentication.ts index e0e96b70..0f1debd0 100644 --- a/webapp/src/utils/authentication.ts +++ b/webapp/src/utils/authentication.ts @@ -65,7 +65,7 @@ export function login(username: string, password: string) { }); } -export function handleResponse(response: Response, emitter: Emitter>, router: Router) { +export function handleResponse(response: Response, emitter: Emitter>, router: Router, ignore_error: boolean = false) { return response.text().then(text => { const data = text && JSON.parse(text); if (!response.ok) { @@ -78,7 +78,9 @@ export function handleResponse(response: Response, emitter: Emitter handleResponse(response, this.$emitter, this.$router)) + .then((response) => handleResponse(response, this.$emitter, this.$router, true)) .then( (data) => { this.pinMappingList = data; @@ -246,6 +246,9 @@ export default defineComponent({ .then( (data) => { this.deviceConfigList = data; + if (this.deviceConfigList.curPin.name === "") { + this.deviceConfigList.curPin.name = "Default"; + } this.dataLoading = false; } ) diff --git a/webapp/yarn.lock b/webapp/yarn.lock index e27e2459..b1a1203c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -17,6 +17,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/parser@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== + "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -171,15 +176,15 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691" - integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ== +"@eslint/js@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.1.1.tgz#eb0f82461d12779bbafc1b5045cde3143d350a8a" + integrity sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ== -"@humanwhocodes/config-array@^0.12.3": - version "0.12.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072" - integrity sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" @@ -195,6 +200,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.2.3.tgz#c9aa036d1afa643f1250e83150f39efb3a15a631" + integrity sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g== + "@intlify/bundle-utils@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-8.0.0.tgz#4e05153ac031bfc7adef70baedc9b0744a93adfd" @@ -210,20 +220,20 @@ source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" -"@intlify/core-base@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.12.0.tgz#79f43faa8eb1f3b2bfe569a9fbae9bc50908d311" - integrity sha512-6EnWQXHnCh2bMiXT5N/IWwkcYQXjmF8nnEZ3YhTm23h1ZfOylz83D7pJYhcU8CsTiEdgbGiNdqyZPKwrHw03Ng== +"@intlify/core-base@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.13.1.tgz#bd1f38e665095993ef9b67aeeb794f3cabcb515d" + integrity sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w== dependencies: - "@intlify/message-compiler" "9.12.0" - "@intlify/shared" "9.12.0" + "@intlify/message-compiler" "9.13.1" + "@intlify/shared" "9.13.1" -"@intlify/message-compiler@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.12.0.tgz#5e152344853c29369911bd5e541e061b09218333" - integrity sha512-2c6VwhvVJ1nur+2cN2NjdrmrV6vXjvyxYVvtUYMXKsWSUwoNURHGds0xJVJmWxbF8qV9oGepcVV6xl9bvadEIg== +"@intlify/message-compiler@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.13.1.tgz#ff8129badf77db3fb648b8d3cceee87c8033ed0a" + integrity sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w== dependencies: - "@intlify/shared" "9.12.0" + "@intlify/shared" "9.13.1" source-map-js "^1.0.2" "@intlify/message-compiler@^9.4.0": @@ -234,10 +244,10 @@ "@intlify/shared" "9.4.0" source-map-js "^1.0.2" -"@intlify/shared@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.12.0.tgz#993383b6a98c8e37a1fa184a677eb39635a14a1c" - integrity sha512-uBcH55x5CfZynnerWHQxrXbT6yD6j6T7Nt+R2+dHAOAneoMd6BoGvfEzfYscE94rgmjoDqdr+PdGDBLk5I5EjA== +"@intlify/shared@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.13.1.tgz#202741d11ece1a9c7480bfd3f27afcf9cb8f72e4" + integrity sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ== "@intlify/shared@9.4.0", "@intlify/shared@^9.4.0": version "9.4.0" @@ -557,26 +567,26 @@ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== -"@volar/language-core@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz#74120a27ff2498ad297e86d17be95a9c7e1b46f5" - integrity sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ== +"@volar/language-core@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.2.0-alpha.10.tgz#e77db9b2ef4826cc55cf929289933d018c48e56c" + integrity sha512-njVJLtpu0zMvDaEk7K5q4BRpOgbyEUljU++un9TfJoJNhxG0z/hWwpwgTRImO42EKvwIxF3XUzeMk+qatAFy7Q== dependencies: - "@volar/source-map" "2.2.0-alpha.8" + "@volar/source-map" "2.2.0-alpha.10" -"@volar/source-map@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz#ca090f828fbef7e09ea06a636c41a06aa2afe153" - integrity sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA== +"@volar/source-map@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.2.0-alpha.10.tgz#d055232eb2a24fb4678db578b55ec095c9925dc3" + integrity sha512-nrdWApVkP5cksAnDEyy1JD9rKdwOJsEq1B+seWO4vNXmZNcxQQCx4DULLBvKt7AzRUAQiAuw5aQkb9RBaSqdVA== dependencies: muggle-string "^0.4.0" -"@volar/typescript@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz#83a056c52995b4142364be3dda41d955a96f7356" - integrity sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg== +"@volar/typescript@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.2.0-alpha.10.tgz#14c002a3549ff3adcf9306933f4bf81e80422eff" + integrity sha512-GCa0vTVVdA9ULUsu2Rx7jwsIuyZQPvPVT9o3NrANTbYv+523Ao1gv3glC5vzNSDPM6bUl37r94HbCj7KINQr+g== dependencies: - "@volar/language-core" "2.2.0-alpha.8" + "@volar/language-core" "2.2.0-alpha.10" path-browserify "^1.0.1" "@vue/compiler-core@3.2.47": @@ -600,6 +610,17 @@ estree-walker "^2.0.2" source-map-js "^1.0.2" +"@vue/compiler-core@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.25.tgz#691f59ee5014f6f2a2488fd4465f892e1e82f729" + integrity sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg== + dependencies: + "@babel/parser" "^7.24.4" + "@vue/shared" "3.4.25" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + "@vue/compiler-dom@3.2.47": version "3.2.47" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305" @@ -608,7 +629,15 @@ "@vue/compiler-core" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-dom@3.4.21", "@vue/compiler-dom@^3.4.0": +"@vue/compiler-dom@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz#b367e0c84e11d9e9f70beabdd6f6b2277fde375f" + integrity sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg== + dependencies: + "@vue/compiler-core" "3.4.25" + "@vue/shared" "3.4.25" + +"@vue/compiler-dom@^3.4.0": version "3.4.21" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803" integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA== @@ -616,20 +645,20 @@ "@vue/compiler-core" "3.4.21" "@vue/shared" "3.4.21" -"@vue/compiler-sfc@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2" - integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ== +"@vue/compiler-sfc@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz#ceab148f81571c8b251e8a8b75a9972addf1db8b" + integrity sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ== dependencies: - "@babel/parser" "^7.23.9" - "@vue/compiler-core" "3.4.21" - "@vue/compiler-dom" "3.4.21" - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + "@babel/parser" "^7.24.4" + "@vue/compiler-core" "3.4.25" + "@vue/compiler-dom" "3.4.25" + "@vue/compiler-ssr" "3.4.25" + "@vue/shared" "3.4.25" estree-walker "^2.0.2" - magic-string "^0.30.7" - postcss "^8.4.35" - source-map-js "^1.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" "@vue/compiler-sfc@^3.2.47": version "3.2.47" @@ -655,13 +684,13 @@ "@vue/compiler-dom" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-ssr@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef" - integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q== +"@vue/compiler-ssr@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz#7fdd540bfdf2d4a3d6cb107b7ba4c77228d36331" + integrity sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ== dependencies: - "@vue/compiler-dom" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-dom" "3.4.25" + "@vue/shared" "3.4.25" "@vue/devtools-api@^6.5.0": version "6.5.0" @@ -682,12 +711,12 @@ "@typescript-eslint/parser" "^7.1.1" vue-eslint-parser "^9.3.1" -"@vue/language-core@2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.13.tgz#2d1638b882011187b4b57115425d52b0901acab5" - integrity sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg== +"@vue/language-core@2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.14.tgz#99d1dcd7df8a859e12606e80863b3cb4cf045f9e" + integrity sha512-3q8mHSNcGTR7sfp2X6jZdcb4yt8AjBXAfKk0qkZIh7GAJxOnoZ10h5HToZglw4ToFvAnq+xu/Z2FFbglh9Icag== dependencies: - "@volar/language-core" "2.2.0-alpha.8" + "@volar/language-core" "2.2.0-alpha.10" "@vue/compiler-dom" "^3.4.0" "@vue/shared" "^3.4.0" computeds "^0.0.1" @@ -706,37 +735,37 @@ estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.21.tgz#affd3415115b8ebf4927c8d2a0d6a24bccfa9f02" - integrity sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw== +"@vue/reactivity@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.25.tgz#74983b146e06ce3341d15382669350125375d36f" + integrity sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ== dependencies: - "@vue/shared" "3.4.21" + "@vue/shared" "3.4.25" -"@vue/runtime-core@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz#3749c3f024a64c4c27ecd75aea4ca35634db0062" - integrity sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA== +"@vue/runtime-core@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.25.tgz#c5545d469ae0827dc471a1376f97c6ace41081ec" + integrity sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q== dependencies: - "@vue/reactivity" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/reactivity" "3.4.25" + "@vue/shared" "3.4.25" -"@vue/runtime-dom@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz#91f867ef64eff232cac45095ab28ebc93ac74588" - integrity sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw== +"@vue/runtime-dom@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz#9bc195e4860edcd0db4303cbba5a160922b963fd" + integrity sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA== dependencies: - "@vue/runtime-core" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/runtime-core" "3.4.25" + "@vue/shared" "3.4.25" csstype "^3.1.3" -"@vue/server-renderer@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz#150751579d26661ee3ed26a28604667fa4222a97" - integrity sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg== +"@vue/server-renderer@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.25.tgz#6cfc96ee631104951d5d6c09a8f1e7cef3ef3972" + integrity sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ== dependencies: - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-ssr" "3.4.25" + "@vue/shared" "3.4.25" "@vue/shared@3.2.47": version "3.2.47" @@ -748,6 +777,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1" integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g== +"@vue/shared@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.25.tgz#243ba8543e7401751e0ca319f75a80f153edd273" + integrity sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA== + "@vue/tsconfig@^0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz#3124ec16cc0c7e04165b88dc091e6b97782fffa9" @@ -1124,10 +1158,10 @@ escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.24.1: - version "9.24.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.1.tgz#0d90330c939f9dd2f4c759da5a2ad91dc1c8bac4" - integrity sha512-wk3SuwmS1pZdcuJlokGYEi/buDOwD6KltvhIZyOnpJ/378dcQ4zchu9PAMbbLAaydCz1iYc5AozszcOOgZIIOg== +eslint-plugin-vue@^9.25.0: + version "9.25.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.25.0.tgz#615cb7bb6d0e2140d21840b9aa51dce69e803e7a" + integrity sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" globals "^13.24.0" @@ -1174,17 +1208,18 @@ eslint-visitor-keys@^4.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== -eslint@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.0.0.tgz#6270548758e390343f78c8afd030566d86927d40" - integrity sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q== +eslint@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.1.1.tgz#39ec657ccd12813cb4a1dab2f9229dcc6e468271" + integrity sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^3.0.2" - "@eslint/js" "9.0.0" - "@humanwhocodes/config-array" "^0.12.3" + "@eslint/js" "9.1.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.2.3" "@nodelib/fs.walk" "^1.2.8" ajv "^6.12.4" chalk "^4.0.0" @@ -1200,7 +1235,6 @@ eslint@^9.0.0: file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" @@ -1786,10 +1820,10 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.30.7: - version "0.30.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" - integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA== +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -2056,15 +2090,6 @@ postcss@^8.1.10: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.35: - version "8.4.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" - integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" @@ -2381,10 +2406,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -terser@^5.30.3: - version "5.30.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" - integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== +terser@^5.30.4: + version "5.30.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.4.tgz#62b4d16a819424e6317fd5ceffb4ee8dc769803a" + integrity sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2494,10 +2519,10 @@ vite-plugin-css-injected-by-js@^3.5.0: resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.0.tgz#784c0f42c2b42155eb4c726c6addfa24aba9f4fb" integrity sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg== -vite@^5.2.8: - version "5.2.8" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" - integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== +vite@^5.2.10: + version "5.2.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.10.tgz#2ac927c91e99d51b376a5c73c0e4b059705f5bd7" + integrity sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw== dependencies: esbuild "^0.20.1" postcss "^8.4.38" @@ -2531,19 +2556,19 @@ vue-eslint-parser@^9.4.2: lodash "^4.17.21" semver "^7.3.6" -vue-i18n@^9.12.0: - version "9.12.0" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.12.0.tgz#8d073b3d7b92e822dcc3268946af4ecf14b778b3" - integrity sha512-rUxCKTws8NH3XP98W71GA7btAQdAuO7j6BC5y5s1bTNQYo/CIgZQf+p7d1Zo5bo/3v8TIq9aSUMDjpfgKsC3Uw== +vue-i18n@^9.13.1: + version "9.13.1" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.13.1.tgz#a292c8021b7be604ebfca5609ae1f8fafe5c36d7" + integrity sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg== dependencies: - "@intlify/core-base" "9.12.0" - "@intlify/shared" "9.12.0" + "@intlify/core-base" "9.13.1" + "@intlify/shared" "9.13.1" "@vue/devtools-api" "^6.5.0" -vue-router@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.0.tgz#d5913f27bf68a0a178ee798c3c88be471811a235" - integrity sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ== +vue-router@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.2.tgz#08096c7765dacc6832f58e35f7a081a8b34116a7" + integrity sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q== dependencies: "@vue/devtools-api" "^6.5.1" @@ -2555,25 +2580,25 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.13.tgz#6ee557705456442e0f43ec0d1774ebf5ffec54f1" - integrity sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ== +vue-tsc@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.14.tgz#5a8a652bcba30fa6fd8f7ac6af5df8e387f25cd8" + integrity sha512-DgAO3U1cnCHOUO7yB35LENbkapeRsBZ7Ugq5hGz/QOHny0+1VQN8eSwSBjYbjLVPfvfw6EY7sNPjbuHHUhckcg== dependencies: - "@volar/typescript" "2.2.0-alpha.8" - "@vue/language-core" "2.0.13" + "@volar/typescript" "2.2.0-alpha.10" + "@vue/language-core" "2.0.14" semver "^7.5.4" -vue@^3.4.21: - version "3.4.21" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.21.tgz#69ec30e267d358ee3a0ce16612ba89e00aaeb731" - integrity sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA== +vue@^3.4.25: + version "3.4.25" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.25.tgz#e59d4ed36389647b52ff2fd7aa84bb6691f4205b" + integrity sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg== dependencies: - "@vue/compiler-dom" "3.4.21" - "@vue/compiler-sfc" "3.4.21" - "@vue/runtime-dom" "3.4.21" - "@vue/server-renderer" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-dom" "3.4.25" + "@vue/compiler-sfc" "3.4.25" + "@vue/runtime-dom" "3.4.25" + "@vue/server-renderer" "3.4.25" + "@vue/shared" "3.4.25" webpack-sources@^3.2.3: version "3.2.3" diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 54b00bf1..80f4a384 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index 0b291d94..feeaebf7 100644 Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ