Compare commits

..

3 Commits

Author SHA1 Message Date
Gunnar Skjold
1ed701fc57 Option to auto reboot if MQTT connection is lost 2025-11-06 17:33:16 +01:00
Gunnar Skjold
473932f1bc Fixed Zmartcharge configuration issue 2025-10-15 16:35:36 +02:00
Gunnar Skjold
d7e68ce692 Fixing board type overwrite, zmartcharge default issues and disabling entsoe 2025-10-10 15:25:12 +02:00
94 changed files with 3198 additions and 4716 deletions

View File

@@ -27,7 +27,6 @@ jobs:
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Cache Python dependencies
@@ -51,7 +50,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '22.x'
node-version: '19.x'
- name: Build with node
run: |
cd lib/SvelteUi/app

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,12 @@
# 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://www.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://amsleser.no/).
<img src="images/dashboard.png">
## 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
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).
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.

View File

@@ -1,48 +0,0 @@
They actually use multiple frames, so this is a "fake" frame combining the two into one, but without checksum fields.
7E
A0 BD
CF 02 23 03 00 00
E6 E7 00
0F 00 03 46 3B
0C 07 E9 0C 13 05 17 37 28 00 FF C4 00
02 21
09 08 39 32 30 32 39 36 39 31
09 04 17 37 28 00
09 05 07 E9 0C 13 05
06 00 6C 28 5A
06 00 4B 76 1A
06 00 20 B2 40
06 00 58 68 AA
06 00 57 A1 62
06 00 00 C7 48
06 00 17 EE D7
06 00 12 F5 5C
06 00 00 D9 6A
06 00 15 36 84
06 00 00 01 7E
06 00 00 00 00
12 03 79
06 00 00 00 7F
06 00 00 00 BD
06 00 00 00 41
06 00 00 00 00
06 00 00 00 00
06 00 00 00 00
12 09 54
12 09 35
12 09 49
12 00 37
12 00 59
12 00 4D
06 00 00 43 62
01 01
12 24 B8
01 01
12 24 B8
01 01
12 24 B8
03 01
00 00 7E

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -50,22 +50,6 @@
#define LED_BEHAVIOUR_ERROR_ONLY 3
#define LED_BEHAVIOUR_OFF 9
#define FIRMWARE_CHANNEL_STABLE 0
#define FIRMWARE_CHANNEL_EARLY 1
#define FIRMWARE_CHANNEL_RC 2
#define FIRMWARE_CHANNEL_SNAPSHOT 3
#define REBOOT_CAUSE_WEB_SYSINFO_JSON 1
#define REBOOT_CAUSE_WEB_SAVE 2
#define REBOOT_CAUSE_WEB_REBOOT 3
#define REBOOT_CAUSE_WEB_FACTORY_RESET 4
#define REBOOT_CAUSE_BTN_FACTORY_RESET 5
#define REBOOT_CAUSE_REPARTITION 6
#define REBOOT_CAUSE_CONFIG_FILE_UPDATE 7
#define REBOOT_CAUSE_FIRMWARE_UPDATE 8
#define REBOOT_CAUSE_MQTT_DISCONNECTED 9
#define REBOOT_CAUSE_SMART_CONFIG 10
struct ResetDataContainer {
uint8_t cause;
uint8_t last_cause;
@@ -79,8 +63,7 @@ struct SystemConfig {
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[3];
uint8_t energyspeedometer;
uint8_t firmwareChannel;
}; // 9
}; // 8
struct NetworkConfig {
char ssid[32];
@@ -175,8 +158,7 @@ struct GpioConfig {
uint16_t vccResistorVcc;
uint8_t ledDisablePin;
uint8_t ledBehaviour;
uint8_t powersaving;
}; // 22
}; // 21
struct GpioConfig103 {
uint8_t hanPin;
@@ -225,12 +207,10 @@ struct PriceServiceConfig {
char entsoeToken[37];
char area[17];
char currency[4];
uint8_t resolutionInMinutes;
uint16_t unused2;
uint16_t unused3;
uint32_t unused1;
bool enabled;
uint16_t unused6;
};
uint16_t unused2;
}; // 64
struct EnergyAccountingConfig {
uint16_t thresholds[10];
@@ -257,14 +237,14 @@ struct UiConfig {
}; // 15
struct UpgradeInformation {
char fromVersion[16];
char toVersion[16];
char fromVersion[8];
char toVersion[8];
uint32_t size;
uint16_t block_position;
uint8_t retry_count;
uint8_t reboot_count;
int8_t errorCode;
}; // 41+3
}; // 25
struct CloudConfig {
bool enabled;

View File

@@ -15,11 +15,6 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
if(config.firmwareChannel > 3) {
config.firmwareChannel = 0;
}
if(configVersion == EEPROM_CHECK_SUM) {
return true;
} else {
@@ -32,7 +27,6 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
}
config.userConfigured = false;
config.dataCollectionConsent = 0;
config.firmwareChannel = 0;
config.energyspeedometer = 0;
memset(config.country, 0, 3);
return false;
@@ -48,9 +42,6 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent;
sysChanged |= strcmp(config.country, existing.country) != 0;
sysChanged |= config.energyspeedometer != existing.energyspeedometer;
sysChanged |= config.firmwareChannel != existing.firmwareChannel;
} else {
sysChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
stripNonAscii((uint8_t*) config.country, 2);
@@ -517,7 +508,6 @@ bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_GPIO_START, config);
EEPROM.end();
if(config.powersaving > 4) config.powersaving = 0;
return true;
} else {
clearGpio(config);
@@ -599,7 +589,6 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.vccResistorGnd = 0;
config.vccResistorVcc = 0;
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
config.powersaving = 0;
}
}
@@ -666,9 +655,6 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
clearPriceServiceConfig(config);
return false;
}
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
config.resolutionInMinutes = 60;
}
return true;
} else {
clearPriceServiceConfig(config);
@@ -683,7 +669,6 @@ 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;
}
@@ -703,8 +688,9 @@ 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.resolutionInMinutes = 60;
config.unused2 = 0;
}
bool AmsConfiguration::isPriceServiceChanged() {
@@ -832,8 +818,8 @@ void AmsConfiguration::ackUiLanguageChange() {
}
bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) {
stripNonAscii((uint8_t*) upinfo.fromVersion, 16);
stripNonAscii((uint8_t*) upinfo.toVersion, 16);
stripNonAscii((uint8_t*) upinfo.fromVersion, 8);
stripNonAscii((uint8_t*) upinfo.toVersion, 8);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
@@ -847,7 +833,7 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.end();
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 16) || stripNonAscii((uint8_t*) upinfo.toVersion, 16)) {
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 8) || stripNonAscii((uint8_t*) upinfo.toVersion, 8)) {
clearUpgradeInformation(upinfo);
return false;
}
@@ -859,8 +845,8 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
}
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
memset(upinfo.fromVersion, 0, 16);
memset(upinfo.toVersion, 0, 16);
memset(upinfo.fromVersion, 0, 8);
memset(upinfo.toVersion, 0, 8);
upinfo.errorCode = 0;
upinfo.size = 0;
upinfo.block_position = 0;
@@ -990,7 +976,6 @@ void AmsConfiguration::clear() {
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
sys.firmwareChannel = 0;
sys.energyspeedometer = 0;
memset(sys.country, 0, 3);
EEPROM.put(CONFIG_SYSTEM_START, sys);
@@ -1153,8 +1138,7 @@ bool AmsConfiguration::relocateConfig103() {
gpio103.vccResistorGnd,
gpio103.vccResistorVcc,
gpio103.ledDisablePin,
gpio103.ledBehaviour,
0
gpio103.ledBehaviour
};
WebConfig web = {web103.security};
@@ -1326,7 +1310,6 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf_P(PSTR("Vcc pin: %i\r\n"), gpio.vccPin);
debugger->printf_P(PSTR("LED disable pin: %i\r\n"), gpio.ledDisablePin);
debugger->printf_P(PSTR("LED behaviour: %i\r\n"), gpio.ledBehaviour);
debugger->printf_P(PSTR("Power saving: %i\r\n"), gpio.powersaving);
if(gpio.vccMultiplier > 0) {
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
}

View File

@@ -74,7 +74,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc);
if(crc > 0 && crc != crc_calc) {
if(crc != crc_calc) {
if(debugger != NULL) {
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
}

View File

@@ -32,7 +32,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
// Verify FCS
if(f->fcs > 0 && ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
// Skip destination address, LSB marks last byte
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
// Verify HCS
if(t3->hcs > 0 && ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
return DATA_PARSE_HEADER_CHECKSUM_ERROR;
ptr += 3;
@@ -69,12 +69,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
if(buf == NULL) return DATA_PARSE_FAIL;
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
pos += ctx.length;
lastSequenceNumber++;
@@ -83,12 +78,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
lastSequenceNumber = 0;
if(buf == NULL) return DATA_PARSE_FAIL;
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
pos += ctx.length;
memcpy((uint8_t *) d, buf, pos);

View File

@@ -39,8 +39,6 @@
#define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123
#define UPDATE_BUF_SIZE 4096
#define UPDATE_MAX_BLOCK_RETRY 25
#define UPDATE_MAX_REBOOT_RETRY 12
class AmsFirmwareUpdater {
public:
@@ -62,13 +60,6 @@ public:
bool isUpgradeInformationChanged();
void ackUpgradeInformationChanged();
void setFirmwareChannel(uint8_t channel) {
if(firmwareChannel != channel) {
firmwareChannel = channel;
lastVersionCheck = 0;
}
}
bool startFirmwareUpload(uint32_t size, const char* version);
bool addFirmwareUploadChunk(uint8_t* buf, size_t length);
bool completeFirmwareUpload(uint32_t size);
@@ -104,11 +95,10 @@ private:
String md5;
uint32_t lastVersionCheck = 0;
uint8_t firmwareChannel;
uint8_t firmwareVariant;
bool autoUpgrade;
char nextVersion[17];
char nextVersion[10];
void getChannelName(char * buffer);
bool fetchNextVersion();
bool fetchVersionDetails();

View File

@@ -22,7 +22,7 @@ this->debugger = debugger;
this->hw = hw;
this->meterState = meterState;
memset(nextVersion, 0, sizeof(nextVersion));
firmwareChannel = 0;
firmwareVariant = 0;
autoUpgrade = false;
}
@@ -74,7 +74,7 @@ void AmsFirmwareUpdater::setUpgradeInformation(UpgradeInformation& upinfo) {
#endif
debugger->printf_P(PSTR("Resuming uprade to %s\n"), updateStatus.toVersion);
if(updateStatus.reboot_count++ < UPDATE_MAX_REBOOT_RETRY) {
if(updateStatus.reboot_count++ < 8) {
updateStatus.errorCode = AMS_UPDATE_ERR_OK;
} else {
updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT;
@@ -129,7 +129,7 @@ void AmsFirmwareUpdater::loop() {
HTTPClient http;
start = millis();
if(!fetchFirmwareChunk(http)) {
if(updateStatus.retry_count++ > UPDATE_MAX_BLOCK_RETRY) {
if(updateStatus.retry_count++ == 3) {
updateStatus.errorCode = AMS_UPDATE_ERR_FETCH;
updateStatusChanged = true;
}
@@ -208,33 +208,15 @@ void AmsFirmwareUpdater::loop() {
}
}
void AmsFirmwareUpdater::getChannelName(char * buffer) {
switch(firmwareChannel) {
case FIRMWARE_CHANNEL_EARLY:
strcpy(buffer, PSTR("early"));
break;
case FIRMWARE_CHANNEL_RC:
strcpy(buffer, PSTR("rc"));
break;
case FIRMWARE_CHANNEL_SNAPSHOT:
strcpy(buffer, PSTR("snapshot"));
break;
default:
strcpy(buffer, PSTR("stable"));
break;
}
}
bool AmsFirmwareUpdater::fetchNextVersion() {
HTTPClient http;
const char * headerkeys[] = { "x-version" };
http.collectHeaders(headerkeys, 1);
char channel[10] = "";
getChannelName(channel);
char firmwareVariant[10] = "stable";
char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, channel);
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, firmwareVariant);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
@@ -248,16 +230,6 @@ bool AmsFirmwareUpdater::fetchNextVersion() {
http.setUserAgent("AMS-Firmware-Updater");
http.addHeader(F("Cache-Control"), "no-cache");
http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString);
http.addHeader(F("x-AMS-STA-MAC"), WiFi.macAddress());
http.addHeader(F("x-AMS-AP-MAC"), WiFi.softAPmacAddress());
http.addHeader(F("x-AMS-chip-size"), String(ESP.getFlashChipSize()));
http.addHeader(F("x-AMS-board-type"), String(hw->getBoardType(), 10));
if(meterState->getMeterType() != AmsTypeAutodetect) {
http.addHeader(F("x-AMS-meter-mfg"), String(meterState->getMeterType(), 10));
}
if(!meterState->getMeterModel().isEmpty()) {
http.addHeader(F("x-AMS-meter-model"), meterState->getMeterModel());
}
int status = http.GET();
if(status == 204) {
String nextVersion = http.header("x-version");
@@ -281,11 +253,10 @@ bool AmsFirmwareUpdater::fetchVersionDetails() {
const char * headerkeys[] = { "x-size" };
http.collectHeaders(headerkeys, 1);
char channel[10] = "";
getChannelName(channel);
char firmwareVariant[10] = "stable";
char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, channel, updateStatus.toVersion);
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, firmwareVariant, updateStatus.toVersion);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
@@ -338,11 +309,10 @@ bool AmsFirmwareUpdater::fetchFirmwareChunk(HTTPClient& http) {
char range[24];
snprintf_P(range, 24, PSTR("bytes=%lu-%lu"), start, end);
char channel[10] = "";
getChannelName(channel);
char firmwareVariant[10] = "stable";
char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, channel, updateStatus.toVersion);
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, firmwareVariant, updateStatus.toVersion);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ public:
#if defined(AMS_REMOTE_DEBUG)
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) {
#else
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) {
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) {
#endif
this->mqttConfig = mqttConfig;
this->mqttConfigChanged = true;
@@ -43,12 +43,10 @@ public:
void setConfig(MqttConfig& mqttConfig);
bool connect();
bool defaultSubscribe();
void disconnect();
lwmqtt_err_t lastError();
bool connected();
bool loop();
bool isRebootSuggested();
virtual uint8_t getFormat() { return 0; };
@@ -57,16 +55,11 @@ public:
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
virtual bool publishPrices(PriceService* ps) { return false; };
virtual bool publishSystem(HwTools*, PriceService*, EnergyAccounting*) { return false; };
virtual bool publishRaw(uint8_t* raw, size_t length) { return false; };
virtual bool publishRaw(String data) { return false; };
virtual bool publishFirmware() { return false; };
virtual void onMessage(String &topic, String &payload) {};
virtual ~AmsMqttHandler() {
if(mqttSecureClient != NULL) {
mqttSecureClient->stop();
delete mqttSecureClient;
}
if(mqttClient != NULL) {
mqttClient->stop();
delete mqttClient;
@@ -86,7 +79,6 @@ protected:
bool caVerification = true;
WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL;
boolean _connected = false;
char* json;
uint16_t BufferSize = 2048;
uint64_t lastStateUpdate = 0;
@@ -96,7 +88,6 @@ protected:
String subTopic;
AmsFirmwareUpdater* updater = NULL;
bool rebootSuggested = false;
};
#endif

View File

@@ -103,17 +103,6 @@ bool AmsMqttHandler::connect() {
actualClient = mqttClient;
}
// This section helps with power saving on ESP32 devices by reducing timeouts
// The timeout is multiplied by 10 because WiFiClient is retrying 10 times internally
// Power drain for this timeout is too great when using the default 3s timeout
// On ESP8266 the timeout is used differently and the following code causes MQTT instability
#if defined(ESP32)
int clientTimeout = mqttConfig.timeout / 1000;
if(clientTimeout > 3) clientTimeout = 3; // 3000ms is default, see WiFiClient.cpp WIFI_CLIENT_DEF_CONN_TIMEOUT_MS
actualClient->setTimeout(clientTimeout);
// Why can't we set number of retries for write here? WiFiClient defaults to 10 (10*3s == 30s)
#endif
mqttConfigChanged = false;
mqtt.setTimeout(mqttConfig.timeout);
mqtt.setKeepAlive(mqttConfig.keepalive);
@@ -136,9 +125,20 @@ bool AmsMqttHandler::connect() {
#endif
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
_connected = mqtt.publish(statusTopic, "online", true, 0);
if(strlen(mqttConfig.subscribeTopic) > 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
}
}
mqtt.publish(statusTopic, "online", true, 0);
mqtt.loop();
defaultSubscribe();
postConnect();
return true;
} else {
@@ -158,29 +158,13 @@ bool AmsMqttHandler::connect() {
}
}
bool AmsMqttHandler::defaultSubscribe() {
bool ret = true;
if(!subTopic.isEmpty()) {
if(mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), subTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), subTopic.c_str());
ret = false;
}
}
return ret;
}
void AmsMqttHandler::disconnect() {
mqtt.disconnect();
mqtt.loop();
_connected = false;
if(mqttSecureClient != NULL) {
delete mqttSecureClient;
mqttSecureClient = NULL;
}
delay(10);
yield();
}
@@ -190,12 +174,12 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
}
bool AmsMqttHandler::connected() {
return _connected && mqtt.connected();
return mqtt.connected();
}
bool AmsMqttHandler::loop() {
uint64_t now = millis64();
bool ret = connected() && mqtt.loop();
bool ret = mqtt.connected() && mqtt.loop();
if(ret) {
lastSuccessfulLoop = now;
} else if(mqttConfig.rebootMinutes > 0) {
@@ -205,10 +189,10 @@ bool AmsMqttHandler::loop() {
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("MQTT connection lost for over %d minutes, rebooting device\n"), mqttConfig.rebootMinutes);
rebootSuggested = true;
ESP.restart();
}
}
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
delay(10);
yield();
#if defined(ESP32)
esp_task_wdt_reset();
@@ -216,8 +200,4 @@ bool AmsMqttHandler::loop() {
ESP.wdtFeed();
#endif
return ret;
}
bool AmsMqttHandler::isRebootSuggested() {
return rebootSuggested;
}

View File

@@ -114,7 +114,6 @@ 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))
@@ -148,9 +147,6 @@ 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)

View File

@@ -17,7 +17,7 @@ public:
this->config = config;
};
#else
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->config = config;
};
#endif
@@ -25,7 +25,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(uint8_t* raw, size_t length);
bool publishRaw(String data);
void onMessage(String &topic, String &payload);

View File

@@ -103,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
return 3;
}
bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
bool DomoticzMqttHandler::publishRaw(String data) {
return false;
}

View File

@@ -13,12 +13,6 @@
#include "PriceService.h"
struct EnergyAccountingPeak {
uint8_t day;
uint8_t hour;
uint16_t value;
};
struct EnergyAccountingPeak6 {
uint8_t day;
uint16_t value;
};
@@ -26,11 +20,9 @@ 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;
@@ -38,22 +30,36 @@ struct EnergyAccountingData {
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak peaks[5];
time_t lastUpdated;
};
struct EnergyAccountingData6 {
struct EnergyAccountingData5 {
uint8_t version;
uint8_t month;
int32_t costYesterday;
int32_t costThisMonth;
int32_t costLastMonth;
int32_t incomeYesterday;
int32_t incomeThisMonth;
int32_t incomeLastMonth;
uint32_t lastMonthImport;
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak6 peaks[5];
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
uint16_t incomeYesterday;
uint16_t incomeThisMonth;
uint16_t incomeLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData4 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData2 {
uint8_t version;
uint8_t month;
uint16_t maxHour;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
};
struct EnergyAccountingRealtimeData {
@@ -118,6 +124,7 @@ public:
void setData(EnergyAccountingData&);
void setCurrency(String currency);
float getPriceForHour(uint8_t d, uint8_t h);
private:
#if defined(AMS_REMOTE_DEBUG)
@@ -130,12 +137,12 @@ 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, 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 };
EnergyAccountingRealtimeData* realtimeData = NULL;
String currency = "";
void calcDayCost();
bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
bool updateMax(uint16_t val, uint8_t day);
};
#endif

View File

@@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData*
rtd->lastImportUpdateMillis = 0;
rtd->lastExportUpdateMillis = 0;
}
realtimeData = rtd;
this->realtimeData = rtd;
}
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
@@ -67,55 +67,59 @@ bool EnergyAccounting::update(AmsData* amsData) {
breakTime(tz->toLocal(now), local);
if(!init) {
realtimeData->lastImportUpdateMillis = 0;
realtimeData->lastExportUpdateMillis = 0;
realtimeData->currentHour = local.Hour;
realtimeData->currentDay = local.Day;
this->realtimeData->lastImportUpdateMillis = 0;
this->realtimeData->lastExportUpdateMillis = 0;
this->realtimeData->currentHour = local.Hour;
this->realtimeData->currentDay = local.Day;
if(!load()) {
data = { 7, local.Month,
0, 0, 0, 0, // Cost
0, 0, 0, 0, // Income
data = { 6, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, 0, // Peak 1
0, 0, 0, // Peak 2
0, 0, 0, // Peak 3
0, 0, 0, // Peak 4
0, 0, 0 // Peak 5
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
};
}
init = true;
}
if(!initPrice && ps != NULL && ps->hasPrice()) {
float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
if(!initPrice && importPrice != PRICE_NO_VALUE) {
calcDayCost();
}
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
if(local.Hour != this->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);
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
ret |= updateMax(val, oneHrAgoLocal.Day, oneHrAgoLocal.Hour);
ret |= updateMax(val, oneHrAgoLocal.Day);
realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
if(local.Hour > 0) {
calcDayCost();
}
realtimeData->use = 0;
realtimeData->produce = 0;
realtimeData->costHour = 0;
realtimeData->incomeHour = 0;
this->realtimeData->use = 0;
this->realtimeData->produce = 0;
this->realtimeData->costHour = 0;
this->realtimeData->incomeHour = 0;
uint8_t prevDay = realtimeData->currentDay;
if(local.Day != realtimeData->currentDay) {
data.costYesterday = realtimeData->costDay * 100;
data.costThisMonth += realtimeData->costDay * 100;
realtimeData->costDay = 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;
data.incomeYesterday = realtimeData->incomeDay * 100;
data.incomeThisMonth += realtimeData->incomeDay * 100;
realtimeData->incomeDay = 0;
data.incomeYesterday = this->realtimeData->incomeDay * 100;
data.incomeThisMonth += this->realtimeData->incomeDay * 100;
this->realtimeData->incomeDay = 0;
realtimeData->currentDay = local.Day;
this->realtimeData->currentDay = local.Day;
ret = true;
}
@@ -145,49 +149,42 @@ bool EnergyAccounting::update(AmsData* amsData) {
data.lastMonthAccuracy = accuracy;
data.month = local.Month;
realtimeData->currentThresholdIdx = 0;
this->realtimeData->currentThresholdIdx = 0;
ret = true;
}
if(ret) {
data.costToday = realtimeData->costDay * 100;
data.incomeToday = realtimeData->incomeDay * 100;
data.lastUpdated = now;
}
}
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis;
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhi > 0) {
realtimeData->use += kwhi;
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
this->realtimeData->use += kwhi;
if(importPrice != PRICE_NO_VALUE) {
float cost = importPrice * kwhi;
realtimeData->costHour += cost;
realtimeData->costDay += cost;
this->realtimeData->costHour += cost;
this->realtimeData->costDay += cost;
}
}
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
}
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis;
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhe > 0) {
realtimeData->produce += kwhe;
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
this->realtimeData->produce += kwhe;
float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
if(exportPrice != PRICE_NO_VALUE) {
float income = exportPrice * kwhe;
realtimeData->incomeHour += income;
realtimeData->incomeDay += income;
this->realtimeData->incomeHour += income;
this->realtimeData->incomeDay += income;
}
}
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
}
if(config != NULL) {
while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++;
while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++;
}
return ret;
@@ -195,36 +192,28 @@ bool EnergyAccounting::update(AmsData* amsData) {
void EnergyAccounting::calcDayCost() {
time_t now = time(nullptr);
tmElements_t local, utc, lastUpdateUtc;
tmElements_t local, utc;
if(tz == NULL) return;
breakTime(tz->toLocal(now), local);
if(ps == NULL) return;
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;
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
if(initPrice) {
this->realtimeData->costDay = 0;
this->realtimeData->incomeDay = 0;
}
for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) {
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
float priceIn = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
if(priceIn != PRICE_NO_VALUE) {
int16_t wh = ds->getHourImport(utc.Hour);
realtimeData->costDay += priceIn * (wh / 1000.0);
this->realtimeData->costDay += priceIn * (wh / 1000.0);
}
float priceOut = ps->getPriceForRelativeHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
if(priceOut != PRICE_NO_VALUE) {
int16_t wh = ds->getHourExport(utc.Hour);
realtimeData->incomeDay += priceOut * (wh / 1000.0);
this->realtimeData->incomeDay += priceOut * (wh / 1000.0);
}
}
initPrice = true;
@@ -232,7 +221,7 @@ void EnergyAccounting::calcDayCost() {
}
float EnergyAccounting::getUseThisHour() {
return realtimeData->use;
return this->realtimeData->use;
}
float EnergyAccounting::getUseToday() {
@@ -242,7 +231,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 < realtimeData->currentHour; i++) {
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0;
}
@@ -253,20 +242,18 @@ float EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
float EnergyAccounting::getUseLastMonth() {
float ret = (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
}
float EnergyAccounting::getProducedThisHour() {
return realtimeData->produce;
return this->realtimeData->produce;
}
float EnergyAccounting::getProducedToday() {
@@ -276,7 +263,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 < realtimeData->currentHour; i++) {
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0;
}
@@ -287,24 +274,22 @@ float EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
float EnergyAccounting::getProducedLastMonth() {
float ret = (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
}
float EnergyAccounting::getCostThisHour() {
return realtimeData->costHour;
return this->realtimeData->costHour;
}
float EnergyAccounting::getCostToday() {
return realtimeData->costDay;
return this->realtimeData->costDay;
}
float EnergyAccounting::getCostYesterday() {
@@ -320,11 +305,11 @@ float EnergyAccounting::getCostLastMonth() {
}
float EnergyAccounting::getIncomeThisHour() {
return realtimeData->incomeHour;
return this->realtimeData->incomeHour;
}
float EnergyAccounting::getIncomeToday() {
return realtimeData->incomeDay;
return this->realtimeData->incomeDay;
}
float EnergyAccounting::getIncomeYesterday() {
@@ -342,7 +327,7 @@ float EnergyAccounting::getIncomeLastMonth() {
uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL)
return 0;
return config->thresholds[realtimeData->currentThresholdIdx];
return config->thresholds[this->realtimeData->currentThresholdIdx];
}
float EnergyAccounting::getMonthMax() {
@@ -422,31 +407,85 @@ bool EnergyAccounting::load() {
char buf[file.size()];
file.readBytes(buf, file.size());
if(buf[0] == 7) {
if(buf[0] == 6) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} 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,
data->lastMonthImport,
data->lastMonthExport,
data->lastMonthAccuracy,
data->peaks[0].day, 0, data->peaks[0].value,
data->peaks[1].day, 0, data->peaks[1].value,
data->peaks[2].day, 0, data->peaks[2].value,
data->peaks[3].day, 0, data->peaks[3].value,
data->peaks[4].day, 0, data->peaks[4].value
} else if(buf[0] == 5) {
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
this->data = { 6, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
((uint32_t) data->incomeYesterday) * 10,
((uint32_t) data->incomeThisMonth) * 100,
((uint32_t) data->incomeLastMonth) * 100,
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 3) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
this->data = { 5, data->month,
data->costYesterday * 10,
data->costThisMonth,
data->costLastMonth,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else {
data = { 5, 0,
0, 0, 0, // Cost
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
};
if(buf[0] == 2) {
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
this->data.month = data->month;
this->data.costYesterday = data->costYesterday * 10;
this->data.costThisMonth = data->costThisMonth;
this->data.costLastMonth = data->costLastMonth;
uint8_t b = 0;
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
this->data.peaks[b].day = b;
memcpy(&this->data.peaks[b].value, buf+i, 2);
b++;
if(b >= config->hours || b >= 5) break;
}
ret = true;
} else {
ret = false;
}
}
file.close();
@@ -479,12 +518,11 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
this->data = data;
}
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
for(uint8_t i = 0; i < 5; i++) {
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
if(val > data.peaks[i].value) {
data.peaks[i].day = day;
data.peaks[i].hour = hour;
data.peaks[i].value = val;
return true;
}
@@ -512,3 +550,8 @@ 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);
}

View File

@@ -15,19 +15,19 @@
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
#endif
this->boardType = boardType;
this->hw = hw;
setHomeAssistantConfig(config, hostname);
setHomeAssistantConfig(config);
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(uint8_t* raw, size_t length);
bool publishRaw(String data);
bool publishFirmware();
bool postConnect();
@@ -36,7 +36,7 @@ public:
uint8_t getFormat();
void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
void setHomeAssistantConfig(HomeAssistantConfig config);
private:
uint8_t boardType;
@@ -51,9 +51,9 @@ private:
String updateTopic;
String sensorNamePrefix;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit;
bool tInit[32] = {false};
uint8_t priceImportInit = 0, priceExportInit = 0;
bool prInit[38] = {false};
uint32_t lastThresholdPublish = 0;
HwTools* hw;
@@ -79,7 +79,6 @@ private:
void publishPriceSensors(PriceService* ps);
void publishSystemSensors();
void publishThresholdSensors();
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
String boardTypeToString(uint8_t b) {
switch(b) {

View File

@@ -17,113 +17,113 @@ 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"};
const HomeAssistantSensor DataSensor PROGMEM = {"Data", "/data", "data", 900, "", "", "", ""};
#endif

View File

@@ -1,4 +1,4 @@
{
"P" : %lu,
"t" : %s
"t" : "%s"
}

View File

@@ -3,6 +3,6 @@
"tPO" : %.3f,
"tQI" : %.3f,
"tQO" : %.3f,
"rtc" : %s,
"t" : %s
"rtc" : "%s",
"t" : "%s"
}

View File

@@ -12,5 +12,5 @@
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"t" : %s
"t" : "%s"
}

View File

@@ -28,5 +28,5 @@
"tPO1" : %.3f,
"tPO2" : %.3f,
"tPO3" : %.3f,
"t" : %s
"t" : "%s"
}

View File

@@ -2,7 +2,8 @@
"name" : "%s%s",
"stat_t" : "%s%s",
"uniq_id" : "%s_%s",
"default_entity_id" : "sensor.%s_%s",
"obj_id" : "%s_%s",
"unit_of_meas" : "%s",
"val_tpl" : "{{ value_json.%s | is_defined }}",
"expire_after" : %d,
"dev" : {
@@ -12,8 +13,5 @@
"sw" : "%s",
"mf" : "%s",
"cu" : "%s"
}
%s%s%s
%s%s%s
%s%s%s
}%s%s%s%s%s%s
}

View File

@@ -19,8 +19,8 @@
#include <esp_task_wdt.h>
#endif
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = false;
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
@@ -28,18 +28,21 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(json);
} else {
snprintf_P(json, 128, PSTR("AMS reader"));
deviceName = String(json);
deviceName = F("AMS reader");
sensorNamePrefix = "";
}
deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType);
deviceUid = String(hostname); // Maybe configurable in the future?
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
char hostname[32];
#if defined(ESP8266)
strcpy(hostname, WiFi.hostname().c_str());
#elif defined(ESP32)
strcpy(hostname, WiFi.getHostname());
#endif
debugger->printf_P(PSTR(" Hostname is [%s]\n"), hostname);
stripNonAscii((uint8_t*) hostname, 32, false);
deviceUid = String(hostname); // Maybe configurable in the future?
if(strlen(config.discoveryHostname) > 0) {
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
@@ -53,42 +56,36 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
deviceUrl = String(json);
}
if(strlen(config.discoveryPrefix) == 0) {
snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
if(strlen(config.discoveryPrefix) > 0) {
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
sensorTopic = String(json);
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
updateTopic = String(json);
} else {
statusTopic = F("homeassistant/status");
sensorTopic = F("homeassistant/sensor");
updateTopic = F("homeassistant/update");
}
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
sensorTopic = String(json);
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
updateTopic = String(json);
strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
}
bool HomeAssistantMqttHandler::postConnect() {
bool ret = true;
if(!statusTopic.isEmpty()) {
if(mqtt.subscribe(statusTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), statusTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), statusTopic.c_str());
ret = false;
}
if(!subTopic.isEmpty() && !mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), subTopic.c_str());
return false;
}
return ret;
return true;
}
bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(pubTopic.isEmpty() || !connected())
if(pubTopic.isEmpty() || !mqtt.connected())
return false;
if(time(nullptr) < FirmwareVersion::BuildEpoch)
@@ -133,7 +130,12 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
publishList1Sensors();
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
return mqtt.publish(pubTopic + "/power", json);
@@ -144,7 +146,12 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
@@ -170,11 +177,20 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
char mt[24];
toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
memset(mt, 0, 24);
if(data->getMeterTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getMeterTimestamp(), tm);
sprintf_P(mt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
char pt[24];
memset(pt, 0, 24);
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(),
@@ -192,7 +208,12 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
@@ -282,8 +303,13 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
json[pos++] = '}';
json[pos] = '\0';
@@ -313,8 +339,13 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
bool ret = mqtt.publish(pubTopic + "/temperatures", json);
@@ -323,9 +354,9 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
}
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
if(pubTopic.isEmpty() || !connected())
if(pubTopic.isEmpty() || !mqtt.connected())
return false;
if(!ps->hasPrice())
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
return false;
publishPriceSensors(ps);
@@ -338,7 +369,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->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
values[i] = val;
if(val == PRICE_NO_VALUE) break;
@@ -386,73 +417,59 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
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\":{\"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,"));
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);
} else {
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("\"%d\":%.4f,"), i, values[i]);
}
}
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}"),
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
);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
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];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
json[pos++] = '}';
json[pos] = '\0';
@@ -463,7 +480,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(pubTopic.isEmpty() || !connected())
if(pubTopic.isEmpty() || !mqtt.connected())
return false;
publishSystemSensors();
@@ -471,9 +488,14 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":%s}"),
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":\"%s\"}"),
WiFi.macAddress().c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
@@ -489,22 +511,18 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
}
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
String uid;
if(strlen(sensor.uid) > 0) {
uid = String(sensor.uid);
} else {
uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
}
String uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensorNamePrefix.c_str(),
sensor.name,
mqttConfig.publishTopic, sensor.topic,
deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(),
sensor.uom,
sensor.path,
sensor.ttl,
deviceUid.c_str(),
@@ -513,21 +531,14 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
FirmwareVersion::VersionString,
manufacturer.c_str(),
deviceUrl.c_str(),
strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "",
strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "",
strlen_P(sensor.devcl) > 0 ? "\"" : "",
strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
strlen_P(sensor.stacl) > 0 ? "\"" : "",
strlen_P(sensor.uom) > 0 ? ",\"unit_of_meas\":\"" : "",
strlen_P(sensor.uom) > 0 ? (char *) FPSTR(sensor.uom) : "",
strlen_P(sensor.uom) > 0 ? "\"" : ""
strlen_P(sensor.stacl) > 0 ? "\"" : ""
);
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
loop();
}
@@ -616,8 +627,7 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
RealtimePeakSensor.ttl,
RealtimePeakSensor.uom,
RealtimePeakSensor.devcl,
RealtimePeakSensor.stacl,
RealtimePeakSensor.uid
RealtimePeakSensor.stacl
};
publishSensor(sensor);
}
@@ -656,8 +666,7 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
TemperatureSensor.ttl,
TemperatureSensor.uom,
TemperatureSensor.devcl,
TemperatureSensor.stacl,
TemperatureSensor.uid
TemperatureSensor.stacl
};
publishSensor(sensor);
tInit[index] = true;
@@ -677,96 +686,45 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
}
pInit = 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++;
}
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;
}
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++;
}
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);
}
}
void HomeAssistantMqttHandler::publishSystemSensors() {
if(sInit) return;
for(uint8_t i = 0; i < SystemSensorCount; i++) {
@@ -789,8 +747,7 @@ void HomeAssistantMqttHandler::publishThresholdSensors() {
RealtimeThresholdSensor.ttl,
RealtimeThresholdSensor.uom,
RealtimeThresholdSensor.devcl,
RealtimeThresholdSensor.stacl,
RealtimeThresholdSensor.uid
RealtimeThresholdSensor.stacl
};
publishSensor(sensor);
}
@@ -801,34 +758,15 @@ uint8_t HomeAssistantMqttHandler::getFormat() {
return 4;
}
bool HomeAssistantMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
if(!dInit) {
// Not sure how this sensor should be defined in HA, so skipping for now
//publishSensor(DataSensor);
dInit = true;
}
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
bool HomeAssistantMqttHandler::publishRaw(String data) {
return false;
}
bool HomeAssistantMqttHandler::publishFirmware() {
if(!fInit) {
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"uniq_id\":\"%s_fwupgrade\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
sensorNamePrefix.c_str(),
pubTopic.c_str(),
deviceUid.c_str(),
subTopic.c_str()
);
fInit = mqtt.publish(updateTopic + "/" + deviceUid + "/config", json, true, 0);
@@ -853,10 +791,9 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
if (debugger->isActive(RemoteDebug::INFO))
#endif
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 = fInit = dInit = false;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = false;
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
priceImportInit = 0;
priceExportInit = 0;
for(uint8_t i = 0; i < 38; i++) prInit[i] = false;
}
} else if(topic.equals(subTopic)) {
if(payload.equals("fwupgrade")) {
@@ -866,14 +803,3 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
}
}
}
void HomeAssistantMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -45,7 +45,6 @@ public:
bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin);
void setup(SystemConfig* sys, GpioConfig* gpio);
float getVcc();
void setMaxVcc(float maxVcc);
uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t);
bool updateTemperatures();
@@ -68,9 +67,7 @@ private:
bool ledInvert, rgbInvert;
uint8_t vccPin, vccGnd_r, vccVcc_r;
float vccOffset, vccMultiplier;
float vcc = 3.3; // Last known Vcc
float maxVcc = 3.28; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
unsigned long lastVccRead = 0;
float maxVcc = 3.25; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc;

View File

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

View File

@@ -12,18 +12,19 @@
class JsonMqttHandler : public AmsMqttHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->hw = hw;
this->ds = ds;
};
#else
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->hw = hw;
};
#endif
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(uint8_t* raw, size_t length);
bool publishRaw(String data);
bool publishFirmware();
void onMessage(String &topic, String &payload);
@@ -32,9 +33,6 @@ public:
private:
HwTools* hw;
bool hasExport = false;
AmsDataStorage* ds;
uint16_t appendJsonHeader(AmsData* data);
uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos);
bool publishList1(AmsData* data, EnergyAccounting* ea);
@@ -42,6 +40,5 @@ private:
bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data);
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
};
#endif

View File

@@ -8,13 +8,12 @@
#include "FirmwareVersion.h"
#include "hexutils.h"
#include "Uptime.h"
#include "AmsJsonGenerator.h"
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0) {
return false;
}
if(!connected()) {
if(!mqtt.connected()) {
return false;
}
@@ -45,15 +44,6 @@ 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;
}
@@ -77,24 +67,14 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
} else {
memset(pf, 0, 4);
}
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"),
strlen(pf) == 0 ? "},\"realtime\":{" : ",",
pf,
ea->getUseThisHour(),
pf,
ea->getUseToday(),
pf,
ea->getUseThisMonth(),
pf,
ea->getCurrentThreshold(),
pf,
ea->getMonthMax(),
@@ -102,9 +82,6 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
ea->getProducedThisHour(),
pf,
ea->getProducedToday(),
pf,
ea->getProducedThisMonth(),
peaks.c_str(),
strlen(pf) == 0 ? "}" : ""
);
}
@@ -295,9 +272,9 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool JsonMqttHandler::publishPrices(PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(!ps->hasPrice())
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
return false;
time_t now = time(nullptr);
@@ -308,7 +285,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->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
values[i] = val;
if(val == PRICE_NO_VALUE) break;
@@ -356,89 +333,59 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
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("\"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("\"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
);
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 {
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
strcpy_P(pf, PSTR("pr_"));
}
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);
}
}
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]);
}
}
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
);
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';
}
bool ret = false;
if(mqttConfig.payloadFormat == 5) {
@@ -453,7 +400,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
}
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
@@ -481,20 +428,8 @@ uint8_t JsonMqttHandler::getFormat() {
return 0;
}
bool JsonMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
bool JsonMqttHandler::publishRaw(String data) {
return false;
}
bool JsonMqttHandler::publishFirmware() {
@@ -512,46 +447,11 @@ bool JsonMqttHandler::publishFirmware() {
}
void JsonMqttHandler::onMessage(String &topic, String &payload) {
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Received command [%s] to [%s]\n"), payload.c_str(), topic.c_str());
if(topic.equals(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - this is our subscribed topic\n"));
if(payload.equals("fwupgrade")) {
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
updater->setTargetVersion(updater->getNextVersion());
}
} else if(payload.equals("dayplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/dayplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateDayPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
} else if(payload.equals("monthplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/monthplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateMonthPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
}
}
}
void JsonMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -25,9 +25,9 @@ struct AmsOctetTimestamp {
class IEC6205675 : public AmsData {
public:
#if defined(AMS_REMOTE_DEBUG)
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
#else
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
#endif
private:
@@ -37,9 +37,8 @@ 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 };

View File

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

View File

@@ -13,7 +13,6 @@
#endif
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "AmsMqttHandler.h"
class MeterCommunicator {
public:
@@ -25,13 +24,6 @@ public:
virtual bool isConfigChanged();
virtual void ackConfigChanged();
virtual void getCurrentConfig(MeterConfig& meterConfig);
virtual void setMqttHandlerForDebugging(AmsMqttHandler* mqttHandler) {
this->mqttDebug = mqttHandler;
};
protected:
AmsMqttHandler* mqttDebug = NULL;
};
#endif

View File

@@ -14,7 +14,7 @@
#include "AmsConfiguration.h"
#include "DataParsers.h"
#include "Timezone.h"
#include "AmsMqttHandler.h"
#include "PassthroughMqttHandler.h"
#if defined(ESP8266)
#include "SoftwareSerial.h"
@@ -36,9 +36,7 @@ public:
bool isConfigChanged();
void ackConfigChanged();
void getCurrentConfig(MeterConfig& meterConfig);
void setTimezone(Timezone* tz) {
this->tz = tz;
};
void setPassthroughMqttHandler(PassthroughMqttHandler*);
HardwareSerial* getHwSerial();
void rxerr(int err);
@@ -53,6 +51,8 @@ protected:
bool configChanged = false;
Timezone* tz;
PassthroughMqttHandler* pt = NULL;
uint8_t *hanBuffer = NULL;
uint16_t hanBufferSize = 0;
Stream *hanSerial;

View File

@@ -12,219 +12,33 @@
#include "hexutils.h"
#if defined(AMS_REMOTE_DEBUG)
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
#else
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
#endif
float val;
char str[64];
this->packageTimestamp = time(nullptr); // ctx.timestamp is mostly garbage, so we use current time as package timestamp
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;
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(val == NOVALUE) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
if(useMeterType == AmsTypeIskra) { // Iskra special case
meterType = AmsTypeIskra;
uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
if(data->base.length == 0x21) {
idx = 4;
// 1.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1
// 1.8.2
idx += 2;
// 2.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1
// 2.8.2
idx += 2;
// 5.8.0
// 6.8.0
// 7.8.0
// 8.8.0
idx += 4;
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 13.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
powerFactor= ntohl(data->dlu.data) / 1000.0;
// 21.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeImportPower = ntohl(data->dlu.data);
// 41.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeImportPower = ntohl(data->dlu.data);
// 61.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeImportPower = ntohl(data->dlu.data);
// 22.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeExportPower = ntohl(data->dlu.data);
// 42.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeExportPower = ntohl(data->dlu.data);
// 62.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
listType = 4;
lastUpdateMillis = millis64();
} else if(data->base.length == 0x0F) {
idx = 1;
// 1.8.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1 ?
// 1.8.2 ?
idx += 2;
// 2.8.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1 ?
// 2.8.2 ?
idx += 2;
idx++; // Unknown empty octet string
CosemData* meterTs = getCosemDataAt(idx++, ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = decodeCosemDateTime(amst->dt);
meterTimestamp = ts;
}
// 2.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 1.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 31.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
// 32.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0 missing?
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60 * (PI/180)), 2) + pow(l3voltage * sin(60 * (PI/180)),2));
listType = 3;
lastUpdateMillis = millis64();
} else {
idx = 5;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
listType = 4;
lastUpdateMillis = millis64();
}
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) { // Kaifa special case
// Kaifa special case...
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
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) { // Assuming first string is a list identifier
} else if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
String listId = String(str);
@@ -232,7 +46,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId;
meterType = AmsTypeKaifa;
uint8_t idx = 0;
int idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
idx+=2;
if(data->base.length == 0x0D || data->base.length == 0x12) {
@@ -313,7 +127,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
if(data->oct.length == 0x0C) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
time_t ts = decodeCosemDateTime(amst->dt);
meterTimestamp = tz != NULL ? tz->toUTC(ts) : ts;
meterTimestamp = tz.toUTC(ts);
}
}
}
@@ -334,7 +148,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId;
meterType = AmsTypeIskra;
uint8_t idx = 0;
int idx = 0;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data->base.length == 0x12) {
apply(state);
@@ -749,31 +563,49 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
}
}
if(meterType == AmsTypeUnknown && useMeterType == AmsTypeUnknown) {
debugger->println("AMS unknown meter type, trying to identify...");
CosemData* d1 = getCosemDataAt(1, ((char *) (d)));
CosemData* d2 = getCosemDataAt(2, ((char *) (d)));
CosemData* d3 = getCosemDataAt(3, ((char *) (d)));
CosemData* d7 = getCosemDataAt(7, ((char *) (d)));
CosemData* d8 = getCosemDataAt(8, ((char *) (d)));
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
meterType = AmsTypeIskra;
uint8_t idx = 5;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
if(d1->base.type == CosemTypeDLongUnsigned &&
d2->base.type == CosemTypeDLongUnsigned &&
d3->base.type == CosemTypeDLongUnsigned &&
d7->base.type == CosemTypeOctetString &&
d8->base.type == CosemTypeOctetString
) {
meterType = AmsTypeIskra;
lastUpdateMillis = millis64();
listType = 3;
} else if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
meterType = AmsTypeIskra;
lastUpdateMillis = millis64();
listType = 3;
} else {
} else if(useMeterType == AmsTypeUnknown) {
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
@@ -784,7 +616,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
}
}
}
} else { // OBIS code parsing
} else {
listType = 1;
activeImportPower = val;
@@ -906,7 +738,18 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
this->meterTimestamp = adjustForKnownIssues(amst->dt, tz, meterType == AmsTypeUnknown ? useMeterType : meterType);
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;
}
}
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
@@ -1282,24 +1125,3 @@ 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 if(tz != NULL) {
// 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;
}

View File

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

View File

@@ -174,8 +174,8 @@ bool PassiveMeterCommunicator::loop() {
lastError = pos;
printHanReadError(pos);
len += hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(hanBuffer, len);
if(pt != NULL) {
pt->publishBytes(hanBuffer, len);
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
@@ -229,8 +229,8 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
char* payload = ((char *) (hanBuffer)) + pos;
if(maxDetectedPayloadSize < pos) maxDetectedPayloadSize = pos;
if(ctx.type == DATA_TAG_DLMS) {
if(mqttDebug != NULL) {
mqttDebug->publishRaw((uint8_t*) payload, ctx.length);
if(pt != NULL) {
pt->publishBytes((uint8_t*) payload, ctx.length);
}
#if defined(AMS_REMOTE_DEBUG)
@@ -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, tz, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
data = new IEC6205675(payload, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
}
} else if(ctx.type == DATA_TAG_DSMR) {
data = new IEC6205621(payload, tz, &meterConfig);
@@ -405,8 +405,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("HDLC frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
if(pt != NULL) {
pt->publishBytes(buf, curLen);
}
break;
case DATA_TAG_MBUS:
@@ -414,8 +414,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("MBUS frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
if(pt != NULL) {
pt->publishBytes(buf, curLen);
}
break;
case DATA_TAG_GBT:
@@ -447,8 +447,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("DSMR frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
if(pt != NULL) {
pt->publishString((char*) buf);
}
break;
case DATA_TAG_SNRM:
@@ -807,7 +807,6 @@ void PassiveMeterCommunicator::rxerr(int err) {
#endif
debugger->printf_P(PSTR("Serial buffer overflow\n"));
rxBufferErrors++;
#if defined(ESP32)
if(rxBufferErrors > 1 && meterConfig.bufferSize < 8) {
meterConfig.bufferSize += 2;
#if defined(AMS_REMOTE_DEBUG)
@@ -817,7 +816,6 @@ void PassiveMeterCommunicator::rxerr(int err) {
configChanged = true;
rxBufferErrors = 0;
}
#endif
break;
case 3:
#if defined(AMS_REMOTE_DEBUG)

View File

@@ -16,7 +16,7 @@ public:
this->topic = String(mqttConfig.publishTopic);
};
#else
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
this->topic = String(mqttConfig.publishTopic);
};
#endif

View File

@@ -15,23 +15,28 @@
#define DOCPOS_MEASUREMENTUNIT 2
#define DOCPOS_POSITION 3
#define DOCPOS_AMOUNT 4
#define DOCPOS_RESOLUTION 5
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser(PricesContainer *container);
EntsoeA44Parser();
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:
PricesContainer *container;
float multiplier = 1.0;
char currency[4];
char measurementUnit[4];
float points[25];
char buf[64];
uint8_t pos = 0;

View File

@@ -27,6 +27,10 @@
#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
@@ -53,13 +57,10 @@ struct PriceConfig {
uint8_t end_dayofmonth;
};
struct AmsPriceV2Header {
char currency[4];
char measurementUnit[4];
char source[4];
uint8_t resolutionInMinutes;
bool differentExportPrices;
uint8_t numberOfPoints;
struct PricePart {
char name[32];
char description[32];
uint32_t value;
};
class PriceService {
@@ -77,25 +78,17 @@ 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);
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
float getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour);
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();
@@ -110,7 +103,7 @@ private:
PriceServiceConfig* config = NULL;
HTTPClient* http = NULL;
uint8_t currentDay = 0, currentPricePoint = 0;
uint8_t currentDay = 0, currentHour = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
uint8_t nextFetchDelayMinutes = 15;
uint64_t lastTodayFetch = 0;
@@ -124,6 +117,9 @@ private:
Timezone* tz = NULL;
Timezone* entsoeTz = NULL;
static const uint16_t BufferSize = 256;
char* buf;
bool hub = false;
uint8_t* key = NULL;
uint8_t* auth = NULL;
@@ -136,7 +132,5 @@ 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 point);
float getEnergyPricePoint(uint8_t direction, uint8_t point);
};
#endif

View File

@@ -4,43 +4,15 @@
*
*/
#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
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];
struct PricesContainer {
char currency[4];
uint8_t resolutionInMinutes;
bool differentExportPrices;
uint8_t numberOfPoints;
int32_t *points;
char measurementUnit[4];
int32_t points[25];
char source[4];
};
#endif

View File

@@ -7,14 +7,27 @@
#include "EntsoeA44Parser.h"
#include "HardwareSerial.h"
EntsoeA44Parser::EntsoeA44Parser(PricesContainer *container) {
this->container = container;
EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 25; i++) points[i] = PRICE_NO_VALUE;
}
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;
}
@@ -44,7 +57,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
container->setCurrency(buf);
memcpy(currency, buf, pos);
docPos = DOCPOS_SEEK;
pos = 0;
}
@@ -52,7 +65,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
if(strcmp_P(buf, PSTR("MWH"))) multiplier = 0.001;
memcpy(measurementUnit, buf, pos);
docPos = DOCPOS_SEEK;
pos = 0;
}
@@ -60,7 +73,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
if(byte == '<') {
buf[pos] = '\0';
long pn = String(buf).toInt() - 1;
if(pn < container->getNumberOfPoints()) {
if(pn < 25) {
pointNum = pn;
}
docPos = DOCPOS_SEEK;
@@ -72,25 +85,8 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
if(byte == '<') {
buf[pos] = '\0';
float val = String(buf).toFloat();
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);
for(uint8_t i = pointNum; i < 25; i++) {
points[i] = val;
}
docPos = DOCPOS_SEEK;
pos = 0;
@@ -105,17 +101,15 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
} else if(byte == '>') {
buf[pos++] = byte;
buf[pos] = '\0';
if(strcmp_P(buf, PSTR("<currency_Unit.name>")) == 0) {
if(strcmp(buf, "<currency_Unit.name>") == 0) {
docPos = DOCPOS_CURRENCY;
} else if(strcmp(buf, PSTR("<price_Measure_Unit.name>")) == 0) {
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
docPos = DOCPOS_MEASUREMENTUNIT;
} else if(strcmp(buf, PSTR("<position>")) == 0) {
} else if(strcmp(buf, "<position>") == 0) {
docPos = DOCPOS_POSITION;
pointNum = 0xFF;
} else if(strcmp(buf, PSTR("<price.amount>")) == 0) {
} else if(strcmp(buf, "<price.amount>") == 0) {
docPos = DOCPOS_AMOUNT;
} else if(strcmp(buf, PSTR("<resolution>")) == 0) {
docPos = DOCPOS_RESOLUTION;
}
pos = 0;
} else {
@@ -124,3 +118,15 @@ 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;
}
}

View File

@@ -25,6 +25,8 @@ PriceService::PriceService(RemoteDebug* Debug) : priceConfig(std::vector<PriceCo
#else
PriceService::PriceService(Stream* Debug) : priceConfig(std::vector<PriceConfig>()) {
#endif
this->buf = (char*) malloc(BufferSize);
debugger = Debug;
// Entso-E uses CET/CEST
@@ -41,10 +43,6 @@ 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;
@@ -93,157 +91,55 @@ char* PriceService::getArea() {
char* PriceService::getSource() {
if(this->today != NULL && this->tomorrow != NULL) {
if(strcmp(this->today->getSource(), this->tomorrow->getSource()) == 0) {
return this->today->getSource();
if(strcmp(this->today->source, this->tomorrow->source) == 0) {
return this->today->source;
} else {
return "MIX";
}
} else if(today != NULL) {
return this->today->getSource();
return this->today->source;
} else if(tomorrow != NULL) {
return this->tomorrow->getSource();
return this->tomorrow->source;
}
return "";
}
uint8_t PriceService::getResolutionInMinutes() {
return today != NULL ? today->getResolutionInMinutes() : 60;
float PriceService::getValueForHour(uint8_t direction, int8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(direction, cur, hour);
}
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);
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
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;
tmElements_t tm;
time_t ts = time(nullptr);
breakTime(entsoeTz->toLocal(ts), tm);
tm.Hour = tm.Minute = tm.Second = 0;
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
breakTime(tz->toLocal(ts), tm);
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), 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:
value += pcVal;
ret += pc.value / 10000.0;
break;
case PRICE_TYPE_SUBTRACT:
value -= pcVal;
ret -= pc.value / 10000.0;
break;
case PRICE_TYPE_PCT:
value += (pcVal * value) / 100.0;
ret += ((pc.value / 10000.0) * ret) / 100.0;
break;
}
}
return value;
return ret;
}
float PriceService::getCurrentPrice(uint8_t direction) {
time_t ts = time(nullptr);
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);
float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour) {
tmElements_t tm;
breakTime(entsoeTz->toLocal(ts), tm);
uint8_t targetHour = tm.Hour + hour;
tm.Hour = tm.Minute = tm.Second = 0;
time_t startOfDay = entsoeTz->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 point) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(entsoeTz->toLocal(ts), tm);
tm.Hour = tm.Minute = tm.Second = 0;
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
breakTime(tz->toLocal(ts), tm);
tm.Minute = tm.Second = 0;
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
float value = PRICE_NO_VALUE;
for (uint8_t i = 0; i < priceConfig.size(); i++) {
@@ -258,7 +154,68 @@ float PriceService::getFixedPrice(uint8_t direction, int8_t point) {
value += pc.value / 10000.0;
}
}
return value;
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;
}
bool PriceService::loop() {
@@ -266,58 +223,42 @@ bool PriceService::loop() {
if(now < 10000) return false; // Grace period
time_t t = time(nullptr);
if(t < FirmwareVersion::BuildEpoch) {
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;
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;
currentPricePoint = getCurrentPricePointIndex();
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
} 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;
if(t < FirmwareVersion::BuildEpoch) return false;
#ifndef AMS2MQTT_PRICE_KEY
if(strlen(getToken()) == 0) {
return false;
}
#endif
if(strlen(config->area) == 0){
if(strlen(config->area) == 0)
return false;
}
if(strlen(config->currency) == 0) {
if(strlen(config->currency) == 0)
return false;
tmElements_t tm;
breakTime(entsoeTz->toLocal(t), tm);
if(currentDay == 0) {
currentDay = tm.Day;
currentHour = tm.Hour;
}
if(currentDay != tm.Day) {
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
tomorrow = NULL;
}
currentDay = tm.Day;
currentHour = tm.Hour;
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;
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
}
if(!config->enabled)
return false;
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
@@ -332,7 +273,6 @@ 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.
}
@@ -349,7 +289,6 @@ bool PriceService::loop() {
}
tomorrow = NULL;
}
currentPricePoint = getCurrentPricePointIndex();
return tomorrow != NULL;
}
@@ -428,12 +367,11 @@ float PriceService::getCurrencyMultiplier(const char* from, const char* to, time
#endif
float currencyMultiplier = 0;
char buf[80];
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
if(retrieve(buf, &p)) {
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
if(retrieve(buf, &p)) {
if(p.getValue() > 0.0) {
currencyMultiplier /= p.getValue();
@@ -476,8 +414,7 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
breakTime(e1, d1);
breakTime(e2, d2);
char buf[256];
snprintf_P(buf, 256, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
snprintf_P(buf, BufferSize, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
getToken(),
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
@@ -497,30 +434,24 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
PricesContainer* ret = new PricesContainer("EOE");
EntsoeA44Parser a44(ret);
if(retrieve(buf, &a44) && ret->hasPrice(0, PRICE_DIRECTION_IMPORT)) {
EntsoeA44Parser a44;
if(retrieve(buf, &a44) && a44.getPoint(0) != PRICE_NO_VALUE) {
PricesContainer* ret = new PricesContainer();
a44.get(ret);
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);
char buf[128];
snprintf_P(buf, 128, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
String data;
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
config->area,
tm.Year+1970,
tm.Month,
tm.Day,
config->resolutionInMinutes,
config->currency
);
#if defined(AMS_REMOTE_DEBUG)
@@ -547,53 +478,23 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
#endif
if(status == HTTP_CODE_OK) {
uint8_t content[1024];
WiFiClient* stream = http->getStreamPtr();
data = http->getString();
http->end();
uint8_t* content = (uint8_t*) (data.c_str());
DataParserContext ctx = {0,0,0,0};
ctx.length = stream->readBytes(content, http->getSize());
http->end();
ctx.length = data.length();
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(gcmRet > 0) {
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];
int32_t intval;
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[i], sizeof(int32_t));
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
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);
PricesContainer* ret = new PricesContainer();
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = PRICE_NO_VALUE;
}
if(header->differentExportPrices) {
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[ret->getNumberOfPoints()+i], sizeof(int32_t));
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
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);
}
memcpy(ret, content+gcmRet, sizeof(*ret));
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = ntohl(ret->points[i]);
}
lastError = 0;
nextFetchDelayMinutes = 1;
@@ -757,11 +658,4 @@ 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(entsoeTz->toLocal(ts), tm);
return ((tm.Hour * 60) + tm.Minute) / getResolutionInMinutes();
}

View File

@@ -1,67 +0,0 @@
#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
}

View File

@@ -17,7 +17,7 @@ public:
topic = String(mqttConfig.publishTopic);
};
#else
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
full = mqttConfig.payloadFormat == 2;
topic = String(mqttConfig.publishTopic);
};
@@ -26,7 +26,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(uint8_t* raw, size_t length);
bool publishRaw(String data);
void onMessage(String &topic, String &payload);
@@ -36,7 +36,6 @@ private:
bool full;
String topic;
uint32_t lastThresholdPublish = 0;
bool hasExport = false;
bool publishList1(AmsData* data, AmsData* meterState);
bool publishList2(AmsData* data, AmsData* meterState);

View File

@@ -10,7 +10,7 @@
#include "FirmwareVersion.h"
bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(topic.isEmpty() || !connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
AmsData data;
@@ -41,15 +41,6 @@ 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();
@@ -237,9 +228,9 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool RawMqttHandler::publishPrices(PriceService* ps) {
if(topic.isEmpty() || !connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
if(!ps->hasPrice())
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
return false;
time_t now = time(nullptr);
@@ -250,7 +241,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->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
values[i] = val;
if(i > 23) continue;
@@ -317,33 +308,15 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
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);
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.loop();
} else {
mqtt.publish(topic + "/price/import/" + String(relativeIndex), String(importVal, 4), true, 0);
mqtt.publish(topic + "/price/" + String(i), String(val, 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);
@@ -365,11 +338,20 @@ 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;
}
bool RawMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(topic.isEmpty() || !connected())
if(topic.isEmpty() || !mqtt.connected())
return false;
mqtt.publish(topic + "/id", WiFi.macAddress(), true, 0);
@@ -396,16 +378,8 @@ uint8_t RawMqttHandler::getFormat() {
return full ? 3 : 2;
}
bool RawMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(topic.isEmpty() || !connected())
return false;
if(length <= 0 || length > BufferSize) return false;
String str = toHex(raw, length);
bool ret = mqtt.publish(topic + "/data", str);
loop();
return ret;
bool RawMqttHandler::publishRaw(String data) {
return false;
}
void RawMqttHandler::onMessage(String &topic, String &payload) {

View File

@@ -1,59 +0,0 @@
# SvelteUi App
Web interface for AMS Reader firmware built with Svelte 5 and Vite 6.
## Development Setup
### Prerequisites
- Node.js 20.x or 22.x LTS (required for Vite 6)
- npm
### Local Development Configuration
To develop against your AMS reader device, you need to configure the proxy target:
1. Copy the example config file:
```bash
cp vite.config.local.example.js vite.config.local.js
```
2. Edit `vite.config.local.js` and update the IP address to match your device:
```javascript
export default {
proxyTarget: "http://192.168.1.100" // Your device's IP
}
```
3. The `vite.config.local.js` file is gitignored, so your personal settings won't be committed.
### Running Development Server
```bash
npm install
npm run dev
```
The dev server will proxy API requests to your configured device IP.
### Building for Production
```bash
npm run build
```
The build output will be in the `dist/` directory.
## Project Structure
- `src/` - Application source code
- `routes/` - Page components using svelte-spa-router
- `lib/` - Shared components and utilities
- `public/` - Static assets (favicon, etc.)
- `dist/` - Build output (not committed to git)
## Key Technologies
- **Svelte 5.17.0** - UI framework
- **Vite 6.0.7** - Build tool
- **svelte-spa-router 4.0.1** - Hash-based routing
- **Tailwind CSS** - Styling

File diff suppressed because one or more lines are too long

View File

@@ -8,9 +8,10 @@
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" crossorigin href="/index.css">
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-gray-100 dark:bg-gray-900">
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,24 @@
"build": "vite build",
"preview": "vite preview"
},
"overrides": {
"svelte-navigator": {
"svelte": ">=4.x"
}
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.2",
"@sveltejs/vite-plugin-svelte": "^2.1.0",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.14",
"http-proxy-middleware": "^2.0.9",
"postcss": "^8.4.31",
"postcss-load-config": "^4.0.1",
"svelte": "^5.17.0",
"svelte-spa-router": "^4.0.1",
"svelte-preprocess": "^6.0.3",
"svelte": "^4.2.19",
"svelte-navigator": "^3.2.2",
"svelte-preprocess": "^5.0.3",
"svelte-qrcode": "^1.0.0",
"tailwindcss": "^3.3.1",
"vite": "^6.0.7"
"vite": "^4.5.14"
},
"dependencies": {
"cssnano": "^5.1.15",

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,27 +1,44 @@
<script>
import Router from "svelte-spa-router";
import { push } from "svelte-spa-router";
import { getTariff, sysinfoStore, dataStore, getSysinfo } from './lib/DataStores.js';
import { Router, Route, navigate } from "svelte-navigator";
import { getTariff, tariffStore, sysinfoStore, dataStore, pricesStore, 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';
import DashboardRoute from './routes/DashboardRoute.svelte';
import ConfigurationRoute from './routes/ConfigurationRoute.svelte';
import StatusRoute from './routes/StatusRoute.svelte';
import PriceConfigRoute from './routes/PriceConfigRoute.svelte';
import MqttCaRoute from './routes/MqttCaRoute.svelte';
import MqttCertRoute from './routes/MqttCertRoute.svelte';
import MqttKeyRoute from './routes/MqttKeyRoute.svelte';
import ConsentRoute from './routes/ConsentRoute.svelte';
import SetupRoute from './routes/SetupRoute.svelte';
import VendorRoute from './routes/VendorRoute.svelte';
import EditDayRoute from './routes/EditDayRoute.svelte';
import EditMonthRoute from './routes/EditMonthRoute.svelte';
import Dashboard from './lib/Dashboard.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
import StatusPage from './lib/StatusPage.svelte';
import VendorPanel from './lib/VendorPanel.svelte';
import SetupPanel from './lib/SetupPanel.svelte';
import Mask from './lib/Mask.svelte';
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
import ConsentComponent from "./lib/ConsentComponent.svelte";
import PriceConfig from "./lib/PriceConfig.svelte";
import DataEdit from "./lib/DataEdit.svelte";
import { updateRealtime } from "./lib/RealtimeStore.js";
let basepath = document.getElementsByTagName('base')[0].getAttribute("href");
if(!basepath) basepath = "/";
let prices;
pricesStore.subscribe(update => {
prices = update;
});
let dayPlot;
dayPlotStore.subscribe(update => {
dayPlot = update;
});
let monthPlot;
monthPlotStore.subscribe(update => {
monthPlot = update;
});
let temperatures;
temperaturesStore.subscribe(update => {
temperatures = update;
});
let translations = {};
translationsStore.subscribe(update => {
translations = update;
@@ -34,11 +51,11 @@
sysinfoStore.subscribe(update => {
sysinfo = update;
if(sysinfo.vndcfg === false) {
push("/vendor");
navigate(basepath + "vendor");
} else if(sysinfo.usrcfg === false) {
push("/setup");
navigate(basepath + "setup");
} else if(sysinfo.fwconsent === 0) {
push("/consent");
navigate(basepath + "consent");
}
if(sysinfo.ui.k === 1) {
@@ -72,26 +89,53 @@
updateRealtime(update);
});
let tariffData = {};
tariffStore.subscribe(update => {
tariffData = update;
});
getTariff();
</script>
<div class="container mx-auto m-3">
<Header data={data} basepath={basepath}/>
<Router routes={{
'/': DashboardRoute,
'/configuration': ConfigurationRoute,
'/priceconfig': PriceConfigRoute,
'/status': StatusRoute,
'/mqtt-ca': MqttCaRoute,
'/mqtt-cert': MqttCertRoute,
'/mqtt-key': MqttKeyRoute,
'/consent': ConsentRoute,
'/setup': SetupRoute,
'/vendor': VendorRoute,
'/edit-day': EditDayRoute,
'/edit-month': EditMonthRoute,
}} />
<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}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo} basepath={basepath} data={data}/>
</Route>
<Route path="/priceconfig">
<PriceConfig basepath={basepath}/>
</Route>
<Route path="/status">
<StatusPage sysinfo={sysinfo} data={data}/>
</Route>
<Route path="/mqtt-ca">
<FileUploadComponent title="CA" action="/mqtt-ca"/>
</Route>
<Route path="/mqtt-cert">
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
</Route>
<Route path="/mqtt-key">
<FileUploadComponent title="private key" action="/mqtt-key"/>
</Route>
<Route path="/consent">
<ConsentComponent sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/setup">
<SetupPanel sysinfo={sysinfo}/>
</Route>
<Route path="/vendor">
<VendorPanel sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/edit-day">
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" basepath={basepath}/>
</Route>
<Route path="/edit-month">
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" basepath={basepath}/>
</Route>
</Router>
{#if sysinfo.booting}
{#if sysinfo.trying}

View File

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

View File

@@ -1,5 +1,5 @@
<script>
import { tooltip } from './tooltip';
import { Link } from "svelte-navigator";
export let config;
@@ -46,7 +46,7 @@
{#if config.link}
<div class="text-xs text-right">
{#if config.link.route}
<a href={"#" + config.link.url}>{config.link.text}</a>
<Link to={config.link.url}>{config.link.text}</Link>
{:else}
<a href={config.link.url} target={config.link.target}>{config.link.text}</a>
{/if}
@@ -61,7 +61,7 @@
{#if !isNaN(yScale(tick.value))}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '90%' : ''}>{tick.label}</text>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
</g>
{/if}
{/each}
@@ -72,7 +72,7 @@
{#each config.x.ticks as point, i}
{#if !isNaN(xScale(i))}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
{#if barWidth > 20 || i%2 == 0 || !config.x.ticks[i-1].label}
{#if barWidth > 20 || i%2 == 0}
<text x="{barWidth/2}" y="-4">{point.label}</text>
{/if}
</g>
@@ -83,25 +83,28 @@
<g class='bars'>
{#each config.points as point, i}
{#if !isNaN(xScale(i)) && !isNaN(yScale(point.value))}
<g data-title="{point.title}" use:tooltip>
<g>
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth * 0.95}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
width="{barWidth * 0.95}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < vertSwitch || point.labelAngle ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-labelOffset && !config.dark ? point.color : 'white'}"
transform="translate({xScale(i) + barWidth/2} {yScale(point.value) > yScale(0) - labelOffset ? yScale(point.value) - labelOffset : yScale(point.value) + 10}) rotate({point.labelAngle ? point.labelAngle : barWidth < vertSwitch ? 90 : 0})"
>{point.label}</text>
{#if point.title}
<title>{point.title}</title>
{/if}
{/if}
{/if}
</g>
@@ -110,13 +113,13 @@
<rect
x="{xScale(i) + 2}"
y="{yScale(0)}"
width="{barWidth * 0.95}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
fill="{point.color2 ? point.color2 : point.color}"
/>
{#if barWidth > 15}
<text
width="{barWidth * 0.95}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{'middle'}"
fill="{yScale(-point.value2) < yScale(0) + 15 && !config.dark ? point.color2 ? point.color2 : point.color : 'white'}"

View File

@@ -1,24 +1,21 @@
<script>
import { getConfiguration, configurationStore } from '../lib/ConfigurationStore'
import { sysinfoStore, networksStore, dataStore } from '../lib/DataStores.js';
import fetchWithTimeout from '../lib/fetchWithTimeout';
import { translationsStore } from '../lib/TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from '../lib/Helpers.js';
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from '../lib/Mask.svelte'
import Badge from '../lib/Badge.svelte';
import CountrySelectOptions from '../lib/CountrySelectOptions.svelte';
import { push } from 'svelte-spa-router';
import SubnetOptions from '../lib/SubnetOptions.svelte';
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore, networksStore } from './DataStores.js';
import fetchWithTimeout from './fetchWithTimeout';
import { translationsStore } from './TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern } from './Helpers.js';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import Badge from './Badge.svelte';
import CountrySelectOptions from './CountrySelectOptions.svelte';
import { Link, navigate } from 'svelte-navigator';
import SubnetOptions from './SubnetOptions.svelte';
import QrCode from 'svelte-qrcode';
let basepath = "/";
let sysinfo = {};
let data;
export let basepath = "/";
export let sysinfo = {};
export let data;
sysinfoStore.subscribe(v => sysinfo = v);
dataStore.subscribe(v => data = v);
let translations = {};
translationsStore.subscribe(update => {
translations = update;
@@ -153,7 +150,7 @@
});
saving = false;
push(basepath);
navigate(basepath);
}
async function reboot() {
@@ -258,7 +255,7 @@
{#if configuration?.g}
<div class="cnt">
<strong class="text-sm">{translations.conf?.general?.title ?? "General"}</strong>
<a href="{wiki('general')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('General-configuration')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="g" value="true"/>
<div class="my-1">
<div class="flex">
@@ -282,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 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>
<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>
{/if}
<option value="10YNO-1--------2">NO1</option>
<option value="10YNO-2--------T">NO2</option>
@@ -320,14 +317,6 @@
<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">
@@ -339,7 +328,7 @@
</div>
</div>
<div class="my-1">
<a href="#/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</a>
<Link to="/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</Link>
</div>
<div class="my-1">
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> {translations.conf?.price?.enabled ?? "Enabled"}</label>
@@ -375,7 +364,7 @@
{#if configuration?.m}
<div class="cnt">
<strong class="text-sm">{translations.conf?.meter?.title ?? "Meter"}</strong>
<a href="{wiki('meter')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Meter-configuration')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="m" value="true"/>
<input type="hidden" name="mo" value="1"/>
<div class="my-1">
@@ -482,7 +471,7 @@
{#if configuration?.w}
<div class="cnt">
<strong class="text-sm">{translations.conf?.connection?.title ?? "Connection"}</strong>
<a href="{wiki('connection')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Network-connection')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="w" value="true"/>
<div class="my-1">
<select name="nc" class="in-s" bind:value={configuration.n.c}>
@@ -544,7 +533,7 @@
{#if configuration?.n}
<div class="cnt">
<strong class="text-sm">{translations.conf?.network?.title ?? "Network"}</strong>
<a href="{wiki('network')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Network-configuration')}" target="_blank" class="float-right">&#9432;</a>
<div class="my-1">
{translations.conf?.network?.ip ?? "IP"}<br/>
<div class="flex">
@@ -589,7 +578,7 @@
{#if configuration?.q}
<div class="cnt">
<strong class="text-sm">{translations.conf?.mqtt?.title ?? "MQTT"}</strong>
<a href="{wiki('mqtt')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('MQTT-configuration')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="q" value="true"/>
<div class="my-1">
{translations.conf?.mqtt?.server ?? "Server"}
@@ -606,28 +595,28 @@
<div class="my-1 flex">
<span class="flex pr-2">
{#if configuration.q.s.c}
<span class="bd-on"><a href="#/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</a></span>
<span class="bd-on"><Link to="/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</Link></span>
<span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>&#128465;</span>
{:else}
<a href="#/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></a>
<Link to="/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></Link>
{/if}
</span>
<span class="flex pr-2">
{#if configuration.q.s.r}
<span class="bd-on"><a href="#/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</a></span>
<span class="bd-on"><Link to="/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</Link></span>
<span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>&#128465;</span>
{:else}
<a href="#/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></a>
<Link to="/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></Link>
{/if}
</span>
<span class="flex pr-2">
{#if configuration.q.s.k}
<span class="bd-on"><a href="#/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</a></span>
<span class="bd-on"><Link to="/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</Link></span>
<span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>&#128465;</span>
{:else}
<a href="#/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></a>
<Link to="/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></Link>
{/if}
</span>
</div>
@@ -739,7 +728,7 @@
{#if configuration?.c}
<div class="cnt">
<strong class="text-sm">{translations.conf?.cloud?.title ?? "Cloud connections"}</strong>
<a href="{wiki('cloud-connections')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Cloud')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="c" value="true"/>
{#if sysinfo?.features?.includes('cloud')}
<div class="my-1">
@@ -788,7 +777,7 @@
{#if configuration?.p?.r?.startsWith("NO") || configuration?.p?.r?.startsWith("10YNO") || configuration?.p?.r?.startsWith('10Y1001A1001A4')}
<div class="cnt">
<strong class="text-sm">{translations.conf?.thresholds?.title ?? "Thresholds"}</strong>
<a href="{wiki('tariff-thresholds')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Threshold-configuration')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="t" value="true"/>
<div class="flex flex-wrap my-1">
{#each {length: 9} as _, i}
@@ -809,7 +798,7 @@
{#if configuration?.u}
<div class="cnt">
<strong class="text-sm">{translations.conf?.ui?.title ?? "User interface"}</strong>
<a href="{wiki('user-interface')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('User-interface')}" target="_blank" class="float-right">&#9432;</a>
<input type="hidden" name="u" value="true"/>
<div class="flex flex-wrap">
{#each uiElements as el}
@@ -836,7 +825,7 @@
{#if configuration?.i?.h && (sysinfo?.board > 20 || sysinfo?.chip == 'esp8266' || configuration?.i?.d?.d > 0)}
<div class="cnt">
<strong class="text-sm">{translations.conf?.hw?.title ?? "Hardware"}</strong>
<a href="{wiki('hardware')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('GPIO-configuration')}" target="_blank" class="float-right">&#9432;</a>
{#if sysinfo.board > 20}
<input type="hidden" name="i" value="true"/>
<div class="flex flex-wrap">
@@ -918,13 +907,6 @@
</select>
</div>
{/if}
{#if isBusPowered(sysinfo.board)}
Power saving:
<select name="ip" bind:value={configuration.i.p} class="in-s">
<option value={0}>{translations.conf?.hw?.powersaving?.[0] ?? "Normal"}</option>
<option value={3}>{translations.conf?.hw?.powersaving?.[3] ?? "Extreme (Experimental)"}</option>
</select>
{/if}
{#if sysinfo.chip == 'esp8266'}
<input type="hidden" name="iv" value="true"/>
<div class="my-1 flex flex-wrap">

View File

@@ -1,19 +1,18 @@
<script>
import { sysinfoStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
import { sysinfoStore } from './DataStores.js';
import { translationsStore } from './TranslationService.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { wiki } from './Helpers';
let basepath = "/";
let sysinfo = {};
export let basepath = "/";
export let sysinfo = {};
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
sysinfoStore.subscribe(v => sysinfo = v);
let loadingOrSaving = false;
async function handleSubmit(e) {
@@ -37,7 +36,7 @@
s.booting = res.reboot;
return s;
});
push(basepath);
navigate(basepath);
}
</script>
@@ -50,7 +49,7 @@
<hr/>
<div class="my-3">
{translations.consent?.one_click ?? "One-click"}<br/>
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Data-collection-on-one-click-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">{translations.consent?.read_more ?? "Read more"}</a><br/>
<a href="{wiki('Data-collection-on-one-click-firmware-upgrade')}" target="_blank" class="text-blue-600 hover:text-blue-800">{translations.consent?.read_more ?? "Read more"}</a><br/>
<label><input type="radio" name="sf" value={1} checked={sysinfo.fwconsent === 1} class="rounded m-2" required/> {translations.consent?.yes ?? "Yes"}</label>
<label><input type="radio" name="sf" value={2} checked={sysinfo.fwconsent === 2} class="rounded m-2" required/> {translations.consent?.no ?? "No"}</label><br/>
</div>

View File

@@ -1,38 +1,25 @@
<script>
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from '../lib/Helpers.js';
import PowerGauge from '../lib/PowerGauge.svelte';
import VoltPlot from '../lib/VoltPlot.svelte';
import ReactiveData from '../lib/ReactiveData.svelte';
import AccountingData from '../lib/AccountingData.svelte';
import PricePlot from '../lib/PricePlot.svelte';
import DayPlot from '../lib/DayPlot.svelte';
import MonthPlot from '../lib/MonthPlot.svelte';
import TemperaturePlot from '../lib/TemperaturePlot.svelte';
import TariffPeakChart from '../lib/TariffPeakChart.svelte';
import RealtimePlot from '../lib/RealtimePlot.svelte';
import PerPhasePlot from '../lib/PerPhasePlot.svelte';
import { dataStore, sysinfoStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, tariffStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import ReactiveData from './ReactiveData.svelte';
import AccountingData from './AccountingData.svelte';
import PricePlot from './PricePlot.svelte';
import DayPlot from './DayPlot.svelte';
import MonthPlot from './MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte';
import RealtimePlot from './RealtimePlot.svelte';
import PerPhasePlot from './PerPhasePlot.svelte';
let data = {}
let sysinfo = {}
let importPrices = {}
let exportPrices = {}
let dayPlot = {}
let monthPlot = {}
let temperatures = {};
let translations = {};
let tariffData = {};
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
importPricesStore.subscribe(v => importPrices = v);
exportPricesStore.subscribe(v => exportPrices = v);
dayPlotStore.subscribe(v => dayPlot = v);
monthPlotStore.subscribe(v => monthPlot = v);
temperaturesStore.subscribe(v => temperatures = v);
translationsStore.subscribe(v => translations = v);
tariffStore.subscribe(v => tariffData = v);
export let data = {}
export let sysinfo = {}
export let prices = {}
export let dayPlot = {}
export let monthPlot = {}
export let temperatures = {};
export let translations = {};
export let tariffData = {};
let it,et,threePhase, l1e, l2e, l3e;
$: {
@@ -141,7 +128,7 @@
{/if}
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("NO") || data.pr.startsWith("10YNO") || data.pr.startsWith('10Y1001A1001A4')))}
<div class="cnt h-64">
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} realtime={data.ea} translations={translations}/>
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} translations={translations}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.l, data.hm == 1)}
@@ -149,20 +136,9 @@
<RealtimePlot title={translations.dashboard?.realtime ?? "Real time"}/>
</div>
{/if}
{#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} sysinfo={sysinfo}/>
</div>
{:else}
<div class="cnt gwf">
<PricePlot title={translations.dashboard?.price ?? "Price"} json={importPrices} sysinfo={sysinfo}/>
</div>
{/if}
{/if}
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0) && uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.pe))}
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
<div class="cnt gwf">
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices} sysinfo={sysinfo}/>
<PricePlot title={translations.dashboard?.price ?? "Price"} json={prices} sysinfo={sysinfo}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}

View File

@@ -1,6 +1,6 @@
<script>
import { translationsStore } from './TranslationService';
import { push } from 'svelte-spa-router';
import { navigate } from 'svelte-navigator';
import Mask from './Mask.svelte'
export let prefix;
@@ -59,7 +59,7 @@
let res = (await response.json())
saving = false;
push(basepath);
navigate(basepath);
}
</script>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
@@ -70,7 +70,7 @@
{#each importElements as el}
<label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span>
</label>
{/each}
@@ -82,7 +82,7 @@
{#each exportElements as el}
<label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span>
</label>
{/each}

View File

@@ -60,7 +60,7 @@ export const dataStore = readable(data, (set) => {
lastTemp = data.t;
setTimeout(getTemperatures, 2000);
}
if(data.pe && data.p != lastPrice) {
if(lastPrice == null && data.pe && data.p != null) {
lastPrice = data.p;
getPrices();
}
@@ -109,31 +109,41 @@ export const dataStore = readable(data, (set) => {
}
});
let priceFetchTimeout;
let importPrices = {};
export const importPricesStore = writable(importPrices);
let exportprices = {};
export const exportPricesStore = writable(exportprices);
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))
}
}
export async function getPrices() {
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);
if(priceShiftTimeout) {
clearTimeout(priceShiftTimeout);
priceShiftTimeout = 0;
}
const response = await fetchWithTimeout("energyprice.json");
prices = (await response.json())
pricesStore.set(prices);
let date = new Date();
priceFetchTimeout = setTimeout(getPrices, ((24-date.getHours())*3600000)+10)
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
}
let dayPlot = {};

View File

@@ -1,6 +1,9 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
import Mask from "./Mask.svelte";
import { translationsStore } from "./TranslationService";
export let action;
export let title;
let translations = {};
translationsStore.subscribe(update => {
@@ -12,12 +15,12 @@
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} CA</strong>
<strong>{translations.upload?.title ?? "Upload"} {title}</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-ca" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
<button type="submit" class="btn-pri"><p class="mb-4">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>

View File

@@ -1,4 +1,5 @@
<script>
import { Link } from "svelte-navigator";
import { sysinfoStore } from './DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper';
import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js';
@@ -45,7 +46,7 @@
<nav class="hdr">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex text-lg text-gray-100 p-2">
<a href={basepath}>AMS reader <span>{sysinfo.version}</span></a>
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
@@ -78,14 +79,14 @@
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}>
<a href="#/configuration"><GearIcon/></a>
<Link to="/configuration"><GearIcon/></Link>
</div>
<div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}>
<a href="#/status"><InfoIcon/></a>
<Link to="/status"><InfoIcon/></Link>
</div>
{/if}
<div class="flex-none px-1 mt-1" title={translations.header?.doc ?? ""}>
<a href="{wiki('')}" target='_blank' rel="noreferrer"><HelpIcon/></a>
<a href={wiki('')} target='_blank' rel="noreferrer"><HelpIcon/></a>
</div>
{#if sysinfo.upgrading}
<div class="flex-none mr-3 mt-1 text-yellow-300">Upgrading to {sysinfo.upgrade.t}, {progress.toFixed(1)}%</div>

View File

@@ -130,11 +130,7 @@ export function uiVisibility(choice, state) {
}
export function wiki(page) {
let ret = "https://wiki.amsleser.no";
if(page) {
ret += "/en/firmware#" + page;
}
return ret;
return "https://github.com/UtilitechAS/amsreader-firmware/wiki/" + page;
}
export function fmtnum(v,d) {
@@ -149,11 +145,6 @@ 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";

View File

@@ -1,9 +1,11 @@
<script>
import { priceConfigStore, getPriceConfig } from '../lib/ConfigurationStore'
import { translationsStore } from '../lib/TranslationService';
import { wiki, zeropad } from '../lib/Helpers.js';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
import { priceConfigStore, getPriceConfig } from './ConfigurationStore'
import { translationsStore } from './TranslationService';
import { wiki, zeropad } from './Helpers.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
export let basepath = "/";
let translations = {};
translationsStore.subscribe(update => {
@@ -51,7 +53,7 @@
let res = (await response.json())
saving = false;
push("/configuration");
navigate(basepath + "configuration");
}
let toggleDay = function(arr, day) {
@@ -106,7 +108,7 @@
</script>
<div class="cnt">
<strong class="text-sm">{translations.conf?.price?.title ?? "Price"}</strong>
<a href="{wiki('configure-price-price-modifiers')}" target="_blank" class="float-right">&#9432;</a>
<a href="{wiki('Price-configuration')}" target="_blank" class="float-right">&#9432;</a>
<hr class="m-3"/>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<input type="hidden" name="r" value="true"/>

View File

@@ -1,7 +1,6 @@
<script>
import { zeropad, addHours, addMinutes, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
import { zeropad, addHours, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
import BarChart from './BarChart.svelte';
import { onMount } from 'svelte';
export let title;
export let json;
@@ -13,124 +12,113 @@
let dark = document.documentElement.classList.contains('dark');
let cur = new Date();
$: {
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);
};
onMount(() => {
let timeout;
function scheduleUpdate() {
cur = new Date();
timeout = setTimeout(() => {
scheduleUpdate();
}, (15 - (cur.getMinutes() % 15)) * 60000);
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;
}
}
scheduleUpdate();
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));
return () => {
clearTimeout(timeout);
};
});
$: {
if(json?.prices?.length > 0) {
cur = new Date();
let currency = json?.currency;
let val = 0;
let yTicks = [];
let xTicks = [];
let values = [];
min = max = 0;
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
let i = json?.cursor ? json.cursor : 0;
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 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;
}
}
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);
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)
});
}
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'
}
};
}
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'
}
};
};
</script>
{#if config.points && config.points.length > 0}
<BarChart config={config} />
{/if}
<BarChart config={config} />

View File

@@ -41,6 +41,12 @@
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) {
@@ -84,6 +90,9 @@
} else {
visible = false;
}
/*
console.log("Min: %d\nMax: %d\nShow: %s", min, max, visible);
*/
}
</script>

View File

@@ -1,9 +1,9 @@
<script>
import { sysinfoStore, networksStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
import Mask from '../lib/Mask.svelte'
import SubnetOptions from '../lib/SubnetOptions.svelte';
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from '../lib/Helpers.js';
import { sysinfoStore, networksStore } from './DataStores.js';
import { translationsStore } from './TranslationService.js';
import Mask from './Mask.svelte'
import SubnetOptions from './SubnetOptions.svelte';
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from './Helpers.js';
let translations = {};
translationsStore.subscribe(update => {
@@ -16,8 +16,7 @@
networks = update;
});
let sysinfo = {}
sysinfoStore.subscribe(v => sysinfo = v);
export let sysinfo = {}
let staticIp = false;
let connectionMode = 1;

View File

@@ -1,70 +1,15 @@
<script>
import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from '../lib/Helpers.js';
import { getSysinfo, sysinfoStore, dataStore } from '../lib/DataStores.js';
import { upgrade, upgradeWarningText } from '../lib/UpgradeHelper';
import { translationsStore } from '../lib/TranslationService.js';
import Clock from '../lib/Clock.svelte';
import Mask from '../lib/Mask.svelte';
import { scanForDevice } from '../lib/Helpers.js';
import { metertype, boardtype, isBusPowered, getBaseChip } from './Helpers.js';
import { getSysinfo, sysinfoStore } from './DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper';
import { translationsStore } from './TranslationService.js';
import { Link } from 'svelte-navigator';
import Clock from './Clock.svelte';
import Mask from './Mask.svelte';
import { scanForDevice } from './Helpers.js';
let data;
let sysinfo;
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
// Format IPv6 address to compact form (RFC 5952)
const formatIPv6 = (addr) => {
if (!addr) return addr;
// Split into groups
const groups = addr.toLowerCase().split(':');
// Remove leading zeros from each group
const normalized = groups.map(g => g.replace(/^0+/, '') || '0');
// Find longest sequence of consecutive zeros
let maxStart = -1, maxLen = 0;
let currStart = -1, currLen = 0;
for (let i = 0; i < normalized.length; i++) {
if (normalized[i] === '0') {
if (currStart === -1) currStart = i;
currLen++;
} else {
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
currStart = -1;
currLen = 0;
}
}
// Check final sequence
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
// Only compress if we have 2 or more consecutive zeros
if (maxLen > 1) {
const before = normalized.slice(0, maxStart);
const after = normalized.slice(maxStart + maxLen);
if (before.length === 0 && after.length === 0) {
return '::';
} else if (before.length === 0) {
return '::' + after.join(':');
} else if (after.length === 0) {
return before.join(':') + '::';
} else {
return before.join(':') + '::' + after.join(':');
}
}
return normalized.join(':');
};
export let data;
export let sysinfo;
let cfgItems = [{
name: 'WiFi',
@@ -127,11 +72,11 @@
}
let firmwareFileInput;
let firmwareFiles = null;
let firmwareFiles = [];
let firmwareUploading = false;
let configFileInput;
let configFiles = null;
let configFiles = [];
let configUploading = false;
getSysinfo();
@@ -163,17 +108,8 @@
});
};
function changeFirmwareChannel() {
const formData = new FormData();
formData.append('channel', sysinfo.upgrade.c);
fetch('fwchannel', {
method: 'POST',
body: formData
});
};
$: {
if(configFiles && configFiles.length == 1) {
if(configFiles.length == 1) {
let file = configFiles[0];
let reader = new FileReader();
let parseConfigFile = ( e ) => {
@@ -200,7 +136,7 @@
{translations.status?.device?.chip ?? "Chip"}: {sysinfo.chip} {#if sysinfo.cpu}({sysinfo.cpu}MHz){/if}
</div>
<div class="my-2">
{translations.status?.device?.device ?? "Device"}: <a href="#/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</a>
{translations.status?.device?.device ?? "Device"}: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link>
</div>
<div class="my-2">
{translations.status?.device?.mac ?? "MAC"}: {sysinfo.mac}
@@ -223,9 +159,9 @@
{/if}
{#if data?.a}
<div class="my-2">
<a href="#/consent">
<Link to="/consent">
<span class="btn-pri-sm">{translations.status?.device?.btn_consents ?? "Consents"}</span>
</a>
</Link>
<button on:click={askReboot} class="btn-yellow-sm float-right">{translations.btn?.reboot ?? "Reboot"}</button>
</div>
{/if}
@@ -262,48 +198,36 @@
</div>
{#if sysinfo.net.ipv6}
<div class="my-2">
IPv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.ipv6)}</span>
IPv6: <span style="font-size: 14px;">{sysinfo.net.ipv6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>
</div>
<div class="my-2">
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns1v6)}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns2v6)}</span>{/if}
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns1v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns2v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
</div>
{/if}
</div>
{/if}
<div class="cnt">
<strong class="text-sm">{translations.status?.firmware?.title ?? "Firmware"}</strong>
<a href="{wiki('statusinformation-screen')}" target="_blank" class="float-right">&#9432;</a>
{#if sysinfo.fwconsent === 1}
<div class="my-2">
Channel:
<select class="in-s w-full" bind:value={sysinfo.upgrade.c} on:change={changeFirmwareChannel}>
<option value={0}>Stable</option>
<option value={1}>Early</option>
<option value={2}>Release Candidate</option>
<option value={3} disabled>Snapshot</option>
</select>
</div>
{/if}
<div class="my-2">
{translations.status?.firmware?.installed ?? "Installed"}: {sysinfo.version}
</div>
{#if sysinfo.upgrade.t && sysinfo.upgrade.t != sysinfo.version && sysinfo.upgrade.e != 0 && sysinfo.upgrade.e != 123}
<div class="my-2">
<div class="bd-yellow">
{(translations.status?.firmware?.failed ?? "Upgrade from {0} to {1} failed").replace('{0}', sysinfo.upgrade.f).replace('{1}', sysinfo.upgrade.t)}
{(translations.errors?.upgrade?.[sysinfo.upgrade.e] ?? sysinfo.upgrade.e)}
</div>
<div class="my-2">
<div class="bd-yellow">
{(translations.status?.firmware?.failed ?? "Upgrade from {0} to {1} failed").replace('{0}', sysinfo.upgrade.f).replace('{1}', sysinfo.upgrade.t)}
{(translations.errors?.upgrade?.[sysinfo.upgrade.e] ?? sysinfo.upgrade.e)}
</div>
</div>
{/if}
{#if sysinfo.upgrade.n}
<div class="my-2 flex">
{translations.status?.firmware?.latest ?? "Latest"}:
<a href={"https://github.com/UtilitechAS/amsreader-firmware/releases/tag/" + sysinfo.upgrade.n} class="ml-2 text-blue-600 hover:text-blue-800" target='_blank' rel="noreferrer">{sysinfo.upgrade.n}</a>
{#if (sysinfo.security == 0 || data.a) && sysinfo.fwconsent === 1 && sysinfo.upgrade.n && sysinfo.upgrade.n != sysinfo.version}
<div class="flex-none ml-2 text-green-500" title={translations.status?.firmware?.install ?? "Install"}>
<button on:click={askUpgrade}>&#8659;</button>
</div>
<div class="flex-none ml-2 text-green-500" title={translations.status?.firmware?.install ?? "Install"}>
<button on:click={askUpgrade}>&#8659;</button>
</div>
{/if}
</div>
{#if sysinfo.fwconsent === 2}
@@ -313,22 +237,22 @@
{/if}
{/if}
{#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) }
<div class="bd-red">
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
</div>
<div class="bd-red">
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
</div>
{/if}
{#if sysinfo.security == 0 || data.a}
<div class="my-2 flex">
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if !firmwareFiles || firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
{:else}
{firmwareFiles[0].name}
<button type="submit" class="btn-pri-sm float-right ml-2">{translations.btn?.upload ?? "Upload"}</button>
{/if}
</form>
</div>
<div class="my-2 flex">
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
{:else}
{firmwareFiles[0].name}
<button type="submit" class="btn-pri-sm float-right ml-2">{translations.btn?.upload ?? "Upload"}</button>
{/if}
</form>
</div>
{/if}
</div>
{#if sysinfo.security == 0 || data.a}
@@ -341,13 +265,13 @@
{/each}
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> {translations.status?.backup?.secrets ?? "Include secrets"}<br/><small>{translations.status?.backup?.secrets_desc ?? ""}</small></label>
</div>
{#if !configFiles || configFiles.length == 0}
{#if configFiles.length == 0}
<button type="submit" class="btn-pri-sm float-right">{translations.status?.backup?.btn_download ?? "Download"}</button>
{/if}
</form>
<form on:submit|preventDefault={uploadConfigFile} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
{#if !configFiles || configFiles.length == 0}
{#if configFiles.length == 0}
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">{translations.status?.backup?.btn_select_file ?? "Select file"}</button>
{:else}
{configFiles[0].name}

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
<script>
import { sysinfoStore } from '../lib/DataStores.js';
import BoardTypeSelectOptions from '../lib/BoardTypeSelectOptions.svelte';
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
let sysinfo = {};
export let basepath = "/";
export let sysinfo = {};
let loadingOrSaving = false;
async function handleSubmit(e) {
@@ -31,7 +32,7 @@
return s;
});
push(sysinfo.usrcfg ? "/" : "/setup");
navigate(basepath + (sysinfo.usrcfg ? "" : "setup"));
}
let cc = true;

View File

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

View File

@@ -1,8 +1,7 @@
import "./app.postcss";
import { mount } from "svelte";
import App from "./App.svelte";
const app = mount(App, {
const app = new App({
target: document.getElementById("app"),
});

View File

@@ -1,11 +0,0 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { dayPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let dayPlot;
dayPlotStore.subscribe(v => dayPlot = v);
</script>
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" {basepath} />

View File

@@ -1,11 +0,0 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { monthPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let monthPlot;
monthPlotStore.subscribe(v => monthPlot = v);
</script>
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" {basepath} />

View File

@@ -1,25 +0,0 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} certificate</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-cert" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -1,25 +0,0 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} private key</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-key" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -16,8 +16,7 @@
"day" : "day",
"days" : "days",
"month" : "month",
"unknown" : "Unknown",
"now" : "Now"
"unknown" : "Unknown"
},
"btn" : {
"reboot" : "Reboot",
@@ -41,8 +40,6 @@
"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"

View File

@@ -1,62 +1,43 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// Try to import local config, fall back to default if not found
let localConfig = { proxyTarget: "http://192.168.4.1" };
try {
const imported = await import('./vite.config.local.js');
localConfig = imported.default;
} catch (e) {
console.log('No vite.config.local.js found, using default proxy target:', localConfig.proxyTarget);
console.log('Copy vite.config.local.example.js to vite.config.local.js to customize');
}
// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: '.',
minify: 'esbuild',
target: 'es2020',
rollupOptions: {
output: {
assetFileNames: '[name][extname]',
chunkFileNames: '[name].js',
entryFileNames: '[name].js',
manualChunks: undefined
entryFileNames: '[name].js'
}
}
},
plugins: [svelte({
compilerOptions: {
dev: false
}
})],
plugins: [svelte()],
server: {
proxy: {
"/data.json": localConfig.proxyTarget,
"/energyprice.json": localConfig.proxyTarget,
"/importprice.json": localConfig.proxyTarget,
"/exportprice.json": localConfig.proxyTarget,
"/dayplot.json": localConfig.proxyTarget,
"/monthplot.json": localConfig.proxyTarget,
"/temperature.json": localConfig.proxyTarget,
"/sysinfo.json": localConfig.proxyTarget,
"/configuration.json": localConfig.proxyTarget,
"/tariff.json": localConfig.proxyTarget,
"/realtime.json": localConfig.proxyTarget,
"/priceconfig.json": localConfig.proxyTarget,
"/translations.json": localConfig.proxyTarget,
"/cloudkey.json": localConfig.proxyTarget,
"/wifiscan.json": localConfig.proxyTarget,
"/save": localConfig.proxyTarget,
"/reboot": localConfig.proxyTarget,
"/configfile": localConfig.proxyTarget,
"/upgrade": localConfig.proxyTarget,
"/mqtt-ca": localConfig.proxyTarget,
"/mqtt-cert": localConfig.proxyTarget,
"/mqtt-key": localConfig.proxyTarget,
"/logo.svg": localConfig.proxyTarget,
"/data.json": "http://192.168.21.122",
"/energyprice.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",
"/sysinfo.json": "http://192.168.21.122",
"/configuration.json": "http://192.168.21.122",
"/tariff.json": "http://192.168.21.122",
"/realtime.json": "http://192.168.21.122",
"/priceconfig.json": "http://192.168.21.122",
"/translations.json": "http://192.168.21.122",
"/cloudkey.json": "http://192.168.21.122",
"/wifiscan.json": "http://192.168.21.122",
"/save": "http://192.168.21.122",
"/reboot": "http://192.168.21.122",
"/configfile": "http://192.168.21.122",
"/upgrade": "http://192.168.21.122",
"/mqtt-ca": "http://192.168.21.122",
"/mqtt-cert": "http://192.168.21.122",
"/mqtt-key": "http://192.168.21.122",
"/logo.svg": "http://192.168.21.122",
}
}
})

View File

@@ -1,7 +0,0 @@
// Copy this file to vite.config.local.js and update with your device's IP address
// vite.config.local.js is ignored by git so your settings won't be committed
export default {
// The IP address of your AMS reader device for local development
proxyTarget: "http://192.168.4.1"
}

View File

@@ -125,10 +125,7 @@ private:
void dataJson();
void dayplotJson();
void monthplotJson();
void energyPriceJson(); // Deprecated
void importPriceJson();
void exportPriceJson();
void priceJson(uint8_t direction);
void energyPriceJson();
void temperatureJson();
void tariffJson();
void realtimeJson();
@@ -146,7 +143,6 @@ private:
void firmwarePost();
void firmwareUpload();
void isAliveCheck();
void fwchannel();
void mqttCaUpload();
void mqttCaDelete();

View File

@@ -32,6 +32,5 @@
"g": %d
},
"b": %.1f
},
"p": %d
}
},

View File

@@ -2,6 +2,5 @@
"e": %s,
"t": "%s",
"r": "%s",
"c": "%s",
"m": %d
"c": "%s"
},

View File

@@ -53,7 +53,6 @@
"boot_reason": %d,
"ex_cause": %d,
"upgrade": {
"c": %d,
"e": %d,
"f": "%s",
"t": "%s",

View File

@@ -9,7 +9,6 @@
#include "FirmwareVersion.h"
#include "base64.h"
#include "hexutils.h"
#include "AmsJsonGenerator.h"
#include "html/index_html.h"
#include "html/index_css.h"
@@ -127,8 +126,6 @@ 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));
@@ -145,7 +142,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa
server.on(context + F("/firmware"), HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this));
server.on(context + F("/firmware"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::firmwareUpload, this));
server.on(context + F("/is-alive"), HTTP_GET, std::bind(&AmsWebServer::isAliveCheck, this));
server.on(context + F("/fwchannel"), HTTP_POST, std::bind(&AmsWebServer::fwchannel, this));
server.on(context + F("/reset"), HTTP_POST, std::bind(&AmsWebServer::factoryResetPost, this));
@@ -480,7 +476,6 @@ void AmsWebServer::sysinfoJson() {
ESP.getResetInfoPtr()->reason,
ESP.getResetInfoPtr()->exccause,
#endif
sys.firmwareChannel,
upinfo.errorCode,
upinfo.fromVersion,
upinfo.toVersion,
@@ -516,9 +511,10 @@ void AmsWebServer::sysinfoJson() {
#endif
debugger->printf_P(PSTR("Rebooting\n"));
debugger->flush();
rdc->cause = REBOOT_CAUSE_WEB_SYSINFO_JSON;
delay(1000);
rdc->cause = 1;
ESP.restart();
performRestart = false;
}
}
@@ -587,8 +583,8 @@ void AmsWebServer::dataJson() {
mqttStatus = 3;
}
float price = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
float price = ea->getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
float exportPrice = ea->getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
String peaks = "";
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
@@ -686,7 +682,12 @@ void AmsWebServer::dayplotJson() {
if(ds == NULL) {
notFound();
} else {
AmsJsonGenerator::generateDayPlotJson(ds, buf, BufferSize);
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 0; i < 24; i++) {
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
}
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
addConditionalCloudHeaders();
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
@@ -704,7 +705,12 @@ void AmsWebServer::monthplotJson() {
if(ds == NULL) {
notFound();
} else {
AmsJsonGenerator::generateMonthPlotJson(ds, buf, BufferSize);
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 1; i < 32; i++) {
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
}
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
addConditionalCloudHeaders();
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
@@ -715,24 +721,18 @@ 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->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
prices[i] = ps == NULL ? PRICE_NO_VALUE : ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
}
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\""),
ps->getCurrency(),
ps->getSource()
ps == NULL ? "" : ps->getCurrency(),
ps == NULL ? "" : ps->getSource()
);
for(uint8_t i = 0;i < 36; i++) {
@@ -753,59 +753,6 @@ 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\",\"cursor\":%d,\"importExportPriceDifferent\":%s,\"prices\":["),
ps->getCurrency(),
ps->getSource(),
ps->getResolutionInMinutes(),
direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both",
ps->getCurrentPricePointIndex(),
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;
@@ -1050,8 +997,7 @@ void AmsWebServer::configurationJson() {
price.enabled ? "true" : "false",
price.entsoeToken,
price.area,
price.currency,
price.resolutionInMinutes
price.currency
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_DEBUG_JSON,
@@ -1080,8 +1026,7 @@ void AmsWebServer::configurationJson() {
gpioConfig->vccMultiplier / 1000.0,
gpioConfig->vccResistorVcc,
gpioConfig->vccResistorGnd,
gpioConfig->vccBootLimit / 10.0,
gpioConfig->powersaving
gpioConfig->vccBootLimit / 10.0
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_UI_JSON,
@@ -1344,7 +1289,6 @@ void AmsWebServer::handleSave() {
sys.userConfigured = success;
sys.dataCollectionConsent = 0;
sys.firmwareChannel = 0;
config->setSystemConfig(sys);
performRestart = true;
@@ -1389,29 +1333,27 @@ void AmsWebServer::handleSave() {
memset(meterConfig.authenticationKey, 0, 16);
}
meterConfig.wattageMultiplier = server.arg(F("mmw")).toDouble() * 1000.0;
meterConfig.voltageMultiplier = server.arg(F("mmv")).toDouble() * 1000.0;
meterConfig.amperageMultiplier = server.arg(F("mma")).toDouble() * 1000.0;
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toDouble() * 1000.0;
meterConfig.wattageMultiplier = server.arg(F("mmw")).toFloat() * 1000;
meterConfig.voltageMultiplier = server.arg(F("mmv")).toFloat() * 1000;
meterConfig.amperageMultiplier = server.arg(F("mma")).toFloat() * 1000;
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toFloat() * 1000;
config->setMeterConfig(meterConfig);
}
if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) {
long mode = server.arg(F("nc")).toInt();
if(mode > 0) {
if(mode > 0 && mode < 3) {
NetworkConfig network;
config->getNetworkConfig(network);
network.mode = mode;
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")).toDouble() * 10.0;
network.sleep = server.arg(F("wz")).toInt();
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
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");
if(server.hasArg(F("nm"))) {
if(server.arg(F("nm")) == "static") {
@@ -1563,15 +1505,10 @@ void AmsWebServer::handleSave() {
config->setGpioConfig(*gpioConfig);
}
if(server.hasArg(F("ip"))) {
gpioConfig->powersaving = server.hasArg(F("ip")) && !server.arg(F("ip")).isEmpty() ? server.arg(F("ip")).toInt() : 0;
config->setGpioConfig(*gpioConfig);
}
if(server.hasArg(F("iv")) && server.arg(F("iv")) == F("true")) {
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toDouble() * 100.0 : 0;
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toDouble() * 1000.0 : 1000;
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toDouble() * 10.0 : 0;
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toFloat() * 100 : 0;
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toFloat() * 1000 : 1000;
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toFloat() * 10 : 0;
config->setGpioConfig(*gpioConfig);
}
@@ -1636,7 +1573,6 @@ 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);
}
@@ -1686,7 +1622,7 @@ void AmsWebServer::handleSave() {
snprintf_P(buf, BufferSize, PSTR("rd%d"), i);
pc.direction = server.arg(buf).toInt();
snprintf_P(buf, BufferSize, PSTR("rv%d"), i);
pc.value = server.arg(buf).toDouble() * 10000.0;
pc.value = server.arg(buf).toFloat() * 10000;
snprintf_P(buf, BufferSize, PSTR("rn%d"), i);
String name = server.arg(buf);
strcpy(pc.name, name.c_str());
@@ -1786,8 +1722,9 @@ void AmsWebServer::handleSave() {
#endif
debugger->printf_P(PSTR("Rebooting\n"));
debugger->flush();
rdc->cause = REBOOT_CAUSE_WEB_SAVE;
delay(1000);
rdc->cause = 2;
performRestart = false;
ESP.restart();
}
}
@@ -1802,12 +1739,14 @@ void AmsWebServer::reboot() {
delay(250);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Rebooting\n"));
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Rebooting\n"));
debugger->flush();
rdc->cause = REBOOT_CAUSE_WEB_REBOOT;
delay(1000);
delay(1000); rdc->cause = 3;
rdc->cause = 3;
performRestart = false;
ESP.restart();
}
@@ -2028,17 +1967,6 @@ void AmsWebServer::isAliveCheck() {
server.send(200);
}
void AmsWebServer::fwchannel() {
if(!checkSecurity(1))
return;
SystemConfig sys;
config->getSystemConfig(sys);
sys.firmwareChannel = server.arg(F("channel")).toInt();
config->setSystemConfig(sys);
server.send(200);
}
void AmsWebServer::factoryResetPost() {
if(!checkSecurity(1))
return;
@@ -2067,8 +1995,8 @@ void AmsWebServer::factoryResetPost() {
#endif
debugger->printf_P(PSTR("Rebooting\n"));
debugger->flush();
rdc->cause = REBOOT_CAUSE_WEB_FACTORY_RESET;
delay(1000);
rdc->cause = 5;
ESP.restart();
}
@@ -2191,9 +2119,8 @@ void AmsWebServer::tariffJson() {
String peaks;
for(uint8_t x = 0;x < min((uint8_t) 5, eac->hours); x++) {
EnergyAccountingPeak peak = ea->getPeak(x+1);
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"h\":%d,\"v\":%.2f}"),
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"v\":%.2f}"),
peak.day,
peak.hour,
peak.value / 100.0
);
buf[len] = '\0';
@@ -2691,7 +2618,7 @@ void AmsWebServer::configFileDownload() {
EnergyAccountingConfig eac;
config->getEnergyAccountingConfig(eac);
EnergyAccountingData ead = ea->getData();
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %.2f %.2f"),
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f %.2f %.2f"),
ead.version,
ead.month,
ea->getCostYesterday(),
@@ -2701,19 +2628,14 @@ void AmsWebServer::configFileDownload() {
ea->getIncomeThisMonth(),
ea->getIncomeLastMonth(),
ead.peaks[0].day,
ead.peaks[0].hour,
ead.peaks[0].value / 100.0,
ead.peaks[1].day,
ead.peaks[1].hour,
ead.peaks[1].value / 100.0,
ead.peaks[2].day,
ead.peaks[2].hour,
ead.peaks[2].value / 100.0,
ead.peaks[3].day,
ead.peaks[3].hour,
ead.peaks[3].value / 100.0,
ead.peaks[4].day,
ead.peaks[4].hour,
ead.peaks[4].value / 100.0,
ea->getUseLastMonth(),
ea->getProducedLastMonth()

View File

@@ -2,7 +2,7 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsJsonGenerator, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
@@ -19,7 +19,7 @@ build_flags =
-fexceptions
[esp32]
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, bblanchon/ArduinoJson@7.0.4, CloudConnector, ZmartCharge, SvelteUi
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, CloudConnector, ZmartCharge, SvelteUi
[env:esp8266]
platform = espressif8266@4.2.1

File diff suppressed because it is too large Load Diff