Merge remote-tracking branch

'tbnobody/OpenDTU/master'
This commit is contained in:
helgeerbe 2022-10-10 11:08:36 +02:00
commit 3febc28c78
21 changed files with 198 additions and 21 deletions

View File

@ -64,7 +64,7 @@ Sends text raw data as difined in VE.Direct spec.
* Show inverters internal event log
* Show inverter information like firmware version, firmware build date, hardware revision and hardware version
* Show and set the current inverter limit
* Function to turn the inverter off an on
* Function to turn the inverter off and on
* Uses ESP32 microcontroller and NRF24L01+
* Multi-Inverter support
* MQTT support (with TLS)

View File

@ -61,6 +61,7 @@ cmd topics are used to set values. Status topics are updated from values set in
| Topic | R / W | Description | Value / Unit |
| ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output |
| [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) |
| [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. | % |

View File

@ -48,6 +48,7 @@ void HoymilesRadio::loop()
f = _rxBuffer.getFront();
memset(f->fragment, 0xcc, MAX_RF_PAYLOAD_SIZE);
f->len = _radio->getDynamicPayloadSize();
f->channel = _radio->getChannel();
if (f->len > MAX_RF_PAYLOAD_SIZE)
f->len = MAX_RF_PAYLOAD_SIZE;
@ -69,7 +70,9 @@ void HoymilesRadio::loop()
if (nullptr != inv) {
// Save packet in inverter rx buffer
dumpBuf("RX ", f->fragment, f->len);
char buf[30];
snprintf(buf, sizeof(buf), "RX Channel: %d --> ", f->channel);
dumpBuf(buf, f->fragment, f->len);
inv->addRxFragment(f->fragment, f->len);
} else {
Serial.println(F("Inverter Not found!"));
@ -161,6 +164,11 @@ bool HoymilesRadio::isIdle()
return !_busyFlag;
}
bool HoymilesRadio::isConnected()
{
return _radio->isChipConnected();
}
void HoymilesRadio::openReadingPipe()
{
serial_u s;

View File

@ -46,9 +46,7 @@ public:
void setDtuSerial(uint64_t serial);
bool isIdle();
void sendEsbPacket(CommandAbstract* cmd);
void sendRetransmitPacket(uint8_t fragment_id);
void sendLastPacketAgain();
bool isConnected();
template <typename T>
T* enqueCommand()
@ -68,6 +66,10 @@ private:
bool checkFragmentCrc(fragment_t* fragment);
void dumpBuf(const char* info, uint8_t buf[], uint8_t len);
void sendEsbPacket(CommandAbstract* cmd);
void sendRetransmitPacket(uint8_t fragment_id);
void sendLastPacketAgain();
std::unique_ptr<SPIClass> _hspi;
std::unique_ptr<RF24> _radio;
uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 };

View File

@ -49,7 +49,12 @@ bool ActivePowerControlCommand::handleResponse(InverterAbstract* inverter, fragm
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
uint16_t max_power = inverter->DevInfo()->getMaxPower();
if (max_power > 0) {
inverter->SystemConfigPara()->setLimitPercent(static_cast<float>(getLimit()) / max_power * 100);
} else {
// TODO(tbnobody): Not implemented yet because we only can publish the percentage value
}
}
inverter->SystemConfigPara()->setLastUpdateCommand(millis());
inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK);

View File

@ -6,7 +6,7 @@ AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_addr
{
setTime(time);
setDataType(0x11);
setTimeout(500);
setTimeout(600);
}
String AlarmDataCommand::getCommandName()

View File

@ -5,7 +5,22 @@ HM_1CH::HM_1CH(uint64_t serial)
bool HM_1CH::isValidSerial(uint64_t serial)
{
return serial >= 0x112100000000 && serial <= 0x112199999999;
// serial >= 0x112100000000 && serial <= 0x112199999999
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) {
return true;
}
if ((((preId[1] & 0xf0) == 0x10) || ((preId[1] & 0xf0) == 0x20))
&& (((preId[0] == 0x10) && (preId[1] == 0x22)) || ((preId[0] == 0x11) && (preId[1] == 0x21)))) {
return true;
}
return false;
}
String HM_1CH::typeName()

View File

@ -5,7 +5,22 @@ HM_2CH::HM_2CH(uint64_t serial)
bool HM_2CH::isValidSerial(uint64_t serial)
{
return serial >= 0x114100000000 && serial <= 0x114199999999;
// serial >= 0x114100000000 && serial <= 0x114199999999
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) {
return true;
}
if ((((preId[1] & 0xf0) == 0x30) || ((preId[1] & 0xf0) == 0x40))
&& (((preId[0] == 0x10) && (preId[1] == 0x42)) || ((preId[0] == 0x11) && (preId[1] == 0x41)))) {
return true;
}
return false;
}
String HM_2CH::typeName()

View File

@ -5,12 +5,27 @@ HM_4CH::HM_4CH(uint64_t serial)
bool HM_4CH::isValidSerial(uint64_t serial)
{
return serial >= 0x116100000000 && serial <= 0x116199999999;
// serial >= 0x116100000000 && serial <= 0x116199999999
uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40);
preId[1] = (uint8_t)(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) {
return true;
}
if ((((preId[1] & 0xf0) == 0x50) || ((preId[1] & 0xf0) == 0x60))
&& (((preId[0] == 0x10) && (preId[1] == 0x62)) || ((preId[0] == 0x11) && (preId[1] == 0x61)))) {
return true;
}
return false;
}
String HM_4CH::typeName()
{
return String(F("HM-1200, HM-1500"));
return String(F("HM-1000, HM-1200, HM-1500"));
}
const byteAssign_t* HM_4CH::getByteAssignment()

View File

@ -1,6 +1,23 @@
#include "DevInfoParser.h"
#include <cstring>
typedef struct {
uint8_t hwPart[3];
uint16_t maxPower;
const char* modelName;
} devInfo_t;
const devInfo_t devInfo[] = {
{ { 0x10, 0x10, 0x10 }, 300, "HM-300" },
{ { 0x10, 0x10, 0x20 }, 350, "HM-350" },
{ { 0x10, 0x10, 0x40 }, 400, "HM-400" },
{ { 0x10, 0x11, 0x10 }, 600, "HM-600" },
{ { 0x10, 0x11, 0x20 }, 700, "HM-700" },
{ { 0x10, 0x11, 0x40 }, 800, "HM-800" },
{ { 0x10, 0x12, 0x10 }, 1200, "HM-1200" },
{ { 0x10, 0x12, 0x30 }, 1500, "HM-1500" }
};
void DevInfoParser::clearBufferAll()
{
memset(_payloadDevInfoAll, 0, DEV_INFO_SIZE);
@ -92,9 +109,40 @@ uint32_t DevInfoParser::getHwPartNumber()
String DevInfoParser::getHwVersion()
{
char buf[6];
snprintf(buf, sizeof(buf), "%02X.%02X", _payloadDevInfoSimple[6], _payloadDevInfoSimple[7]);
return String(buf);
char buf[8];
snprintf(buf, sizeof(buf), "%02d.%02d", _payloadDevInfoSimple[6], _payloadDevInfoSimple[7]);
return buf;
}
uint16_t DevInfoParser::getMaxPower()
{
uint8_t idx = getDevIdx();
if (idx == 0xff) {
return 0;
}
return devInfo[idx].maxPower;
}
String DevInfoParser::getHwModelName()
{
uint8_t idx = getDevIdx();
if (idx == 0xff) {
return "";
}
return devInfo[idx].modelName;
}
uint8_t DevInfoParser::getDevIdx()
{
uint8_t pos;
for (pos = 0; pos < sizeof(devInfo) / sizeof(devInfo_t); pos++) {
if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2]
&& devInfo[pos].hwPart[1] == _payloadDevInfoSimple[3]
&& devInfo[pos].hwPart[2] == _payloadDevInfoSimple[4]) {
return pos;
}
}
return 0xff;
}
/* struct tm to seconds since Unix epoch */

View File

@ -25,8 +25,12 @@ public:
uint32_t getHwPartNumber();
String getHwVersion();
uint16_t getMaxPower();
String getHwModelName();
private:
time_t timegm(struct tm* tm);
uint8_t getDevIdx();
uint32_t _lastUpdateAll = 0;
uint32_t _lastUpdateSimple = 0;

View File

@ -14,5 +14,6 @@ typedef struct {
uint8_t mainCmd;
uint8_t fragment[MAX_RF_PAYLOAD_SIZE];
uint8_t len;
uint8_t channel;
bool wasReceived;
} fragment_t;

View File

@ -65,6 +65,11 @@ void MqttPublishingClass::loop()
if (inv->SystemConfigPara()->getLastUpdate() > 0) {
// Limit
MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent()));
uint16_t maxpower = inv->DevInfo()->getMaxPower();
if (maxpower > 0) {
MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100));
}
}
MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable()));

View File

@ -41,6 +41,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
devInfoObj[F("fw_build_version")] = inv->DevInfo()->getFwBuildVersion();
devInfoObj[F("hw_part_number")] = inv->DevInfo()->getHwPartNumber();
devInfoObj[F("hw_version")] = inv->DevInfo()->getHwVersion();
devInfoObj[F("hw_model_name")] = inv->DevInfo()->getHwModelName();
char timebuffer[32];
const time_t t = inv->DevInfo()->getFwBuildDateTime();

View File

@ -35,7 +35,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
root[buffer]["limit"] = inv->SystemConfigPara()->getLimitPercent();
root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess();
String limitStatus = "Unknown";

View File

@ -7,6 +7,7 @@
#include "AsyncJson.h"
#include "Configuration.h"
#include "NetworkSettings.h"
#include <Hoymiles.h>
#include <LittleFS.h>
#include <ResetReason.h>
@ -64,6 +65,8 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root[F("uptime")] = esp_timer_get_time() / 1000000;
root[F("radio_connected")] = Hoymiles.getRadio()->isConnected();
response->setLength();
request->send(response);
}

View File

@ -495,7 +495,7 @@ export default defineComponent({
fetch("/api/limit/status")
.then((response) => response.json())
.then((data) => {
this.currentLimit = data[serial].limit;
this.currentLimit = data[serial].limit_relative;
this.successCommandLimit = data[serial].limit_set_status;
this.limitSettingSerial = serial;
this.limitSettingLoading = false;

View File

@ -17,6 +17,8 @@
<div class="mt-5"></div>
<MemoryInfo v-bind="systemDataList" />
<div class="mt-5"></div>
<RadioInfo v-bind="systemDataList" />
<div class="mt-5"></div>
</template>
</div>
</template>
@ -26,12 +28,14 @@ import { defineComponent } from 'vue';
import HardwareInfo from "@/components/partials/HardwareInfo.vue";
import FirmwareInfo from "@/components/partials/FirmwareInfo.vue";
import MemoryInfo from "@/components/partials/MemoryInfo.vue";
import RadioInfo from "@/components/partials/RadioInfo.vue";
export default defineComponent({
components: {
HardwareInfo,
FirmwareInfo,
MemoryInfo,
RadioInfo,
},
data() {
return {
@ -60,7 +64,9 @@ export default defineComponent({
littlefs_total: 0,
littlefs_used: 0,
sketch_total: 0,
sketch_used: 0
sketch_used: 0,
// RadioInfo
radio_connected: false,
}
}
},

View File

@ -6,6 +6,12 @@
</BootstrapAlert>
<table v-if="devInfoList.valid_data" class="table table-hover">
<tbody>
<tr>
<td>Model</td>
<td v-if="devInfoList.hw_model_name != ''">{{ devInfoList.hw_model_name }}</td>
<td v-else>Unknown model! Please report the "Hardware Part Number" and model (e.g. HM-350) as an issue
<a href="https://github.com/tbnobody/OpenDTU/issues" target="_blank">here</a>.</td>
</tr>
<tr>
<td>Bootloader Version</td>
<td>{{ formatVersion(devInfoList.fw_bootloader_version) }}</td>
@ -40,7 +46,8 @@ declare interface DevInfoData {
fw_build_version: number,
fw_build_datetime: Date,
hw_part_number: number,
hw_version: number
hw_version: number,
hw_model_name: string,
}
export default defineComponent({

View File

@ -41,14 +41,20 @@ export default defineComponent({
computed: {
timeInHours() {
return (value: number) => {
const hours = Math.floor((value) / 3600);
const minutes = Math.floor((value - hours * 3600) / 60);
const seconds = (value - hours * 3600 + minutes * 60) % 60;
const days = Math.floor(value / (24 * 60 * 60));
const secAfterDays = value - days * (24 * 60 * 60);
const hours = Math.floor(secAfterDays / (60 * 60));
const secAfterHours = secAfterDays - hours * (60 * 60);
const minutes = Math.floor(secAfterHours / 60);
const seconds = secAfterHours - minutes * 60;
const dHours = hours > 9 ? hours : "0" + hours;
const dMins = minutes > 9 ? minutes : "0" + minutes;
const dSecs = seconds > 9 ? seconds : "0" + seconds;
if (days > 0) {
return days + " " + dHours + ":" + dMins + ":" + dSecs;
}
return dHours + ":" + dMins + ":" + dSecs;
};
},

View File

@ -0,0 +1,35 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
Radio Information
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
<tbody>
<tr>
<th>Chip Status</th>
<td class="badge" :class="{
'bg-danger': !radio_connected,
'bg-success': radio_connected,
}">
<span v-if="radio_connected">connected</span>
<span v-else>not connected</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
radio_connected: { type: Boolean, required: true },
},
});
</script>