mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-01-12 00:02:53 +00:00
Added support for 15 minute price resolution (#1031)
* 15min prices WIP * WIP more changes for 15min prices * More work on 15min pricing * Fixed some errors * Some changes after testing * Graphical changes for 15min pricing * Adjustments on MQTT handlers after switching to 15min prices * Reverted some MQTT changes * Adapted HA integration for 15min pricing * Adapted JSON payload for 15min * Adjustments during testing * Set default price interval * Fixed refresh of price graph when data changes * Bugfixes * Fixed some issues with raw payload * Adjustments for meter timestamp from Kamstrup * Updated readme * Added detailed breakdown of payloads coming from Norwegian meters * Minor changes relating to price * Fixed byte alignment on price config * Changes to support RC upgraders
This commit is contained in:
parent
ffd8d46f2e
commit
c648546b61
1
.github/workflows/prerelease.yml
vendored
1
.github/workflows/prerelease.yml
vendored
@ -82,3 +82,4 @@ jobs:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
is_esp32: false
|
||||
|
||||
12
.github/workflows/release-deploy-env.yml
vendored
12
.github/workflows/release-deploy-env.yml
vendored
@ -20,6 +20,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
is_esp32:
|
||||
description: 'Whether the build is for ESP32 based firmware'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@ -79,11 +84,11 @@ jobs:
|
||||
CI: false
|
||||
|
||||
- name: PlatformIO lib install
|
||||
env:
|
||||
GITHUB_TAG: v${{ inputs.version }}
|
||||
run: pio lib install
|
||||
|
||||
- name: Build firmware
|
||||
env:
|
||||
GITHUB_TAG: v${{ inputs.version }}
|
||||
run: pio run -e ${{ inputs.env }}
|
||||
|
||||
- name: Create zip file
|
||||
@ -119,10 +124,13 @@ jobs:
|
||||
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
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
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
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
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
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
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
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@ -76,3 +76,4 @@ jobs:
|
||||
env: esp8266
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
is_esp32: false
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
# AMS Reader
|
||||
This code is designed to decode data from electric smart meters installed in many countries in Europe these days. The data is presented in a graphical web interface and can also send the data to a MQTT broker which makes it suitable for home automation project. Originally it was only designed to work with Norwegian meters, but has since been adapter to read any IEC-62056-7-5 or IEC-62056-21 compliant meters.
|
||||
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://amsleser.no/).
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://www.amsleser.no/).
|
||||
|
||||
|
||||
<img src="images/dashboard.png">
|
||||
|
||||
Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases).
|
||||
## Installing pre-built firmware
|
||||
If you have a device already running this firmware and you for some reason need to upgrade via USB port, you can use a [this web-based tool](https://www.amsleser.cloud/flasher)
|
||||
|
||||
If you are using a development board and want to flash a pre-built firmware manually, get the necessary files from the [release](https://github.com/UtilitechAS/amsreader-firmware/releases) section and visit the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) and have a look at the [Flashing](https://github.com/UtilitechAS/amsreader-firmware/wiki/flashinghttps://github.com/UtilitechAS/amsreader-firmware/wiki/flashing) section
|
||||
|
||||
## Building this project with PlatformIO
|
||||
To build this project, you need [PlatformIO](https://platformio.org/) installed.
|
||||
|
||||
BIN
doc/Norway/Norwegian_payload_breakdown.docx
Normal file
BIN
doc/Norway/Norwegian_payload_breakdown.docx
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 199 KiB |
@ -207,10 +207,12 @@ struct PriceServiceConfig {
|
||||
char entsoeToken[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint32_t unused1;
|
||||
bool enabled;
|
||||
uint8_t resolutionInMinutes;
|
||||
uint16_t unused2;
|
||||
}; // 64
|
||||
uint16_t unused3;
|
||||
bool enabled;
|
||||
uint16_t unused6;
|
||||
};
|
||||
|
||||
struct EnergyAccountingConfig {
|
||||
uint16_t thresholds[10];
|
||||
@ -237,14 +239,14 @@ struct UiConfig {
|
||||
}; // 15
|
||||
|
||||
struct UpgradeInformation {
|
||||
char fromVersion[8];
|
||||
char toVersion[8];
|
||||
char fromVersion[16];
|
||||
char toVersion[16];
|
||||
uint32_t size;
|
||||
uint16_t block_position;
|
||||
uint8_t retry_count;
|
||||
uint8_t reboot_count;
|
||||
int8_t errorCode;
|
||||
}; // 25
|
||||
}; // 41+3
|
||||
|
||||
struct CloudConfig {
|
||||
bool enabled;
|
||||
|
||||
@ -655,6 +655,9 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
|
||||
clearPriceServiceConfig(config);
|
||||
return false;
|
||||
}
|
||||
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
|
||||
config.resolutionInMinutes = 60;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
clearPriceServiceConfig(config);
|
||||
@ -669,6 +672,7 @@ bool AmsConfiguration::setPriceServiceConfig(PriceServiceConfig& config) {
|
||||
priceChanged |= strcmp(config.area, existing.area) != 0;
|
||||
priceChanged |= strcmp(config.currency, existing.currency) != 0;
|
||||
priceChanged |= config.enabled != existing.enabled;
|
||||
priceChanged |= config.resolutionInMinutes != existing.resolutionInMinutes;
|
||||
} else {
|
||||
priceChanged = true;
|
||||
}
|
||||
@ -688,9 +692,8 @@ void AmsConfiguration::clearPriceServiceConfig(PriceServiceConfig& config) {
|
||||
memset(config.entsoeToken, 0, 37);
|
||||
memset(config.area, 0, 17);
|
||||
memset(config.currency, 0, 4);
|
||||
config.unused1 = 1000;
|
||||
config.enabled = false;
|
||||
config.unused2 = 0;
|
||||
config.resolutionInMinutes = 60;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isPriceServiceChanged() {
|
||||
|
||||
@ -97,7 +97,7 @@ private:
|
||||
uint32_t lastVersionCheck = 0;
|
||||
uint8_t firmwareVariant;
|
||||
bool autoUpgrade;
|
||||
char nextVersion[10];
|
||||
char nextVersion[17];
|
||||
|
||||
|
||||
bool fetchNextVersion();
|
||||
|
||||
@ -114,6 +114,7 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
|
||||
debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n"));
|
||||
}
|
||||
}
|
||||
this->config = config;
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
@ -147,6 +148,9 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
|
||||
{
|
||||
debugger->printf_P(PSTR("Successfully connected to Ethernet!\n"));
|
||||
}
|
||||
if(config.ipv6 && !ETH.enableIpV6()) {
|
||||
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
|
||||
@ -26,9 +26,11 @@ struct EnergyAccountingPeak6 {
|
||||
struct EnergyAccountingData {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
int32_t costToday;
|
||||
int32_t costYesterday;
|
||||
int32_t costThisMonth;
|
||||
int32_t costLastMonth;
|
||||
int32_t incomeToday;
|
||||
int32_t incomeYesterday;
|
||||
int32_t incomeThisMonth;
|
||||
int32_t incomeLastMonth;
|
||||
@ -36,6 +38,7 @@ struct EnergyAccountingData {
|
||||
uint32_t lastMonthExport;
|
||||
uint8_t lastMonthAccuracy;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
time_t lastUpdated;
|
||||
};
|
||||
|
||||
struct EnergyAccountingData6 {
|
||||
@ -115,7 +118,6 @@ public:
|
||||
void setData(EnergyAccountingData&);
|
||||
|
||||
void setCurrency(String currency);
|
||||
float getPriceForHour(uint8_t d, uint8_t h);
|
||||
|
||||
private:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@ -128,7 +130,7 @@ private:
|
||||
PriceService *ps = NULL;
|
||||
EnergyAccountingConfig *config = NULL;
|
||||
Timezone *tz = NULL;
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
EnergyAccountingRealtimeData* realtimeData = NULL;
|
||||
String currency = "";
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData*
|
||||
rtd->lastImportUpdateMillis = 0;
|
||||
rtd->lastExportUpdateMillis = 0;
|
||||
}
|
||||
this->realtimeData = rtd;
|
||||
realtimeData = rtd;
|
||||
}
|
||||
|
||||
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
|
||||
@ -67,14 +67,14 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
breakTime(tz->toLocal(now), local);
|
||||
|
||||
if(!init) {
|
||||
this->realtimeData->lastImportUpdateMillis = 0;
|
||||
this->realtimeData->lastExportUpdateMillis = 0;
|
||||
this->realtimeData->currentHour = local.Hour;
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
realtimeData->lastImportUpdateMillis = 0;
|
||||
realtimeData->lastExportUpdateMillis = 0;
|
||||
realtimeData->currentHour = local.Hour;
|
||||
realtimeData->currentDay = local.Day;
|
||||
if(!load()) {
|
||||
data = { 7, local.Month,
|
||||
0, 0, 0, // Cost
|
||||
0, 0, 0, // Income
|
||||
0, 0, 0, 0, // Cost
|
||||
0, 0, 0, 0, // Income
|
||||
0, 0, 0, // Last month import, export and accuracy
|
||||
0, 0, 0, // Peak 1
|
||||
0, 0, 0, // Peak 2
|
||||
@ -86,12 +86,11 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
init = true;
|
||||
}
|
||||
|
||||
float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
|
||||
if(!initPrice && importPrice != PRICE_NO_VALUE) {
|
||||
if(!initPrice && ps != NULL && ps->hasPrice()) {
|
||||
calcDayCost();
|
||||
}
|
||||
|
||||
if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
tmElements_t oneHrAgo, oneHrAgoLocal;
|
||||
breakTime(now-3600, oneHrAgo);
|
||||
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
|
||||
@ -99,27 +98,24 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
|
||||
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) {
|
||||
calcDayCost();
|
||||
}
|
||||
realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
|
||||
this->realtimeData->use = 0;
|
||||
this->realtimeData->produce = 0;
|
||||
this->realtimeData->costHour = 0;
|
||||
this->realtimeData->incomeHour = 0;
|
||||
realtimeData->use = 0;
|
||||
realtimeData->produce = 0;
|
||||
realtimeData->costHour = 0;
|
||||
realtimeData->incomeHour = 0;
|
||||
|
||||
uint8_t prevDay = this->realtimeData->currentDay;
|
||||
if(local.Day != this->realtimeData->currentDay) {
|
||||
data.costYesterday = this->realtimeData->costDay * 100;
|
||||
data.costThisMonth += this->realtimeData->costDay * 100;
|
||||
this->realtimeData->costDay = 0;
|
||||
uint8_t prevDay = realtimeData->currentDay;
|
||||
if(local.Day != realtimeData->currentDay) {
|
||||
data.costYesterday = realtimeData->costDay * 100;
|
||||
data.costThisMonth += realtimeData->costDay * 100;
|
||||
realtimeData->costDay = 0;
|
||||
|
||||
data.incomeYesterday = this->realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += this->realtimeData->incomeDay * 100;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
data.incomeYesterday = realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += realtimeData->incomeDay * 100;
|
||||
realtimeData->incomeDay = 0;
|
||||
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
realtimeData->currentDay = local.Day;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
@ -149,42 +145,49 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
data.lastMonthAccuracy = accuracy;
|
||||
|
||||
data.month = local.Month;
|
||||
this->realtimeData->currentThresholdIdx = 0;
|
||||
realtimeData->currentThresholdIdx = 0;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if(ret) {
|
||||
data.costToday = realtimeData->costDay * 100;
|
||||
data.incomeToday = realtimeData->incomeDay * 100;
|
||||
data.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis;
|
||||
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
|
||||
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhi > 0) {
|
||||
this->realtimeData->use += kwhi;
|
||||
realtimeData->use += kwhi;
|
||||
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
|
||||
if(importPrice != PRICE_NO_VALUE) {
|
||||
float cost = importPrice * kwhi;
|
||||
this->realtimeData->costHour += cost;
|
||||
this->realtimeData->costDay += cost;
|
||||
realtimeData->costHour += cost;
|
||||
realtimeData->costDay += cost;
|
||||
}
|
||||
}
|
||||
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis;
|
||||
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
|
||||
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhe > 0) {
|
||||
this->realtimeData->produce += kwhe;
|
||||
float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
realtimeData->produce += kwhe;
|
||||
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
float income = exportPrice * kwhe;
|
||||
this->realtimeData->incomeHour += income;
|
||||
this->realtimeData->incomeDay += income;
|
||||
realtimeData->incomeHour += income;
|
||||
realtimeData->incomeDay += income;
|
||||
}
|
||||
}
|
||||
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(config != NULL) {
|
||||
while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++;
|
||||
while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -192,28 +195,36 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
|
||||
void EnergyAccounting::calcDayCost() {
|
||||
time_t now = time(nullptr);
|
||||
tmElements_t local, utc;
|
||||
tmElements_t local, utc, lastUpdateUtc;
|
||||
if(tz == NULL) return;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
if(ps == NULL) return;
|
||||
|
||||
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
|
||||
if(initPrice) {
|
||||
this->realtimeData->costDay = 0;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
if(ps->hasPrice()) {
|
||||
breakTime(data.lastUpdated, lastUpdateUtc);
|
||||
uint8_t calcFromHour = 0;
|
||||
if(lastUpdateUtc.Day != local.Day || lastUpdateUtc.Month != local.Month || lastUpdateUtc.Year != local.Year) {
|
||||
realtimeData->costDay = 0;
|
||||
realtimeData->incomeDay = 0;
|
||||
calcFromHour = 0;
|
||||
} else {
|
||||
realtimeData->costDay = data.costToday / 100.0;
|
||||
realtimeData->incomeDay = data.incomeToday / 100.0;
|
||||
calcFromHour = lastUpdateUtc.Hour;
|
||||
}
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
|
||||
float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
float priceIn = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
if(priceIn != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourImport(utc.Hour);
|
||||
this->realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
}
|
||||
|
||||
float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
float priceOut = ps->getPriceForRelativeHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
if(priceOut != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourExport(utc.Hour);
|
||||
this->realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
}
|
||||
}
|
||||
initPrice = true;
|
||||
@ -221,7 +232,7 @@ void EnergyAccounting::calcDayCost() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseThisHour() {
|
||||
return this->realtimeData->use;
|
||||
return realtimeData->use;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseToday() {
|
||||
@ -231,7 +242,7 @@ float EnergyAccounting::getUseToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourImport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@ -242,7 +253,7 @@ float EnergyAccounting::getUseThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayImport(i) / 1000.0;
|
||||
}
|
||||
return ret + getUseToday();
|
||||
@ -253,7 +264,7 @@ float EnergyAccounting::getUseLastMonth() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedThisHour() {
|
||||
return this->realtimeData->produce;
|
||||
return realtimeData->produce;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedToday() {
|
||||
@ -263,7 +274,7 @@ float EnergyAccounting::getProducedToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourExport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@ -274,7 +285,7 @@ float EnergyAccounting::getProducedThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayExport(i) / 1000.0;
|
||||
}
|
||||
return ret + getProducedToday();
|
||||
@ -285,11 +296,11 @@ float EnergyAccounting::getProducedLastMonth() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostThisHour() {
|
||||
return this->realtimeData->costHour;
|
||||
return realtimeData->costHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostToday() {
|
||||
return this->realtimeData->costDay;
|
||||
return realtimeData->costDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostYesterday() {
|
||||
@ -305,11 +316,11 @@ float EnergyAccounting::getCostLastMonth() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeThisHour() {
|
||||
return this->realtimeData->incomeHour;
|
||||
return realtimeData->incomeHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeToday() {
|
||||
return this->realtimeData->incomeDay;
|
||||
return realtimeData->incomeDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeYesterday() {
|
||||
@ -327,7 +338,7 @@ float EnergyAccounting::getIncomeLastMonth() {
|
||||
uint8_t EnergyAccounting::getCurrentThreshold() {
|
||||
if(config == NULL)
|
||||
return 0;
|
||||
return config->thresholds[this->realtimeData->currentThresholdIdx];
|
||||
return config->thresholds[realtimeData->currentThresholdIdx];
|
||||
}
|
||||
|
||||
float EnergyAccounting::getMonthMax() {
|
||||
@ -414,9 +425,11 @@ bool EnergyAccounting::load() {
|
||||
} else if(buf[0] == 6) {
|
||||
EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
|
||||
this->data = { 7, data->month,
|
||||
0, // Cost today
|
||||
data->costYesterday,
|
||||
data->costThisMonth,
|
||||
data->costLastMonth,
|
||||
0, // Income today
|
||||
data->incomeYesterday,
|
||||
data->incomeThisMonth,
|
||||
data->incomeLastMonth,
|
||||
@ -495,8 +508,3 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
|
||||
void EnergyAccounting::setCurrency(String currency) {
|
||||
this->currency = currency;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getPriceForHour(uint8_t d, uint8_t h) {
|
||||
if(ps == NULL) return PRICE_NO_VALUE;
|
||||
return ps->getValueForHour(d, h);
|
||||
}
|
||||
@ -53,7 +53,7 @@ private:
|
||||
|
||||
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit;
|
||||
bool tInit[32] = {false};
|
||||
bool prInit[38] = {false};
|
||||
uint8_t priceImportInit = 0, priceExportInit = 0;
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
|
||||
HwTools* hw;
|
||||
|
||||
@ -17,113 +17,112 @@ struct HomeAssistantSensor {
|
||||
const char* uom;
|
||||
const char* devcl;
|
||||
const char* stacl;
|
||||
const char* uid;
|
||||
};
|
||||
|
||||
|
||||
const uint8_t List1SensorCount PROGMEM = 2;
|
||||
const HomeAssistantSensor List1Sensors[List1SensorCount] PROGMEM = {
|
||||
{"Active import", "/power", "P", 30, "W", "power", "measurement"},
|
||||
{"Data timestamp", "/power", "t", 30, "", "timestamp", ""}
|
||||
{"Active import", "/power", "P", 30, "W", "power", "measurement", ""},
|
||||
{"Data timestamp", "/power", "t", 30, "", "timestamp", "", ""}
|
||||
};
|
||||
|
||||
const uint8_t List2SensorCount PROGMEM = 8;
|
||||
const HomeAssistantSensor List2Sensors[List2SensorCount] PROGMEM = {
|
||||
{"Reactive import", "/power", "Q", 30, "var", "reactive_power", "measurement"},
|
||||
{"Reactive export", "/power", "QO", 30, "var", "reactive_power", "measurement"},
|
||||
{"L1 current", "/power", "I1", 30, "A", "current", "measurement"},
|
||||
{"L2 current", "/power", "I2", 30, "A", "current", "measurement"},
|
||||
{"L3 current", "/power", "I3", 30, "A", "current", "measurement"},
|
||||
{"L1 voltage", "/power", "U1", 30, "V", "voltage", "measurement"},
|
||||
{"L2 voltage", "/power", "U2", 30, "V", "voltage", "measurement"},
|
||||
{"L3 voltage", "/power", "U3", 30, "V", "voltage", "measurement"}
|
||||
{"Reactive import", "/power", "Q", 30, "var", "reactive_power", "measurement", ""},
|
||||
{"Reactive export", "/power", "QO", 30, "var", "reactive_power", "measurement", ""},
|
||||
{"L1 current", "/power", "I1", 30, "A", "current", "measurement", ""},
|
||||
{"L2 current", "/power", "I2", 30, "A", "current", "measurement", ""},
|
||||
{"L3 current", "/power", "I3", 30, "A", "current", "measurement", ""},
|
||||
{"L1 voltage", "/power", "U1", 30, "V", "voltage", "measurement", ""},
|
||||
{"L2 voltage", "/power", "U2", 30, "V", "voltage", "measurement", ""},
|
||||
{"L3 voltage", "/power", "U3", 30, "V", "voltage", "measurement", ""}
|
||||
};
|
||||
|
||||
const uint8_t List2ExportSensorCount PROGMEM = 1;
|
||||
const HomeAssistantSensor List2ExportSensors[List2ExportSensorCount] PROGMEM = {
|
||||
{"Active export", "/power", "PO", 30, "W", "power", "measurement"}
|
||||
{"Active export", "/power", "PO", 30, "W", "power", "measurement", ""}
|
||||
};
|
||||
|
||||
const uint8_t List3SensorCount PROGMEM = 4;
|
||||
const HomeAssistantSensor List3Sensors[List3SensorCount] PROGMEM = {
|
||||
{"Accumulated active import", "/energy", "tPI", 4000, "kWh", "energy", "total_increasing"},
|
||||
{"Accumulated reactive import","/energy", "tQI", 4000, "kvarh","", "total_increasing"},
|
||||
{"Accumulated reactive export","/energy", "tQO", 4000, "kvarh","", "total_increasing"},
|
||||
{"Meter timestamp", "/energy", "rtc", 4000, "", "timestamp", ""}
|
||||
{"Accumulated active import", "/energy", "tPI", 4000, "kWh", "energy", "total_increasing", ""},
|
||||
{"Accumulated reactive import","/energy", "tQI", 4000, "kvarh","", "total_increasing", ""},
|
||||
{"Accumulated reactive export","/energy", "tQO", 4000, "kvarh","", "total_increasing", ""},
|
||||
{"Meter timestamp", "/energy", "rtc", 4000, "", "timestamp", "", ""}
|
||||
};
|
||||
|
||||
const uint8_t List3ExportSensorCount PROGMEM = 1;
|
||||
const HomeAssistantSensor List3ExportSensors[List3ExportSensorCount] PROGMEM = {
|
||||
{"Accumulated active export", "/energy", "tPO", 4000, "kWh", "energy", "total_increasing"}
|
||||
{"Accumulated active export", "/energy", "tPO", 4000, "kWh", "energy", "total_increasing", ""}
|
||||
};
|
||||
|
||||
const uint8_t List4SensorCount PROGMEM = 10;
|
||||
const HomeAssistantSensor List4Sensors[List4SensorCount] PROGMEM = {
|
||||
{"Power factor", "/power", "PF", 30, "%", "power_factor", "measurement"},
|
||||
{"L1 power factor", "/power", "PF1", 30, "%", "power_factor", "measurement"},
|
||||
{"L2 power factor", "/power", "PF2", 30, "%", "power_factor", "measurement"},
|
||||
{"L3 power factor", "/power", "PF3", 30, "%", "power_factor", "measurement"},
|
||||
{"L1 active import", "/power", "P1", 30, "W", "power", "measurement"},
|
||||
{"L2 active import", "/power", "P2", 30, "W", "power", "measurement"},
|
||||
{"L3 active import", "/power", "P3", 30, "W", "power", "measurement"},
|
||||
{"L1 accumulated active import","/power", "tPI1", 30, "kWh", "energy", "total_increasing"},
|
||||
{"L2 accumulated active import","/power", "tPI2", 30, "kWh", "energy", "total_increasing"},
|
||||
{"L3 accumulated active import","/power", "tPI3", 30, "kWh", "energy", "total_increasing"}
|
||||
{"Power factor", "/power", "PF", 30, "%", "power_factor", "measurement", ""},
|
||||
{"L1 power factor", "/power", "PF1", 30, "%", "power_factor", "measurement", ""},
|
||||
{"L2 power factor", "/power", "PF2", 30, "%", "power_factor", "measurement", ""},
|
||||
{"L3 power factor", "/power", "PF3", 30, "%", "power_factor", "measurement", ""},
|
||||
{"L1 active import", "/power", "P1", 30, "W", "power", "measurement", ""},
|
||||
{"L2 active import", "/power", "P2", 30, "W", "power", "measurement", ""},
|
||||
{"L3 active import", "/power", "P3", 30, "W", "power", "measurement", ""},
|
||||
{"L1 accumulated active import","/power", "tPI1", 30, "kWh", "energy", "total_increasing", ""},
|
||||
{"L2 accumulated active import","/power", "tPI2", 30, "kWh", "energy", "total_increasing", ""},
|
||||
{"L3 accumulated active import","/power", "tPI3", 30, "kWh", "energy", "total_increasing", ""}
|
||||
};
|
||||
|
||||
const uint8_t List4ExportSensorCount PROGMEM = 6;
|
||||
const HomeAssistantSensor List4ExportSensors[List4ExportSensorCount] PROGMEM = {
|
||||
{"L1 active export", "/power", "PO1", 30, "W", "power", "measurement"},
|
||||
{"L2 active export", "/power", "PO2", 30, "W", "power", "measurement"},
|
||||
{"L3 active export", "/power", "PO3", 30, "W", "power", "measurement"},
|
||||
{"L1 accumulated active export","/power", "tPO1", 30, "kWh", "energy", "total_increasing"},
|
||||
{"L2 accumulated active export","/power", "tPO2", 30, "kWh", "energy", "total_increasing"},
|
||||
{"L3 accumulated active export","/power", "tPO3", 30, "kWh", "energy", "total_increasing"}
|
||||
{"L1 active export", "/power", "PO1", 30, "W", "power", "measurement", ""},
|
||||
{"L2 active export", "/power", "PO2", 30, "W", "power", "measurement", ""},
|
||||
{"L3 active export", "/power", "PO3", 30, "W", "power", "measurement", ""},
|
||||
{"L1 accumulated active export","/power", "tPO1", 30, "kWh", "energy", "total_increasing", ""},
|
||||
{"L2 accumulated active export","/power", "tPO2", 30, "kWh", "energy", "total_increasing", ""},
|
||||
{"L3 accumulated active export","/power", "tPO3", 30, "kWh", "energy", "total_increasing", ""}
|
||||
};
|
||||
|
||||
const uint8_t RealtimeSensorCount PROGMEM = 8;
|
||||
const HomeAssistantSensor RealtimeSensors[RealtimeSensorCount] PROGMEM = {
|
||||
{"Month max", "/realtime","max", 120, "kWh", "energy", ""},
|
||||
{"Tariff threshold", "/realtime","threshold", 120, "kWh", "energy", ""},
|
||||
{"Current hour used", "/realtime","hour.use", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current hour cost", "/realtime","hour.cost", 120, "", "monetary", ""},
|
||||
{"Current day used", "/realtime","day.use", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current day cost", "/realtime","day.cost", 120, "", "monetary", ""},
|
||||
{"Current month used", "/realtime","month.use", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current month cost", "/realtime","month.cost", 120, "", "monetary", ""}
|
||||
{"Month max", "/realtime","max", 120, "kWh", "energy", "", ""},
|
||||
{"Tariff threshold", "/realtime","threshold", 120, "kWh", "energy", "", ""},
|
||||
{"Current hour used", "/realtime","hour.use", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current hour cost", "/realtime","hour.cost", 120, "", "monetary", "", ""},
|
||||
{"Current day used", "/realtime","day.use", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current day cost", "/realtime","day.cost", 120, "", "monetary", "", ""},
|
||||
{"Current month used", "/realtime","month.use", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current month cost", "/realtime","month.cost", 120, "", "monetary", "", ""}
|
||||
};
|
||||
|
||||
const uint8_t RealtimeExportSensorCount PROGMEM = 6;
|
||||
const HomeAssistantSensor RealtimeExportSensors[RealtimeExportSensorCount] PROGMEM = {
|
||||
{"Current hour produced", "/realtime","hour.produced", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current hour income", "/realtime","hour.income", 120, "", "monetary", ""},
|
||||
{"Current day produced", "/realtime","day.produced", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current day income", "/realtime","day.income", 120, "", "monetary", ""},
|
||||
{"Current month produced", "/realtime","month.produced", 120, "kWh", "energy", "total_increasing"},
|
||||
{"Current month income", "/realtime","month.income", 120, "", "monetary", ""}
|
||||
{"Current hour produced", "/realtime","hour.produced", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current hour income", "/realtime","hour.income", 120, "", "monetary", "", ""},
|
||||
{"Current day produced", "/realtime","day.produced", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current day income", "/realtime","day.income", 120, "", "monetary", "", ""},
|
||||
{"Current month produced", "/realtime","month.produced", 120, "kWh", "energy", "total_increasing", ""},
|
||||
{"Current month income", "/realtime","month.income", 120, "", "monetary", "", ""}
|
||||
};
|
||||
|
||||
const HomeAssistantSensor RealtimePeakSensor PROGMEM = {"Current month peak %d", "/realtime", "peaks[%d]", 4000, "kWh", "energy", ""};
|
||||
const HomeAssistantSensor RealtimeThresholdSensor PROGMEM = {"Tariff threshold %d", "/realtime", "thresholds[%d]", 4000, "kWh", "energy", ""};
|
||||
const HomeAssistantSensor RealtimePeakSensor PROGMEM = {"Current month peak %d", "/realtime", "peaks[%d]", 4000, "kWh", "energy", "", ""};
|
||||
const HomeAssistantSensor RealtimeThresholdSensor PROGMEM = {"Tariff threshold %d", "/realtime", "thresholds[%d]", 4000, "kWh", "energy", "", ""};
|
||||
|
||||
const uint8_t PriceSensorCount PROGMEM = 5;
|
||||
const HomeAssistantSensor PriceSensors[PriceSensorCount] PROGMEM = {
|
||||
{"Minimum price ahead", "/prices", "prices.min", 4000, "", "monetary", ""},
|
||||
{"Maximum price ahead", "/prices", "prices.max", 4000, "", "monetary", ""},
|
||||
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr",4000, "", "timestamp", ""},
|
||||
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr",4000, "", "timestamp", ""},
|
||||
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr",4000, "", "timestamp", ""}
|
||||
{"Minimum price ahead", "/prices", "prices.min", 4000, "", "monetary", "", ""},
|
||||
{"Maximum price ahead", "/prices", "prices.max", 4000, "", "monetary", "", ""},
|
||||
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr", 4000, "", "timestamp", "", ""},
|
||||
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr", 4000, "", "timestamp", "", ""},
|
||||
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr", 4000, "", "timestamp", "", ""}
|
||||
};
|
||||
|
||||
const HomeAssistantSensor PriceSensor PROGMEM = {"Price in %02d %s", "/prices", "prices['%d']", 4000, "", "monetary", ""};
|
||||
|
||||
const uint8_t SystemSensorCount PROGMEM = 3;
|
||||
const HomeAssistantSensor SystemSensors[SystemSensorCount] PROGMEM = {
|
||||
{"Status", "/state", "rssi", 180, "dBm", "signal_strength", "measurement"},
|
||||
{"Supply volt", "/state", "vcc", 180, "V", "voltage", "measurement"},
|
||||
{"Uptime", "/state", "up", 180, "s", "duration", "measurement"}
|
||||
{"Status", "/state", "rssi", 180, "dBm", "signal_strength", "measurement", ""},
|
||||
{"Supply volt", "/state", "vcc", 180, "V", "voltage", "measurement", ""},
|
||||
{"Uptime", "/state", "up", 180, "s", "duration", "measurement", ""}
|
||||
};
|
||||
|
||||
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", 900, "°C", "temperature", "measurement"};
|
||||
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", 900, "°C", "temperature", "measurement", ""};
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@ -35,6 +35,10 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
|
||||
manufacturer = boardManufacturerToString(boardType);
|
||||
|
||||
deviceUid = String(hostname); // Maybe configurable in the future?
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Hostname is [%s]\n"), hostname);
|
||||
|
||||
if(strlen(config.discoveryHostname) > 0) {
|
||||
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
|
||||
@ -348,7 +352,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
|
||||
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
publishPriceSensors(ps);
|
||||
@ -361,7 +365,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@ -430,31 +434,41 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{"), WiFi.macAddress().c_str());
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":null,"), i);
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":%.4f,"), i, values[i]);
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(rteInit && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}"),
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_EXPORT, now, 0);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":null}"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":%.4f}"), val);
|
||||
}
|
||||
|
||||
char pt[24];
|
||||
|
||||
char pt[24];
|
||||
memset(pt, 0, 24);
|
||||
if(now > 0) {
|
||||
tmElements_t tm;
|
||||
@ -503,11 +517,16 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
|
||||
}
|
||||
|
||||
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
|
||||
String uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
String uid;
|
||||
if(strlen(sensor.uid) > 0) {
|
||||
uid = String(sensor.uid);
|
||||
} else {
|
||||
uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
}
|
||||
snprintf_P(json, BufferSize, HADISCOVER_JSON,
|
||||
sensorNamePrefix.c_str(),
|
||||
sensor.name,
|
||||
@ -530,7 +549,7 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
|
||||
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
|
||||
strlen_P(sensor.stacl) > 0 ? "\"" : ""
|
||||
);
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
|
||||
loop();
|
||||
}
|
||||
|
||||
@ -619,7 +638,8 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
|
||||
RealtimePeakSensor.ttl,
|
||||
RealtimePeakSensor.uom,
|
||||
RealtimePeakSensor.devcl,
|
||||
RealtimePeakSensor.stacl
|
||||
RealtimePeakSensor.stacl,
|
||||
RealtimePeakSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@ -658,7 +678,8 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
|
||||
TemperatureSensor.ttl,
|
||||
TemperatureSensor.uom,
|
||||
TemperatureSensor.devcl,
|
||||
TemperatureSensor.stacl
|
||||
TemperatureSensor.stacl,
|
||||
TemperatureSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
tInit[index] = true;
|
||||
@ -678,45 +699,96 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
|
||||
}
|
||||
pInit = true;
|
||||
}
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
if(prInit[i]) continue;
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) continue;
|
||||
|
||||
char name[strlen(PriceSensor.name)+2];
|
||||
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours");
|
||||
char path[strlen(PriceSensor.path)+1];
|
||||
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i);
|
||||
HomeAssistantSensor sensor = {
|
||||
i == 0 ? "Price current hour" : name,
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
i == 0 ? "total" : PriceSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
prInit[i] = true;
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
|
||||
if(priceImportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t importPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(importPriceSensorNo < priceImportInit) {
|
||||
importPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.import[%d]"), importPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("prices%d"), importPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Import price in %02d hour%s"), importPriceSensorNo, importPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Import price in %03d minutes"), importPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
importPriceSensorNo == 0 ? "Current import price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
importPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceImportInit = importPriceSensorNo++;
|
||||
}
|
||||
}
|
||||
|
||||
float exportPrice = ps->getValueForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
char path[20];
|
||||
snprintf(path, 20, "exportprices['%d']", 0);
|
||||
HomeAssistantSensor sensor = {
|
||||
"Export price current hour",
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
"total"
|
||||
};
|
||||
publishSensor(sensor);
|
||||
if(priceExportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t exportPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(exportPriceSensorNo < priceExportInit) {
|
||||
exportPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.export[%d]"), exportPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("exportprices%d"), exportPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Export price in %02d hour%s"), exportPriceSensorNo, exportPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Export price in %03d minutes"), exportPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
exportPriceSensorNo == 0 ? "Current export price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
exportPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceExportInit = exportPriceSensorNo++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void HomeAssistantMqttHandler::publishSystemSensors() {
|
||||
if(sInit) return;
|
||||
for(uint8_t i = 0; i < SystemSensorCount; i++) {
|
||||
@ -739,7 +811,8 @@ void HomeAssistantMqttHandler::publishThresholdSensors() {
|
||||
RealtimeThresholdSensor.ttl,
|
||||
RealtimeThresholdSensor.uom,
|
||||
RealtimeThresholdSensor.devcl,
|
||||
RealtimeThresholdSensor.stacl
|
||||
RealtimeThresholdSensor.stacl,
|
||||
RealtimeThresholdSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@ -786,7 +859,8 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
|
||||
debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n"));
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = false;
|
||||
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
|
||||
for(uint8_t i = 0; i < 38; i++) prInit[i] = false;
|
||||
priceImportInit = 0;
|
||||
priceExportInit = 0;
|
||||
}
|
||||
} else if(topic.equals(subTopic)) {
|
||||
if(payload.equals("fwupgrade")) {
|
||||
|
||||
@ -34,6 +34,7 @@ public:
|
||||
|
||||
private:
|
||||
HwTools* hw;
|
||||
bool hasExport = false;
|
||||
AmsDataStorage* ds;
|
||||
|
||||
uint16_t appendJsonHeader(AmsData* data);
|
||||
|
||||
@ -56,6 +56,15 @@ bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcc
|
||||
ret = publishList4(&data, ea);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
loop();
|
||||
return ret;
|
||||
}
|
||||
@ -299,7 +308,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@ -310,7 +319,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@ -379,38 +388,59 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
char pf[4];
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
|
||||
if(mqttConfig.payloadFormat != 6) {
|
||||
memset(pf, 0, 4);
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"prices\":{"));
|
||||
} else {
|
||||
strcpy_P(pf, PSTR("pr_"));
|
||||
}
|
||||
if(mqttConfig.payloadFormat == 6) {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
|
||||
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":null,"), pf, i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":%.4f,"), pf, i, values[i]);
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":null,"), i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":%.4f,"), i, values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%smin\":%.4f,\"%smax\":%.4f,\"%scheapest1hr\":\"%s\",\"%scheapest3hr\":\"%s\",\"%scheapest6hr\":\"%s\"}"),
|
||||
pf,
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
pf,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
pf,
|
||||
ts1hr,
|
||||
pf,
|
||||
ts3hr,
|
||||
pf,
|
||||
ts6hr
|
||||
);
|
||||
if(mqttConfig.payloadFormat != 6) {
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":\"%s\",\"pr_cheapest3hr\":\"%s\",\"pr_cheapest6hr\":\"%s\"}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
} else {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
}
|
||||
bool ret = false;
|
||||
if(mqttConfig.payloadFormat == 5) {
|
||||
|
||||
@ -25,9 +25,9 @@ struct AmsOctetTimestamp {
|
||||
class IEC6205675 : public AmsData {
|
||||
public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
|
||||
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
|
||||
#else
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
|
||||
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
|
||||
#endif
|
||||
|
||||
private:
|
||||
@ -37,8 +37,9 @@ private:
|
||||
float getNumber(uint8_t* obis, int matchlength, const char* ptr);
|
||||
float getNumber(CosemData*);
|
||||
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
|
||||
time_t adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType);
|
||||
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
|
||||
uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 };
|
||||
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };
|
||||
|
||||
@ -12,18 +12,14 @@
|
||||
#include "hexutils.h"
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
|
||||
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
|
||||
#else
|
||||
IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
|
||||
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
|
||||
#endif
|
||||
float val;
|
||||
char str[64];
|
||||
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
Timezone tz(CEST, CET);
|
||||
|
||||
this->packageTimestamp = ctx.timestamp == 0 ? time(nullptr) : ctx.timestamp;
|
||||
this->packageTimestamp = time(nullptr); // ctx.timestamp is mostly garbage, so we use current time as package timestamp
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
|
||||
if(val == NOVALUE) {
|
||||
@ -31,13 +27,13 @@ IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* m
|
||||
|
||||
// Kaifa special case...
|
||||
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz->toUTC(this->packageTimestamp) : 0;
|
||||
listType = 1;
|
||||
meterType = AmsTypeKaifa;
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(data->base.type == CosemTypeOctetString) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz->toUTC(this->packageTimestamp) : 0;
|
||||
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
@ -127,7 +123,7 @@ IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* m
|
||||
if(data->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
meterTimestamp = tz->toUTC(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -738,18 +734,7 @@ IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* m
|
||||
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
int16_t deviation = ntohs(amst->dt.deviation);
|
||||
if(deviation < -720 || deviation > 720) { // Deviation not specified, adjust from localtime to UTC
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
if(ctx.timestamp > 0) {
|
||||
this->packageTimestamp = tz.toUTC(ctx.timestamp);
|
||||
}
|
||||
} else if(meterType == AmsTypeAidon) {
|
||||
meterTimestamp = ts - 3600; // 21.09.24, the clock is now correct
|
||||
} else {
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
this->meterTimestamp = adjustForKnownIssues(amst->dt, tz, meterType == AmsTypeUnknown ? useMeterType : meterType);
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
|
||||
@ -1125,3 +1110,24 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
time_t IEC6205675::adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType) {
|
||||
time_t ts = decodeCosemDateTime(dt);
|
||||
int16_t deviation = ntohs(dt.deviation);
|
||||
if(deviation < -720 || deviation > 720) {
|
||||
// Time zone not specified
|
||||
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
|
||||
// Special known case
|
||||
// 21.09.24, the clock is now correct for Aidon
|
||||
// 23.10.25, the clock is now correct for Kamstrup
|
||||
ts -= 3600;
|
||||
} else {
|
||||
// Adjust from localtime to UTC
|
||||
ts = tz->toUTC(ts);
|
||||
}
|
||||
} else if(meterType == AmsTypeAidon) {
|
||||
// 21.09.24, the clock is now correct for Aidon
|
||||
ts -= 3600;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
@ -278,7 +278,7 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("DLMS\n"));
|
||||
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
|
||||
data = new IEC6205675(payload, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
|
||||
data = new IEC6205675(payload, tz, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
|
||||
}
|
||||
} else if(ctx.type == DATA_TAG_DSMR) {
|
||||
data = new IEC6205621(payload, tz, &meterConfig);
|
||||
|
||||
@ -15,28 +15,23 @@
|
||||
#define DOCPOS_MEASUREMENTUNIT 2
|
||||
#define DOCPOS_POSITION 3
|
||||
#define DOCPOS_AMOUNT 4
|
||||
#define DOCPOS_RESOLUTION 5
|
||||
|
||||
class EntsoeA44Parser: public Stream {
|
||||
public:
|
||||
EntsoeA44Parser();
|
||||
EntsoeA44Parser(PricesContainer *container);
|
||||
virtual ~EntsoeA44Parser();
|
||||
|
||||
char* getCurrency();
|
||||
char* getMeasurementUnit();
|
||||
float getPoint(uint8_t position);
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
void flush();
|
||||
size_t write(const uint8_t *buffer, size_t size);
|
||||
size_t write(uint8_t);
|
||||
void get(PricesContainer*);
|
||||
|
||||
private:
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
float points[25];
|
||||
PricesContainer *container;
|
||||
float multiplier = 1.0;
|
||||
|
||||
char buf[64];
|
||||
uint8_t pos = 0;
|
||||
|
||||
@ -27,10 +27,6 @@
|
||||
|
||||
#define SSL_BUF_SIZE 512
|
||||
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
#define PRICE_DAY_MO 0x01
|
||||
#define PRICE_DAY_TU 0x02
|
||||
#define PRICE_DAY_WE 0x04
|
||||
@ -57,10 +53,13 @@ struct PriceConfig {
|
||||
uint8_t end_dayofmonth;
|
||||
};
|
||||
|
||||
struct PricePart {
|
||||
char name[32];
|
||||
char description[32];
|
||||
uint32_t value;
|
||||
struct AmsPriceV2Header {
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
char source[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
};
|
||||
|
||||
class PriceService {
|
||||
@ -78,17 +77,25 @@ public:
|
||||
char* getCurrency();
|
||||
char* getArea();
|
||||
char* getSource();
|
||||
float getValueForHour(uint8_t direction, int8_t hour);
|
||||
float getValueForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
|
||||
float getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPointsAvailable();
|
||||
uint8_t getCurrentPricePointIndex();
|
||||
|
||||
bool isExportPricesDifferentFromImport();
|
||||
|
||||
bool hasPrice() { return hasPrice(PRICE_DIRECTION_IMPORT); }
|
||||
bool hasPrice(uint8_t direction) { return getCurrentPrice(direction) != PRICE_NO_VALUE; }
|
||||
bool hasPricePoint(uint8_t direction, int8_t point) { return getPricePoint(direction, point) != PRICE_NO_VALUE; }
|
||||
|
||||
float getCurrentPrice(uint8_t direction);
|
||||
float getPricePoint(uint8_t direction, uint8_t point);
|
||||
float getPriceForRelativeHour(uint8_t direction, int8_t hour); // If not 60min interval, average
|
||||
|
||||
std::vector<PriceConfig>& getPriceConfig();
|
||||
void setPriceConfig(uint8_t index, PriceConfig &priceConfig);
|
||||
void cropPriceConfig(uint8_t size);
|
||||
|
||||
PricePart getPricePart(uint8_t index);
|
||||
|
||||
int16_t getLastError();
|
||||
|
||||
bool load();
|
||||
@ -103,7 +110,7 @@ private:
|
||||
PriceServiceConfig* config = NULL;
|
||||
HTTPClient* http = NULL;
|
||||
|
||||
uint8_t currentDay = 0, currentHour = 0;
|
||||
uint8_t currentDay = 0, currentPricePoint = 0;
|
||||
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
|
||||
uint8_t nextFetchDelayMinutes = 15;
|
||||
uint64_t lastTodayFetch = 0;
|
||||
@ -132,5 +139,7 @@ private:
|
||||
bool retrieve(const char* url, Stream* doc);
|
||||
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
|
||||
bool timeIsInPeriod(tmElements_t tm, PriceConfig pc);
|
||||
float getFixedPrice(uint8_t direction, int8_t hour);
|
||||
float getEnergyPricePoint(uint8_t direction, uint8_t point);
|
||||
};
|
||||
#endif
|
||||
|
||||
@ -4,15 +4,43 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef _PRICESCONTAINER_H
|
||||
#define _PRICESCONTAINER_H
|
||||
|
||||
#define PRICE_NO_VALUE -127
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
struct PricesContainer {
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
int32_t points[25];
|
||||
class PricesContainer {
|
||||
public:
|
||||
PricesContainer(char* source);
|
||||
|
||||
void setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices);
|
||||
|
||||
char* getSource();
|
||||
void setCurrency(char* currency);
|
||||
char* getCurrency();
|
||||
|
||||
bool isExportPricesDifferentFromImport() {
|
||||
return differentExportPrices;
|
||||
}
|
||||
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPoints();
|
||||
|
||||
void setPrice(uint8_t point, float value, uint8_t direction);
|
||||
bool hasPrice(uint8_t point, uint8_t direction);
|
||||
float getPrice(uint8_t point, uint8_t direction); // int32_t / 10_000
|
||||
|
||||
private:
|
||||
char source[4];
|
||||
char currency[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
int32_t *points;
|
||||
};
|
||||
#endif
|
||||
|
||||
@ -7,27 +7,14 @@
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
EntsoeA44Parser::EntsoeA44Parser() {
|
||||
for(int i = 0; i < 25; i++) points[i] = PRICE_NO_VALUE;
|
||||
EntsoeA44Parser::EntsoeA44Parser(PricesContainer *container) {
|
||||
this->container = container;
|
||||
}
|
||||
|
||||
EntsoeA44Parser::~EntsoeA44Parser() {
|
||||
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getMeasurementUnit() {
|
||||
return measurementUnit;
|
||||
}
|
||||
|
||||
float EntsoeA44Parser::getPoint(uint8_t position) {
|
||||
if(position >= 25) return PRICE_NO_VALUE;
|
||||
return points[position];
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::available() {
|
||||
return 0;
|
||||
}
|
||||
@ -57,7 +44,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(currency, buf, pos);
|
||||
container->setCurrency(buf);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@ -65,7 +52,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(measurementUnit, buf, pos);
|
||||
if(strcmp_P(buf, PSTR("MWH"))) multiplier = 0.001;
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@ -73,7 +60,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
long pn = String(buf).toInt() - 1;
|
||||
if(pn < 25) {
|
||||
if(pn < container->getNumberOfPoints()) {
|
||||
pointNum = pn;
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
@ -85,8 +72,25 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
float val = String(buf).toFloat();
|
||||
for(uint8_t i = pointNum; i < 25; i++) {
|
||||
points[i] = val;
|
||||
for(uint8_t i = pointNum; i < container->getNumberOfPoints(); i++) {
|
||||
container->setPrice(i, val * multiplier, PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(docPos == DOCPOS_RESOLUTION) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
|
||||
// This happens if there are two time series in the XML. We are only interrested in the first one, so we ignore the rest of the document
|
||||
if(container->hasPrice(0, PRICE_DIRECTION_IMPORT)) return 1;
|
||||
|
||||
if(strcmp_P(buf, PSTR("PT15M"))) {
|
||||
container->setup(15, 100, false);
|
||||
} else if(strcmp_P(buf, PSTR("PT60M"))) {
|
||||
container->setup(60, 25, false);
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
@ -101,15 +105,17 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
buf[pos] = '\0';
|
||||
if(strcmp(buf, "<currency_Unit.name>") == 0) {
|
||||
if(strcmp_P(buf, PSTR("<currency_Unit.name>")) == 0) {
|
||||
docPos = DOCPOS_CURRENCY;
|
||||
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<price_Measure_Unit.name>")) == 0) {
|
||||
docPos = DOCPOS_MEASUREMENTUNIT;
|
||||
} else if(strcmp(buf, "<position>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<position>")) == 0) {
|
||||
docPos = DOCPOS_POSITION;
|
||||
pointNum = 0xFF;
|
||||
} else if(strcmp(buf, "<price.amount>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<price.amount>")) == 0) {
|
||||
docPos = DOCPOS_AMOUNT;
|
||||
} else if(strcmp(buf, PSTR("<resolution>")) == 0) {
|
||||
docPos = DOCPOS_RESOLUTION;
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
@ -118,15 +124,3 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void EntsoeA44Parser::get(PricesContainer* container) {
|
||||
memset(container, 0, sizeof(*container));
|
||||
|
||||
strcpy(container->currency, currency);
|
||||
strcpy(container->measurementUnit, measurementUnit);
|
||||
strcpy(container->source, "EOE");
|
||||
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
container->points[i] = points[i] == PRICE_NO_VALUE ? PRICE_NO_VALUE : points[i] * 10000;
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,10 @@ void PriceService::setup(PriceServiceConfig& config) {
|
||||
this->config = new PriceServiceConfig();
|
||||
}
|
||||
memcpy(this->config, &config, sizeof(config));
|
||||
if(this->config->resolutionInMinutes != 15 && this->config->resolutionInMinutes != 60) {
|
||||
this->config->resolutionInMinutes = 60;
|
||||
}
|
||||
|
||||
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) delete tomorrow;
|
||||
@ -91,55 +95,156 @@ char* PriceService::getArea() {
|
||||
|
||||
char* PriceService::getSource() {
|
||||
if(this->today != NULL && this->tomorrow != NULL) {
|
||||
if(strcmp(this->today->source, this->tomorrow->source) == 0) {
|
||||
return this->today->source;
|
||||
if(strcmp(this->today->getSource(), this->tomorrow->getSource()) == 0) {
|
||||
return this->today->getSource();
|
||||
} else {
|
||||
return "MIX";
|
||||
}
|
||||
} else if(today != NULL) {
|
||||
return this->today->source;
|
||||
return this->today->getSource();
|
||||
} else if(tomorrow != NULL) {
|
||||
return this->tomorrow->source;
|
||||
return this->tomorrow->getSource();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
float PriceService::getValueForHour(uint8_t direction, int8_t hour) {
|
||||
time_t cur = time(nullptr);
|
||||
return getValueForHour(direction, cur, hour);
|
||||
uint8_t PriceService::getResolutionInMinutes() {
|
||||
return today != NULL ? today->getResolutionInMinutes() : 60;
|
||||
}
|
||||
|
||||
float PriceService::getValueForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
float ret = getEnergyPriceForHour(direction, ts, hour);
|
||||
if(ret == PRICE_NO_VALUE)
|
||||
return ret;
|
||||
uint8_t PriceService::getNumberOfPointsAvailable() {
|
||||
if(today == NULL) return getResolutionInMinutes() == 15 ? 192 : 48;
|
||||
if(tomorrow != NULL) return today->getNumberOfPoints() + tomorrow->getNumberOfPoints();
|
||||
return today->getNumberOfPoints();
|
||||
}
|
||||
|
||||
bool PriceService::isExportPricesDifferentFromImport() {
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.direction != PRICE_DIRECTION_BOTH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return today != NULL && today->isExportPricesDifferentFromImport();
|
||||
}
|
||||
|
||||
float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
|
||||
float value = getFixedPrice(direction, point * getResolutionInMinutes() / 60);
|
||||
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
|
||||
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
time_t ts = time(nullptr);
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
tm.Hour = tm.Minute = tm.Second = 0;
|
||||
breakTime(makeTime(tm) + (point * SECS_PER_MIN * getResolutionInMinutes()), tm);
|
||||
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.type == PRICE_TYPE_FIXED) continue;
|
||||
if((pc.direction & direction) != direction) continue;
|
||||
if(!timeIsInPeriod(tm, pc)) continue;
|
||||
float pcVal = pc.value / 10000.0;
|
||||
|
||||
switch(pc.type) {
|
||||
case PRICE_TYPE_ADD:
|
||||
ret += pc.value / 10000.0;
|
||||
value += pcVal;
|
||||
break;
|
||||
case PRICE_TYPE_SUBTRACT:
|
||||
ret -= pc.value / 10000.0;
|
||||
value -= pcVal;
|
||||
break;
|
||||
case PRICE_TYPE_PCT:
|
||||
ret += ((pc.value / 10000.0) * ret) / 100.0;
|
||||
value += (pcVal * value) / 100.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
float PriceService::getCurrentPrice(uint8_t direction) {
|
||||
time_t ts = time(nullptr);
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
uint8_t pos = getCurrentPricePointIndex();
|
||||
|
||||
return getPricePoint(direction, pos);
|
||||
}
|
||||
|
||||
float PriceService::getEnergyPricePoint(uint8_t direction, uint8_t point) {
|
||||
uint8_t pos = point;
|
||||
float multiplier = 1.0;
|
||||
uint8_t numberOfPointsToday = 24;
|
||||
if(today != NULL) {
|
||||
numberOfPointsToday = today->getNumberOfPoints();
|
||||
}
|
||||
|
||||
float value = PRICE_NO_VALUE;
|
||||
if(pos >= numberOfPointsToday) {
|
||||
pos = pos - numberOfPointsToday;
|
||||
if(tomorrow == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(pos >= tomorrow->getNumberOfPoints()) return PRICE_NO_VALUE;
|
||||
if(!tomorrow->hasPrice(pos, direction))
|
||||
return PRICE_NO_VALUE;
|
||||
value = tomorrow->getPrice(pos, direction);
|
||||
float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
} else if(pos >= 0) {
|
||||
if(today == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(!today->hasPrice(pos, direction))
|
||||
return PRICE_NO_VALUE;
|
||||
value = today->getPrice(pos, direction);
|
||||
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
}
|
||||
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
|
||||
}
|
||||
|
||||
float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
|
||||
time_t ts = time(nullptr);
|
||||
tmElements_t tm;
|
||||
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
int8_t targetHour = tm.Hour + hour;
|
||||
tm.Hour = tm.Minute = tm.Second = 0;
|
||||
time_t startOfDay = tz->toUTC(makeTime(tm));
|
||||
|
||||
if((ts + (hour * SECS_PER_HOUR)) < startOfDay) {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
if(getResolutionInMinutes() == 60) {
|
||||
return getPricePoint(direction, targetHour);
|
||||
}
|
||||
|
||||
float valueSum = 0.0f;
|
||||
uint8_t valueCount = 0;
|
||||
float indexIncrements = 60.0 / today->getResolutionInMinutes();
|
||||
uint8_t priceMapIndexStart = (uint8_t) floor(indexIncrements * targetHour);
|
||||
uint8_t priceMapIndexEnd = (uint8_t) ceil(indexIncrements * (targetHour+1));
|
||||
|
||||
for(uint8_t mi = priceMapIndexStart; mi < priceMapIndexEnd; mi++) {
|
||||
float val = getPricePoint(direction, mi);
|
||||
if(val == PRICE_NO_VALUE) continue;
|
||||
valueSum += val;
|
||||
valueCount++;
|
||||
}
|
||||
if(valueCount == 0) return PRICE_NO_VALUE;
|
||||
return valueSum / valueCount;
|
||||
}
|
||||
|
||||
float PriceService::getFixedPrice(uint8_t direction, int8_t hour) {
|
||||
time_t ts = time(nullptr);
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
breakTime(makeTime(tm) + (hour * SECS_PER_HOUR), tm);
|
||||
|
||||
float value = PRICE_NO_VALUE;
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
@ -154,68 +259,7 @@ float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t h
|
||||
value += pc.value / 10000.0;
|
||||
}
|
||||
}
|
||||
if(value != PRICE_NO_VALUE) return value;
|
||||
|
||||
int8_t pos = hour;
|
||||
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
while(tm.Hour > 0) {
|
||||
ts -= 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
pos++;
|
||||
}
|
||||
uint8_t hoursToday = 0;
|
||||
uint8_t todayDate = tm.Day;
|
||||
while(tm.Day == todayDate) {
|
||||
ts += 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
hoursToday++;
|
||||
}
|
||||
uint8_t hoursTomorrow = 0;
|
||||
uint8_t tomorrowDate = tm.Day;
|
||||
while(tm.Day == tomorrowDate) {
|
||||
ts += 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
hoursTomorrow++;
|
||||
}
|
||||
|
||||
float multiplier = 1.0;
|
||||
if(pos >= hoursToday) {
|
||||
pos = pos - hoursToday;
|
||||
if(pos >= hoursTomorrow) return PRICE_NO_VALUE;
|
||||
if(tomorrow == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(tomorrow->points[pos] == PRICE_NO_VALUE)
|
||||
return PRICE_NO_VALUE;
|
||||
value = tomorrow->points[pos] / 10000.0;
|
||||
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
|
||||
// Multiplier is 1
|
||||
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
} else if(pos >= 0) {
|
||||
if(today == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(today->points[pos] == PRICE_NO_VALUE)
|
||||
return PRICE_NO_VALUE;
|
||||
value = today->points[pos] / 10000.0;
|
||||
if(strcmp(today->measurementUnit, "KWH") == 0) {
|
||||
// Multiplier is 1
|
||||
} else if(strcmp(today->measurementUnit, "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
float mult = getCurrencyMultiplier(today->currency, config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
}
|
||||
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool PriceService::loop() {
|
||||
@ -223,43 +267,59 @@ bool PriceService::loop() {
|
||||
if(now < 10000) return false; // Grace period
|
||||
|
||||
time_t t = time(nullptr);
|
||||
if(t < FirmwareVersion::BuildEpoch) return false;
|
||||
|
||||
#ifndef AMS2MQTT_PRICE_KEY
|
||||
if(strlen(getToken()) == 0) {
|
||||
if(t < FirmwareVersion::BuildEpoch) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if(strlen(config->area) == 0)
|
||||
return false;
|
||||
if(strlen(config->currency) == 0)
|
||||
return false;
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
if(currentDay == 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day init\n"));
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
}
|
||||
|
||||
if(currentDay != tm.Day) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day reset\n"));
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) {
|
||||
today = tomorrow;
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
} else if(currentHour != tm.Hour) {
|
||||
currentHour = tm.Hour;
|
||||
} else if(currentPricePoint != getCurrentPricePointIndex()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Price point reset\n"));
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
}
|
||||
|
||||
if(!config->enabled)
|
||||
return false;
|
||||
|
||||
#ifndef AMS2MQTT_PRICE_KEY
|
||||
if(strlen(getToken()) == 0) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if(strlen(config->area) == 0){
|
||||
return false;
|
||||
}
|
||||
if(strlen(config->currency) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
|
||||
|
||||
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > (nextFetchDelayMinutes*60000))) {
|
||||
@ -273,6 +333,7 @@ bool PriceService::loop() {
|
||||
}
|
||||
today = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL && !readyToFetchForTomorrow; // Only trigger MQTT publish if we have todays prices and we are not immediately ready to fetch price for tomorrow.
|
||||
}
|
||||
|
||||
@ -289,6 +350,7 @@ bool PriceService::loop() {
|
||||
}
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return tomorrow != NULL;
|
||||
}
|
||||
|
||||
@ -434,24 +496,30 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
EntsoeA44Parser a44;
|
||||
if(retrieve(buf, &a44) && a44.getPoint(0) != PRICE_NO_VALUE) {
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
a44.get(ret);
|
||||
PricesContainer* ret = new PricesContainer("EOE");
|
||||
EntsoeA44Parser a44(ret);
|
||||
if(retrieve(buf, &a44) && ret->hasPrice(0, PRICE_DIRECTION_IMPORT)) {
|
||||
return ret;
|
||||
} else {
|
||||
delete ret;
|
||||
return NULL;
|
||||
}
|
||||
} else if(hub) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Going to fetch prices from hub\n"));
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
String data;
|
||||
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
|
||||
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
|
||||
config->area,
|
||||
tm.Year+1970,
|
||||
tm.Month,
|
||||
tm.Day,
|
||||
config->resolutionInMinutes,
|
||||
config->currency
|
||||
);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@ -488,13 +556,37 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
GCMParser gcm(key, auth);
|
||||
int8_t gcmRet = gcm.parse(content, ctx);
|
||||
if(gcmRet > 0) {
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = PRICE_NO_VALUE;
|
||||
AmsPriceV2Header* header = (AmsPriceV2Header*) (content+gcmRet);
|
||||
|
||||
PricesContainer* ret = new PricesContainer(header->source);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Setting up price container with pt%dm, %dpts, edi: %d\n"), header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
|
||||
ret->setup(header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
ret->setCurrency(header->currency);
|
||||
int32_t* points = (int32_t*) &header[1];
|
||||
|
||||
for(uint8_t i = 0; i < header->numberOfPoints; i++) {
|
||||
int32_t intval = ntohl(points[i]);
|
||||
float value = intval / 10000.0;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Import price position and value received: %d :: %.2f\n"), i, value);
|
||||
ret->setPrice(i, value, PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
memcpy(ret, content+gcmRet, sizeof(*ret));
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = ntohl(ret->points[i]);
|
||||
if(header->differentExportPrices) {
|
||||
for(uint8_t i = 0; i < header->numberOfPoints; i++) {
|
||||
int32_t intval = ntohl(points[i]);
|
||||
float value = intval / 10000.0;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Export price position and value received: %d :: %.2f\n"), i, value);
|
||||
ret->setPrice(i, value, PRICE_DIRECTION_EXPORT);
|
||||
}
|
||||
}
|
||||
lastError = 0;
|
||||
nextFetchDelayMinutes = 1;
|
||||
@ -658,4 +750,11 @@ bool PriceService::timeIsInPeriod(tmElements_t tm, PriceConfig pc) {
|
||||
}
|
||||
|
||||
return makeTime(tms) <= makeTime(tm) && makeTime(tme) >= makeTime(tm);
|
||||
}
|
||||
|
||||
uint8_t PriceService::getCurrentPricePointIndex() {
|
||||
time_t ts = time(nullptr);
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
return ((tm.Hour * 60) + tm.Minute) / getResolutionInMinutes();
|
||||
}
|
||||
67
lib/PriceService/src/PricesContainer.cpp
Normal file
67
lib/PriceService/src/PricesContainer.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
#include "PricesContainer.h"
|
||||
#include <cstring>
|
||||
|
||||
PricesContainer::PricesContainer(char* source) {
|
||||
strncpy(this->source, source, 4);
|
||||
}
|
||||
|
||||
void PricesContainer::setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices) {
|
||||
this->resolutionInMinutes = resolutionInMinutes;
|
||||
this->differentExportPrices = differentExportPrices;
|
||||
this->numberOfPoints = numberOfPoints;
|
||||
this->points = new int32_t[numberOfPoints * (differentExportPrices ? 2 : 1)];
|
||||
memset(this->points, PRICE_NO_VALUE * 10000, numberOfPoints * (differentExportPrices ? 2 : 1) * sizeof(int32_t));
|
||||
}
|
||||
|
||||
char* PricesContainer::getSource() {
|
||||
return this->source;
|
||||
}
|
||||
|
||||
void PricesContainer::setCurrency(char* currency) {
|
||||
strncpy(this->currency, currency, 4);
|
||||
}
|
||||
|
||||
char* PricesContainer::getCurrency() {
|
||||
return this->currency;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getResolutionInMinutes() {
|
||||
return this->resolutionInMinutes;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getNumberOfPoints() {
|
||||
return this->numberOfPoints;
|
||||
}
|
||||
|
||||
void PricesContainer::setPrice(uint8_t point, float value, uint8_t direction) {
|
||||
if(direction == PRICE_DIRECTION_EXPORT && !differentExportPrices) {
|
||||
return; // Export prices not supported
|
||||
}
|
||||
if(direction != PRICE_DIRECTION_EXPORT) {
|
||||
points[point] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
if(differentExportPrices && direction != PRICE_DIRECTION_IMPORT) {
|
||||
points[point + numberOfPoints] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
bool PricesContainer::hasPrice(uint8_t point, uint8_t direction) {
|
||||
float val = getPrice(point, direction);
|
||||
return val != PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
float PricesContainer::getPrice(uint8_t point, uint8_t direction) {
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_EXPORT) {
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point + numberOfPoints]) / 10000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_BOTH) return PRICE_NO_VALUE; // Can't get a price for both directions if the export prices are different
|
||||
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point]) / 10000.0f;
|
||||
}
|
||||
|
||||
return PRICE_NO_VALUE; // Invalid point
|
||||
}
|
||||
@ -36,6 +36,7 @@ private:
|
||||
bool full;
|
||||
String topic;
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
bool hasExport = false;
|
||||
|
||||
bool publishList1(AmsData* data, AmsData* meterState);
|
||||
bool publishList2(AmsData* data, AmsData* meterState);
|
||||
|
||||
@ -41,6 +41,15 @@ bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcco
|
||||
publishList1(&data, previousState);
|
||||
loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(ea->isInitialized()) {
|
||||
publishRealtime(ea);
|
||||
loop();
|
||||
@ -230,7 +239,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@ -241,7 +250,7 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[34];
|
||||
for(int i = 0;i < 34; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 34; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(i > 23) continue;
|
||||
@ -308,15 +317,33 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 34; i++) {
|
||||
float val = values[i];
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/" + String(i), "", true, 0);
|
||||
mqtt.publish(topic + "/price/resolution", String(ps->getResolutionInMinutes()), true, 0);
|
||||
mqtt.loop();
|
||||
|
||||
uint8_t relativeIndex = 0;
|
||||
uint8_t startIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = startIndex; i < numberOfPoints; i++) {
|
||||
float importVal = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(importVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/" + String(i), String(val, 4), true, 0);
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), String(importVal, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
float exportVal = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(exportVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), String(exportVal, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
}
|
||||
relativeIndex++;
|
||||
}
|
||||
if(min != INT16_MAX) {
|
||||
mqtt.publish(topic + "/price/min", String(min, 4), true, 0);
|
||||
@ -338,15 +365,6 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
mqtt.publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
float exportPrice = ps->getEnergyPriceForHour(PRICE_DIRECTION_EXPORT, now, 0);
|
||||
if(exportPrice == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/exportprice/0", "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/exportprice/0", String(exportPrice, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
12
lib/SvelteUi/app/dist/index.js
vendored
12
lib/SvelteUi/app/dist/index.js
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { Router, Route, navigate } from "svelte-navigator";
|
||||
import { getTariff, tariffStore, sysinfoStore, dataStore, pricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js';
|
||||
import { getTariff, tariffStore, sysinfoStore, dataStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js';
|
||||
import { translationsStore, getTranslations } from "./lib/TranslationService.js";
|
||||
import Favicon from './assets/favicon.svg'; // Need this for the build
|
||||
import Header from './lib/Header.svelte';
|
||||
@ -19,9 +19,14 @@
|
||||
let basepath = document.getElementsByTagName('base')[0].getAttribute("href");
|
||||
if(!basepath) basepath = "/";
|
||||
|
||||
let prices;
|
||||
pricesStore.subscribe(update => {
|
||||
prices = update;
|
||||
let importPrices;
|
||||
importPricesStore.subscribe(update => {
|
||||
importPrices = update;
|
||||
});
|
||||
|
||||
let exportPrices;
|
||||
exportPricesStore.subscribe(update => {
|
||||
exportPrices = update;
|
||||
});
|
||||
|
||||
let dayPlot;
|
||||
@ -100,7 +105,7 @@
|
||||
<Router basepath={basepath}>
|
||||
<Header data={data} basepath={basepath}/>
|
||||
<Route path="/">
|
||||
<Dashboard data={data} sysinfo={sysinfo} prices={prices} dayPlot={dayPlot} monthPlot={monthPlot} temperatures={temperatures} translations={translations} tariffData={tariffData}/>
|
||||
<Dashboard data={data} sysinfo={sysinfo} importPrices={importPrices} exportPrices={exportPrices} dayPlot={dayPlot} monthPlot={monthPlot} temperatures={temperatures} translations={translations} tariffData={tariffData}/>
|
||||
</Route>
|
||||
<Route path="/configuration">
|
||||
<ConfigurationPanel sysinfo={sysinfo} basepath={basepath} data={data}/>
|
||||
|
||||
@ -89,14 +89,14 @@
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(point.value)}"
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
|
||||
fill="{point.color}"
|
||||
/>
|
||||
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{barWidth < vertSwitch || point.labelAngle ? 'left' : 'middle'}"
|
||||
fill="{yScale(point.value) > yScale(0)-labelOffset && !config.dark ? point.color : 'white'}"
|
||||
@ -111,13 +111,13 @@
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(0)}"
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
|
||||
fill="{point.color2 ? point.color2 : point.color}"
|
||||
/>
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{'middle'}"
|
||||
fill="{yScale(-point.value2) < yScale(0) + 15 && !config.dark ? point.color2 ? point.color2 : point.color : 'white'}"
|
||||
|
||||
@ -279,11 +279,11 @@
|
||||
<select name="pr" bind:value={configuration.p.r} on:change={enablePriceFetch} class="in-f w-full">
|
||||
<optgroup label="Norway">
|
||||
{#if !configuration.p.t}
|
||||
<option value="NO1S">NO1 with support</option>
|
||||
<option value="NO2S">NO2 with support</option>
|
||||
<option value="NO3S">NO3 with support</option>
|
||||
<option value="NO4S">NO4 with support</option>
|
||||
<option value="NO5S">NO5 with support</option>
|
||||
<option value="NO1S">NO1 w/support</option>
|
||||
<option value="NO2S">NO2 w/support</option>
|
||||
<option value="NO3S">NO3 w/support</option>
|
||||
<option value="NO4S">NO4 w/support</option>
|
||||
<option value="NO5S">NO5 w/support</option>
|
||||
{/if}
|
||||
<option value="10YNO-1--------2">NO1</option>
|
||||
<option value="10YNO-2--------T">NO2</option>
|
||||
@ -317,6 +317,14 @@
|
||||
<option value="10YCH-SWISSGRIDZ">Switzerland</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.resolution ?? "Resolution"}<br/>
|
||||
<select name="pm" bind:value={configuration.p.m} class="in-m">
|
||||
{#each [15,60] as m}
|
||||
<option value={m}>{m}M</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.currency ?? "Currency"}<br/>
|
||||
<select name="pc" bind:value={configuration.p.c} class="in-l">
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
|
||||
export let data = {}
|
||||
export let sysinfo = {}
|
||||
export let prices = {}
|
||||
export let importPrices = {}
|
||||
export let exportPrices = {}
|
||||
export let dayPlot = {}
|
||||
export let monthPlot = {}
|
||||
export let temperatures = {};
|
||||
@ -136,9 +137,20 @@
|
||||
<RealtimePlot title={translations.dashboard?.realtime ?? "Real time"}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.p && !Number.isNaN(data.p))}
|
||||
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0)}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title="{translations.dashboard?.price_import ?? "Price import"}" json={importPrices}/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title={translations.dashboard?.price ?? "Price"} json={importPrices}/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0) && uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.pe))}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title={translations.dashboard?.price ?? "Price"} json={prices} sysinfo={sysinfo}/>
|
||||
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
|
||||
|
||||
@ -60,7 +60,7 @@ export const dataStore = readable(data, (set) => {
|
||||
lastTemp = data.t;
|
||||
setTimeout(getTemperatures, 2000);
|
||||
}
|
||||
if(lastPrice == null && data.pe && data.p != null) {
|
||||
if(data.pe && data.p != lastPrice) {
|
||||
lastPrice = data.p;
|
||||
getPrices();
|
||||
}
|
||||
@ -109,41 +109,31 @@ export const dataStore = readable(data, (set) => {
|
||||
}
|
||||
});
|
||||
|
||||
let prices = {};
|
||||
let priceShiftTimeout;
|
||||
export const pricesStore = writable(prices);
|
||||
|
||||
export async function shiftPrices() {
|
||||
let fetchUpdate = false;
|
||||
pricesStore.update(p => {
|
||||
for(var i = 0; i < 36; i++) {
|
||||
if(p[zeropad(i)] == null) {
|
||||
fetchUpdate = i < 12;
|
||||
break;
|
||||
}
|
||||
p[zeropad(i)] = p[zeropad(i+1)];
|
||||
}
|
||||
return p;
|
||||
});
|
||||
if(fetchUpdate) {
|
||||
getPrices();
|
||||
} else {
|
||||
let date = new Date();
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
}
|
||||
}
|
||||
let priceFetchTimeout;
|
||||
let importPrices = {};
|
||||
export const importPricesStore = writable(importPrices);
|
||||
let exportprices = {};
|
||||
export const exportPricesStore = writable(exportprices);
|
||||
|
||||
export async function getPrices() {
|
||||
if(priceShiftTimeout) {
|
||||
clearTimeout(priceShiftTimeout);
|
||||
priceShiftTimeout = 0;
|
||||
if(priceFetchTimeout) {
|
||||
clearTimeout(priceFetchTimeout);
|
||||
priceFetchTimeout = 0;
|
||||
}
|
||||
{
|
||||
const response = await fetchWithTimeout("importprice.json");
|
||||
importPrices = (await response.json())
|
||||
importPricesStore.set(importPrices);
|
||||
}
|
||||
|
||||
if(importPrices?.importExportPriceDifferent) {
|
||||
const response = await fetchWithTimeout("exportprice.json");
|
||||
exportprices = (await response.json())
|
||||
exportPricesStore.set(exportprices);
|
||||
}
|
||||
const response = await fetchWithTimeout("energyprice.json");
|
||||
prices = (await response.json())
|
||||
pricesStore.set(prices);
|
||||
|
||||
let date = new Date();
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
priceFetchTimeout = setTimeout(getPrices, ((24-date.getHours())*3600000)+10)
|
||||
}
|
||||
|
||||
let dayPlot = {};
|
||||
|
||||
@ -145,6 +145,11 @@ export function addHours(date, hours) {
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMinutes(date, minutes) {
|
||||
date.setTime(date.getTime() + minutes * 60000);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getPriceSourceName(code) {
|
||||
if(code == "EOE") return "ENTSO-E";
|
||||
if(code == "HKS") return "hvakosterstrommen.no";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { zeropad, addHours, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import { zeropad, addHours, addMinutes, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import BarChart from './BarChart.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let title;
|
||||
export let json;
|
||||
export let sysinfo;
|
||||
|
||||
let config = {};
|
||||
let max;
|
||||
@ -12,113 +12,123 @@
|
||||
|
||||
let dark = document.documentElement.classList.contains('dark');
|
||||
|
||||
let cur = new Date();
|
||||
|
||||
onMount(() => {
|
||||
let timeout;
|
||||
function scheduleUpdate() {
|
||||
cur = new Date();
|
||||
timeout = setTimeout(() => {
|
||||
scheduleUpdate();
|
||||
}, (15 - (cur.getMinutes() % 15)) * 60000);
|
||||
}
|
||||
|
||||
scheduleUpdate();
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
});
|
||||
|
||||
$: {
|
||||
let currency = json.currency;
|
||||
let hour = new Date().getUTCHours();
|
||||
let i = 0;
|
||||
let val = 0;
|
||||
let h = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
let cur = new Date();
|
||||
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
|
||||
for(i = hour; i<24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
for(i = 0; i < 24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
if(json?.prices?.length > 0) {
|
||||
cur = new Date();
|
||||
let currency = json?.currency;
|
||||
let val = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
let i = Math.floor(((cur.getHours()*60) + cur.getMinutes()) / json?.resolution);
|
||||
cur.setMinutes(Math.floor(cur.getMinutes()/json?.resolution)*json?.resolution,0,0);
|
||||
while(i < json?.prices?.length) {
|
||||
val = json.prices[i];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: values.length > 0 && json?.resolution < 60 && cur.getMinutes() != 0 ? '' : zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addMinutes(cur, json?.resolution);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
}
|
||||
}
|
||||
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
let points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<BarChart config={config} />
|
||||
{#if config.points && config.points.length > 0}
|
||||
<BarChart config={config} />
|
||||
{/if}
|
||||
@ -41,12 +41,6 @@
|
||||
min = 0;
|
||||
max = 0;
|
||||
|
||||
/*
|
||||
console.log("\n--Realtime plot debug--")
|
||||
console.log("Data length: %d\nSize: %d", realtime?.data?.length, realtime?.size);
|
||||
console.log("Height: %d\nWidth: %d\nBar width: %s", heightAvailable, widthAvailable, barWidth);
|
||||
*/
|
||||
|
||||
if(realtime.data && heightAvailable > 10 && widthAvailable > 100 && barWidth > 0.1) {
|
||||
visible = true;
|
||||
for(let p in realtime.data) {
|
||||
@ -90,9 +84,6 @@
|
||||
} else {
|
||||
visible = false;
|
||||
}
|
||||
/*
|
||||
console.log("Min: %d\nMax: %d\nShow: %s", min, max, visible);
|
||||
*/
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -25,8 +25,6 @@
|
||||
label: 0
|
||||
});
|
||||
|
||||
console.log(realtime);
|
||||
|
||||
if(tariffData && !isNaN(realtime?.h?.u)) {
|
||||
points.push({
|
||||
label: realtime.h.u.toFixed(2),
|
||||
|
||||
@ -40,6 +40,8 @@
|
||||
"tariffpeak" : "Tariff peaks",
|
||||
"realtime" : "Real-time plot",
|
||||
"price" : "Future energy price",
|
||||
"price_import" : "Future import price",
|
||||
"price_export" : "Future export price",
|
||||
"day" : "Energy use last 24 hours",
|
||||
"month" : "Energy use last {0} days",
|
||||
"temperature" : "Temperature sensors"
|
||||
|
||||
@ -19,6 +19,8 @@ export default defineConfig({
|
||||
proxy: {
|
||||
"/data.json": "http://192.168.21.122",
|
||||
"/energyprice.json": "http://192.168.21.122",
|
||||
"/importprice.json": "http://192.168.21.122",
|
||||
"/exportprice.json": "http://192.168.21.122",
|
||||
"/dayplot.json": "http://192.168.21.122",
|
||||
"/monthplot.json": "http://192.168.21.122",
|
||||
"/temperature.json": "http://192.168.21.122",
|
||||
|
||||
@ -125,7 +125,10 @@ private:
|
||||
void dataJson();
|
||||
void dayplotJson();
|
||||
void monthplotJson();
|
||||
void energyPriceJson();
|
||||
void energyPriceJson(); // Deprecated
|
||||
void importPriceJson();
|
||||
void exportPriceJson();
|
||||
void priceJson(uint8_t direction);
|
||||
void temperatureJson();
|
||||
void tariffJson();
|
||||
void realtimeJson();
|
||||
|
||||
@ -2,5 +2,6 @@
|
||||
"e": %s,
|
||||
"t": "%s",
|
||||
"r": "%s",
|
||||
"c": "%s"
|
||||
"c": "%s",
|
||||
"m": %d
|
||||
},
|
||||
@ -127,6 +127,8 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa
|
||||
server.on(context + F("/dayplot.json"), HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this));
|
||||
server.on(context + F("/monthplot.json"), HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this));
|
||||
server.on(context + F("/energyprice.json"), HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this));
|
||||
server.on(context + F("/importprice.json"), HTTP_GET, std::bind(&AmsWebServer::importPriceJson, this));
|
||||
server.on(context + F("/exportprice.json"), HTTP_GET, std::bind(&AmsWebServer::exportPriceJson, this));
|
||||
server.on(context + F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this));
|
||||
server.on(context + F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this));
|
||||
server.on(context + F("/realtime.json"), HTTP_GET, std::bind(&AmsWebServer::realtimeJson, this));
|
||||
@ -584,8 +586,8 @@ void AmsWebServer::dataJson() {
|
||||
mqttStatus = 3;
|
||||
}
|
||||
|
||||
float price = ea->getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
|
||||
float exportPrice = ea->getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
float price = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
|
||||
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
|
||||
|
||||
String peaks = "";
|
||||
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
|
||||
@ -712,18 +714,24 @@ void AmsWebServer::monthplotJson() {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
void AmsWebServer::energyPriceJson() {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
|
||||
if(ps == NULL || !ps->hasPrice()) {
|
||||
notFound();
|
||||
return;
|
||||
}
|
||||
|
||||
float prices[36];
|
||||
for(int i = 0; i < 36; i++) {
|
||||
prices[i] = ps == NULL ? PRICE_NO_VALUE : ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
|
||||
prices[i] = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
}
|
||||
|
||||
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\""),
|
||||
ps == NULL ? "" : ps->getCurrency(),
|
||||
ps == NULL ? "" : ps->getSource()
|
||||
ps->getCurrency(),
|
||||
ps->getSource()
|
||||
);
|
||||
|
||||
for(uint8_t i = 0;i < 36; i++) {
|
||||
@ -744,6 +752,58 @@ void AmsWebServer::energyPriceJson() {
|
||||
server.send(200, MIME_JSON, buf);
|
||||
}
|
||||
|
||||
void AmsWebServer::importPriceJson() {
|
||||
priceJson(PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
|
||||
void AmsWebServer::exportPriceJson() {
|
||||
priceJson(PRICE_DIRECTION_EXPORT);
|
||||
}
|
||||
|
||||
void AmsWebServer::priceJson(uint8_t direction) {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
|
||||
if(ps == NULL || !ps->hasPrice()) {
|
||||
notFound();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
|
||||
float prices[numberOfPoints];
|
||||
for(int i = 0; i < numberOfPoints; i++) {
|
||||
prices[i] = ps->getPricePoint(direction, i);
|
||||
}
|
||||
|
||||
snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"resolution\":%d,\"direction\":\"%s\",\"importExportPriceDifferent\":%s,\"prices\":["),
|
||||
ps->getCurrency(),
|
||||
ps->getSource(),
|
||||
ps->getResolutionInMinutes(),
|
||||
direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both",
|
||||
ps->isExportPricesDifferentFromImport() ? "true" : "false"
|
||||
);
|
||||
|
||||
addConditionalCloudHeaders();
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.send(200, MIME_JSON, buf);
|
||||
|
||||
for(uint8_t i = 0;i < numberOfPoints; i++) {
|
||||
if(prices[i] == PRICE_NO_VALUE) {
|
||||
snprintf_P(buf, BufferSize, PSTR("%snull"), i == 0 ? "" : ",");
|
||||
server.sendContent(buf);
|
||||
} else {
|
||||
snprintf_P(buf, BufferSize, PSTR("%s%.4f"), i == 0 ? "" : ",", prices[i]);
|
||||
server.sendContent(buf);
|
||||
}
|
||||
}
|
||||
server.sendContent_P(PSTR("]}"));
|
||||
}
|
||||
|
||||
void AmsWebServer::temperatureJson() {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
@ -988,7 +1048,8 @@ void AmsWebServer::configurationJson() {
|
||||
price.enabled ? "true" : "false",
|
||||
price.entsoeToken,
|
||||
price.area,
|
||||
price.currency
|
||||
price.currency,
|
||||
price.resolutionInMinutes
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_DEBUG_JSON,
|
||||
@ -1333,18 +1394,20 @@ void AmsWebServer::handleSave() {
|
||||
|
||||
if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) {
|
||||
long mode = server.arg(F("nc")).toInt();
|
||||
if(mode > 0 && mode < 3) {
|
||||
if(mode > 0) {
|
||||
NetworkConfig network;
|
||||
config->getNetworkConfig(network);
|
||||
network.mode = mode;
|
||||
strcpy(network.ssid, server.arg(F("ws")).c_str());
|
||||
String psk = server.arg(F("wp"));
|
||||
if(!psk.equals("***")) {
|
||||
strcpy(network.psk, psk.c_str());
|
||||
if(mode < 3) {
|
||||
strcpy(network.ssid, server.arg(F("ws")).c_str());
|
||||
String psk = server.arg(F("wp"));
|
||||
if(!psk.equals("***")) {
|
||||
strcpy(network.psk, psk.c_str());
|
||||
}
|
||||
network.power = server.arg(F("ww")).toFloat() * 10;
|
||||
network.sleep = server.arg(F("wz")).toInt();
|
||||
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
|
||||
}
|
||||
network.power = server.arg(F("ww")).toFloat() * 10;
|
||||
network.sleep = server.arg(F("wz")).toInt();
|
||||
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
|
||||
|
||||
if(server.hasArg(F("nm"))) {
|
||||
if(server.arg(F("nm")) == "static") {
|
||||
@ -1564,6 +1627,7 @@ void AmsWebServer::handleSave() {
|
||||
strcpy(price.entsoeToken, server.arg(F("pt")).c_str());
|
||||
strcpy(price.area, priceRegion.c_str());
|
||||
strcpy(price.currency, server.arg(F("pc")).c_str());
|
||||
price.resolutionInMinutes = server.arg(F("pm")).toInt();
|
||||
config->setPriceServiceConfig(price);
|
||||
}
|
||||
|
||||
|
||||
@ -253,6 +253,7 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
if(setupMode) return; // None of this necessary in setup mode
|
||||
if(ch != NULL) ch->eventHandler(event, info);
|
||||
switch(event) {
|
||||
case ARDUINO_EVENT_ETH_CONNECTED:
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED: {
|
||||
dnsState = 0;
|
||||
if(ch != NULL) {
|
||||
@ -265,6 +266,7 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP: {
|
||||
if(dnsState == 0) {
|
||||
const ip_addr_t* dns = dns_getserver(0);
|
||||
@ -282,6 +284,7 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: {
|
||||
if(WiFi.getMode() == WIFI_STA) {
|
||||
wifi_err_reason_t reason = (wifi_err_reason_t) info.wifi_sta_disconnected.reason;
|
||||
@ -1631,7 +1634,7 @@ void MQTT_connect() {
|
||||
HomeAssistantConfig haconf;
|
||||
config.getHomeAssistantConfig(haconf);
|
||||
NetworkConfig network;
|
||||
ch->getCurrentConfig(network);
|
||||
config.getNetworkConfig(network);
|
||||
HomeAssistantMqttHandler* hamh = (HomeAssistantMqttHandler*) &mqttHandler;
|
||||
hamh->setHomeAssistantConfig(haconf, network.hostname);
|
||||
break;
|
||||
@ -1660,7 +1663,7 @@ void MQTT_connect() {
|
||||
HomeAssistantConfig haconf;
|
||||
config.getHomeAssistantConfig(haconf);
|
||||
NetworkConfig network;
|
||||
ch->getCurrentConfig(network);
|
||||
config.getNetworkConfig(network);
|
||||
mqttHandler = new HomeAssistantMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, sysConfig.boardType, haconf, &hw, &updater, network.hostname);
|
||||
break;
|
||||
case 255:
|
||||
@ -1673,7 +1676,7 @@ void MQTT_connect() {
|
||||
if(mqttHandler != NULL) {
|
||||
mqttHandler->connect();
|
||||
mqttHandler->publishSystem(&hw, ps, &ea);
|
||||
if(ps != NULL && ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
|
||||
if(ps != NULL && ps->hasPrice()) {
|
||||
mqttHandler->publishPrices(ps);
|
||||
}
|
||||
}
|
||||
@ -1966,12 +1969,6 @@ void configFileParse() {
|
||||
} else if(strncmp_P(buf, PSTR("entsoeCurrency "), 15) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
strcpy(price.currency, buf+15);
|
||||
} else if(strncmp_P(buf, PSTR("entsoeMultiplier "), 17) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
price.unused1 = String(buf+17).toFloat() * 1000;
|
||||
} else if(strncmp_P(buf, PSTR("entsoeFixedPrice "), 17) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
price.unused2 = String(buf+17).toFloat() * 1000;
|
||||
} else if(strncmp_P(buf, PSTR("priceEnabled "), 13) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
price.enabled = String(buf+13).toInt() == 1;
|
||||
@ -1984,12 +1981,6 @@ void configFileParse() {
|
||||
} else if(strncmp_P(buf, PSTR("priceCurrency "), 14) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
strcpy(price.currency, buf+14);
|
||||
} else if(strncmp_P(buf, PSTR("priceMultiplier "), 16) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
price.unused1 = String(buf+16).toFloat() * 1000;
|
||||
} else if(strncmp_P(buf, PSTR("priceFixedPrice "), 16) == 0) {
|
||||
if(!lPrice) { config.getPriceServiceConfig(price); lPrice = true; };
|
||||
price.unused2 = String(buf+16).toFloat() * 1000;
|
||||
} else if(strncmp_P(buf, PSTR("priceModifier "), 14) == 0) {
|
||||
PriceConfig pc;
|
||||
memset(&pc, 0, sizeof(PriceConfig));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user