Merge branch 'main' into fix/mqtt_auto_reboot

This commit is contained in:
Gunnar Skjold 2025-11-06 18:25:57 +01:00
commit 60a6d76f6f
30 changed files with 1027 additions and 673 deletions

View File

@ -27,6 +27,7 @@ jobs:
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Cache Python dependencies

View File

@ -38,4 +38,4 @@ jobs:
run: localazy download -k localazy-keys.json
- name: Upload translations to S3
run: aws s3 sync ./localazy/language/ s3://amscloud-private/language/
run: aws s3 sync ./localazy/language/ s3://${{ secrets.AWS_S3_BUCKET }}/language/

84
.github/workflows/prerelease.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Release candidate build and upload
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Create release with release notes
id: create_release
uses: ncipollo/release-action@v1
with:
name: Release candidate v${{ steps.release_tag.outputs.tag }}
prerelease: true
outputs:
version: ${{ steps.release_tag.outputs.tag }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
esp32s2:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s2
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32s3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32c3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32c3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32solo:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32solo
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp8266:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp8266
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc

128
.github/workflows/release-deploy-env.yml vendored Normal file
View File

@ -0,0 +1,128 @@
name: Build with env and deploy
on:
workflow_call:
inputs:
env:
description: 'The environment to build for'
required: true
type: string
upload_url:
description: 'The upload URL for the release assets'
required: true
type: string
version:
description: 'The version tag for the release assets'
required: true
type: string
subfolder:
description: 'The subfolder in S3 to upload the binary to'
required: false
type: string
default: ''
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-north-1
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies
uses: actions/cache@v4
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '19.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
env:
GITHUB_TAG: v${{ inputs.version }}
run: pio lib install
- name: Build firmware
run: pio run -e ${{ inputs.env }}
- name: Create zip file
run: /bin/sh scripts/${{ inputs.env }}/mkzip.sh
- name: Upload binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ inputs.upload_url }}
asset_path: .pio/build/${{ inputs.env }}/firmware.bin
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
asset_content_type: application/octet-stream
- name: Upload zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ${{ inputs.env }}.zip
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.zip
asset_content_type: application/zip
- name: Create MD5 checksum file
run: md5sum .pio/build/${{ inputs.env }}/firmware.bin | cut -z -d ' ' -f 1 > firmware.md5
- name: Upload binary to S3
run: aws s3 cp .pio/build/${{ inputs.env }}/firmware.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
- name: Upload MD5 checksum to S3
run: aws s3 cp firmware.md5 s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.md5
- name: Upload bootloader to S3
run: aws s3 cp .pio/build/${{ inputs.env }}/bootloader.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-bootloader.bin
- name: Upload partition table to S3
run: aws s3 cp .pio/build/${{ inputs.env }}/partitions.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-partitions.bin
- name: Upload app0 to S3
run: aws s3 cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-app0.bin

View File

@ -1,213 +1,78 @@
name: Release
name: Release build and upload
on:
push:
tags:
- 'v*.*.*'
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
prepare:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Get release version for code
env:
GITHUB_REF: ${{ github.ref }}
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
- name: Check out code from repo
uses: actions/checkout@v4
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Create release with release notes
id: create_release
uses: ncipollo/release-action@v1
with:
name: Release v${{ steps.release_tag.outputs.tag }}
generateReleaseNotes: true
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies
uses: actions/cache@v4
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
outputs:
version: ${{ steps.release_tag.outputs.tag }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '19.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
run: pio lib install
- name: Create release with release notes
id: create_release
uses: ncipollo/release-action@v1
with:
name: Release v${{ steps.release_tag.outputs.tag }}
generateReleaseNotes: true
- name: Build esp8266 firmware
run: pio run -e esp8266
- name: Create esp8266 zip file
run: /bin/sh scripts/esp8266/mkzip.sh
- name: Upload esp8266 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/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp8266 zip 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: esp8266.zip
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32 firmware
run: pio run -e esp32
- name: Create esp32 zip file
run: /bin/sh scripts/esp32/mkzip.sh
- name: Upload esp32 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/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 zip 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: esp32.zip
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s2 firmware
run: pio run -e esp32s2
- name: Create esp32s2 zip file
run: /bin/sh scripts/esp32s2/mkzip.sh
- name: Upload esp32s2 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/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 zip 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: esp32s2.zip
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s3 firmware
run: pio run -e esp32s3
- name: Create esp32s3 zip file
run: /bin/sh scripts/esp32s3/mkzip.sh
- name: Upload esp32s3 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/esp32s3/firmware.bin
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s3 zip 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: esp32s3.zip
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32solo firmware
run: pio run -e esp32solo
- name: Create esp32solo zip file
run: /bin/sh scripts/esp32solo/mkzip.sh
- 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 esp32solo zip 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: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32c3 firmware
run: pio run -e esp32c3
- name: Create esp32c3 zip file
run: /bin/sh scripts/esp32c3/mkzip.sh
- name: Upload esp32c3 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/esp32c3/firmware.bin
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32c3 zip 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: esp32c3.zip
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
esp32s2:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s2
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32s3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32c3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32c3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32solo:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32solo
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp8266:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp8266
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}

View File

@ -230,6 +230,16 @@ bool AmsFirmwareUpdater::fetchNextVersion() {
http.setUserAgent("AMS-Firmware-Updater");
http.addHeader(F("Cache-Control"), "no-cache");
http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString);
http.addHeader(F("x-AMS-STA-MAC"), WiFi.macAddress());
http.addHeader(F("x-AMS-AP-MAC"), WiFi.softAPmacAddress());
http.addHeader(F("x-AMS-chip-size"), String(ESP.getFlashChipSize()));
http.addHeader(F("x-AMS-board-type"), String(hw->getBoardType(), 10));
if(meterState->getMeterType() != AmsTypeAutodetect) {
http.addHeader(F("x-AMS-meter-mfg"), String(meterState->getMeterType(), 10));
}
if(!meterState->getMeterModel().isEmpty()) {
http.addHeader(F("x-AMS-meter-model"), meterState->getMeterModel());
}
int status = http.GET();
if(status == 204) {
String nextVersion = http.header("x-version");

View File

@ -0,0 +1,9 @@
#pragma once
#include "AmsDataStorage.h"
class AmsJsonGenerator {
public:
static void generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
static void generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
};

View File

@ -0,0 +1,17 @@
#include "AmsJsonGenerator.h"
void AmsJsonGenerator::generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 0; i < 24; i++) {
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
}
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
}
void AmsJsonGenerator::generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 1; i < 32; i++) {
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
}
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
}

View File

@ -125,18 +125,6 @@ bool AmsMqttHandler::connect() {
#endif
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
if(strlen(mqttConfig.subscribeTopic) > 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
}
}
mqtt.publish(statusTopic, "online", true, 0);
mqtt.loop();
postConnect();
@ -192,7 +180,7 @@ bool AmsMqttHandler::loop() {
ESP.restart();
}
}
delay(10);
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
yield();
#if defined(ESP32)
esp_task_wdt_reset();

View File

@ -13,6 +13,12 @@
#include "PriceService.h"
struct EnergyAccountingPeak {
uint8_t day;
uint8_t hour;
uint16_t value;
};
struct EnergyAccountingPeak6 {
uint8_t day;
uint16_t value;
};
@ -32,34 +38,19 @@ struct EnergyAccountingData {
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData5 {
struct EnergyAccountingData6 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
uint16_t incomeYesterday;
uint16_t incomeThisMonth;
uint16_t incomeLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData4 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData2 {
uint8_t version;
uint8_t month;
uint16_t maxHour;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
int32_t costYesterday;
int32_t costThisMonth;
int32_t costLastMonth;
int32_t incomeYesterday;
int32_t incomeThisMonth;
int32_t incomeLastMonth;
uint32_t lastMonthImport;
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak6 peaks[5];
};
struct EnergyAccountingRealtimeData {
@ -142,7 +133,7 @@ private:
String currency = "";
void calcDayCost();
bool updateMax(uint16_t val, uint8_t day);
bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
};
#endif

View File

@ -72,15 +72,15 @@ bool EnergyAccounting::update(AmsData* amsData) {
this->realtimeData->currentHour = local.Hour;
this->realtimeData->currentDay = local.Day;
if(!load()) {
data = { 6, local.Month,
data = { 7, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
0, 0, 0, // Peak 1
0, 0, 0, // Peak 2
0, 0, 0, // Peak 3
0, 0, 0, // Peak 4
0, 0, 0 // Peak 5
};
}
init = true;
@ -97,7 +97,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
ret |= updateMax(val, oneHrAgoLocal.Day);
ret |= updateMax(val, oneHrAgoLocal.Day, oneHrAgoLocal.Hour);
this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
if(local.Hour > 0) {
@ -407,85 +407,29 @@ bool EnergyAccounting::load() {
char buf[file.size()];
file.readBytes(buf, file.size());
if(buf[0] == 6) {
if(buf[0] == 7) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else if(buf[0] == 5) {
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
this->data = { 6, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
((uint32_t) data->incomeYesterday) * 10,
((uint32_t) data->incomeThisMonth) * 100,
((uint32_t) data->incomeLastMonth) * 100,
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 3) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
this->data = { 5, data->month,
data->costYesterday * 10,
} else if(buf[0] == 6) {
EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
this->data = { 7, data->month,
data->costYesterday,
data->costThisMonth,
data->costLastMonth,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
data->incomeYesterday,
data->incomeThisMonth,
data->incomeLastMonth,
data->lastMonthImport,
data->lastMonthExport,
data->lastMonthAccuracy,
data->peaks[0].day, 0, data->peaks[0].value,
data->peaks[1].day, 0, data->peaks[1].value,
data->peaks[2].day, 0, data->peaks[2].value,
data->peaks[3].day, 0, data->peaks[3].value,
data->peaks[4].day, 0, data->peaks[4].value
};
ret = true;
} else {
data = { 5, 0,
0, 0, 0, // Cost
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
};
if(buf[0] == 2) {
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
this->data.month = data->month;
this->data.costYesterday = data->costYesterday * 10;
this->data.costThisMonth = data->costThisMonth;
this->data.costLastMonth = data->costLastMonth;
uint8_t b = 0;
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
this->data.peaks[b].day = b;
memcpy(&this->data.peaks[b].value, buf+i, 2);
b++;
if(b >= config->hours || b >= 5) break;
}
ret = true;
} else {
ret = false;
}
}
file.close();
@ -518,11 +462,12 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
this->data = data;
}
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
for(uint8_t i = 0; i < 5; i++) {
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
if(val > data.peaks[i].value) {
data.peaks[i].day = day;
data.peaks[i].hour = hour;
data.peaks[i].value = val;
return true;
}

View File

@ -15,13 +15,13 @@
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
#endif
this->boardType = boardType;
this->hw = hw;
setHomeAssistantConfig(config);
setHomeAssistantConfig(config, hostname);
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
@ -36,7 +36,7 @@ public:
uint8_t getFormat();
void setHomeAssistantConfig(HomeAssistantConfig config);
void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
private:
uint8_t boardType;

View File

@ -19,7 +19,7 @@
#include <esp_task_wdt.h>
#endif
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config) {
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = false;
if(strlen(config.discoveryNameTag) > 0) {
@ -34,14 +34,6 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType);
char hostname[32];
#if defined(ESP8266)
strcpy(hostname, WiFi.hostname().c_str());
#elif defined(ESP32)
strcpy(hostname, WiFi.getHostname());
#endif
stripNonAscii((uint8_t*) hostname, 32, false);
deviceUid = String(hostname); // Maybe configurable in the future?
if(strlen(config.discoveryHostname) > 0) {
@ -764,9 +756,10 @@ bool HomeAssistantMqttHandler::publishRaw(String data) {
bool HomeAssistantMqttHandler::publishFirmware() {
if(!fInit) {
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"uniq_id\":\"%s_fwupgrade\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
sensorNamePrefix.c_str(),
pubTopic.c_str(),
deviceUid.c_str(),
subTopic.c_str()
);
fInit = mqtt.publish(updateTopic + "/" + deviceUid + "/config", json, true, 0);

View File

@ -67,7 +67,9 @@ private:
bool ledInvert, rgbInvert;
uint8_t vccPin, vccGnd_r, vccVcc_r;
float vccOffset, vccMultiplier;
float vcc = 3.3; // Last known Vcc
float maxVcc = 3.25; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
unsigned long lastVccRead = 0;
uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc;

View File

@ -657,8 +657,12 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
}
bool HwTools::isVoltageOptimal(float range) {
if(boardType >= 5 && boardType <= 7 && maxVcc > 2.8) { // Pow-*
float vcc = getVcc();
if(boardType >= 1 && boardType <= 8 && maxVcc > 2.8) { // BUS-Power boards
unsigned long now = millis();
if(now - lastVccRead > 250) {
vcc = getVcc();
lastVccRead = now;
}
if(vcc > 3.4 || vcc < 2.8) {
maxVcc = 0; // Voltage is outside the operating range, we have to assume voltage is OK
} else if(vcc > maxVcc) {

View File

@ -12,14 +12,13 @@
class JsonMqttHandler : public AmsMqttHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->hw = hw;
};
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->hw = hw;
};
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif
this->hw = hw;
this->ds = ds;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
@ -27,12 +26,16 @@ public:
bool publishRaw(String data);
bool publishFirmware();
bool postConnect();
void onMessage(String &topic, String &payload);
uint8_t getFormat();
private:
HwTools* hw;
AmsDataStorage* ds;
uint16_t appendJsonHeader(AmsData* data);
uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos);
bool publishList1(AmsData* data, EnergyAccounting* ea);

View File

@ -8,6 +8,18 @@
#include "FirmwareVersion.h"
#include "hexutils.h"
#include "Uptime.h"
#include "AmsJsonGenerator.h"
bool JsonMqttHandler::postConnect() {
if(!subTopic.isEmpty() && !mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), subTopic.c_str());
return false;
}
return true;
}
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0) {
@ -67,14 +79,24 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
} else {
memset(pf, 0, 4);
}
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"),
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
strlen(pf) == 0 ? "},\"realtime\":{" : ",",
pf,
ea->getUseThisHour(),
pf,
ea->getUseToday(),
pf,
ea->getUseThisMonth(),
pf,
ea->getCurrentThreshold(),
pf,
ea->getMonthMax(),
@ -82,6 +104,9 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
ea->getProducedThisHour(),
pf,
ea->getProducedToday(),
pf,
ea->getProducedThisMonth(),
peaks.c_str(),
strlen(pf) == 0 ? "}" : ""
);
}
@ -447,11 +472,35 @@ bool JsonMqttHandler::publishFirmware() {
}
void JsonMqttHandler::onMessage(String &topic, String &payload) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Received command [%s] to [%s]\n"), payload.c_str(), topic.c_str());
if(topic.equals(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - this is our subscribed topic\n"));
if(payload.equals("fwupgrade")) {
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
updater->setTargetVersion(updater->getNextVersion());
}
} else if(payload.equals("dayplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/dayplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateDayPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
} else if(payload.equals("monthplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/monthplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateMonthPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
}
}
}

View File

@ -12,7 +12,22 @@
#include "DataParser.h"
#include "Cosem.h"
struct Lng2Data_3p {
struct Lng2Data_3p_0b {
CosemBasic header;
CosemLongUnsigned u1;
CosemLongUnsigned u2;
CosemLongUnsigned u3;
CosemLongUnsigned i1;
CosemLongUnsigned i2;
CosemLongUnsigned i3;
CosemDLongUnsigned activeImport;
CosemDLongUnsigned activeExport;
CosemDLongUnsigned acumulatedImport;
CosemDLongUnsigned accumulatedExport;
CosemString meterId;
} __attribute__((packed));
struct Lng2Data_3p_0e {
CosemBasic header;
CosemLongUnsigned u1;
CosemLongUnsigned u2;

View File

@ -14,7 +14,33 @@ LNG2::LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, Meter
meterType = AmsTypeLandisGyr;
this->packageTimestamp = ctx.timestamp;
Lng2Data_3p* d = (Lng2Data_3p*) payload;
Lng2Data_3p_0e* d = (Lng2Data_3p_0e*) payload;
this->l1voltage = ntohs(d->u1.data);
this->l2voltage = ntohs(d->u2.data);
this->l3voltage = ntohs(d->u3.data);
this->l1current = ntohs(d->i1.data) / 100.0;
this->l2current = ntohs(d->i2.data) / 100.0;
this->l3current = ntohs(d->i3.data) / 100.0;
this->activeImportPower = ntohl(d->activeImport.data);
this->activeExportPower = ntohl(d->activeExport.data);
this->activeImportCounter = ntohl(d->acumulatedImport.data) / 1000.0;
this->activeExportCounter = ntohl(d->accumulatedExport.data) / 1000.0;
char str[64];
uint8_t str_len = getString((CosemData*) &d->meterId, str);
if(str_len > 0) {
this->meterId = String(str);
}
listType = 3;
lastUpdateMillis = millis64();
} else if(h->length == 0x0b) {
apply(meterState);
meterType = AmsTypeLandisGyr;
this->packageTimestamp = ctx.timestamp;
Lng2Data_3p_0b* d = (Lng2Data_3p_0b*) payload;
this->l1voltage = ntohs(d->u1.data);
this->l2voltage = ntohs(d->u2.data);
this->l3voltage = ntohs(d->u3.data);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -188,3 +188,22 @@ svg {
display: block;
text-align: center;
}
.tooltip {
border: 1px solid #ddd;
background: white;
border-radius: 4px;
padding: 4px;
position: absolute;
}
.tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -9px;
border-width: 9px;
border-style: solid;
border-color: #ddd transparent transparent transparent;
}

View File

@ -1,5 +1,6 @@
<script>
import { Link } from "svelte-navigator";
import { tooltip } from './tooltip';
export let config;
@ -61,7 +62,7 @@
{#if !isNaN(yScale(tick.value))}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
<text y="-4" x={tick.align == 'right' ? '90%' : ''}>{tick.label}</text>
</g>
{/if}
{/each}
@ -83,7 +84,7 @@
<g class='bars'>
{#each config.points as point, i}
{#if !isNaN(xScale(i)) && !isNaN(yScale(point.value))}
<g>
<g data-title="{point.title}" use:tooltip>
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
@ -102,9 +103,6 @@
transform="translate({xScale(i) + barWidth/2} {yScale(point.value) > yScale(0) - labelOffset ? yScale(point.value) - labelOffset : yScale(point.value) + 10}) rotate({point.labelAngle ? point.labelAngle : barWidth < vertSwitch ? 90 : 0})"
>{point.label}</text>
{#if point.title}
<title>{point.title}</title>
{/if}
{/if}
{/if}
</g>

View File

@ -128,7 +128,7 @@
{/if}
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("NO") || data.pr.startsWith("10YNO") || data.pr.startsWith('10Y1001A1001A4')))}
<div class="cnt h-64">
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} translations={translations}/>
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} realtime={data.ea} translations={translations}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.l, data.hm == 1)}

View File

@ -1,5 +1,5 @@
<script>
import { zeropad } from './Helpers.js';
import { ampcol, zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let title;
@ -12,6 +12,7 @@
let min = 0;
export let tariffData;
export let realtime;
$: {
let i = 0;
@ -24,16 +25,37 @@
label: 0
});
console.log(realtime);
if(tariffData && !isNaN(realtime?.h?.u)) {
points.push({
label: realtime.h.u.toFixed(2),
value: realtime.h.u,
title: realtime.h.u.toFixed(2) + ' kWh',
color: ampcol(realtime.h.u/tariffData.c*100.0)
});
xTicks.push({
label: translations.common?.now ?? "Now"
});
}
if(tariffData && tariffData.p) {
for(i = 0; i < tariffData.p.length; i++) {
let peak = tariffData.p[i];
let daylabel = peak.d > 0 ? zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1)) : "-";
let title = daylabel;
if(!isNaN(peak.h))
title = title + ' ' + zeropad(peak.h) + ':00';
title = title + ': ' + peak.v.toFixed(2) + ' kWh';
points.push({
label: peak.v.toFixed(2),
title: peak.v.toFixed(2) + ' kWh',
value: peak.v,
title: title,
color: dark ? '#5c2da5' : '#7c3aed'
});
xTicks.push({
label: peak.d > 0 ? zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1)) : "-"
label: daylabel
})
max = Math.max(max, peak.v);
}
@ -71,7 +93,7 @@
config = {
title: title,
dark: document.documentElement.classList.contains('dark'),
padding: { top: 20, right: 35, bottom: 20, left: 35 },
padding: { top: 20, right: 20, bottom: 20, left: 20 },
y: {
min: min,
max: max,

View File

@ -0,0 +1,14 @@
<script>
export let title;
export let x;
export let y;
let width;
let height;
</script>
<div
class="tooltip"
style="top: {y - height - 10}px; left: {x - (width / 2)}px;"
bind:clientHeight={height}
bind:clientWidth={width}
>{title}</div>

View File

@ -0,0 +1,41 @@
import Tooltip from './Tooltip.svelte';
export function tooltip(element) {
let title;
let tooltipComponent;
function click(event) {
if(tooltipComponent) tooltipComponent.$destroy();
title = element.dataset.title || element.getAttribute('title');
var rect = element.getBoundingClientRect();
tooltipComponent = new Tooltip({
props: {
title: title,
x: rect.left + window.scrollX + (rect.width / 2),
y: rect.top + window.scrollY,
},
target: document.body,
});
}
function mouseLeave() {
if(tooltipComponent) {
setTimeout(() => {
tooltipComponent.$destroy();
tooltipComponent = null;
}, 500);
}
}
element.addEventListener('click', click);
element.addEventListener('mouseleave', mouseLeave);
return {
destroy() {
element.removeEventListener('click', click);
element.removeEventListener('mouseleave', mouseLeave);
}
}
}

View File

@ -9,6 +9,7 @@
#include "FirmwareVersion.h"
#include "base64.h"
#include "hexutils.h"
#include "AmsJsonGenerator.h"
#include "html/index_html.h"
#include "html/index_css.h"
@ -682,12 +683,7 @@ void AmsWebServer::dayplotJson() {
if(ds == NULL) {
notFound();
} else {
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 0; i < 24; i++) {
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
}
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
AmsJsonGenerator::generateDayPlotJson(ds, buf, BufferSize);
addConditionalCloudHeaders();
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
@ -705,12 +701,7 @@ void AmsWebServer::monthplotJson() {
if(ds == NULL) {
notFound();
} else {
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 1; i < 32; i++) {
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
}
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
AmsJsonGenerator::generateMonthPlotJson(ds, buf, BufferSize);
addConditionalCloudHeaders();
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
@ -2119,8 +2110,9 @@ void AmsWebServer::tariffJson() {
String peaks;
for(uint8_t x = 0;x < min((uint8_t) 5, eac->hours); x++) {
EnergyAccountingPeak peak = ea->getPeak(x+1);
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"v\":%.2f}"),
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"h\":%d,\"v\":%.2f}"),
peak.day,
peak.hour,
peak.value / 100.0
);
buf[len] = '\0';
@ -2618,7 +2610,7 @@ void AmsWebServer::configFileDownload() {
EnergyAccountingConfig eac;
config->getEnergyAccountingConfig(eac);
EnergyAccountingData ead = ea->getData();
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f %.2f %.2f"),
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %.2f %.2f"),
ead.version,
ead.month,
ea->getCostYesterday(),
@ -2628,14 +2620,19 @@ void AmsWebServer::configFileDownload() {
ea->getIncomeThisMonth(),
ea->getIncomeLastMonth(),
ead.peaks[0].day,
ead.peaks[0].hour,
ead.peaks[0].value / 100.0,
ead.peaks[1].day,
ead.peaks[1].hour,
ead.peaks[1].value / 100.0,
ead.peaks[2].day,
ead.peaks[2].hour,
ead.peaks[2].value / 100.0,
ead.peaks[3].day,
ead.peaks[3].hour,
ead.peaks[3].value / 100.0,
ead.peaks[4].day,
ead.peaks[4].hour,
ead.peaks[4].value / 100.0,
ea->getUseLastMonth(),
ea->getProducedLastMonth()

View File

@ -2,7 +2,7 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsJsonGenerator, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py

View File

@ -215,7 +215,6 @@ void connectToNetwork();
void toggleSetupMode();
void postConnect();
void MQTT_connect();
void handleNtpChange();
void handleDataSuccess(AmsData* data);
void handleTemperature(unsigned long now);
void handleSystem(unsigned long now);
@ -223,11 +222,27 @@ void handleButton(unsigned long now);
void handlePriceService(unsigned long now);
void handleClear(unsigned long now);
void handleUiLanguage();
void handleEnergyAccountingChanged();
bool handleVoltageCheck();
void handleEnergyAccounting();
bool readHanPort();
void errorBlink();
void handleUpdater();
char ntpServerName[64] = "";
void handleNtp();
#if defined(ESP8266)
void handleMdns();
#endif
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
void handleEnergySpeedometer();
#endif
#if defined(AMS_CLOUD)
void handleCloud();
#endif
void handleMqtt();
void handleWebserver();
void handleSmartConfig();
void handleMeterConfig();
uint8_t pulses = 0;
void onPulse();
@ -498,7 +513,6 @@ void setup() {
if(config.hasConfig()) {
config.print(&Debug);
connectToNetwork();
handleNtpChange();
ds.load();
} else {
debugI_P(PSTR("No configuration, booting AP"));
@ -550,6 +564,9 @@ uint64_t lastErrorBlink = 0;
unsigned long lastVoltageCheck = 0;
int lastError = 0;
bool vccLevel1 = true;
bool vccLevel2 = true;
void loop() {
unsigned long now = millis();
unsigned long start = now;
@ -567,7 +584,7 @@ void loop() {
errorBlink();
}
// Only do normal stuff if we're not booted as AP
// Only do normal stuff if we're not booted into setup mode
if (!setupMode) {
if (ch != NULL && !ch->isConnected()) {
if(networkConnected) {
@ -586,120 +603,50 @@ void loop() {
if(!networkConnected) {
postConnect();
}
if(config.isNtpChanged()) {
handleNtpChange();
}
#if defined ESP8266
if(mdnsEnabled) {
start = millis();
MDNS.update();
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to update mDNS"), millis()-start);
}
}
#endif
if (mqttEnabled || config.isMqttChanged()) {
if(mqttHandler == NULL || !mqttHandler->connected() || config.isMqttChanged()) {
if(mqttHandler != NULL && config.isMqttChanged()) {
mqttHandler->disconnect();
}
MQTT_connect();
config.ackMqttChange();
}
} else if(mqttHandler != NULL) {
mqttHandler->disconnect();
}
handleUpdater();
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
if(sysConfig.energyspeedometer == 7) {
if(!meterState.getMeterId().isEmpty()) {
if(energySpeedometer == NULL) {
uint16_t chipId;
#if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
#else
chipId = ESP.getChipId();
#endif
strcpy(energySpeedometerConfig.clientId, (String("ams") + String(chipId, HEX)).c_str());
energySpeedometer = new JsonMqttHandler(energySpeedometerConfig, &Debug, (char*) commonBuffer, &hw, &updater);
energySpeedometer->setCaVerification(false);
}
if(!energySpeedometer->connected()) {
lwmqtt_err_t err = energySpeedometer->lastError();
if(err > 0)
debugE_P(PSTR("Energyspeedometer connector reporting error (%d)"), err);
energySpeedometer->connect();
energySpeedometer->publishSystem(&hw, ps, &ea);
}
energySpeedometer->loop();
delay(10);
}
} else if(energySpeedometer != NULL) {
if(energySpeedometer->connected()) {
energySpeedometer->disconnect();
energySpeedometer->loop();
} else {
delete energySpeedometer;
energySpeedometer = NULL;
}
}
#endif
try {
// Only do these tasks if we have super-smooth voltage
if(hw.isVoltageOptimal(0.1)) {
handleNtp();
#if defined(ESP8266)
handleMdns();
#endif
#if defined(ESP32)
#if defined(ENERGY_SPEEDOMETER_PASS)
handleEnergySpeedometer();
#endif
#endif
handlePriceService(now);
} catch(const std::exception& e) {
debugE_P(PSTR("Exception in PriceService loop (%s)"), e.what());
}
start = millis();
ws.loop();
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle web"), millis()-start);
#if defined(AMS_CLOUD)
handleCloud();
#endif
handleUiLanguage();
vccLevel1 = true;
} else if(vccLevel1) {
vccLevel1 = false;
debugW_P(PSTR("Vcc below level 1"));
}
if(mqttHandler != NULL) {
start = millis();
mqttHandler->loop();
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle mqtt"), millis()-start);
// Only do these task if we have smooth voltage
if(hw.isVoltageOptimal(0.2)) {
handleMqtt();
vccLevel2 = true;
} else if(vccLevel2) {
vccLevel2 = false;
debugW_P(PSTR("Vcc below level 2"));
}
handleWebserver();
#if defined(ESP32)
// At this point, if the voltage is not optimal, disconnect from WiFi to preserve power
if(!hw.isVoltageOptimal(0.35)) {
if(WiFi.getMode() == WIFI_STA) {
debugW_P(PSTR("Vcc dropped below limit, disconnecting WiFi for 5 seconds to preserve power"));
ch->disconnect(5000);
}
}
#if defined(_CLOUDCONNECTOR_H)
if(config.isCloudChanged()) {
CloudConfig cc;
if(config.getCloudConfig(cc) && cc.enabled) {
if(cloud == NULL) {
cloud = new CloudConnector(&Debug);
}
NtpConfig ntp;
config.getNtpConfig(ntp);
if(cloud->setup(cc, meterConfig, sysConfig, ntp, &hw, &rdc, ps)) {
config.setCloudConfig(cc);
}
cloud->setConnectionHandler(ch);
PriceServiceConfig price;
config.getPriceServiceConfig(price);
cloud->setPriceConfig(price);
EnergyAccountingConfig *eac = ea.getConfig();
cloud->setEnergyAccountingConfig(*eac);
ws.setCloud(cloud);
} else if(cloud != NULL) {
delete cloud;
cloud = NULL;
}
config.ackCloudConfig();
}
if(cloud != NULL) {
cloud->update(meterState, ea);
}
#endif
#if defined(ZMART_CHARGE)
@ -760,45 +707,255 @@ void loop() {
debugW_P(PSTR("Used %dms to handle firmware updater"), end-start);
}
}
#if defined(ESP32)
if(now - lastVoltageCheck > 1000) {
start = millis();
handleVoltageCheck();
end = millis();
lastVoltageCheck = now;
if(end-start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle language update"), end-start);
}
}
#endif
} else {
if(WiFi.smartConfigDone()) {
debugI_P(PSTR("Smart config DONE!"));
NetworkConfig network;
config.getNetworkConfig(network);
strcpy(network.ssid, WiFi.SSID().c_str());
strcpy(network.psk, WiFi.psk().c_str());
network.mode = 1;
network.mdns = true;
config.setNetworkConfig(network);
SystemConfig sys;
config.getSystemConfig(sys);
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
config.setSystemConfig(sys);
config.save();
delay(1000);
ESP.restart();
}
handleSmartConfig();
if(dnsServer != NULL) {
dnsServer->processNextRequest();
}
ws.loop();
}
handleMeterConfig();
handleEnergyAccounting();
try {
start = millis();
if(readHanPort() || now - meterState.getLastUpdateMillis() > 30000) {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to read HAN port (true)"), millis()-start);
}
if(hw.isVoltageOptimal(0.1)) {
handleTemperature(now);
handleSystem(now);
}
hw.setBootSuccessful(true);
} else {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to read HAN port (false)"), millis()-start);
}
}
if(millis() - meterState.getLastUpdateMillis() > 1800000 && !ds.isHappy(time(nullptr))) {
handleClear(now);
}
} catch(const std::exception& e) {
debugE_P(PSTR("Exception in readHanPort (%s)"), e.what());
meterState.setLastError(METER_ERROR_EXCEPTION);
}
delay(10); // Needed for auto modem sleep
start = millis();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
yield();
end = millis();
if(end-start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to feed WDT"), end-start);
}
if(end-now > SLOW_PROC_TRIGGER_MS*2) {
debugW_P(PSTR("loop() used %dms"), end-now);
}
}
void handleUpdater() {
unsigned long start = millis();
updater.loop();
if(updater.isUpgradeInformationChanged()) {
UpgradeInformation upinfo;
updater.getUpgradeInformation(upinfo);
config.setUpgradeInformation(upinfo);
updater.ackUpgradeInformationChanged();
if(mqttHandler != NULL)
mqttHandler->publishFirmware();
if(upinfo.errorCode == AMS_UPDATE_ERR_SUCCESS_SIGNAL) {
debugW_P(PSTR("Rebooting to firmware version %s"), upinfo.toVersion);
upinfo.errorCode == AMS_UPDATE_ERR_SUCCESS_CONFIRMED;
config.setUpgradeInformation(upinfo);
delay(1000);
ESP.restart();
}
}
unsigned long end = millis();
if(end-start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle firmware updater"), end-start);
}
}
void handleNtp() {
if(config.isNtpChanged()) {
NtpConfig ntp;
if(config.getNtpConfig(ntp)) {
tz = resolveTimezone(ntp.timezone);
if(ntp.enable && strlen(ntp.server) > 0) {
strcpy(ntpServerName, ntp.server);
} else if(ntp.enable) {
strcpy(ntpServerName, "pool.ntp.org");
} else {
memset(ntpServerName, 0, 64);
}
configTime(tz->toLocal(0), tz->toLocal(JULY1970)-JULY1970, ntpServerName, "", "");
sntp_servermode_dhcp(ntp.enable && ntp.dhcp ? 1 : 0); // Not implemented on ESP32?
ntpEnabled = ntp.enable;
ws.setTimezone(tz);
ds.setTimezone(tz);
ea.setTimezone(tz);
ps->setTimezone(tz);
}
config.ackNtpChange();
}
}
#if defined(ESP8266)
void handleMdns() {
if(mdnsEnabled) {
unsigned long start = millis();
MDNS.update();
unsigned long end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to update mDNS"), millis()-start);
}
}
}
#endif
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
void handleEnergySpeedometer() {
if(sysConfig.energyspeedometer == 7) {
if(!meterState.getMeterId().isEmpty()) {
if(energySpeedometer == NULL) {
uint16_t chipId;
#if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
#else
chipId = ESP.getChipId();
#endif
strcpy(energySpeedometerConfig.clientId, (String("ams") + String(chipId, HEX)).c_str());
energySpeedometer = new JsonMqttHandler(energySpeedometerConfig, &Debug, (char*) commonBuffer, &hw, &ds, &updater);
energySpeedometer->setCaVerification(false);
}
if(!energySpeedometer->connected()) {
lwmqtt_err_t err = energySpeedometer->lastError();
if(err > 0)
debugE_P(PSTR("Energyspeedometer connector reporting error (%d)"), err);
energySpeedometer->connect();
energySpeedometer->publishSystem(&hw, ps, &ea);
}
energySpeedometer->loop();
delay(10);
}
} else if(energySpeedometer != NULL) {
if(energySpeedometer->connected()) {
energySpeedometer->disconnect();
energySpeedometer->loop();
} else {
delete energySpeedometer;
energySpeedometer = NULL;
}
}
}
#endif
#if defined(AMS_CLOUD)
void handleCloud() {
if(config.isCloudChanged()) {
CloudConfig cc;
if(config.getCloudConfig(cc) && cc.enabled) {
if(cloud == NULL) {
cloud = new CloudConnector(&Debug);
}
NtpConfig ntp;
config.getNtpConfig(ntp);
if(cloud->setup(cc, meterConfig, sysConfig, ntp, &hw, &rdc, ps)) {
config.setCloudConfig(cc);
}
cloud->setConnectionHandler(ch);
PriceServiceConfig price;
config.getPriceServiceConfig(price);
cloud->setPriceConfig(price);
EnergyAccountingConfig *eac = ea.getConfig();
cloud->setEnergyAccountingConfig(*eac);
ws.setCloud(cloud);
} else if(cloud != NULL) {
delete cloud;
cloud = NULL;
}
config.ackCloudConfig();
}
if(cloud != NULL) {
cloud->update(meterState, ea);
}
}
#endif
void handleMqtt() {
if (mqttEnabled || config.isMqttChanged()) {
if(mqttHandler == NULL || !mqttHandler->connected() || config.isMqttChanged()) {
if(mqttHandler != NULL && config.isMqttChanged()) {
mqttHandler->disconnect();
}
MQTT_connect();
config.ackMqttChange();
}
} else if(mqttHandler != NULL) {
mqttHandler->disconnect();
}
if(mqttHandler != NULL) {
unsigned long start = millis();
mqttHandler->loop();
unsigned long end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle mqtt"), millis()-start);
}
}
}
void handleWebserver() {
unsigned long start = millis();
ws.loop();
unsigned long end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle web"), millis()-start);
}
}
void handleSmartConfig() {
if(WiFi.smartConfigDone()) {
debugI_P(PSTR("Smart config DONE!"));
NetworkConfig network;
config.getNetworkConfig(network);
strcpy(network.ssid, WiFi.SSID().c_str());
strcpy(network.psk, WiFi.psk().c_str());
network.mode = 1;
network.mdns = true;
config.setNetworkConfig(network);
SystemConfig sys;
config.getSystemConfig(sys);
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
config.setSystemConfig(sys);
config.save();
delay(1000);
ESP.restart();
}
}
void handleMeterConfig() {
if(config.isMeterChanged()) {
config.getMeterConfig(meterConfig);
if(meterConfig.source == METER_SOURCE_GPIO) {
@ -872,54 +1029,10 @@ void loop() {
ws.setMeterConfig(meterConfig.distributionSystem, meterConfig.mainFuse, meterConfig.productionCapacity);
config.ackMeterChanged();
}
if(config.isEnergyAccountingChanged()) {
handleEnergyAccountingChanged();
}
try {
start = millis();
if(readHanPort() || now - meterState.getLastUpdateMillis() > 30000) {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to read HAN port (true)"), millis()-start);
}
handleTemperature(now);
handleSystem(now);
hw.setBootSuccessful(true);
} else {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to read HAN port (false)"), millis()-start);
}
}
if(millis() - meterState.getLastUpdateMillis() > 1800000 && !ds.isHappy(time(nullptr))) {
handleClear(now);
}
} catch(const std::exception& e) {
debugE_P(PSTR("Exception in readHanPort (%s)"), e.what());
meterState.setLastError(METER_ERROR_EXCEPTION);
}
delay(10); // Needed for auto modem sleep
start = millis();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
yield();
end = millis();
if(end-start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to feed WDT"), end-start);
}
if(end-now > SLOW_PROC_TRIGGER_MS*2) {
debugW_P(PSTR("loop() used %dms"), end-now);
}
}
void handleUiLanguage() {
unsigned long start = millis();
if(config.isUiLanguageChanged()) {
debugD_P(PSTR("Language has changed"));
if(LittleFS.begin()) {
@ -969,6 +1082,10 @@ void handleUiLanguage() {
config.ackUiLanguageChange();
}
unsigned long end = millis();
if(end-start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle language update"), end-start);
}
}
void handleClear(unsigned long now) {
@ -982,42 +1099,18 @@ void handleClear(unsigned long now) {
}
}
void handleEnergyAccountingChanged() {
EnergyAccountingConfig *eac = ea.getConfig();
config.getEnergyAccountingConfig(*eac);
ea.setup(&ds, eac);
config.ackEnergyAccountingChange();
#if defined(_CLOUDCONNECTOR_H)
if(cloud != NULL) {
cloud->setEnergyAccountingConfig(*eac);
}
#endif
}
char ntpServerName[64] = "";
void handleNtpChange() {
NtpConfig ntp;
if(config.getNtpConfig(ntp)) {
tz = resolveTimezone(ntp.timezone);
if(ntp.enable && strlen(ntp.server) > 0) {
strcpy(ntpServerName, ntp.server);
} else if(ntp.enable) {
strcpy(ntpServerName, "pool.ntp.org");
} else {
memset(ntpServerName, 0, 64);
void handleEnergyAccounting() {
if(config.isEnergyAccountingChanged()) {
EnergyAccountingConfig *eac = ea.getConfig();
config.getEnergyAccountingConfig(*eac);
ea.setup(&ds, eac);
config.ackEnergyAccountingChange();
#if defined(AMS_CLOUD)
if(cloud != NULL) {
cloud->setEnergyAccountingConfig(*eac);
}
configTime(tz->toLocal(0), tz->toLocal(JULY1970)-JULY1970, ntpServerName, "", "");
sntp_servermode_dhcp(ntp.enable && ntp.dhcp ? 1 : 0); // Not implemented on ESP32?
ntpEnabled = ntp.enable;
ws.setTimezone(tz);
ds.setTimezone(tz);
ea.setTimezone(tz);
ps->setTimezone(tz);
#endif
}
config.ackNtpChange();
}
void handleSystem(unsigned long now) {
@ -1058,17 +1151,6 @@ void handleSystem(unsigned long now) {
}
}
bool handleVoltageCheck() {
if(!hw.isVoltageOptimal()) {
if(WiFi.getMode() == WIFI_STA) {
debugW_P(PSTR("Vcc dropped below limit, disconnecting WiFi for 5 seconds to preserve power"));
ch->disconnect(5000);
}
return false;
}
return true;
}
void handleTemperature(unsigned long now) {
unsigned long start, end;
if(now - lastTemperatureRead > 15000) {
@ -1088,51 +1170,55 @@ void handleTemperature(unsigned long now) {
}
void handlePriceService(unsigned long now) {
unsigned long start, end;
if(ps != NULL && ntpEnabled) {
start = millis();
if(ps->loop() && mqttHandler != NULL) {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to update prices"), millis()-start);
}
try {
unsigned long start, end;
if(ps != NULL && ntpEnabled) {
start = millis();
mqttHandler->publishPrices(ps);
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to publish prices to MQTT"), millis()-start);
}
} else {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle price API"), millis()-start);
}
}
}
if(config.isPriceServiceChanged()) {
PriceServiceConfig price;
if(config.getPriceServiceConfig(price) && price.enabled && strlen(price.area) > 0) {
if(ps == NULL) {
ps = new PriceService(&Debug);
ea.setPriceService(ps);
ws.setPriceService(ps);
#if defined(_CLOUDCONNECTOR_H)
if(cloud != NULL) {
cloud->setPriceConfig(price);
if(ps->loop() && mqttHandler != NULL) {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to update prices"), millis()-start);
}
start = millis();
mqttHandler->publishPrices(ps);
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to publish prices to MQTT"), millis()-start);
}
} else {
end = millis();
if(end - start > SLOW_PROC_TRIGGER_MS) {
debugW_P(PSTR("Used %dms to handle price API"), millis()-start);
}
#endif
}
ps->setup(price);
} else if(ps != NULL) {
delete ps;
ps = NULL;
ws.setPriceService(NULL);
}
ws.setPriceSettings(price.area, price.currency);
config.ackPriceServiceChange();
ea.setCurrency(price.currency);
if(config.isPriceServiceChanged()) {
PriceServiceConfig price;
if(config.getPriceServiceConfig(price) && price.enabled && strlen(price.area) > 0) {
if(ps == NULL) {
ps = new PriceService(&Debug);
ea.setPriceService(ps);
ws.setPriceService(ps);
#if defined(_CLOUDCONNECTOR_H)
if(cloud != NULL) {
cloud->setPriceConfig(price);
}
#endif
}
ps->setup(price);
} else if(ps != NULL) {
delete ps;
ps = NULL;
ws.setPriceService(NULL);
}
ws.setPriceSettings(price.area, price.currency);
config.ackPriceServiceChange();
ea.setCurrency(price.currency);
}
} catch(const std::exception& e) {
debugE_P(PSTR("Exception in PriceService loop (%s)"), e.what());
}
}
@ -1204,7 +1290,7 @@ void connectToNetwork() {
return;
}
lastConnectRetry = millis();
if(!handleVoltageCheck()) {
if(!hw.isVoltageOptimal(0.1) && (millis() - lastConnectRetry) < 60000) {
debugW_P(PSTR("Voltage is not high enough to reconnect"));
return;
}
@ -1358,20 +1444,18 @@ void handleDataSuccess(AmsData* data) {
if(!setupMode && !hw.ledBlink(LED_GREEN, 1))
hw.ledBlink(LED_INTERNAL, 1);
if(mqttHandler != NULL) {
if(mqttHandler != NULL && hw.isVoltageOptimal(0.2)) {
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
yield();
if(mqttHandler->publish(data, &meterState, &ea, ps)) {
delay(10);
}
mqttHandler->publish(data, &meterState, &ea, ps);
}
#if defined(ESP32) && defined(ENERGY_SPEEDOMETER_PASS)
if(energySpeedometer != NULL && energySpeedometer->publish(&meterState, &meterState, &ea, ps)) {
delay(10);
if(energySpeedometer != NULL && hw.isVoltageOptimal(0.1)) {
energySpeedometer->publish(&meterState, &meterState, &ea, ps);
}
#endif
@ -1546,8 +1630,10 @@ void MQTT_connect() {
case 4: {
HomeAssistantConfig haconf;
config.getHomeAssistantConfig(haconf);
NetworkConfig network;
ch->getCurrentConfig(network);
HomeAssistantMqttHandler* hamh = (HomeAssistantMqttHandler*) &mqttHandler;
hamh->setHomeAssistantConfig(haconf);
hamh->setHomeAssistantConfig(haconf, network.hostname);
break;
}
}
@ -1559,7 +1645,7 @@ void MQTT_connect() {
case 0:
case 5:
case 6:
mqttHandler = new JsonMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, &hw, &updater);
mqttHandler = new JsonMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, &hw, &ds, &updater);
break;
case 1:
case 2:
@ -1573,7 +1659,9 @@ void MQTT_connect() {
case 4:
HomeAssistantConfig haconf;
config.getHomeAssistantConfig(haconf);
mqttHandler = new HomeAssistantMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, sysConfig.boardType, haconf, &hw, &updater);
NetworkConfig network;
ch->getCurrentConfig(network);
mqttHandler = new HomeAssistantMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, sysConfig.boardType, haconf, &hw, &updater, network.hostname);
break;
case 255:
mqttHandler = new PassthroughMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, &updater);
@ -2084,11 +2172,11 @@ void configFileParse() {
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
0, 0, 0, // Peak 1
0, 0, 0, // Peak 2
0, 0, 0, // Peak 3
0, 0, 0, // Peak 4
0, 0, 0 // Peak 5
};
uint8_t peak = 0;
uint64_t totalImport = 0, totalExport = 0;
@ -2104,7 +2192,7 @@ void configFileParse() {
} else if(i == 2) {
float val = String(pch).toFloat();
if(val > 0.0) {
ead.peaks[0] = { 1, (uint16_t) (val*100) };
ead.peaks[0] = { 1, 0, (uint16_t) (val*100) };
}
} else if(i == 3) {
float val = String(pch).toFloat();
@ -2116,7 +2204,6 @@ void configFileParse() {
float val = String(pch).toFloat();
ead.costLastMonth = val * 100;
} else if(i >= 6 && i < 18) {
uint8_t hour = i-6;
{
long val = String(pch).toInt();
ead.peaks[peak].day = val;
@ -2129,6 +2216,47 @@ void configFileParse() {
}
peak++;
}
} else if(ead.version < 7) {
if(i == 1) {
long val = String(pch).toInt();
ead.month = val;
} else if(i == 2) {
float val = String(pch).toFloat();
ead.costYesterday = val * 100;
} else if(i == 3) {
float val = String(pch).toFloat();
ead.costThisMonth = val * 100;
} else if(i == 4) {
float val = String(pch).toFloat();
ead.costLastMonth = val * 100;
} else if(i == 5) {
float val = String(pch).toFloat();
ead.incomeYesterday= val * 100;
} else if(i == 6) {
float val = String(pch).toFloat();
ead.incomeThisMonth = val * 100;
} else if(i == 7) {
float val = String(pch).toFloat();
ead.incomeLastMonth = val * 100;
} else if(i >= 8 && i < 18) {
{
long val = String(pch).toInt();
ead.peaks[peak].day = val;
}
pch = strtok (NULL, " ");
i++;
{
float val = String(pch).toFloat();
ead.peaks[peak].value = val * 100;
}
peak++;
} else if(i == 18) {
float val = String(pch).toFloat();
totalImport = val * 1000;
} else if(i == 19) {
float val = String(pch).toFloat();
totalExport = val * 1000;
}
} else {
if(i == 1) {
long val = String(pch).toInt();
@ -2151,14 +2279,19 @@ void configFileParse() {
} else if(i == 7) {
float val = String(pch).toFloat();
ead.incomeLastMonth = val * 100;
} else if(i >= 8 && i < 18) {
uint8_t hour = i-8;
} else if(i >= 8 && i < 23) {
{
long val = String(pch).toInt();
ead.peaks[peak].day = val;
}
pch = strtok (NULL, " ");
i++;
{
long val = String(pch).toInt();
ead.peaks[peak].hour = val;
}
pch = strtok (NULL, " ");
i++;
{
float val = String(pch).toFloat();
ead.peaks[peak].value = val * 100;
@ -2185,7 +2318,7 @@ void configFileParse() {
ead.lastMonthImport = importUpdate;
ead.lastMonthExport = exportUpdate;
ead.version = 6;
ead.version = 7;
ea.setData(ead);
sEa = true;
}