Compare commits

..

29 Commits

Author SHA1 Message Date
Gunnar Skjold
648cac0b06 Some adjustmenst 2022-06-23 20:22:15 +02:00
Gunnar Skjold
a5d1a2c0ee Some adjustments 2022-06-23 20:03:29 +02:00
Gunnar Skjold
55f1b4b129 Fixed ESP32-S2 ADC 2022-06-23 19:25:41 +02:00
Gunnar Skjold
b805363c60 MQTT/SSL config re-enabled for ESP8266 in case it actually is in use 2022-06-23 08:13:13 +02:00
Gunnar Skjold
846b166ae9 MQTT/SSL config re-enabled for ESP8266 in case it actually is in use 2022-06-23 08:12:25 +02:00
Gunnar Skjold
4e039c2cc9 Escape backslash in JSON 2022-06-23 08:10:57 +02:00
Gunnar Skjold
e5556fe80b Fixed minimum value for lowest threshold 2022-06-19 08:31:13 +02:00
Gunnar Skjold
c5aa65a780 Fixed previous commit 2022-06-12 10:55:28 +02:00
Gunnar Skjold
30cf1435da Fixed firmware upgrade for ESP32S2 2022-06-12 10:34:01 +02:00
Gunnar Skjold
0dca85d67b Fixed currency conversion between nordic currencies 2022-06-04 10:28:23 +02:00
Gunnar Skjold
d9a5a21fe0 Added link to known hardware configurations 2022-06-04 10:12:52 +02:00
Gunnar Skjold
68eaa7d39b Reload prices when ENTSO-E config has changed 2022-06-04 10:07:15 +02:00
Gunnar Skjold
3231d0747e Fixed code error 2022-06-04 10:05:09 +02:00
Gunnar Skjold
2617956b38 Disabled MQTT SSL for ESP8266 2022-06-04 09:55:25 +02:00
Gunnar Skjold
07614226b3 Fixed 404 after uploading SSL certificates to MQTT 2022-06-04 08:20:42 +02:00
Gunnar Skjold
cddb170f24 Added build target for esp32 solo 2022-06-01 20:51:25 +02:00
Gunnar Skjold
ba7915ca7c Added UART1 HW serial for ESP32-S2 2022-05-25 09:27:31 +02:00
Gunnar Skjold
ee12db4f51 Added baud rates 2022-05-15 08:56:26 +02:00
Gunnar Skjold
f01fbfca53 Fixed code that modified original byte stream 2022-05-14 10:59:48 +02:00
Gunnar Skjold
fab637cf60 Fixed GBT multiframe 2022-05-13 09:16:40 +02:00
Gunnar Skjold
b2e144efcf Fixed error 2022-05-13 09:03:38 +02:00
Gunnar Skjold
10308ce738 Added support for GBT transfer 2022-05-07 10:01:30 +02:00
Gunnar Skjold
70f5b0f912 Change default MQTT buffer 2022-04-23 08:48:34 +02:00
Gunnar Skjold
d724a90085 Fixed analog read of voltage 2022-04-17 10:07:25 +02:00
Gunnar Skjold
1ab529785a Fixed loop error 2022-04-14 10:50:00 +02:00
Gunnar Skjold
3a81e62bbe Support for encrypted DSMR 2022-04-14 10:38:04 +02:00
Gunnar Skjold
4882916b5c Fixed comment 2022-04-14 08:19:07 +02:00
Gunnar Skjold
1f7e43256a Fixed detection of sagemcom 2022-04-14 08:18:51 +02:00
Gunnar Skjold
a0d3632fd7 Fixed clearing of prices on MQTT 2022-04-04 07:31:08 +02:00
26 changed files with 772 additions and 314 deletions

View File

@@ -104,6 +104,15 @@ jobs:
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32solo binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32solo/firmware.bin
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:

Binary file not shown.

View File

@@ -6,7 +6,7 @@ DB // Encrypted
08 4B 41 4D 45 01 AC 4D 6E // System title
82 // Prefix for 2-byte length
01 D0 // Length 464
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 0=Authentication, 0000= Security Suite ID
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 1=Authentication, 0000= Security Suite ID
00 00 A3 2F // Frame counter
// Decrypted frame below

45
frames/lng.raw Normal file
View File

@@ -0,0 +1,45 @@
7E // Flag
A08B
CEFF03
13
EEE1
E6E700
E0 // GBT (Green book 9.4.6.13)
40 // Block control 0100 0000, last block=no, streaming=yes, remainig=window
0001 // Block sequence
0000 // Block sequence ack
77 // How many bytes in this block
0F 00000DB7 // APDU tag, Invoke ID and priority
0C07E604020607220FFF800000 // Date and time
0205 // Structure with 5 items
0105 // Array with 5 items
020412002809060008190900FF0F02120000 // Structure with 4 items, uint16, OBIS, int8, uint16 (0-8:25.9.0;2)
020412002809060008190900FF0F01120000 // Structure with 4 items, uint16, OBIS, int8, uint16 (0-8:25.9.0;1)
020412000109060000600101FF0F02120000 // Structure with 4 items, uint16, OBIS, int8, uint16 (96.1.1 - Meter model)
020412000309060100010700FF0F02120000 // Structure with 4 items, uint16, OBIS, int8, uint16 (1.7.0 Active import)
020412000309060100020700FF0F02120000 // Structure with 4 items, uint16, OBIS, int8, uint16 (2.7.0 Active export)
09060008190900 // OBIS 0-8:25.9.0 Object list push settings consumer information 1
ABA6
7E
7E
A024
CEFF03
13
D661
E0 // GBT
C0 // Block control 0100 0000, last block=yes, streaming=yes, remainig=window
0002 // Block sequence
0000 // Block sequence ack
13 // How many bytes in this block
FF // Last byte of OBIS in previous block
0906363031313039 // Device ID
0600000028 // Accumulated import
0600000000 // Accumulated export
8BA4
7E

View File

@@ -17,7 +17,6 @@ extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32]
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
framework = arduino
@@ -30,9 +29,12 @@ extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
# Tasmota has pre-built platform for C3, S2, S3 and Solo, more information at:
# https://github.com/Jason2866/esp32-arduino-lib-builder
[env:esp32s2]
platform = https://github.com/tasmota/platform-espressif32.git#v2.0.3
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.2
platform = https://github.com/tasmota/platform-espressif32/releases/download/v.2.0.3/platform-espressif32-v.2.0.3.zip
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.3
framework = arduino
board = esp32dev
board_build.mcu = esp32s2
@@ -46,3 +48,15 @@ lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32solo]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v.2.0.3/platform-espressif32-solo1-v.2.0.3.zip
framework = arduino
board = esp32dev
board_build.f_cpu = 160000000L
build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py

40
scripts/esp32solo/flash.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/sh
if [ -z "$1" ];then
echo "Usage: "
echo " Flashing first time : $0 flash /dev/ttyUSB0"
echo " When upgrading to new version : $0 upgrade /dev/ttyUSB0"
echo " NOTE: Replace /dev/ttyUSB0 with correct port"
exit 1
fi
if [ -z "$2" ];then
echo "Please specify port"
exit 1
fi
esptool=`which esptool`
if [ -z "$esptool" ];then
esptool=`which esptool.py`
fi
if [ -z "$esptool" ];then
if [ -f esptool.py ];then
esptool="esptool.py"
fi
fi
if [ -z "$esptool" ];then
echo "esptool.py not available to run following command: "
esptool="echo esptool.py"
fi
if [ "$1" = "flash" ];then
$esptool --chip esp32 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect \
0x1000 bootloader_dio_40m.bin \
0x8000 partitions.bin \
0xe000 boot_app0.bin \
0x10000 firmware.bin
exit $?
elif [ "$1" = "upgrade" ];then
$esptool --chip esp32 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x10000 firmware.bin
exit $?
fi

14
scripts/esp32solo/mkzip.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
env="esp32solo"
build_dir=".pio/build/$env/"
if [ ! -d $build_dir ];then
echo "No build directory"
exit 1
fi
cp ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/$env/bin/bootloader_dio_40m.bin $build_dir
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin $build_dir
chmod +x scripts/$env/flash.sh
zip -j $env.zip $build_dir/*.bin scripts/$env/flash.sh

View File

@@ -64,6 +64,7 @@ ADC_MODE(ADC_VCC);
#define BUF_SIZE_HAN (1024)
#include "ams/hdlc.h"
#include "MbusAssembler.h"
#include "GBTAssembler.h"
#include "IEC6205621.h"
#include "IEC6205675.h"
@@ -86,7 +87,7 @@ Timezone* tz;
AmsWebServer ws(commonBuffer, &Debug, &hw);
MQTTClient *mqtt = NULL;
WiFiClient *mqttClient = new WiFiClient();
WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL;
AmsMqttHandler* mqttHandler = NULL;
@@ -607,6 +608,9 @@ void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert
hwSerial = &Serial2;
}
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
if(pin == 18) {
hwSerial = &Serial1;
}
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
#endif
#endif
@@ -765,46 +769,72 @@ void swapWifiMode() {
int len = 0;
MbusAssembler* ma = NULL;
GBTAssembler* ga = NULL;
int currentMeterType = -1;
bool readHanPort() {
if(!hanSerial->available()) return false;
// Before autodetect starts, empty serial buffer to increase chance of getting first byte of a data transfer
if(currentMeterType == -1) {
hanSerial->readBytes(hanBuffer, BUF_SIZE_HAN);
currentMeterType = 0;
currentMeterType = 0; // Start autodetection
return false;
}
// Data type autodetect
if(currentMeterType == 0) {
uint8_t flag = hanSerial->read();
if(flag == 0x7E || flag == 0x68) currentMeterType = 1;
else currentMeterType = 2;
if(flag == 0x7E || flag == 0x68) {
debugD("HDLC or MBUS");
currentMeterType = 1;
} else if(flag == 0xDB) {
debugD("Encrypted DSMR");
hc = new HDLCConfig();
memcpy(hc->encryption_key, meterConfig.encryptionKey, 16);
memcpy(hc->authentication_key, meterConfig.authenticationKey, 16);
currentMeterType = 2;
} else if(flag == 0x2F) {
debugD("DSMR");
currentMeterType = 2;
} else {
currentMeterType = -1; // Unable to detect, reset to flush serial buffer
}
// Empty serial buffer before continuing
hanSerial->readBytes(hanBuffer, BUF_SIZE_HAN);
return false;
}
CosemDateTime timestamp = {0};
HDLCContext context = {0,0,0};
AmsData data;
if(currentMeterType == 1) {
if(currentMeterType == 1) { // DLMS
int pos = HDLC_FRAME_INCOMPLETE;
// For each byte received, check if we have a complete HDLC (or MBUS) frame we can handle
while(hanSerial->available() && pos == HDLC_FRAME_INCOMPLETE) {
hanBuffer[len++] = hanSerial->read();
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp);
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp, &context);
}
if(len > 0) {
// If buffer was overflowed, reset
if(len >= BUF_SIZE_HAN) {
hanSerial->readBytes(hanBuffer, BUF_SIZE_HAN);
len = 0;
debugI("Buffer overflow, resetting");
return false;
}
// In case we get segmented MBUS frames, assemble before parsing
if(pos == MBUS_FRAME_INTERMEDIATE_SEGMENT) {
debugI("Intermediate segment");
if(ma == NULL) {
ma = new MbusAssembler();
}
if(ma->append((uint8_t *) hanBuffer, len) < 0)
pos = -77;
if(ma->append((uint8_t *) hanBuffer, len) < 0) {
debugE("MBUS assembler failed");
len = 0;
return false;
}
if(Debug.isActive(RemoteDebug::VERBOSE)) {
debugD("Frame dump (%db):", len);
debugD("Intermediate degment dump (%db):", len);
debugPrint(hanBuffer, 0, len);
}
len = 0;
@@ -812,28 +842,73 @@ bool readHanPort() {
} else if(pos == MBUS_FRAME_LAST_SEGMENT) {
debugI("Final segment");
if(Debug.isActive(RemoteDebug::VERBOSE)) {
debugD("Frame dump (%db):", len);
debugD("Final segment dump (%db):", len);
debugPrint(hanBuffer, 0, len);
}
if(ma->append((uint8_t *) hanBuffer, len) >= 0) {
len = ma->write((uint8_t *) hanBuffer);
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp);
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp, &context);
} else {
pos = -77;
debugE("MBUS assembler failed");
len = 0;
return false;
}
}
if(pos == HDLC_FRAME_INCOMPLETE) {
// In case we get segmented HDLC frames (General Block Transfer), assemble before parsing
if(pos == HDLC_GBT_INTERMEDIATE) {
debugI("Intermediate block");
if(ga == NULL) {
ga = new GBTAssembler();
}
if(ga->append(&context, (uint8_t *) hanBuffer, len, &Debug) < 0) {
debugE("GBT assembler failed");
len = 0;
return false;
}
if(Debug.isActive(RemoteDebug::VERBOSE)) {
debugD("Intermediate block dump (%db):", len);
debugPrint(hanBuffer, 0, len);
}
len = 0;
return false;
} else if(pos == HDLC_GBT_LAST) {
debugI("Final block");
if(Debug.isActive(RemoteDebug::VERBOSE)) {
debugD("Final block dump (%db):", len);
debugPrint(hanBuffer, 0, len);
}
if(ga->append(&context, (uint8_t *) hanBuffer, len, &Debug) >= 0) {
len = ga->write((uint8_t *) hanBuffer);
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp, &context);
} else {
debugE("GBT assembler failed");
len = 0;
return false;
}
}
for(int i = len; i<BUF_SIZE_HAN; i++) {
hanBuffer[i] = 0x00;
}
// Encryption, but config was not initialized
if(pos == HDLC_ENCRYPTION_CONFIG_MISSING) {
hc = new HDLCConfig();
memcpy(hc->encryption_key, meterConfig.encryptionKey, 16);
memcpy(hc->authentication_key, meterConfig.authenticationKey, 16);
pos = HDLC_validate((uint8_t *) hanBuffer, len, hc, &timestamp, &context);
}
// Received frame was incomplete, return to loop and wait for more data
if(pos == HDLC_FRAME_INCOMPLETE) {
return false;
}
// Data is valid, clear the rest of the buffer to avoid tainted read
for(int i = len; i<BUF_SIZE_HAN; i++) {
hanBuffer[i] = 0x00;
}
if(Debug.isActive(RemoteDebug::VERBOSE)) {
debugW("APDU tag %02X", context.apdu);
debugW("APDU start %d", context.apduStart);
debugD("Frame dump (%db):", len);
debugPrint(hanBuffer, 0, len);
}
@@ -847,78 +922,70 @@ bool readHanPort() {
debugD("Authentication tag:");
debugPrint(hc->authentication_tag, 0, 12);
}
// If MQTT bytestream payload is selected (mqttHandler == NULL), send the payload to MQTT
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), toHex(hanBuffer, len));
}
len = 0;
len = 0; // Reset length for next frame
if(pos > 0) {
// Parse valid data
debugD("Valid data, start at byte %d", pos);
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
data = IEC6205675(((char *) (hanBuffer)) + pos, meterState.getMeterType(), &meterConfig, timestamp, hc);
} else {
if(Debug.isActive(RemoteDebug::WARNING)) {
switch(pos) {
case HDLC_BOUNDRY_FLAG_MISSING:
debugW("Boundry flag missing");
break;
case HDLC_HCS_ERROR:
debugW("Header checksum error");
break;
case HDLC_FCS_ERROR:
debugW("Frame checksum error");
break;
case HDLC_FRAME_INCOMPLETE:
debugW("Received frame is incomplete");
break;
case HDLC_ENCRYPTION_CONFIG_MISSING:
debugI("Encryption configuration requested, initializing");
break;
case HDLC_ENCRYPTION_AUTH_FAILED:
debugW("Decrypt authentication failed");
break;
case HDLC_ENCRYPTION_KEY_FAILED:
debugW("Setting decryption key failed");
break;
case HDLC_ENCRYPTION_DECRYPT_FAILED:
debugW("Decryption failed");
break;
case MBUS_FRAME_LENGTH_NOT_EQUAL:
debugW("Frame length mismatch");
break;
case MBUS_FRAME_INTERMEDIATE_SEGMENT:
case MBUS_FRAME_LAST_SEGMENT:
debugW("Partial frame dropped");
break;
case HDLC_TIMESTAMP_UNKNOWN:
debugW("Frame timestamp is not correctly formatted");
break;
case HDLC_UNKNOWN_DATA:
debugW("Unknown data format %02X", hanBuffer[0]);
currentMeterType = 0;
break;
default:
debugW("Unspecified error while reading data: %d", pos);
}
}
printHanReadError(pos);
return false;
}
} else {
return false;
}
} else if(currentMeterType == 2) {
String payload = hanSerial->readString();
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), payload);
} else if(currentMeterType == 2) { // DSMR
int pos = HDLC_FRAME_INCOMPLETE;
if(hc != NULL) {
while(hanSerial->available() && pos == HDLC_FRAME_INCOMPLETE) {
hanBuffer[len++] = hanSerial->read();
pos = mbus_decrypt((uint8_t *) hanBuffer, len, hc);
}
} else {
while(hanSerial->available()) {
hanBuffer[len++] = hanSerial->read();
}
if(len > 10) {
String end = String((char*) hanBuffer+len-5);
if(end.startsWith("!")) pos = 0;
while(hanSerial->available()) hanSerial->read();
}
}
data = IEC6205621(payload);
if(len == 0) return false;
if(pos == HDLC_FRAME_INCOMPLETE) return false;
if(len >= BUF_SIZE_HAN) {
len = 0;
debugI("Buffer overflow, resetting");
return false;
}
if(pos < 0) {
printHanReadError(pos);
while(hanSerial->available()) hanSerial->read();
len = 0;
return false;
}
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
mqtt->publish(topic.c_str(), (char*) hanBuffer);
}
len = 0;
data = IEC6205621(((char *) (hanBuffer)) + pos);
if(data.getListType() == 0) {
currentMeterType = 1;
currentMeterType = 0; // Did not receive valid data, go bach to autodetect
return false;
} else {
if(Debug.isActive(RemoteDebug::DEBUG)) {
debugD("Frame dump: %d", payload.length());
debugD("%s", payload.c_str());
debugD("Frame dump: %d", strlen((char*) (hanBuffer+pos)));
debugD("%s", hanBuffer+pos);
}
}
for(int i = len; i<BUF_SIZE_HAN; i++) hanBuffer[i] = 0x00;
}
if(data.getListType() > 0) {
@@ -978,6 +1045,53 @@ bool readHanPort() {
return true;
}
void printHanReadError(int pos) {
if(Debug.isActive(RemoteDebug::WARNING)) {
switch(pos) {
case HDLC_BOUNDRY_FLAG_MISSING:
debugW("Boundry flag missing");
break;
case HDLC_HCS_ERROR:
debugW("Header checksum error");
break;
case HDLC_FCS_ERROR:
debugW("Frame checksum error");
break;
case HDLC_FRAME_INCOMPLETE:
debugW("Received frame is incomplete");
break;
case HDLC_ENCRYPTION_CONFIG_MISSING:
debugI("Encryption configuration requested, initializing");
break;
case HDLC_ENCRYPTION_AUTH_FAILED:
debugW("Decrypt authentication failed");
break;
case HDLC_ENCRYPTION_KEY_FAILED:
debugW("Setting decryption key failed");
break;
case HDLC_ENCRYPTION_DECRYPT_FAILED:
debugW("Decryption failed");
break;
case MBUS_FRAME_LENGTH_NOT_EQUAL:
debugW("Frame length mismatch");
break;
case MBUS_FRAME_INTERMEDIATE_SEGMENT:
case MBUS_FRAME_LAST_SEGMENT:
debugW("Partial frame dropped");
break;
case HDLC_TIMESTAMP_UNKNOWN:
debugW("Frame timestamp is not correctly formatted");
break;
case HDLC_UNKNOWN_DATA:
debugW("Unknown data format %02X", hanBuffer[0]);
currentMeterType = 0; // Did not receive valid data, go back to autodetect
break;
default:
debugW("Unspecified error while reading data: %d", pos);
}
}
}
void debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
@@ -1149,15 +1263,12 @@ void MQTT_connect() {
mqtt->disconnect();
yield();
} else {
uint16_t size = 128;
uint16_t size = 256;
switch(mqttConfig.payloadFormat) {
case 0: // JSON
case 4: // Home Assistant
size = 768;
break;
case 3: // Domoticz
size = 256;
break;
case 255: // Raw frame
size = 1024;
break;
@@ -1195,22 +1306,19 @@ void MQTT_connect() {
}
if(mqttConfig.ssl) {
debugI("MQTT SSL is configured");
debugI("MQTT SSL is configured (%dkb free heap)", ESP.getFreeHeap());
if(mqttSecureClient == NULL) {
mqttSecureClient = new WiFiClientSecure();
}
#if defined(ESP8266)
mqttSecureClient->setBufferSizes(512, 512);
#endif
if(LittleFS.begin()) {
char *ca = NULL;
char *cert = NULL;
char *key = NULL;
File file;
if(LittleFS.exists(FILE_MQTT_CA)) {
debugI("Found MQTT CA file");
debugI("Found MQTT CA file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CA, "r");
#if defined(ESP8266)
BearSSL::X509List *serverTrustedCA = new BearSSL::X509List(file);
@@ -1218,26 +1326,37 @@ void MQTT_connect() {
#elif defined(ESP32)
mqttSecureClient->loadCACert(file, file.size());
#endif
file.close();
}
if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) {
#if defined(ESP8266)
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
BearSSL::X509List *serverCertList = new BearSSL::X509List(file);
file.close();
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file);
file.close();
debugD("Setting client certificates (%dkb free heap)", ESP.getFreeHeap());
mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey);
#elif defined(ESP32)
debugI("Found MQTT certificate file");
debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_CERT, "r");
mqttSecureClient->loadCertificate(file, file.size());
file.close();
debugI("Found MQTT key file");
debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap());
file = LittleFS.open(FILE_MQTT_KEY, "r");
mqttSecureClient->loadPrivateKey(file, file.size());
file.close();
#endif
}
LittleFS.end();
debugD("MQTT SSL setup complete (%dkb free heap)", ESP.getFreeHeap());
}
mqttClient = mqttSecureClient;
} else if(mqttClient == NULL) {

53
src/GBTAssembler.cpp Normal file
View File

@@ -0,0 +1,53 @@
#include "Arduino.h"
#include "GBTAssembler.h"
#include "ams/crc.h"
GBTAssembler::GBTAssembler() {
buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?
}
void GBTAssembler::init(const uint8_t* d, HDLCContext* context) {
memcpy(buf, d, context->headersize);
pos = headersize = context->headersize;
buf[pos++] = 0x00; // HCS
buf[pos++] = 0x00; // HCS
buf[pos++] = 0xE6;
buf[pos++] = 0xE7;
buf[pos++] = 0x00;
lastSequenceNumber = 0;
}
int GBTAssembler::append(HDLCContext* context, const uint8_t* d, int length, Print* debugger) {
GBTHeader* h = (GBTHeader*) (d+context->apduStart);
uint16_t sequence = ntohs(h->sequence);
if(h->flag != 0xE0) return -9;
if(sequence == 1) {
init(d, context);
} else if(lastSequenceNumber != sequence-1) {
return -1;
}
uint8_t* ptr = (uint8_t*) &h[1];
memcpy(buf + pos, ptr, h->size);
pos += h->size;
lastSequenceNumber = sequence;
return 0;
}
uint16_t GBTAssembler::write(const uint8_t* d) {
uint16_t head = (0xA000) | pos+1;
buf[1] = (head>>8) & 0xFF;
buf[2] = head & 0xFF;
uint16_t hcs = crc16_x25(buf+1, headersize-1);
buf[headersize] = (hcs>>8) & 0xFF;
buf[headersize+1] = hcs & 0xFF;
uint16_t fcs = crc16_x25(buf+1, pos-1);
buf[pos++] = (fcs>>8) & 0xFF;
buf[pos++] = fcs & 0xFF;
buf[pos++] = HDLC_FLAG;
memcpy((uint8_t *) d, buf, pos);
return pos;
}

29
src/GBTAssembler.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef _GBT_ASSEMBLER_H
#define _GBT_ASSEMBLER_H
#include <stdint.h>
#include "ams/hdlc.h"
typedef struct GBTHeader {
uint8_t flag;
uint8_t control;
uint16_t sequence;
uint16_t sequenceAck;
uint8_t size;
} __attribute__((packed)) GBTHeader;
class GBTAssembler {
public:
GBTAssembler();
void init(const uint8_t* d, HDLCContext* context);
int append(HDLCContext* context, const uint8_t* d, int length, Print* debugger);
uint16_t write(const uint8_t* d);
private:
uint16_t pos = 0;
uint8_t headersize = 0;
uint8_t *buf;
uint8_t lastSequenceNumber = 0;
};
#endif

View File

@@ -14,10 +14,32 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
config->tempSensorPin = 0xFF;
}
#if defined(CONFIG_IDF_TARGET_ESP32S2)
analogReadResolution(13);
analogRange = 8192;
analogSetAttenuation(ADC_11db);
#elif defined(ESP32)
analogReadResolution(12);
analogRange = 4096;
analogSetAttenuation(ADC_6db);
#endif
if(config->vccPin > 0 && config->vccPin < 40) {
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
#if defined(CONFIG_IDF_TARGET_ESP32)
#if defined(CONFIG_IDF_TARGET_ESP32S2)
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
if(voltAdc.unit == ADC_UNIT_1) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_13, 1100, voltAdcChar);
adc1_config_channel_atten((adc1_channel_t) voltAdc.channel, ADC_ATTEN_DB_11);
} else if(voltAdc.unit == ADC_UNIT_2) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_13, 1100, voltAdcChar);
adc2_config_channel_atten((adc2_channel_t) voltAdc.channel, ADC_ATTEN_DB_11);
}
}
#elif defined(ESP32)
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
if(voltAdc.unit == ADC_UNIT_1) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
@@ -27,10 +49,10 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
adc2_config_channel_atten((adc2_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
}
#endif
} else {
}
#else
pinMode(config->vccPin, INPUT);
}
#endif
} else {
voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF;
@@ -109,6 +131,16 @@ void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_7;
break;
#if defined(CONFIG_IDF_TARGET_ESP32S2)
case ADC1_CHANNEL_8_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_8;
break;
case ADC1_CHANNEL_9_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_9;
break;
#endif
case ADC2_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_0;
@@ -156,7 +188,7 @@ void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
double HwTools::getVcc() {
double volts = 0.0;
if(config->vccPin != 0xFF) {
#if defined(CONFIG_IDF_TARGET_ESP32)
#if defined(ESP32)
if(voltAdc.unit != 0xFF) {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
@@ -164,7 +196,11 @@ double HwTools::getVcc() {
x += adc1_get_raw((adc1_channel_t) voltAdc.channel);
} else if(voltAdc.unit == ADC_UNIT_2) {
int v = 0;
#if defined(CONFIG_IDF_TARGET_ESP32S2)
adc2_get_raw((adc2_channel_t) voltAdc.channel, ADC_WIDTH_BIT_13, &v);
#elif defined(CONFIG_IDF_TARGET_ESP32)
adc2_get_raw((adc2_channel_t) voltAdc.channel, ADC_WIDTH_BIT_12, &v);
#endif
x += v;
}
}
@@ -176,20 +212,22 @@ double HwTools::getVcc() {
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 40950;
volts = (x * 3.3) / 10.0 / analogRange;
}
#else
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 10240;
volts = (x * 3.3) / 10.0 / analogRange;
#endif
} else {
#if defined(ESP8266)
volts = ESP.getVcc() / 1024.0;
#endif
}
if(volts == 0.0) return 0.0;
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
@@ -303,12 +341,7 @@ double HwTools::getTemperature() {
double HwTools::getTemperatureAnalog() {
if(config->tempAnalogSensorPin != 0xFF) {
float adcCalibrationFactor = 1.06587;
int volts;
#if defined(ESP8266)
volts = (analogRead(config->tempAnalogSensorPin) / 1024.0) * 3.3;
#elif defined(ESP32)
volts = (analogRead(config->tempAnalogSensorPin) / 4095.0) * 3.3;
#endif
int volts = ((double) analogRead(config->tempAnalogSensorPin) / analogRange) * 3.3;
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
}
return DEVICE_DISCONNECTED_C;

View File

@@ -51,6 +51,7 @@ public:
HwTools() {};
private:
uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc;
#if defined(ESP32)
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;

View File

@@ -1,11 +1,20 @@
#include "IEC6205621.h"
#include "ams/crc.h"
IEC6205621::IEC6205621(String payload) {
if(payload.length() < 16)
IEC6205621::IEC6205621(const char* p) {
if(strlen(p) < 16)
return;
String payload(p+1);
int crc_pos = payload.lastIndexOf("!");
String crc = payload.substring(crc_pos+1, crc_pos+5);
//uint16_t crc_calc = crc16_x25((uint8_t*) (payload.startsWith("/") ? p+1 : p), crc_pos);
//Serial.printf("CRC %s :: %04X\n", crc.c_str(), crc_calc);
lastUpdateMillis = millis();
listId = payload.substring(payload.startsWith("/") ? 1 : 0, payload.indexOf("\n"));
if(listId.startsWith("ADN")) {
meterType = AmsTypeAidon;
listId = listId.substring(0,4);
@@ -21,7 +30,7 @@ IEC6205621::IEC6205621(String payload) {
} else if(listId.startsWith("XMX")) {
meterType = AmsTypeLandis;
listId = listId.substring(0,6);
} else if(listId.startsWith("Ene")) {
} else if(listId.startsWith("Ene") || listId.startsWith("EST")) {
meterType = AmsTypeSagemcom;
listId = listId.substring(0,4);
} else {
@@ -55,10 +64,10 @@ IEC6205621::IEC6205621(String payload) {
meterTimestamp = makeTime(tm); // TODO: Adjust for time zone
}
activeImportPower = (uint16_t) (extractDouble(payload, "1.7.0") * 1000);
activeExportPower = (uint16_t) (extractDouble(payload, "2.7.0") * 1000);
reactiveImportPower = (uint16_t) (extractDouble(payload, "3.7.0") * 1000);
reactiveExportPower = (uint16_t) (extractDouble(payload, "4.7.0") * 1000);
activeImportPower = (uint16_t) (extractDouble(payload, "1.7.0"));
activeExportPower = (uint16_t) (extractDouble(payload, "2.7.0"));
reactiveImportPower = (uint16_t) (extractDouble(payload, "3.7.0"));
reactiveExportPower = (uint16_t) (extractDouble(payload, "4.7.0"));
if(activeImportPower > 0)
listType = 1;
@@ -73,11 +82,40 @@ IEC6205621::IEC6205621(String payload) {
if(l1voltage > 0 || l2voltage > 0 || l3voltage > 0)
listType = 2;
double val = 0.0;
activeImportCounter = extractDouble(payload, "1.8.0");
activeExportCounter = extractDouble(payload, "2.8.0");
reactiveImportCounter = extractDouble(payload, "3.8.0");
reactiveExportCounter = extractDouble(payload, "4.8.0");
val = extractDouble(payload, "1.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "1.8." + String(i,10));
}
}
if(val > 0) activeImportCounter = val / 1000;
val = extractDouble(payload, "2.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "2.8." + String(i,10));
}
}
if(val > 0) activeExportCounter = val / 1000;
val = extractDouble(payload, "3.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "3.8." + String(i,10));
}
}
if(val > 0) reactiveImportCounter = val / 1000;
val = extractDouble(payload, "4.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "4.8." + String(i,10));
}
}
if(val > 0) reactiveExportCounter = val / 1000;
if(activeImportCounter > 0 || activeExportCounter > 0 || reactiveImportCounter > 0 || reactiveExportCounter > 0)
listType = 3;
@@ -104,5 +142,14 @@ String IEC6205621::extract(String payload, String obis) {
}
double IEC6205621::extractDouble(String payload, String obis) {
return extract(payload, obis).toDouble();
String str = extract(payload, obis);
if(str.isEmpty()) {
return 0.0;
}
int a = str.indexOf("*");
String val = str.substring(0,a);
String unit = str.substring(a+1);
return unit.startsWith("k") ? val.toDouble() * 1000 : val.toDouble();
}

View File

@@ -1,11 +1,12 @@
#ifndef _IEC62056_21_H
#define _IEC62056_21_H
#include "Arduino.h"
#include "AmsData.h"
class IEC6205621 : public AmsData {
public:
IEC6205621(String payload);
IEC6205621(const char* payload);
private:
String extract(String payload, String obis);

View File

@@ -158,7 +158,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
}
// Try system title
if(meterType == AmsTypeUnknown && hc != NULL) {
if(memcmp(hc->system_title, "SAGY", 4)) {
if(memcmp(hc->system_title, "SAGY", 4) == 0) {
meterType = AmsTypeSagemcom;
}
}

View File

@@ -15,7 +15,7 @@ void mbus_hexdump(const uint8_t* buf, int len) {
printf("]\n");
}
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp) {
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp, HDLCContext* context) {
int len;
int headersize = 3;
int footersize = 1;
@@ -64,6 +64,8 @@ int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTim
headersize++;
ptr++;
context->headersize = headersize + 1; // Include control byte in reported header size
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
headersize += 3;
@@ -73,10 +75,12 @@ int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTim
ptr += sizeof *t3;
// Extract LLC
HDLCLLC* llc = (HDLCLLC*) ptr;
ptr += sizeof *llc;
headersize += sizeof *llc;
// Extract LLC if present
if(((*ptr) & 0xFF) == 0xE6) {
HDLCLLC* llc = (HDLCLLC*) ptr;
ptr += sizeof *llc;
headersize += sizeof *llc;
}
} else {
return HDLC_UNKNOWN_DATA;
}
@@ -133,167 +137,172 @@ int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTim
return HDLC_UNKNOWN_DATA;
}
if(((*ptr) & 0xFF) == 0x0F) {
// Unencrypted APDU
HDLCADPU* adpu = (HDLCADPU*) (ptr);
ptr += sizeof *adpu;
context->apdu = *ptr;
context->apduStart = ptr-d;
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length+1);
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 13);
ptr += 13;
} else {
return HDLC_TIMESTAMP_UNKNOWN;
}
return ptr-d;
} else if(((*ptr) & 0xFF) == 0xDB) {
// Encrypted
if(((*ptr) & 0xFF) == 0xDB) {
if(length < headersize + 18)
return HDLC_FRAME_INCOMPLETE;
ptr++;
// Encrypted APDU
// http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html
if(config == NULL)
return HDLC_ENCRYPTION_CONFIG_MISSING;
int ret = mbus_decrypt(ptr, length - headersize - footersize, config);
if(ret < 0) return ret;
ptr += ret;
}
uint8_t systemTitleLength = *ptr;
ptr++;
memcpy(config->system_title, ptr, systemTitleLength);
memcpy(config->initialization_vector, config->system_title, systemTitleLength);
headersize += 2 + systemTitleLength;
ptr += systemTitleLength;
if(((*ptr) & 0xFF) == 0x81) {
ptr++;
len = *ptr;
// 1-byte payload length
ptr++;
headersize += 2;
} else if(((*ptr) & 0xFF) == 0x82) {
HDLCHeader* h = (HDLCHeader*) ptr;
// 2-byte payload length
len = (ntohs(h->format) & 0xFFFF);
ptr += 3;
headersize += 3;
}
if(len + headersize + footersize > length)
return HDLC_FRAME_INCOMPLETE;
//Serial.printf("\nL: %d : %d, %d : %d\n", length, len, headersize, footersize);
memcpy(config->additional_authenticated_data, ptr, 1);
// Security tag
uint8_t sec = *ptr;
ptr++;
headersize++;
// Frame counter
memcpy(config->initialization_vector + 8, ptr, 4);
ptr += 4;
headersize += 4;
// Authentication enabled
uint8_t authkeylen = 0, aadlen = 0;
if((sec & 0x10) == 0x10) {
authkeylen = 12;
aadlen = 17;
footersize += authkeylen;
memcpy(config->additional_authenticated_data + 1, config->authentication_key, 16);
memcpy(config->authentication_tag, ptr + len - footersize - 2, authkeylen);
}
#if defined(ESP8266)
br_gcm_context gcmCtx;
br_aes_ct_ctr_keys bc;
br_aes_ct_ctr_init(&bc, config->encryption_key, 16);
br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcmCtx, config->initialization_vector, sizeof(config->initialization_vector));
if(authkeylen > 0) {
br_gcm_aad_inject(&gcmCtx, config->additional_authenticated_data, aadlen);
}
br_gcm_flip(&gcmCtx);
br_gcm_run(&gcmCtx, 0, (void*) (ptr), len - authkeylen - 5); // 5 == security tag and frame counter
if(authkeylen > 0 && br_gcm_check_tag_trunc(&gcmCtx, config->authentication_tag, authkeylen) != 1) {
return HDLC_ENCRYPTION_AUTH_FAILED;
}
#elif defined(ESP32)
uint8_t cipher_text[len - authkeylen - 5];
memcpy(cipher_text, ptr, len - authkeylen - 5);
mbedtls_gcm_context m_ctx;
mbedtls_gcm_init(&m_ctx);
int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, config->encryption_key, 128);
if (0 != success) {
return HDLC_ENCRYPTION_KEY_FAILED;
}
if (0 < authkeylen) {
success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), config->initialization_vector, sizeof(config->initialization_vector),
config->additional_authenticated_data, aadlen, config->authentication_tag, authkeylen,
cipher_text, (unsigned char*)(ptr));
if (authkeylen > 0 && success == MBEDTLS_ERR_GCM_AUTH_FAILED) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_AUTH_FAILED;
} else if(success == MBEDTLS_ERR_GCM_BAD_INPUT) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
} else {
success = mbedtls_gcm_starts(&m_ctx, MBEDTLS_GCM_DECRYPT, config->initialization_vector, sizeof(config->initialization_vector),NULL, 0);
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
success = mbedtls_gcm_update(&m_ctx, sizeof(cipher_text), cipher_text, (unsigned char*)(ptr));
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
}
mbedtls_gcm_free(&m_ctx);
#endif
HDLCADPU* adpu = (HDLCADPU*) (ptr);
ptr += sizeof *adpu;
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length);
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 13);
ptr += 13;
// GBT (General Block Transfer)
if(((*ptr) & 0xFF) == 0xE0) {
uint8_t control = *(ptr+1); // 1100 0000, 1=last frame, 1=streaming, remainig=window
// TODO GBT data from ptr-d
if((control & 0x80) == 0x00) {
return HDLC_GBT_INTERMEDIATE;
} else {
return HDLC_TIMESTAMP_UNKNOWN;
return HDLC_GBT_LAST;
}
}
return ptr-d;
}
// Yes, we are doing this again, after potential decryption
context->apdu = *ptr;
context->apduStart = ptr-d;
ptr++;
ptr += 4; // Skip invoke ID and priority
// Unknown payload
return HDLC_UNKNOWN_DATA;
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length+1);
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 13);
ptr += 13;
} else {
return HDLC_TIMESTAMP_UNKNOWN;
}
return ptr-d;
}
int mbus_decrypt(const uint8_t* d, int length, HDLCConfig* config) {
if(length < 12) return HDLC_FRAME_INCOMPLETE;
uint8_t* ptr = (uint8_t*) d;
if(*ptr != 0xDB) return HDLC_ENCRYPTION_INVALID;
ptr++;
// Encrypted APDU
// http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html
if(config == NULL)
return HDLC_ENCRYPTION_CONFIG_MISSING;
uint8_t systemTitleLength = *ptr;
ptr++;
memcpy(config->system_title, ptr, systemTitleLength);
memcpy(config->initialization_vector, config->system_title, systemTitleLength);
int len;
int headersize = 2 + systemTitleLength;
ptr += systemTitleLength;
if(((*ptr) & 0xFF) == 0x81) {
ptr++;
len = *ptr;
// 1-byte payload length
ptr++;
headersize += 2;
} else if(((*ptr) & 0xFF) == 0x82) {
HDLCHeader* h = (HDLCHeader*) ptr;
// 2-byte payload length
len = (ntohs(h->format) & 0xFFFF);
ptr += 3;
headersize += 3;
}
if(len + headersize > length)
return HDLC_FRAME_INCOMPLETE;
//Serial.printf("\nL: %d : %d, %d\n", length, len, headersize);
memcpy(config->additional_authenticated_data, ptr, 1);
// Security tag
uint8_t sec = *ptr;
ptr++;
headersize++;
// Frame counter
memcpy(config->initialization_vector + 8, ptr, 4);
ptr += 4;
headersize += 4;
int footersize = 0;
// Authentication enabled
uint8_t authkeylen = 0, aadlen = 0;
if((sec & 0x10) == 0x10) {
authkeylen = 12;
aadlen = 17;
footersize += authkeylen;
memcpy(config->additional_authenticated_data + 1, config->authentication_key, 16);
memcpy(config->authentication_tag, ptr + len - footersize - 5, authkeylen);
}
#if defined(ESP8266)
br_gcm_context gcmCtx;
br_aes_ct_ctr_keys bc;
br_aes_ct_ctr_init(&bc, config->encryption_key, 16);
br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcmCtx, config->initialization_vector, sizeof(config->initialization_vector));
if(authkeylen > 0) {
br_gcm_aad_inject(&gcmCtx, config->additional_authenticated_data, aadlen);
}
br_gcm_flip(&gcmCtx);
br_gcm_run(&gcmCtx, 0, (void*) (ptr), len - authkeylen - 5); // 5 == security tag and frame counter
if(authkeylen > 0 && br_gcm_check_tag_trunc(&gcmCtx, config->authentication_tag, authkeylen) != 1) {
return HDLC_ENCRYPTION_AUTH_FAILED;
}
#elif defined(ESP32)
uint8_t cipher_text[len - authkeylen - 5];
memcpy(cipher_text, ptr, len - authkeylen - 5);
mbedtls_gcm_context m_ctx;
mbedtls_gcm_init(&m_ctx);
int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, config->encryption_key, 128);
if (0 != success) {
return HDLC_ENCRYPTION_KEY_FAILED;
}
if (0 < authkeylen) {
success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), config->initialization_vector, sizeof(config->initialization_vector),
config->additional_authenticated_data, aadlen, config->authentication_tag, authkeylen,
cipher_text, (unsigned char*)(ptr));
if (authkeylen > 0 && success == MBEDTLS_ERR_GCM_AUTH_FAILED) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_AUTH_FAILED;
} else if(success == MBEDTLS_ERR_GCM_BAD_INPUT) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
} else {
success = mbedtls_gcm_starts(&m_ctx, MBEDTLS_GCM_DECRYPT, config->initialization_vector, sizeof(config->initialization_vector),NULL, 0);
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
success = mbedtls_gcm_update(&m_ctx, sizeof(cipher_text), cipher_text, (unsigned char*)(ptr));
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
}
mbedtls_gcm_free(&m_ctx);
#endif
return ptr-d;
}
uint8_t mbusChecksum(const uint8_t* p, int len) {

View File

@@ -11,10 +11,13 @@
#define HDLC_HCS_ERROR -3
#define HDLC_FRAME_INCOMPLETE -4
#define HDLC_UNKNOWN_DATA -9
#define HDLC_GBT_INTERMEDIATE -21
#define HDLC_GBT_LAST -22
#define HDLC_ENCRYPTION_CONFIG_MISSING -90
#define HDLC_ENCRYPTION_AUTH_FAILED -91
#define HDLC_ENCRYPTION_KEY_FAILED -92
#define HDLC_ENCRYPTION_DECRYPT_FAILED -93
#define HDLC_ENCRYPTION_INVALID -98
#define HDLC_TIMESTAMP_UNKNOWN -99
#define MBUS_START 0x68
@@ -34,6 +37,12 @@ struct HDLCConfig {
uint8_t authentication_tag[12];
};
struct HDLCContext {
uint8_t apdu;
uint8_t apduStart;
uint8_t headersize;
};
typedef struct HDLCHeader {
uint8_t flag;
uint16_t format;
@@ -55,11 +64,6 @@ typedef struct HDLCLLC {
uint8_t control;
} __attribute__((packed)) HDLCLLC;
typedef struct HDLCADPU {
uint8_t flag;
uint32_t id;
} __attribute__((packed)) HDLCADPU;
typedef struct MbusHeader {
uint8_t flag1;
uint8_t len1;
@@ -158,7 +162,8 @@ typedef union {
} CosemData;
void mbus_hexdump(const uint8_t* buf, int len);
int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config, CosemDateTime* timestamp);
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp, HDLCContext* context);
int mbus_decrypt(const uint8_t* d, int length, HDLCConfig* config);
uint8_t mbusChecksum(const uint8_t* p, int len);

View File

@@ -1,6 +1,5 @@
#include "DnbCurrParser.h"
#include "Arduino.h"
#include "HardwareSerial.h"
float DnbCurrParser::getValue() {
return value;
@@ -30,13 +29,14 @@ size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
}
size_t DnbCurrParser::write(uint8_t byte) {
if(pos >= 64) pos = 0;
if(pos >= 128) pos = 0;
if(pos == 0) {
if(byte == '<') {
buf[pos++] = byte;
}
} else if(byte == '>') {
buf[pos++] = byte;
buf[pos++] = '\0';
if(strncmp(buf, "<Series", 7) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "UNIT_MULT=\"", 11) == 0) {

View File

@@ -18,7 +18,7 @@ private:
uint8_t scale = 0;
float value = 1.0;
char buf[64];
char buf[128];
uint8_t pos = 0;
uint8_t mode = 0;
};

View File

@@ -27,7 +27,10 @@ void EntsoeApi::setup(EntsoeConfig& config) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
lastCurrencyFetch = 0;
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
if(today != NULL) delete today;
if(tomorrow != NULL) delete tomorrow;
today = tomorrow = NULL;
}
char* EntsoeApi::getToken() {

View File

@@ -25,6 +25,8 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
);
mqtt->publish(topic + "/energy", json);
}
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
if(data->getListType() == 1) { // publish power counts
snprintf_P(json, BufferSize, HA1_JSON,
data->getActiveImportPower()
@@ -34,7 +36,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
@@ -92,7 +94,8 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
float values[24];
for(int i = 0;i < 24; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;

View File

@@ -12,6 +12,8 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
if(topic.isEmpty() || !mqtt->connected())
return false;
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
if(data->getListType() == 1) {
snprintf_P(json, BufferSize, JSON1_JSON,
WiFi.macAddress().c_str(),
@@ -39,7 +41,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
@@ -67,7 +69,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
@@ -100,7 +102,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccou
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
@@ -165,7 +167,8 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
float values[24];
for(int i = 0;i < 24; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;

View File

@@ -104,8 +104,8 @@ bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[34] = {0};
memset(values, ENTSOE_NO_VALUE, 34);
float values[34];
for(int i = 0;i < 34; i++) values[i] = ENTSOE_NO_VALUE;
for(uint8_t i = 0; i < 34; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;

View File

@@ -356,7 +356,11 @@ void AmsWebServer::configMeterHtml() {
html.replace("{mid}", meterState->getMeterId());
html.replace("{b}", String(meterConfig->baud));
html.replace("{b2400}", meterConfig->baud == 2400 ? "selected" : "");
html.replace("{b4800}", meterConfig->baud == 4800 ? "selected" : "");
html.replace("{b9600}", meterConfig->baud == 9600 ? "selected" : "");
html.replace("{b19200}", meterConfig->baud == 19200 ? "selected" : "");
html.replace("{b38400}", meterConfig->baud == 38400 ? "selected" : "");
html.replace("{b57600}", meterConfig->baud == 57600 ? "selected" : "");
html.replace("{b115200}", meterConfig->baud == 115200 ? "selected" : "");
html.replace("{c}", String(meterConfig->baud));
html.replace("{c2}", meterConfig->parity == 2 ? "selected" : "");
@@ -1482,8 +1486,13 @@ String AmsWebServer::getSerialSelectOptions(int selected) {
gpioOptions += "<option value=\"3\">UART0 (GPIO3)</option>";
}
#if defined(CONFIG_IDF_TARGET_ESP32S2)
int numGpio = 31;
int gpios[] = {4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,21,22,23,25,32,33,34,35,36,39,40,41,42,43,44};
int numGpio = 30;
int gpios[] = {4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,21,22,23,25,32,33,34,35,36,39,40,41,42,43,44};
if(selected == 18) {
gpioOptions += "<option value=\"18\" selected>UART1 (GPIO18)</option>";
} else {
gpioOptions += "<option value=\"18\">UART1 (GPIO18)</option>";
}
#elif defined(ESP32)
int numGpio = 24;
int gpios[] = {4,5,6,7,8,10,11,12,13,14,15,17,18,19,21,22,23,25,32,33,34,35,36,39};
@@ -1605,11 +1614,15 @@ void AmsWebServer::firmwareHtml() {
String html = String((const __FlashStringHelper*) FIRMWARE_HTML);
#if defined(ESP8266)
html.replace("{chipset}", "ESP8266");
html.replace("{chipset}", "ESP8266");
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
html.replace("{chipset}", "ESP32S2");
html.replace("{chipset}", "ESP32S2");
#elif defined(ESP32)
html.replace("{chipset}", "ESP32");
#if defined(CONFIG_FREERTOS_UNICORE)
html.replace("{chipset}", "ESP32SOLO");
#else
html.replace("{chipset}", "ESP32");
#endif
#endif
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
@@ -1657,8 +1670,9 @@ void AmsWebServer::firmwareDownload() {
printI("Downloading firmware...");
HTTPClient httpClient;
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setTimeout(20000);
httpClient.addHeader("User-Agent", "ams2mqtt/" + String(VERSION));
httpClient.setReuse(false);
httpClient.setTimeout(60000);
httpClient.setUserAgent("ams2mqtt/" + String(VERSION));
#if defined(ESP8266)
WiFiClient client;
@@ -1687,18 +1701,28 @@ void AmsWebServer::firmwareDownload() {
return;
*/
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
WiFiClientSecure client;
client.setInsecure();
httpClient.setConnectTimeout(60000);
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32s2-" + versionStripped + ".bin";
httpClient.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
#elif defined(ESP32)
WiFiClientSecure client;
client.setInsecure();
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32-" + versionStripped + ".bin";
httpClient.setConnectTimeout(60000);
#if defined(CONFIG_FREERTOS_UNICORE)
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32solo-" + versionStripped + ".bin";
#else
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32-" + versionStripped + ".bin";
#endif
httpClient.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
#endif
printD("Downloading from URL:");
printD(url);
#if defined(ESP8266)
if(httpClient.begin(client, url)) {
#elif defined(ESP32)
if(httpClient.begin(url)) {
#endif
printD("HTTP client setup successful");
int status = httpClient.GET();
if(status == HTTP_CODE_OK) {
@@ -1726,8 +1750,8 @@ void AmsWebServer::firmwareDownload() {
}
} else {
printE("Communication error: ");
debugger->printf("%d\n", status);
printE(httpClient.errorToString(status));
printI(url);
printD(httpClient.getString());
server.sendHeader("Location","/");
server.send(303);
@@ -1738,7 +1762,6 @@ void AmsWebServer::firmwareDownload() {
server.send(303);
}
httpClient.end();
client.stop();
} else {
printI("No firmware version specified...");
server.sendHeader("Location","/");
@@ -1857,7 +1880,7 @@ void AmsWebServer::mqttCa() {
}
LittleFS.end();
} else {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
}
}
@@ -1869,7 +1892,7 @@ void AmsWebServer::mqttCaUpload() {
uploadFile(FILE_MQTT_CA);
HTTPUpload& upload = server.upload();
if(upload.status == UPLOAD_FILE_END) {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
@@ -1885,7 +1908,7 @@ void AmsWebServer::mqttCaDelete() {
if(!uploading) { // Not an upload
deleteFile(FILE_MQTT_CA);
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) {
@@ -1911,7 +1934,7 @@ void AmsWebServer::mqttCert() {
}
LittleFS.end();
} else {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
}
}
@@ -1923,7 +1946,7 @@ void AmsWebServer::mqttCertUpload() {
uploadFile(FILE_MQTT_CERT);
HTTPUpload& upload = server.upload();
if(upload.status == UPLOAD_FILE_END) {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) {
@@ -1938,7 +1961,7 @@ void AmsWebServer::mqttCertDelete() {
if(!uploading) { // Not an upload
deleteFile(FILE_MQTT_CERT);
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) {
@@ -1964,7 +1987,7 @@ void AmsWebServer::mqttKey() {
}
LittleFS.end();
} else {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
}
}
@@ -1976,7 +1999,7 @@ void AmsWebServer::mqttKeyUpload() {
uploadFile(FILE_MQTT_KEY);
HTTPUpload& upload = server.upload();
if(upload.status == UPLOAD_FILE_END) {
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) {
@@ -1991,7 +2014,7 @@ void AmsWebServer::mqttKeyDelete() {
if(!uploading) { // Not an upload
deleteFile(FILE_MQTT_KEY);
server.sendHeader("Location","/config-mqtt");
server.sendHeader("Location","/mqtt");
server.send(303);
MqttConfig mqttConfig;
if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) {

View File

@@ -36,7 +36,11 @@
</div>
<select class="form-control sd" name="b">
<option value="2400" {b2400}>2400</option>
<option value="4800" {b4800}>4800</option>
<option value="9600" {b9600}>9600</option>
<option value="19200" {b19200}>19200</option>
<option value="38400" {b38400}>38400</option>
<option value="57600" {b57600}>57600</option>
<option value="115200" {b115200}>115200</option>
</select>
</div>
@@ -54,11 +58,14 @@
</select>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-3 col-6">
<div class="col-lg-2 col-md-4 col-sm-3 col-6">
<div class="m-2">
<label class="small"><input type="checkbox" name="i" value="true" {i}/> Invert <span class="d-none d-md-inline">signal</span></label>
</div>
</div>
<div class="col-lg-4 col-sm-8">
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Known-hardware-configurations" target="_blank">Known hardware configurations</a>
</div>
</div>
<div class="row">
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">

View File

@@ -8,7 +8,7 @@
<div class="input-group-prepend">
<span class="input-group-text">1</span>
</div>
<input class="form-control text-right" name="t0" type="number" min="5" max="255" step="1" value="{t0}"/>
<input class="form-control text-right" name="t0" type="number" min="1" max="255" step="1" value="{t0}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>