Skip to content

Commit

Permalink
Feature: Add support for HERF inverters
Browse files Browse the repository at this point in the history
  • Loading branch information
tbnobody committed Mar 6, 2024
1 parent 10cd2e4 commit f995287
Show file tree
Hide file tree
Showing 22 changed files with 301 additions and 32 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 |
| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 |
10 changes: 8 additions & 2 deletions lib/Hoymiles/src/Hoymiles.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "Hoymiles.h"
#include "Utils.h"
#include "inverters/HERF_2CH.h"
#include "inverters/HERF_4CH.h"
#include "inverters/HMS_1CH.h"
#include "inverters/HMS_1CHv2.h"
#include "inverters/HMS_2CH.h"
Expand Down Expand Up @@ -168,6 +170,10 @@ std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, c
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
} else if (HM_1CH::isValidSerial(serial)) {
i = std::make_shared<HM_1CH>(_radioNrf.get(), serial);
} else if (HERF_2CH::isValidSerial(serial)) {
i = std::make_shared<HERF_2CH>(_radioNrf.get(), serial);
} else if (HERF_4CH::isValidSerial(serial)) {
i = std::make_shared<HERF_4CH>(_radioNrf.get(), serial);
}

if (i) {
Expand Down Expand Up @@ -271,4 +277,4 @@ void HoymilesClass::setMessageOutput(Print* output)
Print* HoymilesClass::getMessageOutput()
{
return _messageOutput;
}
}
62 changes: 62 additions & 0 deletions lib/Hoymiles/src/inverters/HERF_2CH.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HERF_2CH.h"

static const byteAssign_t byteAssignment[] = {
{ TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 },
{ TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 },
{ TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 },
{ TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 },

{ TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 },
{ TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 },
{ TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 },
{ TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 },
{ TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 },
{ TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 },

{ TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_Q, UNIT_VAR, 32, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 },

{ TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 },
{ TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 },

{ TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 },
{ TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 },
{ TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 },
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};

HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};

bool HERF_2CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x282100000000 && serial <= 0x282199999999
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x2821;
}

String HERF_2CH::typeName() const
{
return "HERF-800-2T";
}

const byteAssign_t* HERF_2CH::getByteAssignment() const
{
return byteAssignment;
}

uint8_t HERF_2CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}
13 changes: 13 additions & 0 deletions lib/Hoymiles/src/inverters/HERF_2CH.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include "HM_Abstract.h"

class HERF_2CH : public HM_Abstract {
public:
explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial);
static bool isValidSerial(const uint64_t serial);
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};
20 changes: 20 additions & 0 deletions lib/Hoymiles/src/inverters/HERF_4CH.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HERF_4CH.h"

HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_4CH(radio, serial) {};

bool HERF_4CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x280100000000 && serial <= 0x280199999999
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x2801;
}

String HERF_4CH::typeName() const
{
return "HERF-1600/1800-4T";
}
11 changes: 11 additions & 0 deletions lib/Hoymiles/src/inverters/HERF_4CH.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include "HM_4CH.h"

class HERF_4CH : public HM_4CH {
public:
explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial);
static bool isValidSerial(const uint64_t serial);
String typeName() const;
};
2 changes: 2 additions & 0 deletions lib/Hoymiles/src/inverters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 |
| HERF_2CH | HERF 800 | 2821 |
| HERF_4CH | HERF 1800 | 2801 |
6 changes: 5 additions & 1 deletion lib/Hoymiles/src/parser/DevInfoParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0

{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01

{ { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00
{ { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00
{ { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00
};

DevInfoParser::DevInfoParser()
Expand Down
8 changes: 5 additions & 3 deletions src/WebApi_dtu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
return;
}

if (root["serial"].as<uint64_t>() == 0) {
// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);

if (serial == 0) {
retMsg["message"] = "Serial cannot be zero!";
retMsg["code"] = WebApiError::DtuSerialZero;
response->setLength();
Expand Down Expand Up @@ -185,8 +188,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)

CONFIG_T& config = Configuration.get();

// Interpret the string as a hex value and convert it to uint64_t
config.Dtu.Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
config.Dtu.Serial = serial;
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
Expand Down
14 changes: 10 additions & 4 deletions src/WebApi_inverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
return;
}

if (root["serial"].as<uint64_t>() == 0) {
// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);

if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength();
Expand Down Expand Up @@ -158,7 +161,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
}

// Interpret the string as a hex value and convert it to uint64_t
inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
inverter->Serial = serial;

strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);

Expand Down Expand Up @@ -233,7 +236,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
return;
}

if (root["serial"].as<uint64_t>() == 0) {
// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);

if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength();
Expand Down Expand Up @@ -261,7 +267,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)

INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];

uint64_t new_serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
uint64_t new_serial = serial;
uint64_t old_serial = inverter.Serial;

// Interpret the string as a hex value and convert it to uint64_t
Expand Down
6 changes: 4 additions & 2 deletions src/WebApi_limit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
return;
}

if (root["serial"].as<uint64_t>() == 0) {
// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);

if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::LimitSerialZero;
response->setLength();
Expand Down Expand Up @@ -129,7 +132,6 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
return;
}

uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
uint16_t limit = root["limit_value"].as<uint16_t>();
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();

Expand Down
6 changes: 4 additions & 2 deletions src/WebApi_power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,17 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
return;
}

if (root["serial"].as<uint64_t>() == 0) {
// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);

if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::PowerSerialZero;
response->setLength();
request->send(response);
return;
}

uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv == nullptr) {
retMsg["message"] = "Invalid inverter specified!";
Expand Down
114 changes: 114 additions & 0 deletions webapp/src/components/InputSerial.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<input v-model="inputSerial" type="text" :id="id" :required="required" class="form-control" :class="inputClass" />
<BootstrapAlert show :variant="formatShow" v-if="formatHint">{{ formatHint }}</BootstrapAlert>
</template>

<script lang="ts">
import BootstrapAlert from './BootstrapAlert.vue';
import { defineComponent } from 'vue';
const chars32 = '0123456789ABCDEFGHJKLMNPRSTUVWXY';
export default defineComponent({
components: {
BootstrapAlert,
},
props: {
'modelValue': { type: [String, Number], required: true },
'id': String,
'inputClass': String,
'required': Boolean,
},
data() {
return {
inputSerial: "",
formatHint: "",
formatShow: "info",
};
},
computed: {
model: {
get(): any {
return this.modelValue;
},
set(value: any) {
this.$emit('update:modelValue', value);
},
},
},
watch: {
modelValue: function (val) {
this.inputSerial = val;
},
inputSerial: function (val) {
const serial = val.toString().toUpperCase(); // Convert to lowercase for case-insensitivity
if (serial == "") {
this.formatHint = "";
this.model = "";
return;
}
this.formatShow = "info";
// Contains only numbers
if (/^[\d]{12}$/.test(serial)) {
this.model = serial;
this.formatHint = this.$t('inputserial.format_hoymiles');
}
// Contains numbers and hex characters but at least one number
else if (/^(?=.*\d)[\dA-F]{12}$/.test(serial)) {
this.model = serial;
this.formatHint = this.$t('inputserial.format_converted');
}
// Has format: xxxxxxxxx-xxx
else if (/^((A01)|(A11)|(A21))[\dA-HJ-NR-YP]{6}-[\dA-HJ-NP-Z]{3}$/.test(serial)) {
if (this.checkHerfChecksum(serial)) {
this.model = this.convertHerfToHoy(serial);
this.$nextTick(() => {
this.formatHint = this.$t('inputserial.format_herf_valid', { serial: this.model });
});
} else {
this.formatHint = this.$t('inputserial.format_herf_invalid');
this.formatShow = "danger";
}
// Any other format
} else {
this.formatHint = this.$t('inputserial.format_unknown');
this.formatShow = "danger";
}
}
},
methods: {
checkHerfChecksum(sn: string) {
const chars64 = 'HMFLGW5XC301234567899Z67YRT2S8ABCDEFGHJKDVEJ4KQPUALMNPRSTUVWXYNB';
const checksum = sn.substring(sn.indexOf("-") + 1);
const serial = sn.substring(0, sn.indexOf("-"));
const first_char = '1';
const i = chars32.indexOf(first_char)
const sum1: number = Array.from(serial).reduce((sum, c) => sum + c.charCodeAt(0), 0) & 31;
const sum2: number = Array.from(serial).reduce((sum, c) => sum + chars32.indexOf(c), 0) & 31;
const ext = first_char + chars64[sum1 + i] + chars64[sum2 + i];
return checksum == ext;
},
convertHerfToHoy(sn: string) {
let sn_int: bigint = 0n;
for (let i = 0; i < 9; i++) {
const pos: bigint = BigInt(chars32.indexOf(sn[i].toUpperCase()));
const shift: bigint = BigInt(42 - 5 * i - (i <= 2 ? 0 : 2));
sn_int |= (pos << shift);
}
return sn_int.toString(16);
}
},
});
</script>
7 changes: 7 additions & 0 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,5 +618,12 @@
"Name": "Name",
"ValueSelected": "Ausgewählt",
"ValueActive": "Aktiv"
},
"inputserial": {
"format_hoymiles": "Hoymiles Seriennummerformat",
"format_converted": "Bereits konvertierte Seriennummer",
"format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}",
"format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme",
"format_unknown": "Unbekanntes Format"
}
}
Loading

0 comments on commit f995287

Please sign in to comment.