Merge remote-tracking branch 'tbnobody/OpenDTU/master'
This commit is contained in:
commit
df7c821bd4
@ -7,6 +7,9 @@
|
||||
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
|
||||
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
|
||||
|
||||
## Screenshots
|
||||
Several screenshots of the frontend can be found here: [Screenshots](docs/screenshots)
|
||||
|
||||
I extended the original OpenDTU software to support also Victron's Ve.Direct protocol on the same chip. Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
|
||||
|
||||
Web-Live-Interface:
|
||||
@ -52,13 +55,15 @@ Sends text raw data as difined in VE.Direct spec.
|
||||
* Hoymiles HM-1000
|
||||
* Hoymiles HM-1200
|
||||
* Hoymiles HM-1500
|
||||
* TSUN TSOL-M350 (Maybe depending on firmware on the inverter)
|
||||
* TSUN TSOL-M800 (Maybe depending on firmware on the inverter)
|
||||
* TSUN TSOL-M1600 (Maybe depending on firmware on the inverter)
|
||||
|
||||
## Features for end users
|
||||
* Read live data from inverter
|
||||
* Show inverters internal event log
|
||||
* Show inverter information like firmware version, firmware build date, hardware revision and hardware version
|
||||
* Show current inverter limit (setting the limit is not yet implemented)
|
||||
* Show and set the current inverter limit
|
||||
* Uses ESP32 microcontroller and NRF24L01+
|
||||
* Multi-Inverter support
|
||||
* MQTT support (with TLS)
|
||||
@ -185,6 +190,8 @@ You'll find the firmware file (after a successfull build process) under `.pio/bu
|
||||
|
||||
After the successful upload, the OpenDTU immediately restarts into the new firmware.
|
||||
|
||||
## MQTT Topic Documentation
|
||||
A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md)
|
||||
|
||||
## Available cases
|
||||
* [https://www.thingiverse.com/thing:5435911](https://www.thingiverse.com/thing:5435911)
|
||||
|
||||
69
docs/MQTT_Topics.md
Normal file
69
docs/MQTT_Topics.md
Normal file
@ -0,0 +1,69 @@
|
||||
# MQTT Topics
|
||||
|
||||
The base topic, as configured in the web GUI is prepended to all follwing topics.
|
||||
|
||||
## General topics
|
||||
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| dtu/ip | R | IP address of OpenDTU | IP address |
|
||||
| dtu/hostname | R | Current hostname of the dtu (as set in web GUI) | |
|
||||
| dtu/rssi | R | WiFi network quality | db value |
|
||||
| dtu/status | R | Indicates whether OpenDTU network is reachable | online / offline |
|
||||
| dtu/uptime | R | Time in seconds since startup | seconds |
|
||||
|
||||
## Inverter specific topics
|
||||
|
||||
serial will be replaced with the serial number of the inverter.
|
||||
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| [serial]/name | R | Name of the inverter as configured in web GUI | |
|
||||
| [serial]/device/bootloaderversion | R | Bootloader version of the inverter | |
|
||||
| [serial]/device/fwbuildversion | R | Firmware version of the inverter | |
|
||||
| [serial]/device/fwbuilddatetime | R | Build date / time of inverter firmware | |
|
||||
| [serial]/device/hwpartnumber | R | Hardware part number of the inverter | |
|
||||
| [serial]/device/hwversion | R | Hardware version of the inverter | |
|
||||
|
||||
### AC channel / global specific topics
|
||||
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| [serial]/0/current | R | AC current in ampere | Ampere (A) |
|
||||
| [serial]/0/efficiency | R | Ratio AC Power over DC Power in percent | % |
|
||||
| [serial]/0/frequency | R | AC frequency in hertz | Hertz (Hz) |
|
||||
| [serial]/0/power | R | AC active power in watts | Watt (W) |
|
||||
| [serial]/0/powerdc | R | DC power in watts | Watt (W) |
|
||||
| [serial]/0/powerfactor | R | Power factor in percent | % |
|
||||
| [serial]/0/reactivepower | R | AC reactive power in VAr | VAr |
|
||||
| [serial]/0/temperature | R | Temperature of inverter in degree celsius | Degree Celsius (°C) |
|
||||
| [serial]/0/voltage | R | AC voltage in volt | Volt (V) |
|
||||
| [serial]/0/yieldday | R | Energy converted to AC per day in watt hours | Watt hours (Wh) |
|
||||
| [serial]/0/yieldtotal | R | Energy converted to AC since reset watt hours | Watt hours (Wh) |
|
||||
|
||||
### DC input channel topics
|
||||
|
||||
[1-4] represents the different inputs. The amount depends on the inverter model.
|
||||
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| [serial]/[1-4]/current | R | DC current of specific input in ampere | Ampere (A) |
|
||||
| [serial]/[1-4]/irradiation | R | Ratio DC Power over set maximum power (in web GUI) | % |
|
||||
| [serial]/[1-4]/power | R | DC power of specific input in watt | Watt (W) |
|
||||
| [serial]/[1-4]/voltage | R | DC voltage of specific input in volt | Volt (V) |
|
||||
| [serial]/[1-4]/yieldday | R | Energy converted to AC per day on specific input | Watt hours (Wh) |
|
||||
| [serial]/[1-4]/yieldtotal | R | Energy converted to AC since reset on specific input | Watt hours (Wh) |
|
||||
|
||||
### Inverter limit specific topics
|
||||
|
||||
cmd topics are used to set values. Status topics are updated from values set in the inverter.
|
||||
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output |
|
||||
| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 |
|
||||
| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 |
|
||||
| [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
|
||||
| [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
|
||||
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
|
||||
| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
|
||||
@ -23,6 +23,7 @@ private:
|
||||
|
||||
void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason);
|
||||
void onMqttConnect(bool sessionPresent);
|
||||
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
void performConnect();
|
||||
void performDisconnect();
|
||||
|
||||
@ -10,6 +10,7 @@ public:
|
||||
|
||||
private:
|
||||
void onLimitStatus(AsyncWebServerRequest* request);
|
||||
void onLimitPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -29,23 +29,32 @@ void HoymilesClass::loop()
|
||||
iv->sendStatsRequest(_radio.get());
|
||||
|
||||
// Fetch event log
|
||||
iv->sendAlarmLogRequest(_radio.get());
|
||||
bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK;
|
||||
iv->sendAlarmLogRequest(_radio.get(), force);
|
||||
|
||||
// Fetch limit
|
||||
if ((iv->SystemConfigPara()->getLastUpdate() == 0) || (millis() - iv->SystemConfigPara()->getLastUpdate() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL)) {
|
||||
if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK)
|
||||
|| ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL)
|
||||
&& (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) {
|
||||
Serial.println("Request SystemConfigPara");
|
||||
iv->sendSystemConfigParaRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Set limit if required
|
||||
if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) {
|
||||
Serial.println(F("Resend ActivePowerControl"));
|
||||
iv->resendActivePowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Fetch dev info (but first fetch stats)
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSample() == 0)) {
|
||||
Serial.println(F("Request device info"));
|
||||
iv->sendDevInfoRequest(_radio.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (++inverterPos >= getNumInverters()) {
|
||||
inverterPos = 0;
|
||||
if (++inverterPos >= getNumInverters()) {
|
||||
inverterPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_lastPoll = millis();
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#define HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL (10 * 60 * 1000) // 10 minutes
|
||||
#define HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL (2 * 60 * 1000) // 2 minutes
|
||||
#define HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION (4 * 60 * 1000) // at least 4 minutes between sending limit command and read request. Otherwise eventlog entry
|
||||
|
||||
class HoymilesClass {
|
||||
public:
|
||||
|
||||
@ -91,15 +91,14 @@ void HoymilesRadio::loop()
|
||||
if (nullptr != inv) {
|
||||
CommandAbstract* cmd = _commandQueue.front().get();
|
||||
uint8_t verifyResult = inv->verifyAllFragments(cmd);
|
||||
if (verifyResult == FRAGMENT_ALL_MISSING) {
|
||||
if (_commandQueue.front().get()->getSendCount() <= MAX_RESEND_COUNT) {
|
||||
Serial.println(F("Nothing received, resend whole request"));
|
||||
sendLastPacketAgain();
|
||||
} else {
|
||||
Serial.println(F("Nothing received, resend count exeeded"));
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
}
|
||||
if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) {
|
||||
Serial.println(F("Nothing received, resend whole request"));
|
||||
sendLastPacketAgain();
|
||||
|
||||
} else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) {
|
||||
Serial.println(F("Nothing received, resend count exeeded"));
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
|
||||
} else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) {
|
||||
Serial.println(F("Retransmit timeout"));
|
||||
@ -197,12 +196,9 @@ uint8_t HoymilesRadio::getTxNxtChannel()
|
||||
|
||||
void HoymilesRadio::switchRxCh()
|
||||
{
|
||||
|
||||
// portDISABLE_INTERRUPTS();
|
||||
_radio->stopListening();
|
||||
_radio->setChannel(getRxNxtChannel());
|
||||
_radio->startListening();
|
||||
// portENABLE_INTERRUPTS();
|
||||
}
|
||||
|
||||
serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial)
|
||||
@ -237,7 +233,9 @@ void HoymilesRadio::sendEsbPacket(CommandAbstract* cmd)
|
||||
openWritingPipe(s);
|
||||
_radio->setRetries(3, 15);
|
||||
|
||||
Serial.print(F("TX Channel: "));
|
||||
Serial.print(F("TX "));
|
||||
Serial.print(cmd->getCommandName());
|
||||
Serial.print(F(" Channel: "));
|
||||
Serial.print(_radio->getChannel());
|
||||
Serial.print(F(" --> "));
|
||||
cmd->dumpDataPayload(Serial);
|
||||
@ -275,8 +273,7 @@ void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len)
|
||||
Serial.print(String(info));
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
Serial.print(buf[i], 16);
|
||||
Serial.print(" ");
|
||||
Serial.printf("%02X ", buf[i]);
|
||||
}
|
||||
Serial.println(F(""));
|
||||
}
|
||||
@ -12,8 +12,6 @@
|
||||
// number of fragments hold in buffer
|
||||
#define FRAGMENT_BUFFER_SIZE 30
|
||||
|
||||
#define MAX_RESEND_COUNT 4
|
||||
|
||||
#ifndef HOYMILES_PIN_MISO
|
||||
#define HOYMILES_PIN_MISO 19
|
||||
#endif
|
||||
|
||||
73
lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp
Normal file
73
lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
#include "ActivePowerControlCommand.h"
|
||||
#include "inverters/InverterAbstract.h"
|
||||
|
||||
#define CRC_SIZE 6
|
||||
|
||||
ActivePowerControlCommand::ActivePowerControlCommand(uint64_t target_address, uint64_t router_address)
|
||||
: DevControlCommand(target_address, router_address)
|
||||
{
|
||||
_payload[10] = 0x0b;
|
||||
_payload[11] = 0x00;
|
||||
_payload[12] = 0x00;
|
||||
_payload[13] = 0x00;
|
||||
_payload[14] = 0x00;
|
||||
_payload[15] = 0x00;
|
||||
|
||||
udpateCRC(CRC_SIZE); // 2 byte crc
|
||||
|
||||
_payload_size = 18;
|
||||
|
||||
setTimeout(2000);
|
||||
}
|
||||
|
||||
String ActivePowerControlCommand::getCommandName()
|
||||
{
|
||||
return "ActivePowerControl";
|
||||
}
|
||||
|
||||
void ActivePowerControlCommand::setActivePowerLimit(float limit, PowerLimitControlType type)
|
||||
{
|
||||
uint16_t l = limit * 10;
|
||||
|
||||
// limit
|
||||
_payload[12] = (l >> 8) & 0xff;
|
||||
_payload[13] = (l) & 0xff;
|
||||
|
||||
// type
|
||||
_payload[14] = (type >> 8) & 0xff;
|
||||
_payload[15] = (type) & 0xff;
|
||||
|
||||
udpateCRC(CRC_SIZE);
|
||||
}
|
||||
|
||||
bool ActivePowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) {
|
||||
inverter->SystemConfigPara()->setLimitPercent(getLimit());
|
||||
} else {
|
||||
// TODO(tbnobody): Not implemented yet because we only can publish the percentage value
|
||||
}
|
||||
inverter->SystemConfigPara()->setLastUpdateCommand(millis());
|
||||
inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK);
|
||||
return true;
|
||||
}
|
||||
|
||||
float ActivePowerControlCommand::getLimit()
|
||||
{
|
||||
uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]);
|
||||
return l / 10;
|
||||
}
|
||||
|
||||
PowerLimitControlType ActivePowerControlCommand::getType()
|
||||
{
|
||||
return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]);
|
||||
}
|
||||
|
||||
void ActivePowerControlCommand::gotTimeout(InverterAbstract* inverter)
|
||||
{
|
||||
inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK);
|
||||
}
|
||||
24
lib/Hoymiles/src/commands/ActivePowerControlCommand.h
Normal file
24
lib/Hoymiles/src/commands/ActivePowerControlCommand.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "DevControlCommand.h"
|
||||
|
||||
typedef enum { // ToDo: to be verified by field tests
|
||||
AbsolutNonPersistent = 0x0000, // 0
|
||||
RelativNonPersistent = 0x0001, // 1
|
||||
AbsolutPersistent = 0x0100, // 256
|
||||
RelativPersistent = 0x0101 // 257
|
||||
} PowerLimitControlType;
|
||||
|
||||
class ActivePowerControlCommand : public DevControlCommand {
|
||||
public:
|
||||
explicit ActivePowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
virtual void gotTimeout(InverterAbstract* inverter);
|
||||
|
||||
void setActivePowerLimit(float limit, PowerLimitControlType type = RelativNonPersistent);
|
||||
float getLimit();
|
||||
PowerLimitControlType getType();
|
||||
};
|
||||
@ -6,7 +6,12 @@ AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_addr
|
||||
{
|
||||
setTime(time);
|
||||
setDataType(0x11);
|
||||
setTimeout(400);
|
||||
setTimeout(500);
|
||||
}
|
||||
|
||||
String AlarmDataCommand::getCommandName()
|
||||
{
|
||||
return "AlarmData";
|
||||
}
|
||||
|
||||
bool AlarmDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
@ -23,6 +28,12 @@ bool AlarmDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fra
|
||||
inverter->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
|
||||
offs += (fragment[i].len);
|
||||
}
|
||||
inverter->EventLog()->setLastAlarmRequestSuccess(CMD_OK);
|
||||
inverter->EventLog()->setLastUpdate(millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
void AlarmDataCommand::gotTimeout(InverterAbstract* inverter)
|
||||
{
|
||||
inverter->EventLog()->setLastAlarmRequestSuccess(CMD_NOK);
|
||||
}
|
||||
@ -6,5 +6,8 @@ class AlarmDataCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
virtual void gotTimeout(InverterAbstract* inverter);
|
||||
};
|
||||
@ -13,12 +13,6 @@ CommandAbstract::CommandAbstract(uint64_t target_address, uint64_t router_addres
|
||||
setTimeout(0);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool CommandAbstract::isA()
|
||||
{
|
||||
return dynamic_cast<T*>(this) != NULL;
|
||||
}
|
||||
|
||||
const uint8_t* CommandAbstract::getDataPayload()
|
||||
{
|
||||
_payload[_payload_size] = crc8(_payload, _payload_size);
|
||||
@ -29,8 +23,7 @@ void CommandAbstract::dumpDataPayload(Stream& stream)
|
||||
{
|
||||
const uint8_t* payload = getDataPayload();
|
||||
for (uint8_t i = 0; i < getDataSize(); i++) {
|
||||
stream.print(payload[i], HEX);
|
||||
stream.print(" ");
|
||||
stream.printf("%02X ", payload[i]);
|
||||
}
|
||||
stream.println("");
|
||||
}
|
||||
@ -99,4 +92,8 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial)
|
||||
buffer[2] = s.b[1];
|
||||
buffer[1] = s.b[2];
|
||||
buffer[0] = s.b[3];
|
||||
}
|
||||
|
||||
void CommandAbstract::gotTimeout(InverterAbstract* inverter)
|
||||
{
|
||||
}
|
||||
@ -13,9 +13,6 @@ public:
|
||||
explicit CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
virtual ~CommandAbstract() {};
|
||||
|
||||
template <typename T>
|
||||
bool isA();
|
||||
|
||||
const uint8_t* getDataPayload();
|
||||
void dumpDataPayload(Stream& stream);
|
||||
|
||||
@ -30,6 +27,8 @@ public:
|
||||
void setTimeout(uint32_t timeout);
|
||||
uint32_t getTimeout();
|
||||
|
||||
virtual String getCommandName() = 0;
|
||||
|
||||
void setSendCount(uint8_t count);
|
||||
uint8_t getSendCount();
|
||||
uint8_t incrementSendCount();
|
||||
@ -37,6 +36,7 @@ public:
|
||||
virtual CommandAbstract* getRequestFrameCommand(uint8_t frame_no);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) = 0;
|
||||
virtual void gotTimeout(InverterAbstract* inverter);
|
||||
|
||||
protected:
|
||||
uint8_t _payload[RF_LEN];
|
||||
|
||||
@ -1,8 +1,29 @@
|
||||
#include "DevControlCommand.h"
|
||||
#include "crc.h"
|
||||
|
||||
DevControlCommand::DevControlCommand(uint64_t target_address, uint64_t router_address)
|
||||
: CommandAbstract(target_address, router_address)
|
||||
{
|
||||
_payload[0] = 0x51;
|
||||
_payload[9] = 0x81;
|
||||
|
||||
setTimeout(1000);
|
||||
}
|
||||
|
||||
void DevControlCommand::udpateCRC(uint8_t len)
|
||||
{
|
||||
uint16_t crc = crc16(&_payload[10], len);
|
||||
_payload[10 + len] = (uint8_t)(crc >> 8);
|
||||
_payload[10 + len + 1] = (uint8_t)(crc);
|
||||
}
|
||||
|
||||
bool DevControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
for (uint8_t i = 0; i < max_fragment_id; i++) {
|
||||
if (fragment[i].mainCmd != (_payload[0] | 0x80)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -5,4 +5,9 @@
|
||||
class DevControlCommand : public CommandAbstract {
|
||||
public:
|
||||
explicit DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
|
||||
protected:
|
||||
void udpateCRC(uint8_t len);
|
||||
};
|
||||
@ -9,6 +9,11 @@ DevInfoAllCommand::DevInfoAllCommand(uint64_t target_address, uint64_t router_ad
|
||||
setTimeout(200);
|
||||
}
|
||||
|
||||
String DevInfoAllCommand::getCommandName()
|
||||
{
|
||||
return "DevInfoAll";
|
||||
}
|
||||
|
||||
bool DevInfoAllCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
// Check CRC of whole payload
|
||||
|
||||
@ -6,5 +6,7 @@ class DevInfoAllCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
#include "DevInfoSampleCommand.h"
|
||||
#include "DevInfoSimpleCommand.h"
|
||||
#include "inverters/InverterAbstract.h"
|
||||
|
||||
DevInfoSampleCommand::DevInfoSampleCommand(uint64_t target_address, uint64_t router_address, time_t time)
|
||||
DevInfoSimpleCommand::DevInfoSimpleCommand(uint64_t target_address, uint64_t router_address, time_t time)
|
||||
: MultiDataCommand(target_address, router_address)
|
||||
{
|
||||
setTime(time);
|
||||
@ -9,7 +9,12 @@ DevInfoSampleCommand::DevInfoSampleCommand(uint64_t target_address, uint64_t rou
|
||||
setTimeout(200);
|
||||
}
|
||||
|
||||
bool DevInfoSampleCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
String DevInfoSimpleCommand::getCommandName()
|
||||
{
|
||||
return "DevInfoSimple";
|
||||
}
|
||||
|
||||
bool DevInfoSimpleCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
// Check CRC of whole payload
|
||||
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
|
||||
@ -18,9 +23,9 @@ bool DevInfoSampleCommand::handleResponse(InverterAbstract* inverter, fragment_t
|
||||
|
||||
// Move all fragments into target buffer
|
||||
uint8_t offs = 0;
|
||||
inverter->DevInfo()->clearBufferSample();
|
||||
inverter->DevInfo()->clearBufferSimple();
|
||||
for (uint8_t i = 0; i < max_fragment_id; i++) {
|
||||
inverter->DevInfo()->appendFragmentSample(offs, fragment[i].fragment, fragment[i].len);
|
||||
inverter->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len);
|
||||
offs += (fragment[i].len);
|
||||
}
|
||||
inverter->DevInfo()->setLastUpdateSample(millis());
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
#include "MultiDataCommand.h"
|
||||
|
||||
class DevInfoSampleCommand : public MultiDataCommand {
|
||||
class DevInfoSimpleCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit DevInfoSampleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
explicit DevInfoSimpleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
* CommandAbstract
|
||||
* DevControlCommand
|
||||
* ActivePowerControlCommand
|
||||
* MultiDataCommand
|
||||
* AlarmDataCommand
|
||||
* DevInfoAllCommand
|
||||
* DevInfoSampleCommand
|
||||
* DevInfoSimpleCommand
|
||||
* RealTimeRunDataCommand
|
||||
* SystemConfigParaCommand
|
||||
* ParaSetCommand
|
||||
|
||||
@ -9,6 +9,11 @@ RealTimeRunDataCommand::RealTimeRunDataCommand(uint64_t target_address, uint64_t
|
||||
setTimeout(200);
|
||||
}
|
||||
|
||||
String RealTimeRunDataCommand::getCommandName()
|
||||
{
|
||||
return "RealTimeRunData";
|
||||
}
|
||||
|
||||
bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
// Check CRC of whole payload
|
||||
@ -23,6 +28,12 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment
|
||||
inverter->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
|
||||
offs += (fragment[i].len);
|
||||
}
|
||||
inverter->Statistics()->resetRxFailureCount();
|
||||
inverter->Statistics()->setLastUpdate(millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter)
|
||||
{
|
||||
inverter->Statistics()->incrementRxFailureCount();
|
||||
}
|
||||
@ -6,5 +6,8 @@ class RealTimeRunDataCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit RealTimeRunDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
virtual void gotTimeout(InverterAbstract* inverter);
|
||||
};
|
||||
@ -10,6 +10,11 @@ RequestFrameCommand::RequestFrameCommand(uint64_t target_address, uint64_t route
|
||||
_payload_size = 10;
|
||||
}
|
||||
|
||||
String RequestFrameCommand::getCommandName()
|
||||
{
|
||||
return "RequestFrame";
|
||||
}
|
||||
|
||||
void RequestFrameCommand::setFrameNo(uint8_t frame_no)
|
||||
{
|
||||
_payload[9] = frame_no | 0x80;
|
||||
|
||||
@ -6,6 +6,8 @@ class RequestFrameCommand : public SingleDataCommand {
|
||||
public:
|
||||
explicit RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
void setFrameNo(uint8_t frame_no);
|
||||
uint8_t getFrameNo();
|
||||
|
||||
|
||||
@ -9,6 +9,11 @@ SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64
|
||||
setTimeout(200);
|
||||
}
|
||||
|
||||
String SystemConfigParaCommand::getCommandName()
|
||||
{
|
||||
return "SystemConfigPara";
|
||||
}
|
||||
|
||||
bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
// Check CRC of whole payload
|
||||
@ -23,6 +28,12 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragmen
|
||||
inverter->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
|
||||
offs += (fragment[i].len);
|
||||
}
|
||||
inverter->SystemConfigPara()->setLastUpdate(millis());
|
||||
inverter->SystemConfigPara()->setLastUpdateRequest(millis());
|
||||
inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SystemConfigParaCommand::gotTimeout(InverterAbstract* inverter)
|
||||
{
|
||||
inverter->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK);
|
||||
}
|
||||
@ -6,5 +6,8 @@ class SystemConfigParaCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit SystemConfigParaCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual String getCommandName();
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
virtual void gotTimeout(InverterAbstract* inverter);
|
||||
};
|
||||
@ -11,7 +11,7 @@ public:
|
||||
const uint8_t getAssignmentCount();
|
||||
|
||||
private:
|
||||
const byteAssign_t byteAssignment[17] = {
|
||||
const byteAssign_t byteAssignment[18] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 },
|
||||
@ -24,8 +24,9 @@ private:
|
||||
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10 },
|
||||
{ FLD_PRA, UNIT_VA, CH0, 20, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100 },
|
||||
{ FLD_PCT, UNIT_PCT, CH0, 24, 2, 10 },
|
||||
{ FLD_T, UNIT_C, CH0, 26, 2, 10 },
|
||||
{ FLD_EVT_LOG, UNIT_CNT, CH0, 24, 2, 1 },
|
||||
{ FLD_EVT_LOG, UNIT_CNT, CH0, 28, 2, 1 },
|
||||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
|
||||
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
|
||||
|
||||
@ -11,7 +11,7 @@ public:
|
||||
const uint8_t getAssignmentCount();
|
||||
|
||||
private:
|
||||
const byteAssign_t byteAssignment[23] = {
|
||||
const byteAssign_t byteAssignment[24] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 },
|
||||
@ -31,6 +31,7 @@ private:
|
||||
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10 },
|
||||
{ FLD_PRA, UNIT_VA, CH0, 32, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100 },
|
||||
{ FLD_PCT, UNIT_PCT, CH0, 36, 2, 10 },
|
||||
{ FLD_T, UNIT_C, CH0, 38, 2, 10 },
|
||||
{ FLD_EVT_LOG, UNIT_CNT, CH0, 40, 2, 1 },
|
||||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
#include "HM_Abstract.h"
|
||||
#include "HoymilesRadio.h"
|
||||
#include "commands/ActivePowerControlCommand.h"
|
||||
#include "commands/AlarmDataCommand.h"
|
||||
#include "commands/DevInfoAllCommand.h"
|
||||
#include "commands/DevInfoSampleCommand.h"
|
||||
#include "commands/DevInfoSimpleCommand.h"
|
||||
#include "commands/RealTimeRunDataCommand.h"
|
||||
#include "commands/SystemConfigParaCommand.h"
|
||||
|
||||
@ -26,16 +27,18 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio)
|
||||
bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) {
|
||||
if ((uint8_t)Statistics()->getChannelFieldValue(CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) {
|
||||
return false;
|
||||
if (!force) {
|
||||
if (Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) {
|
||||
if ((uint8_t)Statistics()->getChannelFieldValue(CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,6 +50,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio)
|
||||
AlarmDataCommand* cmd = radio->enqueCommand<AlarmDataCommand>();
|
||||
cmd->setTime(now);
|
||||
cmd->setTargetAddress(serial());
|
||||
EventLog()->setLastAlarmRequestSuccess(CMD_PENDING);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -65,7 +69,7 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
|
||||
cmdAll->setTime(now);
|
||||
cmdAll->setTargetAddress(serial());
|
||||
|
||||
DevInfoSampleCommand* cmdSample = radio->enqueCommand<DevInfoSampleCommand>();
|
||||
DevInfoSimpleCommand* cmdSample = radio->enqueCommand<DevInfoSimpleCommand>();
|
||||
cmdSample->setTime(now);
|
||||
cmdSample->setTargetAddress(serial());
|
||||
|
||||
@ -85,6 +89,25 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
|
||||
SystemConfigParaCommand* cmd = radio->enqueCommand<SystemConfigParaCommand>();
|
||||
cmd->setTime(now);
|
||||
cmd->setTargetAddress(serial());
|
||||
SystemConfigPara()->setLastLimitRequestSuccess(CMD_PENDING);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type)
|
||||
{
|
||||
_activePowerControlLimit = limit;
|
||||
_activePowerControlType = type;
|
||||
|
||||
ActivePowerControlCommand* cmd = radio->enqueCommand<ActivePowerControlCommand>();
|
||||
cmd->setActivePowerLimit(limit, type);
|
||||
cmd->setTargetAddress(serial());
|
||||
SystemConfigPara()->setLastLimitCommandSuccess(CMD_PENDING);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio)
|
||||
{
|
||||
return sendActivePowerControlRequest(radio, _activePowerControlLimit, _activePowerControlType);
|
||||
}
|
||||
@ -6,10 +6,14 @@ class HM_Abstract : public InverterAbstract {
|
||||
public:
|
||||
explicit HM_Abstract(uint64_t serial);
|
||||
bool sendStatsRequest(HoymilesRadio* radio);
|
||||
bool sendAlarmLogRequest(HoymilesRadio* radio);
|
||||
bool sendAlarmLogRequest(HoymilesRadio* radio, bool force = false);
|
||||
bool sendDevInfoRequest(HoymilesRadio* radio);
|
||||
bool sendSystemConfigParaRequest(HoymilesRadio* radio);
|
||||
bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type);
|
||||
bool resendActivePowerControlRequest(HoymilesRadio* radio);
|
||||
|
||||
private:
|
||||
uint8_t _lastAlarmLogCnt = 0;
|
||||
float _activePowerControlLimit = 0;
|
||||
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;
|
||||
};
|
||||
@ -40,6 +40,20 @@ const char* InverterAbstract::name()
|
||||
return _name;
|
||||
}
|
||||
|
||||
bool InverterAbstract::isProducing()
|
||||
{
|
||||
if (!Statistics()->hasChannelFieldValue(CH0, FLD_PAC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Statistics()->getChannelFieldValue(CH0, FLD_PAC) > 0;
|
||||
}
|
||||
|
||||
bool InverterAbstract::isReachable()
|
||||
{
|
||||
return Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT;
|
||||
}
|
||||
|
||||
AlarmLogParser* InverterAbstract::EventLog()
|
||||
{
|
||||
return _alarmLogParser.get();
|
||||
@ -90,6 +104,7 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len)
|
||||
// Packets with 0x81 will be seen as 1
|
||||
memcpy(_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].fragment, &fragment[10], len - 11);
|
||||
_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].len = len - 11;
|
||||
_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].mainCmd = fragment[0];
|
||||
_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].wasReceived = true;
|
||||
|
||||
if ((fragmentCount & 0b01111111) > _rxFragmentLastPacketId) {
|
||||
@ -109,7 +124,12 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
// All missing
|
||||
if (_rxFragmentLastPacketId == 0) {
|
||||
Serial.println(F("All missing"));
|
||||
return FRAGMENT_ALL_MISSING;
|
||||
if (cmd->getSendCount() <= MAX_RESEND_COUNT) {
|
||||
return FRAGMENT_ALL_MISSING_RESEND;
|
||||
} else {
|
||||
cmd->gotTimeout(this);
|
||||
return FRAGMENT_ALL_MISSING_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
// Last fragment is missing (thte one with 0x80)
|
||||
@ -118,6 +138,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) {
|
||||
return _rxFragmentLastPacketId + 1;
|
||||
} else {
|
||||
cmd->gotTimeout(this);
|
||||
return FRAGMENT_RETRANSMIT_TIMEOUT;
|
||||
}
|
||||
}
|
||||
@ -129,12 +150,14 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) {
|
||||
return i + 1;
|
||||
} else {
|
||||
cmd->gotTimeout(this);
|
||||
return FRAGMENT_RETRANSMIT_TIMEOUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cmd->handleResponse(this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) {
|
||||
cmd->gotTimeout(this);
|
||||
return FRAGMENT_HANDLE_ERROR;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "../commands/ActivePowerControlCommand.h"
|
||||
#include "../parser/AlarmLogParser.h"
|
||||
#include "../parser/DevInfoParser.h"
|
||||
#include "../parser/StatisticsParser.h"
|
||||
@ -12,14 +13,17 @@
|
||||
#define MAX_NAME_LENGTH 32
|
||||
|
||||
enum {
|
||||
FRAGMENT_ALL_MISSING = 255,
|
||||
FRAGMENT_RETRANSMIT_TIMEOUT = 254,
|
||||
FRAGMENT_HANDLE_ERROR = 253,
|
||||
FRAGMENT_ALL_MISSING_RESEND = 255,
|
||||
FRAGMENT_ALL_MISSING_TIMEOUT = 254,
|
||||
FRAGMENT_RETRANSMIT_TIMEOUT = 253,
|
||||
FRAGMENT_HANDLE_ERROR = 252,
|
||||
FRAGMENT_OK = 0
|
||||
};
|
||||
|
||||
#define MAX_RF_FRAGMENT_COUNT 12
|
||||
#define MAX_RETRANSMIT_COUNT 5
|
||||
#define MAX_RF_FRAGMENT_COUNT 13
|
||||
#define MAX_RETRANSMIT_COUNT 5 // Used to send the retransmit package
|
||||
#define MAX_RESEND_COUNT 4 // Used if all packages are missing
|
||||
#define MAX_ONLINE_FAILURE_COUNT 2
|
||||
|
||||
class CommandAbstract;
|
||||
|
||||
@ -34,14 +38,19 @@ public:
|
||||
virtual const byteAssign_t* getByteAssignment() = 0;
|
||||
virtual const uint8_t getAssignmentCount() = 0;
|
||||
|
||||
bool isProducing();
|
||||
bool isReachable();
|
||||
|
||||
void clearRxFragmentBuffer();
|
||||
void addRxFragment(uint8_t fragment[], uint8_t len);
|
||||
uint8_t verifyAllFragments(CommandAbstract* cmd);
|
||||
|
||||
virtual bool sendStatsRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendAlarmLogRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendAlarmLogRequest(HoymilesRadio* radio, bool force = false) = 0;
|
||||
virtual bool sendDevInfoRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendSystemConfigParaRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0;
|
||||
virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0;
|
||||
|
||||
AlarmLogParser* EventLog();
|
||||
DevInfoParser* DevInfo();
|
||||
|
||||
@ -3,14 +3,14 @@
|
||||
|
||||
void AlarmLogParser::clearBuffer()
|
||||
{
|
||||
memset(_payloadAlarmLog, 0, ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE);
|
||||
memset(_payloadAlarmLog, 0, ALARM_LOG_PAYLOAD_SIZE);
|
||||
_alarmLogLength = 0;
|
||||
}
|
||||
|
||||
void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len)
|
||||
{
|
||||
if (offset + len > (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE)) {
|
||||
Serial.printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__);
|
||||
if (offset + len > ALARM_LOG_PAYLOAD_SIZE) {
|
||||
Serial.printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE);
|
||||
return;
|
||||
}
|
||||
memcpy(&_payloadAlarmLog[offset], payload, len);
|
||||
@ -22,6 +22,16 @@ uint8_t AlarmLogParser::getEntryCount()
|
||||
return (_alarmLogLength - 2) / ALARM_LOG_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
void AlarmLogParser::setLastAlarmRequestSuccess(LastCommandSuccess status)
|
||||
{
|
||||
_lastAlarmRequestSuccess = status;
|
||||
}
|
||||
|
||||
LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess()
|
||||
{
|
||||
return _lastAlarmRequestSuccess;
|
||||
}
|
||||
|
||||
void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry)
|
||||
{
|
||||
uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE;
|
||||
@ -57,6 +67,9 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry)
|
||||
case 121:
|
||||
entry->Message = String(F("Over temperature protection"));
|
||||
break;
|
||||
case 124:
|
||||
entry->Message = String(F("Shut down by remote control"));
|
||||
break;
|
||||
case 125:
|
||||
entry->Message = String(F("Grid configuration parameter error"));
|
||||
break;
|
||||
@ -70,91 +83,91 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry)
|
||||
entry->Message = String(F("Software error code 128"));
|
||||
break;
|
||||
case 129:
|
||||
entry->Message = String(F("Software error code 129"));
|
||||
entry->Message = String(F("Abnormal bias"));
|
||||
break;
|
||||
case 130:
|
||||
entry->Message = String(F("Offline"));
|
||||
break;
|
||||
case 141:
|
||||
entry->Message = String(F("Grid overvoltage"));
|
||||
entry->Message = String(F("Grid: Grid overvoltage"));
|
||||
break;
|
||||
case 142:
|
||||
entry->Message = String(F("Average grid overvoltage"));
|
||||
entry->Message = String(F("Grid: 10 min value grid overvoltage"));
|
||||
break;
|
||||
case 143:
|
||||
entry->Message = String(F("Grid undervoltage"));
|
||||
entry->Message = String(F("Grid: Grid undervoltage"));
|
||||
break;
|
||||
case 144:
|
||||
entry->Message = String(F("Grid overfrequency"));
|
||||
entry->Message = String(F("Grid: Grid overfrequency"));
|
||||
break;
|
||||
case 145:
|
||||
entry->Message = String(F("Grid underfrequency"));
|
||||
entry->Message = String(F("Grid: Grid underfrequency"));
|
||||
break;
|
||||
case 146:
|
||||
entry->Message = String(F("Rapid grid frequency change"));
|
||||
entry->Message = String(F("Grid: Rapid grid frequency change rate"));
|
||||
break;
|
||||
case 147:
|
||||
entry->Message = String(F("Power grid outage"));
|
||||
entry->Message = String(F("Grid: Power grid outage"));
|
||||
break;
|
||||
case 148:
|
||||
entry->Message = String(F("Grid disconnection"));
|
||||
entry->Message = String(F("Grid: Grid disconnection"));
|
||||
break;
|
||||
case 149:
|
||||
entry->Message = String(F("Island detected"));
|
||||
entry->Message = String(F("Grid: Island detected"));
|
||||
break;
|
||||
case 205:
|
||||
entry->Message = String(F("Input port 1 & 2 overvoltage"));
|
||||
entry->Message = String(F("MPPT-A: Input overvoltage"));
|
||||
break;
|
||||
case 206:
|
||||
entry->Message = String(F("Input port 3 & 4 overvoltage"));
|
||||
entry->Message = String(F("MPPT-B: Input overvoltage"));
|
||||
break;
|
||||
case 207:
|
||||
entry->Message = String(F("Input port 1 & 2 undervoltage"));
|
||||
entry->Message = String(F("MPPT-A: Input undervoltage"));
|
||||
break;
|
||||
case 208:
|
||||
entry->Message = String(F("Input port 3 & 4 undervoltage"));
|
||||
entry->Message = String(F("MPPT-B: Input undervoltage"));
|
||||
break;
|
||||
case 209:
|
||||
entry->Message = String(F("Port 1 no input"));
|
||||
entry->Message = String(F("PV-1: No input"));
|
||||
break;
|
||||
case 210:
|
||||
entry->Message = String(F("Port 2 no input"));
|
||||
entry->Message = String(F("PV-2: No input"));
|
||||
break;
|
||||
case 211:
|
||||
entry->Message = String(F("Port 3 no input"));
|
||||
entry->Message = String(F("PV-3: No input"));
|
||||
break;
|
||||
case 212:
|
||||
entry->Message = String(F("Port 4 no input"));
|
||||
entry->Message = String(F("PV-4: No input"));
|
||||
break;
|
||||
case 213:
|
||||
entry->Message = String(F("PV-1 & PV-2 abnormal wiring"));
|
||||
entry->Message = String(F("MPPT-A: PV-1 & PV-2 abnormal wiring"));
|
||||
break;
|
||||
case 214:
|
||||
entry->Message = String(F("PV-3 & PV-4 abnormal wiring"));
|
||||
entry->Message = String(F("MPPT-B: PV-3 & PV-4 abnormal wiring"));
|
||||
break;
|
||||
case 215:
|
||||
entry->Message = String(F("PV-1 Input overvoltage"));
|
||||
entry->Message = String(F("PV-1: Input overvoltage"));
|
||||
break;
|
||||
case 216:
|
||||
entry->Message = String(F("PV-1 Input undervoltage"));
|
||||
entry->Message = String(F("PV-1: Input undervoltage"));
|
||||
break;
|
||||
case 217:
|
||||
entry->Message = String(F("PV-2 Input overvoltage"));
|
||||
entry->Message = String(F("PV-2: Input overvoltage"));
|
||||
break;
|
||||
case 218:
|
||||
entry->Message = String(F("PV-2 Input undervoltage"));
|
||||
entry->Message = String(F("PV-2: Input undervoltage"));
|
||||
break;
|
||||
case 219:
|
||||
entry->Message = String(F("PV-3 Input overvoltage"));
|
||||
entry->Message = String(F("PV-3: Input overvoltage"));
|
||||
break;
|
||||
case 220:
|
||||
entry->Message = String(F("PV-3 Input undervoltage"));
|
||||
entry->Message = String(F("PV-3: Input undervoltage"));
|
||||
break;
|
||||
case 221:
|
||||
entry->Message = String(F("PV-4 Input overvoltage"));
|
||||
entry->Message = String(F("PV-4: Input overvoltage"));
|
||||
break;
|
||||
case 222:
|
||||
entry->Message = String(F("PV-4 Input undervoltage"));
|
||||
entry->Message = String(F("PV-4: Input undervoltage"));
|
||||
break;
|
||||
case 301:
|
||||
entry->Message = String(F("Hardware error code 301"));
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
#define ALARM_LOG_ENTRY_COUNT 15
|
||||
#define ALARM_LOG_ENTRY_SIZE 12
|
||||
#define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4)
|
||||
|
||||
struct AlarmLogEntry_t {
|
||||
uint16_t MessageId;
|
||||
@ -21,9 +22,14 @@ public:
|
||||
uint8_t getEntryCount();
|
||||
void getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry);
|
||||
|
||||
void setLastAlarmRequestSuccess(LastCommandSuccess status);
|
||||
LastCommandSuccess getLastAlarmRequestSuccess();
|
||||
|
||||
private:
|
||||
static int getTimezoneOffset();
|
||||
|
||||
uint8_t _payloadAlarmLog[ALARM_LOG_ENTRY_SIZE * ALARM_LOG_ENTRY_COUNT];
|
||||
uint8_t _payloadAlarmLog[ALARM_LOG_PAYLOAD_SIZE];
|
||||
uint8_t _alarmLogLength;
|
||||
|
||||
LastCommandSuccess _lastAlarmRequestSuccess = CMD_NOK; // Set to NOK to fetch at startup
|
||||
};
|
||||
@ -17,20 +17,20 @@ void DevInfoParser::appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t
|
||||
_devInfoAllLength += len;
|
||||
}
|
||||
|
||||
void DevInfoParser::clearBufferSample()
|
||||
void DevInfoParser::clearBufferSimple()
|
||||
{
|
||||
memset(_payloadDevInfoSample, 0, DEV_INFO_SIZE);
|
||||
_devInfoSampleLength = 0;
|
||||
memset(_payloadDevInfoSimple, 0, DEV_INFO_SIZE);
|
||||
_devInfoSimpleLength = 0;
|
||||
}
|
||||
|
||||
void DevInfoParser::appendFragmentSample(uint8_t offset, uint8_t* payload, uint8_t len)
|
||||
void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len)
|
||||
{
|
||||
if (offset + len > DEV_INFO_SIZE) {
|
||||
Serial.printf("FATAL: (%s, %d) dev info Sample packet too large for buffer\n", __FILE__, __LINE__);
|
||||
return;
|
||||
}
|
||||
memcpy(&_payloadDevInfoSample[offset], payload, len);
|
||||
_devInfoSampleLength += len;
|
||||
memcpy(&_payloadDevInfoSimple[offset], payload, len);
|
||||
_devInfoSimpleLength += len;
|
||||
}
|
||||
|
||||
uint32_t DevInfoParser::getLastUpdateAll()
|
||||
@ -84,15 +84,15 @@ uint32_t DevInfoParser::getHwPartNumber()
|
||||
uint16_t hwpn_h;
|
||||
uint16_t hwpn_l;
|
||||
|
||||
hwpn_h = (((uint16_t)_payloadDevInfoSample[2]) << 8) | _payloadDevInfoSample[3];
|
||||
hwpn_l = (((uint16_t)_payloadDevInfoSample[4]) << 8) | _payloadDevInfoSample[5];
|
||||
hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3];
|
||||
hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5];
|
||||
|
||||
return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l);
|
||||
}
|
||||
|
||||
uint16_t DevInfoParser::getHwVersion()
|
||||
{
|
||||
return (((uint16_t)_payloadDevInfoSample[6]) << 8) | _payloadDevInfoSample[7];
|
||||
return (((uint16_t)_payloadDevInfoSimple[6]) << 8) | _payloadDevInfoSimple[7];
|
||||
}
|
||||
|
||||
/* struct tm to seconds since Unix epoch */
|
||||
|
||||
@ -9,8 +9,8 @@ public:
|
||||
void clearBufferAll();
|
||||
void appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len);
|
||||
|
||||
void clearBufferSample();
|
||||
void appendFragmentSample(uint8_t offset, uint8_t* payload, uint8_t len);
|
||||
void clearBufferSimple();
|
||||
void appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len);
|
||||
|
||||
uint32_t getLastUpdateAll();
|
||||
void setLastUpdateAll(uint32_t lastUpdate);
|
||||
@ -34,6 +34,6 @@ private:
|
||||
uint8_t _payloadDevInfoAll[DEV_INFO_SIZE] = {};
|
||||
uint8_t _devInfoAllLength = 0;
|
||||
|
||||
uint8_t _payloadDevInfoSample[DEV_INFO_SIZE] = {};
|
||||
uint8_t _devInfoSampleLength = 0;
|
||||
uint8_t _payloadDevInfoSimple[DEV_INFO_SIZE] = {};
|
||||
uint8_t _devInfoSimpleLength = 0;
|
||||
};
|
||||
@ -1,6 +1,12 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
typedef enum {
|
||||
CMD_OK,
|
||||
CMD_NOK,
|
||||
CMD_PENDING
|
||||
} LastCommandSuccess;
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
uint32_t getLastUpdate();
|
||||
|
||||
@ -112,6 +112,21 @@ void StatisticsParser::setChannelMaxPower(uint8_t channel, uint16_t power)
|
||||
}
|
||||
}
|
||||
|
||||
void StatisticsParser::resetRxFailureCount()
|
||||
{
|
||||
_rxFailureCount = 0;
|
||||
}
|
||||
|
||||
void StatisticsParser::incrementRxFailureCount()
|
||||
{
|
||||
_rxFailureCount++;
|
||||
}
|
||||
|
||||
uint32_t StatisticsParser::getRxFailureCount()
|
||||
{
|
||||
return _rxFailureCount;
|
||||
}
|
||||
|
||||
static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0)
|
||||
{
|
||||
float yield = 0;
|
||||
@ -168,4 +183,4 @@ static float calcIrradiation(StatisticsParser* iv, uint8_t arg0)
|
||||
return iv->getChannelFieldValue(arg0, FLD_PDC) / iv->getChannelMaxPower(arg0 - 1) * 100.0f;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,10 @@ public:
|
||||
uint16_t getChannelMaxPower(uint8_t channel);
|
||||
void setChannelMaxPower(uint8_t channel, uint16_t power);
|
||||
|
||||
void resetRxFailureCount();
|
||||
void incrementRxFailureCount();
|
||||
uint32_t getRxFailureCount();
|
||||
|
||||
private:
|
||||
uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {};
|
||||
uint8_t _statisticLength = 0;
|
||||
@ -120,4 +124,6 @@ private:
|
||||
|
||||
const byteAssign_t* _byteAssignment;
|
||||
uint8_t _byteAssignmentCount;
|
||||
|
||||
uint32_t _rxFailureCount = 0;
|
||||
};
|
||||
@ -20,4 +20,52 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui
|
||||
float SystemConfigParaParser::getLimitPercent()
|
||||
{
|
||||
return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10;
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::setLimitPercent(float value)
|
||||
{
|
||||
_payload[2] = ((uint16_t)(value * 10)) >> 8;
|
||||
_payload[3] = ((uint16_t)(value * 10));
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::setLastLimitCommandSuccess(LastCommandSuccess status)
|
||||
{
|
||||
_lastLimitCommandSuccess = status;
|
||||
}
|
||||
|
||||
LastCommandSuccess SystemConfigParaParser::getLastLimitCommandSuccess()
|
||||
{
|
||||
return _lastLimitCommandSuccess;
|
||||
}
|
||||
|
||||
uint32_t SystemConfigParaParser::getLastUpdateCommand()
|
||||
{
|
||||
return _lastUpdateCommand;
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::setLastUpdateCommand(uint32_t lastUpdate)
|
||||
{
|
||||
_lastUpdateCommand = lastUpdate;
|
||||
setLastUpdate(lastUpdate);
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::setLastLimitRequestSuccess(LastCommandSuccess status)
|
||||
{
|
||||
_lastLimitRequestSuccess = status;
|
||||
}
|
||||
|
||||
LastCommandSuccess SystemConfigParaParser::getLastLimitRequestSuccess()
|
||||
{
|
||||
return _lastLimitRequestSuccess;
|
||||
}
|
||||
|
||||
uint32_t SystemConfigParaParser::getLastUpdateRequest()
|
||||
{
|
||||
return _lastUpdateRequest;
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::setLastUpdateRequest(uint32_t lastUpdate)
|
||||
{
|
||||
_lastUpdateRequest = lastUpdate;
|
||||
setLastUpdate(lastUpdate);
|
||||
}
|
||||
@ -10,8 +10,25 @@ public:
|
||||
void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len);
|
||||
|
||||
float getLimitPercent();
|
||||
void setLimitPercent(float value);
|
||||
|
||||
void setLastLimitCommandSuccess(LastCommandSuccess status);
|
||||
LastCommandSuccess getLastLimitCommandSuccess();
|
||||
uint32_t getLastUpdateCommand();
|
||||
void setLastUpdateCommand(uint32_t lastUpdate);
|
||||
|
||||
void setLastLimitRequestSuccess(LastCommandSuccess status);
|
||||
LastCommandSuccess getLastLimitRequestSuccess();
|
||||
uint32_t getLastUpdateRequest();
|
||||
void setLastUpdateRequest(uint32_t lastUpdate);
|
||||
|
||||
private:
|
||||
uint8_t _payload[SYSTEM_CONFIG_PARA_SIZE];
|
||||
uint8_t _payloadLength;
|
||||
|
||||
LastCommandSuccess _lastLimitCommandSuccess = CMD_OK; // Set to OK because we have to assume nothing is done at startup
|
||||
LastCommandSuccess _lastLimitRequestSuccess = CMD_NOK; // Set to NOK to fetch at startup
|
||||
|
||||
uint32_t _lastUpdateCommand = 0;
|
||||
uint32_t _lastUpdateRequest = 0;
|
||||
};
|
||||
@ -11,6 +11,7 @@ union serial_u {
|
||||
#define MAX_RF_PAYLOAD_SIZE 32
|
||||
|
||||
typedef struct {
|
||||
uint8_t mainCmd;
|
||||
uint8_t fragment[MAX_RF_PAYLOAD_SIZE];
|
||||
uint8_t len;
|
||||
bool wasReceived;
|
||||
|
||||
@ -22,7 +22,7 @@ build_flags =
|
||||
lib_deps =
|
||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||
bblanchon/ArduinoJson @ ^6.19.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.2.3
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.3.1
|
||||
nrf24/RF24 @ ^1.4.5
|
||||
|
||||
extra_scripts =
|
||||
|
||||
@ -24,6 +24,7 @@ void MqttPublishingClass::loop()
|
||||
if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) {
|
||||
MqttSettings.publish("dtu/uptime", String(millis() / 1000));
|
||||
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
|
||||
MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname());
|
||||
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
|
||||
MqttSettings.publish("dtu/rssi", String(WiFi.RSSI()));
|
||||
}
|
||||
@ -63,9 +64,12 @@ void MqttPublishingClass::loop()
|
||||
|
||||
if (inv->SystemConfigPara()->getLastUpdate() > 0) {
|
||||
// Limit
|
||||
MqttSettings.publish(subtopic + "/settings/limit", String(inv->SystemConfigPara()->getLimitPercent()));
|
||||
MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent()));
|
||||
}
|
||||
|
||||
MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable()));
|
||||
MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing()));
|
||||
|
||||
uint32_t lastUpdate = inv->Statistics()->getLastUpdate();
|
||||
if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) {
|
||||
_lastPublishStats[i] = lastUpdate;
|
||||
|
||||
@ -5,10 +5,16 @@
|
||||
#include "MqttSettings.h"
|
||||
#include "Configuration.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include <Hoymiles.h>
|
||||
#include <MqttClientSetup.h>
|
||||
#include <Ticker.h>
|
||||
#include <espMqttClient.h>
|
||||
|
||||
#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative"
|
||||
#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute"
|
||||
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
|
||||
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
|
||||
|
||||
MqttSettingsClass::MqttSettingsClass()
|
||||
{
|
||||
}
|
||||
@ -32,6 +38,12 @@ void MqttSettingsClass::onMqttConnect(bool sessionPresent)
|
||||
Serial.println(F("Connected to MQTT."));
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online);
|
||||
|
||||
String topic = getPrefix();
|
||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE).c_str(), 0);
|
||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE).c_str(), 0);
|
||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0);
|
||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0);
|
||||
}
|
||||
|
||||
void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason)
|
||||
@ -65,10 +77,91 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
|
||||
2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this);
|
||||
}
|
||||
|
||||
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
Serial.print(F("Received MQTT message on topic: "));
|
||||
Serial.println(topic);
|
||||
|
||||
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics
|
||||
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
|
||||
|
||||
char* serial_str;
|
||||
char* subtopic;
|
||||
char* setting;
|
||||
char* rest = &token_topic[strlen(config.Mqtt_Topic)];
|
||||
|
||||
serial_str = strtok_r(rest, "/", &rest);
|
||||
subtopic = strtok_r(rest, "/", &rest);
|
||||
setting = strtok_r(rest, "/", &rest);
|
||||
|
||||
if (serial_str == NULL || subtopic == NULL || setting == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial;
|
||||
serial = strtoull(serial_str, 0, 16);
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv == nullptr) {
|
||||
Serial.println(F("Inverter not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// check if subtopic is unequal cmd
|
||||
if (strcmp(subtopic, "cmd")) {
|
||||
return;
|
||||
}
|
||||
|
||||
char* strlimit = new char[len + 1];
|
||||
memcpy(strlimit, payload, len);
|
||||
strlimit[len] = '\0';
|
||||
uint32_t limit = strtol(strlimit, NULL, 10);
|
||||
delete[] strlimit;
|
||||
|
||||
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
|
||||
// Set inverter limit relative persistent
|
||||
limit = min<uint32_t>(100, limit);
|
||||
Serial.printf("Limit Persistent: %d %%\n", limit);
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::RelativPersistent);
|
||||
|
||||
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) {
|
||||
// Set inverter limit absolute persistent
|
||||
Serial.printf("Limit Persistent: %d W\n", limit);
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::AbsolutPersistent);
|
||||
|
||||
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) {
|
||||
// Set inverter limit relative non persistent
|
||||
limit = min<uint32_t>(100, limit);
|
||||
Serial.printf("Limit Non-Persistent: %d %%\n", limit);
|
||||
if (!properties.retain) {
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::RelativNonPersistent);
|
||||
} else {
|
||||
Serial.println("Ignored because retained");
|
||||
}
|
||||
|
||||
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) {
|
||||
// Set inverter limit absolute non persistent
|
||||
Serial.printf("Limit Non-Persistent: %d W\n", limit);
|
||||
if (!properties.retain) {
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::AbsolutNonPersistent);
|
||||
} else {
|
||||
Serial.println("Ignored because retained");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MqttSettingsClass::performConnect()
|
||||
{
|
||||
if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) {
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
Serial.println(F("Connecting to MQTT..."));
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
willTopic = getPrefix() + config.Mqtt_LwtTopic;
|
||||
@ -81,6 +174,7 @@ void MqttSettingsClass::performConnect()
|
||||
static_cast<espMqttClientSecure*>(mqttClient)->setClientId(clientId.c_str());
|
||||
static_cast<espMqttClientSecure*>(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1));
|
||||
static_cast<espMqttClientSecure*>(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1));
|
||||
static_cast<espMqttClientSecure*>(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
} else {
|
||||
static_cast<espMqttClient*>(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port);
|
||||
static_cast<espMqttClient*>(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password);
|
||||
@ -88,6 +182,7 @@ void MqttSettingsClass::performConnect()
|
||||
static_cast<espMqttClient*>(mqttClient)->setClientId(clientId.c_str());
|
||||
static_cast<espMqttClient*>(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1));
|
||||
static_cast<espMqttClient*>(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1));
|
||||
static_cast<espMqttClient*>(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
mqttClient->connect();
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ void WebApiLimitClass::init(AsyncWebServer* server)
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1));
|
||||
_server->on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiLimitClass::loop()
|
||||
@ -35,8 +36,108 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
root[buffer]["limit"] = inv->SystemConfigPara()->getLimitPercent();
|
||||
|
||||
LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess();
|
||||
String limitStatus = "Unknown";
|
||||
if (status == LastCommandSuccess::CMD_OK) {
|
||||
limitStatus = "Ok";
|
||||
}
|
||||
else if (status == LastCommandSuccess::CMD_NOK) {
|
||||
limitStatus = "Failure";
|
||||
}
|
||||
else if (status == LastCommandSuccess::CMD_PENDING) {
|
||||
limitStatus = "Pending";
|
||||
}
|
||||
root[buffer]["limit_set_status"] = limitStatus;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("limit_value")
|
||||
&& root.containsKey("limit_type"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial must be a number > 0!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("limit_value")].as<uint16_t>() == 0 || root[F("limit_value")].as<uint16_t>() > 1500) {
|
||||
retMsg[F("message")] = F("Limit must between 1 and 1500!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!((root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::AbsolutNonPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::AbsolutPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::RelativNonPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::RelativPersistent))) {
|
||||
|
||||
retMsg[F("message")] = F("Invalid type specified!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
uint16_t limit = root[F("limit_value")].as<uint16_t>();
|
||||
PowerLimitControlType type = root[F("limit_type")].as<PowerLimitControlType>();
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
if (inv == nullptr) {
|
||||
retMsg[F("message")] = F("Invalid inverter specified!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, type);
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
@ -85,7 +85,8 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
root[i][F("serial")] = String(buffer);
|
||||
root[i][F("name")] = inv->name();
|
||||
root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
|
||||
root[i][F("age_critical")] = ((millis() - inv->Statistics()->getLastUpdate()) / 1000) > Configuration.get().Dtu_PollInterval * 5;
|
||||
root[i][F("reachable")] = inv->isReachable();
|
||||
root[i][F("producing")] = inv->isProducing();
|
||||
|
||||
// Loop all channels
|
||||
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.1",
|
||||
"bootstrap-icons-vue": "^1.8.1",
|
||||
"core-js": "^3.25.2",
|
||||
"core-js": "^3.25.3",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.2.39",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
@ -21,7 +21,7 @@
|
||||
"@babel/core": "^7.19.1",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@types/bootstrap": "^5.2.4",
|
||||
"@types/node": "^18.7.16",
|
||||
"@types/node": "^18.7.21",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
@ -29,8 +29,8 @@
|
||||
"@vue/cli-plugin-router": "^5.0.6",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"eslint": "^8.23.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"typescript": "^4.8.3",
|
||||
"vue-cli-plugin-compression": "~2.0.0"
|
||||
|
||||
@ -19,6 +19,9 @@
|
||||
:id="'v-pills-' + inverter.serial + '-tab'" data-bs-toggle="pill"
|
||||
:data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab"
|
||||
aria-controls="'v-pills-' + inverter.serial" aria-selected="true">
|
||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||
{{ inverter.name }}
|
||||
</button>
|
||||
</div>
|
||||
@ -31,8 +34,9 @@
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'bg-danger': inverter.age_critical,
|
||||
'bg-primary': !inverter.age_critical,
|
||||
'bg-danger': !inverter.reachable,
|
||||
'bg-warning': inverter.reachable && !inverter.producing,
|
||||
'bg-primary': inverter.reachable && inverter.producing,
|
||||
}">
|
||||
{{ inverter.name }} (Inverter Serial Number:
|
||||
{{ inverter.serial }}) (Data Age:
|
||||
@ -41,7 +45,8 @@
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit">
|
||||
@click="onShowLimitSettings(inverter.serial)"
|
||||
title="Show / Set Inverter Limit">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
@ -138,27 +143,92 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="limitSettingView" tabindex="-1">
|
||||
<div class="modal" id="limitSettingView" ref="limitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Limit Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="limitSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Limit Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<BootstrapAlert v-model="showAlertLimit" :variant="alertTypeLimit">
|
||||
{{ alertMessageLimit }}
|
||||
</BootstrapAlert>
|
||||
<div class="text-center" v-if="limitSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!limitSettingLoading">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
|
||||
Limit:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputCurrentLimit"
|
||||
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
|
||||
<span class="input-group-text" id="currentLimitType">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputLastLimitSet" class="col-sm-3 col-form-label">Last Limit Set
|
||||
Status:</label>
|
||||
<div class="col-sm-9">
|
||||
<span class="badge" :class="{
|
||||
'bg-danger': successCommandLimit == 'Failure',
|
||||
'bg-warning': successCommandLimit == 'Pending',
|
||||
'bg-success': successCommandLimit == 'Ok',
|
||||
'bg-secondary': successCommandLimit == 'Unknown',
|
||||
}">
|
||||
{{ successCommandLimit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTargetLimit" class="col-sm-3 col-form-label">Set Limit:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input type="number" name="inputTargetLimit" class="form-control"
|
||||
id="inputTargetLimit" :min="targetLimitMin" :max="targetLimitMax"
|
||||
v-model="targetLimit">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">{{ targetLimitTypeText
|
||||
}}</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" @click="onSelectType(1)" href="#">Relative
|
||||
(%)</a></li>
|
||||
<li><a class="dropdown-item" @click="onSelectType(0)" href="#">Absolute
|
||||
(W)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="targetLimitType == 0" class="alert alert-secondary mt-3"
|
||||
role="alert">
|
||||
<b>Hint:</b> If you set the limit as absolute value the display of the
|
||||
current value will only be updated after ~4 minutes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<LimitSettingsCurrent v-if="!limitSettingLoading" :limitData="limitSettingList" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">Set Limit
|
||||
Persistent</button>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideLimitSettings"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">Set Limit
|
||||
Non-Persistent</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -171,13 +241,14 @@ import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import EventLog from '@/components/partials/EventLog.vue';
|
||||
import DevInfo from '@/components/partials/DevInfo.vue';
|
||||
import LimitSettingsCurrent from '@/components/partials/LimitSettingsCurrent.vue';
|
||||
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
||||
import VedirectView from '@/components/partials/VedirectView.vue';
|
||||
|
||||
declare interface Inverter {
|
||||
serial: number,
|
||||
name: string,
|
||||
age_critical: boolean,
|
||||
reachable: boolean,
|
||||
producing: boolean,
|
||||
data_age: 0,
|
||||
events: 0
|
||||
}
|
||||
@ -187,7 +258,7 @@ export default defineComponent({
|
||||
InverterChannelInfo,
|
||||
EventLog,
|
||||
DevInfo,
|
||||
LimitSettingsCurrent,
|
||||
BootstrapAlert,,
|
||||
VedirectView
|
||||
},
|
||||
data() {
|
||||
@ -204,9 +275,23 @@ export default defineComponent({
|
||||
devInfoView: {} as bootstrap.Modal,
|
||||
devInfoList: {},
|
||||
devInfoLoading: true,
|
||||
|
||||
limitSettingView: {} as bootstrap.Modal,
|
||||
limitSettingList: {},
|
||||
limitSettingSerial: 0,
|
||||
limitSettingLoading: true,
|
||||
|
||||
currentLimit: 0,
|
||||
successCommandLimit: "",
|
||||
targetLimit: 0,
|
||||
targetLimitMin: 10,
|
||||
targetLimitMax: 100,
|
||||
targetLimitTypeText: "Relative (%)",
|
||||
targetLimitType: 1,
|
||||
targetLimitPersistent: false,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@ -218,6 +303,8 @@ export default defineComponent({
|
||||
this.eventLogView = new bootstrap.Modal('#eventView');
|
||||
this.devInfoView = new bootstrap.Modal('#devInfoView');
|
||||
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
|
||||
|
||||
(this.$refs.limitSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHideLimitSettings);
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
@ -326,19 +413,76 @@ export default defineComponent({
|
||||
this.devInfoView.show();
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.limitSettingView.hide();
|
||||
this.limitSettingSerial = 0;
|
||||
this.targetLimit = 0;
|
||||
this.targetLimitType = 1;
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings(serial: number) {
|
||||
this.limitSettingLoading = true;
|
||||
fetch("/api/limit/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.limitSettingList = data[serial];
|
||||
this.currentLimit = data[serial].limit;
|
||||
this.successCommandLimit = data[serial].limit_set_status;
|
||||
this.limitSettingSerial = serial;
|
||||
this.limitSettingLoading = false;
|
||||
});
|
||||
|
||||
this.limitSettingView.show();
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
serial: this.limitSettingSerial,
|
||||
limit_value: this.targetLimit,
|
||||
limit_type: (this.targetLimitPersistent ? 256 : 0) + this.targetLimitType,
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
console.log(data);
|
||||
|
||||
fetch("/api/limit/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.limitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = response.message;
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
onSetLimitSettings(setPersistent: boolean) {
|
||||
this.targetLimitPersistent = setPersistent;
|
||||
},
|
||||
onSelectType(type: number) {
|
||||
if (type == 1) {
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 100;
|
||||
} else {
|
||||
this.targetLimitTypeText = "Absolute (W)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 1500;
|
||||
}
|
||||
this.targetLimitType = type;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1168,10 +1168,10 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
|
||||
integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==
|
||||
"@humanwhocodes/config-array@^0.10.5":
|
||||
version "0.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.5.tgz#bb679745224745fff1e9a41961c1d45a49f81c04"
|
||||
integrity sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
@ -1446,10 +1446,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
|
||||
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
|
||||
|
||||
"@types/node@^18.7.16":
|
||||
version "18.7.16"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.16.tgz#0eb3cce1e37c79619943d2fd903919fc30850601"
|
||||
integrity sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==
|
||||
"@types/node@^18.7.21":
|
||||
version "18.7.22"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.22.tgz#76f7401362ad63d9d7eefa7dcdfa5fcd9baddff3"
|
||||
integrity sha512-TsmoXYd4zrkkKjJB0URF/mTIKPl+kVcbqClB2F/ykU7vil1BfWZVndOnpEIozPv4fURD28gyPFeIkW2G+KXOvw==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.1"
|
||||
@ -1992,10 +1992,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
|
||||
integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
|
||||
|
||||
"@vue/eslint-config-typescript@^11.0.1":
|
||||
version "11.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.1.tgz#d79b3656aecea844ec9875bc93155163f684dde7"
|
||||
integrity sha512-0U+nL0nA7ahnGPk3rTN49x76miUwuQtQPQNWOFvAcjg6nFJkIkA8qbGNtXwsuHtwBwRtWpHhShL3zK07v+632w==
|
||||
"@vue/eslint-config-typescript@^11.0.2":
|
||||
version "11.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.2.tgz#03353f404d4472900794e653450bb6623de3c642"
|
||||
integrity sha512-EiKud1NqlWmSapBFkeSrE994qpKx7/27uCGnhdqzllYDpQZroyX/O6bwjEpeuyKamvLbsGdO6PMR2faIf+zFnw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "^5.0.0"
|
||||
"@typescript-eslint/parser" "^5.0.0"
|
||||
@ -2880,10 +2880,10 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.8.3:
|
||||
browserslist "^4.20.3"
|
||||
semver "7.0.0"
|
||||
|
||||
core-js@^3.25.2:
|
||||
version "3.25.2"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.2.tgz#2d3670c1455432b53fa780300a6fc1bd8304932c"
|
||||
integrity sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A==
|
||||
core-js@^3.25.3:
|
||||
version "3.25.3"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.3.tgz#cbc2be50b5ddfa7981837bd8c41639f27b166593"
|
||||
integrity sha512-y1hvKXmPHvm5B7w4ln1S4uc9eV/O5+iFExSRUimnvIph11uaizFR8LFMdONN8hG3P2pipUfX4Y/fR8rAEtcHcQ==
|
||||
|
||||
core-js@^3.8.3:
|
||||
version "3.24.1"
|
||||
@ -3387,13 +3387,13 @@ eslint-webpack-plugin@^3.1.0:
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^3.1.1"
|
||||
|
||||
eslint@^8.23.1:
|
||||
version "8.23.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.1.tgz#cfd7b3f7fdd07db8d16b4ac0516a29c8d8dca5dc"
|
||||
integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==
|
||||
eslint@^8.24.0:
|
||||
version "8.24.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8"
|
||||
integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.2"
|
||||
"@humanwhocodes/config-array" "^0.10.4"
|
||||
"@humanwhocodes/config-array" "^0.10.5"
|
||||
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
ajv "^6.10.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user