Merge remote-tracking branch 'tbnobody/OpenDTU/master'

This commit is contained in:
helgeerbe 2022-10-03 13:34:05 +02:00
commit df7c821bd4
51 changed files with 945 additions and 160 deletions

View File

@ -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
View 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) |

View File

@ -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();

View File

@ -10,6 +10,7 @@ public:
private:
void onLimitStatus(AsyncWebServerRequest* request);
void onLimitPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -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();

View File

@ -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:

View File

@ -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(""));
}

View File

@ -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

View 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);
}

View 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();
};

View File

@ -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);
}

View File

@ -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);
};

View File

@ -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)
{
}

View File

@ -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];

View File

@ -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;
}

View File

@ -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);
};

View File

@ -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

View File

@ -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);
};

View File

@ -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());

View File

@ -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);
};

View File

@ -2,10 +2,11 @@
* CommandAbstract
* DevControlCommand
* ActivePowerControlCommand
* MultiDataCommand
* AlarmDataCommand
* DevInfoAllCommand
* DevInfoSampleCommand
* DevInfoSimpleCommand
* RealTimeRunDataCommand
* SystemConfigParaCommand
* ParaSetCommand

View File

@ -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();
}

View File

@ -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);
};

View File

@ -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;

View File

@ -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();

View File

@ -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);
}

View File

@ -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);
};

View File

@ -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 },

View File

@ -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 },

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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();

View File

@ -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"));

View File

@ -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
};

View File

@ -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 */

View File

@ -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;
};

View File

@ -1,6 +1,12 @@
#pragma once
#include <cstdint>
typedef enum {
CMD_OK,
CMD_NOK,
CMD_PENDING
} LastCommandSuccess;
class Parser {
public:
uint32_t getLastUpdate();

View File

@ -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;
}
}

View File

@ -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;
};

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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;

View File

@ -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 =

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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++) {

View File

@ -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"

View File

@ -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>

View File

@ -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"